import { randomUUIDv7 } from 'bun';

import { logger } from '@/shared/logger';
import { chatJson } from '@/shared/model-boss';

import { getDb, pgAll } from '@/shared/db';
import { touchDevice } from '@/entities/device';
import { upsertConversation, listConversations, getConversation, updateConversationStats } from '@/entities/conversation';
import {
  bulkUpsertMessages,
  listMessages,
  listMessagesByHandle,
  getLastSyncedAt,
  searchMessages,
} from '@/entities/message';
import { upsertAttachment } from '@/entities/attachment';
import { decodeAttributedBody } from '@/shared/typedstream/decode';
import { upsertContact, listContactsByDevice, listAllContacts, countContactsByDevice, updateContactSummary, searchContacts } from '@/entities/contact';
import type { Conversation } from '@/entities/conversation';
import type {
  Message,
  MessageFilter,
  MessageSearchFilter,
  SearchMode,
  SearchResult,
} from '@/entities/message';
import type { Contact } from '@/entities/contact';
import { generateContactSummary } from '@/features/contact-summary';
import { listInboundSince } from '@/entities/message';
import { listActiveConversations, type ActiveConversation } from '@/entities/conversation';

export interface AttachmentPayload {
  filename?: string | null;
  mimeType?: string | null;
  transferName?: string | null;
  size: number;
  data?: string | null;
}

export interface MessagePayload {
  imessageGuid: string;
  senderId?: string | null;
  direction: 'incoming' | 'outgoing';
  text?: string | null;
  attributedBody?: string | null;
  associatedMessageType?: number | null;
  associatedMessageGuid?: string | null;
  attachments?: AttachmentPayload[] | null;
  attachmentsCount?: number | null;
  attachmentsTotalSize?: number | null;
  attachmentsFiletypes?: string[] | null;
  isAudioMessage?: boolean | null;
  expressiveSendStyleId?: string | null;
  replyToGuid?: string | null;
  threadOriginatorGuid?: string | null;
  groupTitle?: string | null;
  balloonBundleId?: string | null;
  service?: string | null;
  senderIdentifier?: string | null;
  senderDisplayName?: string | null;
  senderPhoneNumber?: string | null;
  senderEmail?: string | null;
  sentAt: string;
  deliveredAt?: string | null;
  readAt?: string | null;
}

export interface SyncMessagesPayload {
  conversationImessageId: string;
  conversationDisplayName?: string | null;
  isGroup: boolean;
  participantIds: string[];
  messages: MessagePayload[];
}

export interface SyncContactPayload {
  appleId?: string | null;
  phoneNumber?: string | null;
  email?: string | null;
  displayName: string;
  avatarHash?: string | null;
  birthday?: string | null;
}

export interface SyncContactsPayload {
  contacts: SyncContactPayload[];
}

