import { Hono } from 'hono';
import { z } from 'zod';

import { getImessageStats, ingestContacts, ingestMessages, resetImessageSync } from '@/features/imessage';
import { withSyncHistory } from '@/shared/sync-history';
import { getDb, pgAll, pgRunReturning } from '@/shared/db';
import { listMissingAttachments, uploadAttachmentBlob } from '@/entities/attachment';
import type { SendQueueRow } from '@/entities/send-queue/types';

const attachmentSchema = z.object({
  filename: z.string().nullish(),
  mimeType: z.string().nullish(),
  transferName: z.string().nullish(),
  size: z.number(),
  data: z.string().nullish(),
});

const messageSchema = z.object({
  imessageGuid: z.string().min(1),
  senderId: z.string().nullish(),
  direction: z.enum(['incoming', 'outgoing']),
  text: z.string().nullish(),
  attributedBody: z.string().nullish(),
  associatedMessageType: z.number().nullish(),
  associatedMessageGuid: z.string().nullish(),
  attachments: z.array(attachmentSchema).nullish(),
  attachmentsCount: z.number().nullish(),
  attachmentsTotalSize: z.number().nullish(),
  attachmentsFiletypes: z.array(z.string()).nullish(),
  isAudioMessage: z.boolean().nullish(),
  expressiveSendStyleId: z.string().nullish(),
  replyToGuid: z.string().nullish(),
  threadOriginatorGuid: z.string().nullish(),
  groupTitle: z.string().nullish(),
  balloonBundleId: z.string().nullish(),
  service: z.string().nullish(),
  senderIdentifier: z.string().nullish(),
  senderDisplayName: z.string().nullish(),
  senderPhoneNumber: z.string().nullish(),
  senderEmail: z.string().nullish(),
  sentAt: z.string(),
  deliveredAt: z.string().nullish(),
  readAt: z.string().nullish(),
});

const syncMessagesSchema = z.object({
  conversationImessageId: z.string().min(1),
  conversationDisplayName: z.string().nullish(),
  isGroup: z.boolean(),
  participantIds: z.array(z.string()),
  messages: z.array(messageSchema),
});

const syncContactSchema = z.object({
  appleId: z.string().nullish(),
  phoneNumber: z.string().nullish(),
  email: z.string().nullish(),
  displayName: z.string().min(1),
  avatarHash: z.string().nullish(),
  birthday: z.string().nullish(),
});

const syncContactsSchema = z.object({
  contacts: z.array(syncContactSchema),
});

const syncMessagesBatchSchema = z.object({
  conversations: z.array(syncMessagesSchema),
});

const sendQueueResultSchema = z.object({
  status: z.enum(['sent', 'failed']),
  error: z.string().optional(),
});

export const imessageClientRouter = new Hono()
  .post('/sync', async (c) => {
    const deviceId = c.get('deviceId');
    const payload = syncMessagesSchema.parse(await c.req.json());
    const result = await withSyncHistory(deviceId, 'messages', () => ingestMessages(deviceId, payload));
    return c.json({ success: true, data: result });
  })
  .post('/sync/batch', async (c) => {
    const deviceId = c.get('deviceId');
    if (!deviceId || deviceId === 'operator') {
      return c.json({ error: 'device token required for batch sync', code: 'bad_token' }, 401);
    }
    const payload = syncMessagesBatchSchema.parse(await c.req.json());
    const result = await withSyncHistory(deviceId, 'messages', async () => {
      let totalSynced = 0;
      for (const conversation of payload.conversations) {
        const r = await ingestMessages(deviceId, conversation);
        totalSynced += r.synced;
      }
      return { synced: totalSynced };
    });
    return c.json({ success: true, data: { synced: result.synced } });
  })
  .post('/contacts', async (c) => {
    const deviceId = c.get('deviceId');
    const payload = syncContactsSchema.parse(await c.req.json());
    const result = await withSyncHistory(deviceId, 'contacts', () => ingestContacts(deviceId, payload));
    return c.json({ success: true, data: result });
  })
  .get('/stats', async (c) => {
    const deviceId = c.get('deviceId');
    const stats = await getImessageStats(deviceId);
    return c.json({ success: true, data: stats });
  })
  .get('/send-queue/pending', async (c) => {
    try {
      const deviceId = c.get('deviceId');
      const db = getDb();

      const items = await pgAll<SendQueueRow>(
        db,
        `
          SELECT id, device_id, to_handle, body, status, send_queue_id, sent_at, failure_reason, created_at, updated_at
          FROM macsync.send_queue
          WHERE device_id = $1 AND status = 'queued'
          ORDER BY created_at ASC
          LIMIT 50
        `,
        [deviceId],
      );

      const formatted = items.map((item) => ({
        id: item.id,
        toHandle: item.to_handle,
        body: item.body,
        createdAt: item.created_at,
      }));

      return c.json({ success: true, data: { items: formatted } });
    } catch (err) {
      return c.json(
        {
          success: false,
          error: {
            code: 'send_queue_error',
            message: err instanceof Error ? err.message : 'failed to fetch pending messages',
          },
        },
        500,
      );
    }
  })
  .post('/send-queue/:id/result', async (c) => {
    try {
      const deviceId = c.get('deviceId');
      const queueId = c.req.param('id');
      const payload = sendQueueResultSchema.parse(await c.req.json());

      const db = getDb();

      if (payload.status === 'sent') {
        await pgRunReturning(
          db,
          `
            UPDATE macsync.send_queue
            SET status = 'sent', sent_at = now(), updated_at = now()
            WHERE id = $1 AND device_id = $2
            RETURNING id
          `,
          [queueId, deviceId],
        );
      } else if (payload.status === 'failed') {
        await pgRunReturning(
          db,
          `
            UPDATE macsync.send_queue
            SET status = 'failed', failure_reason = $3, updated_at = now()
            WHERE id = $1 AND device_id = $2
            RETURNING id
          `,
          [queueId, deviceId, payload.error ?? null],
        );
      }

      return c.json({ success: true, data: { id: queueId, status: payload.status } });
    } catch (err) {
      return c.json(
        {
          success: false,
          error: {
            code: 'send_queue_error',
            message: err instanceof Error ? err.message : 'failed to update message status',
          },
        },
        500,
      );
    }
  })
  .get('/attachments/missing', async (c) => {
    const deviceId = c.get('deviceId');
    const limit = Math.min(Number(c.req.query('limit') ?? '200'), 500);
    const items = await listMissingAttachments(getDb(), deviceId, limit);
    return c.json({ success: true, data: { items, total: items.length } });
  })
  .put('/attachments/:externalId/blob', async (c) => {
    const deviceId = c.get('deviceId');
    const externalId = decodeURIComponent(c.req.param('externalId'));
    const formData = await c.req.formData();
    const blobField = formData.get('blob');
    if (!blobField || typeof (blobField as Blob).arrayBuffer !== 'function') {
      return c.json({ error: 'missing blob field', code: 'bad_request' }, 400);
    }
    const buf = Buffer.from(await (blobField as Blob).arrayBuffer());
    if (buf.byteLength === 0) {
      return c.json({ error: 'empty blob', code: 'bad_request' }, 400);
    }
    const result = await uploadAttachmentBlob(getDb(), deviceId, externalId, buf);
    if (!result) {
      return c.json({ error: 'attachment not found', code: 'not_found' }, 404);
    }
    return c.json({ success: true, data: { id: result.id, storageKind: result.storageKind } });
  })
  .post('/reset', async (c) => {
    const deviceId = c.get('deviceId');
    const result = await resetImessageSync(deviceId);
    return c.json({ success: true, data: result });
  });