export async function ingestMessages(deviceId: string, payload: SyncMessagesPayload): Promise<{ synced: number }> {
  const pool = getDb();

  const convId = randomUUIDv7();
  const conv = await upsertConversation(pool, {
    id: convId,
    deviceId,
    externalId: payload.conversationImessageId,
    displayName: payload.conversationDisplayName ?? '',
    isGroup: payload.isGroup,
    participants: payload.participantIds,
  });

  // Assign IDs up front so we can reference them for attachments after the bulk insert
  const drafts = payload.messages.map((msg) => {
    // Server-side typedstream decode: when the client only sent the
    // attributedBody blob and `text` is empty, recover plain text and
    // persist it in `body_decoded` so search/embedding pipelines see it.
    let bodyDecoded: string | null = null;
    if ((msg.text ?? '') === '' && msg.attributedBody) {
      const decoded = decodeAttributedBody(msg.attributedBody);
      bodyDecoded = decoded.text;
    }
    return {
    draft: {
      id: randomUUIDv7(),
      deviceId,
      conversationId: conv.id,
      externalId: msg.imessageGuid,
      provider: 'imessage' as const,
      fromHandle: msg.senderIdentifier ?? msg.senderId ?? '',
      body: msg.text ?? '',
      isFromMe: msg.direction === 'outgoing',
      hasAttachments: (msg.attachments?.length ?? msg.attachmentsCount ?? 0) > 0,
      attachmentCount: msg.attachments?.length ?? msg.attachmentsCount ?? 0,
      sentAt: msg.sentAt,
      deliveredAt: msg.deliveredAt ?? null,
      readAt: msg.readAt ?? null,
      attributedBody: msg.attributedBody ?? null,
      bodyDecoded,
      associatedMessageType: msg.associatedMessageType ?? null,
      associatedMessageGuid: msg.associatedMessageGuid ?? null,
      expressiveSendStyleId: msg.expressiveSendStyleId ?? null,
      balloonBundleId: msg.balloonBundleId ?? null,
      isAudioMessage: msg.isAudioMessage ?? false,
      replyToGuid: msg.replyToGuid ?? null,
      threadOriginatorGuid: msg.threadOriginatorGuid ?? null,
      groupTitle: msg.groupTitle ?? null,
      service: msg.service ?? null,
      senderIdentifier: msg.senderIdentifier ?? null,
      senderDisplayName: msg.senderDisplayName ?? null,
      senderPhoneNumber: msg.senderPhoneNumber ?? null,
      senderEmail: msg.senderEmail ?? null,
    },
    msg,
    };
  });

  const { count: synced, idByExternalId } = await bulkUpsertMessages(pool, drafts.map((d) => d.draft));

  for (const { draft, msg } of drafts) {
    // Use the actual DB id — on conflict the existing row keeps its original id,
    // not draft.id, so referencing draft.id directly would violate the FK.
    const actualMessageId = idByExternalId.get(msg.imessageGuid) ?? draft.id;

    if (msg.attachments?.length) {
      for (const att of msg.attachments) {
        await upsertAttachment(pool, {
          id: randomUUIDv7(),
          messageId: actualMessageId,
          deviceId,
          externalId: `${msg.imessageGuid}:${att.filename ?? att.transferName ?? 'attachment'}`,
          filename: att.filename ?? null,
          transferName: att.transferName ?? null,
          mimeType: att.mimeType ?? null,
          size: att.size ?? null,
          data: att.data ?? null,
        });
      }
    }

  }

  // Single update: set message_count to the real DB count and last_message_at to the latest sentAt.
  await updateConversationStats(pool, conv.id);

  await touchDevice(pool, deviceId);
  return { synced };
}

export async function ingestContacts(deviceId: string, payload: SyncContactsPayload): Promise<{ synced: number }> {
  const pool = getDb();
  let synced = 0;
  const upsertedContacts: Contact[] = [];

  for (const c of payload.contacts) {
    const handle = c.appleId ?? c.phoneNumber ?? c.email ?? c.displayName;
    const phoneNumbers = c.phoneNumber ? [c.phoneNumber] : [];
    const emails: string[] = [];
    if (c.email) emails.push(c.email);
    if (c.appleId && c.appleId.includes('@')) emails.push(c.appleId);

    const contact = await upsertContact(pool, {
      id: randomUUIDv7(),
      deviceId,
      handle,
      displayName: c.displayName,
      phoneNumbers,
      emails,
      rawData: JSON.stringify(c),
    });
    upsertedContacts.push(contact);
    synced++;
  }

  // Fire-and-forget: generate summaries sequentially to avoid overwhelming model-boss.
  void (async () => {
    for (const contact of upsertedContacts) {
      try {
        const summaryData = await generateContactSummary(pool, contact, chatJson);
        if (summaryData) {
          await updateContactSummary(pool, contact.id, summaryData);
        }
      } catch (err) {
        logger.warn('contact-summary: generation failed', {
          contactId: contact.id,
          handle: contact.handle,
          err: err instanceof Error ? err.message : String(err),
        });
      }
    }
  })();

  return { synced };
}

const MESSAGES_BY_HANDLE_DEFAULT_LIMIT = 50;
const MESSAGES_BY_HANDLE_MAX_LIMIT = 500;

export async function queryMessagesByHandle(
  filter: { deviceId?: string; handle: string; since?: string; limit?: number },
): Promise<{ messages: readonly Message[]; count: number }> {
  const limit = Math.min(filter.limit ?? MESSAGES_BY_HANDLE_DEFAULT_LIMIT, MESSAGES_BY_HANDLE_MAX_LIMIT);
  try {
    const messages = await listMessagesByHandle(getDb(), { ...filter, limit });
    return { messages, count: messages.length };
  } catch (err) {
    throw new Error(
      `queryMessagesByHandle: failed to list messages for handle "${filter.handle}": ${err instanceof Error ? err.message : String(err)}`,
      { cause: err },
    );
  }
}

export async function queryConversations(deviceId?: string): Promise<readonly Conversation[]> {
  return listConversations(getDb(), deviceId);
}

export async function queryMessages(filter: MessageFilter): Promise<readonly Message[]> {
  return listMessages(getDb(), filter);
}

export async function queryMessageSearch(
  filter: MessageSearchFilter & { mode?: SearchMode },
): Promise<SearchResult> {
  const db = getDb();
  const mode = filter.mode ?? 'hybrid';

  if (mode === 'lexical') {
    return searchMessages(db, { ...filter, mode: 'lexical' });
  }

  const { embedQuery } = await import('@/shared/model-boss');

  if (mode === 'semantic') {
    // Mandatory — propagate; route layer converts to 503
    const queryEmbedding = await embedQuery(filter.query, 5_000);
    return searchMessages(db, { ...filter, mode: 'semantic', queryEmbedding });
  }

  // hybrid — opportunistic; hybridSearch skips semantic arm when embedding is absent
  let queryEmbedding: number[] | undefined;
  try {
    queryEmbedding = await embedQuery(filter.query, 5_000);
  } catch (err) {
    logger.warn('embedQuery unavailable — hybrid running lexical-only', {
      error: err instanceof Error ? err.message : String(err),
    });
  }
  return searchMessages(db, { ...filter, mode: 'hybrid', queryEmbedding });
}

export async function queryContacts(deviceId?: string): Promise<readonly Contact[]> {
  const pool = getDb();
  return deviceId ? listContactsByDevice(pool, deviceId) : listAllContacts(pool);
}

/**
 * Public-API contact lookup by name. Delegates to the unified `searchContacts`
 * helper and projects to a flat `Contact[]` to keep the `/my/contacts/by-name`
 * endpoint contract stable. Hits are already ranked
 * exact > prefix > substring across display_name/handle/phone/email.
 */
export async function queryContactsByName(filter: {
  name: string;
  deviceId?: string;
  limit: number;
}): Promise<readonly Contact[]> {
  const hits = await searchContacts(getDb(), {
    query: filter.name,
    deviceId: filter.deviceId,
    limit: filter.limit,
  });
  return hits.map((h) => h.contact);
}

export async function getLastSync(deviceId: string): Promise<string | null> {
  return getLastSyncedAt(getDb(), { deviceId, provider: 'imessage' });
}

export interface ImessageStats {
  totalMessages: number;
  totalConversations: number;
  totalContacts: number;
  lastSyncAt: string | null;
}

export async function getImessageStats(deviceId: string): Promise<ImessageStats> {
  const pool = getDb();
  const msgRows = await pgAll<{ n: number }>(
    pool,
    "SELECT COUNT(*) as n FROM macsync.messages WHERE device_id = $1 AND provider = 'imessage'",
    [deviceId],
  );
  const convRows = await pgAll<{ n: number }>(
    pool,
    'SELECT COUNT(*) as n FROM macsync.conversations WHERE device_id = $1',
    [deviceId],
  );
  const msgRow = msgRows[0];
  const convRow = convRows[0];
  return {
    totalMessages: msgRow?.n ?? 0,
    totalConversations: convRow?.n ?? 0,
    totalContacts: await countContactsByDevice(pool, deviceId),
    lastSyncAt: await getLastSyncedAt(pool, { deviceId, provider: 'imessage' }),
  };
}

export interface ResetResult {
  deletedMessages: number;
  deletedConversations: number;
}

export async function resetImessageSync(deviceId: string): Promise<ResetResult> {
  const pool = getDb();
  const msgRows = await pgAll<{ n: number }>(
    pool,
    "DELETE FROM macsync.messages WHERE device_id = $1 AND provider = 'imessage' RETURNING 1",
    [deviceId],
  );
  const convRows = await pgAll<{ n: number }>(
    pool,
    'DELETE FROM macsync.conversations WHERE device_id = $1 RETURNING 1',
    [deviceId],
  );
  return {
    deletedMessages: msgRows.length,
    deletedConversations: convRows.length,
  };
}

export async function queryInboundSince(
  filter: { sinceIso: string; limit?: number },
): Promise<readonly Message[]> {
  try {
    return await listInboundSince(getDb(), filter);
  } catch (err) {
    throw new Error(
      `queryInboundSince: failed since "${filter.sinceIso}": ${err instanceof Error ? err.message : String(err)}`,
      { cause: err },
    );
  }
}

export async function queryActiveConversations(hours: number): Promise<readonly ActiveConversation[]> {
  try {
    return await listActiveConversations(getDb(), hours);
  } catch (err) {
    throw new Error(
      `queryActiveConversations: failed (hours=${hours}): ${err instanceof Error ? err.message : String(err)}`,
      { cause: err },
    );
  }
}
