#!/usr/bin/env bun
/**
 * mac-sync MCP server.
 *
 * Exposes mac-sync's own `/my/*` surface — messages, contacts, prospects (H01),
 * calls (H02), and the outbox (H03) — as MCP tools, talking to the mac-sync
 * server directly (no quinn-api hop; mac-sync owns this data in Postgres).
 *
 * Env: MAC_SYNC_BASE_URL (default http://209.38.51.98:3201 — the DO backend
 * droplet; override to the wg mesh IP http://10.9.0.5:3201), MAC_SYNC_TOKEN
 * (the server's shared service token).
 */

import { Server } from '@modelcontextprotocol/sdk/server';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types';

import {
  searchMessages,
  listConversations,
  getThread,
  listContacts,
  searchContactsByName,
  listProspects,
  getProspect,
  recentCalls,
  enqueueReply,
  outboxStatus,
  outboxStats,
  markConversationRead,
  markAllRepliedRead,
  conversationReadState,
} from './client';

const str = { type: 'string' as const };
const num = { type: 'number' as const };
const bool = { type: 'boolean' as const };

const TOOLS = [
  {
    name: 'search_messages',
    description:
      'Search iMessage/SMS history (lexical, semantic, or hybrid RRF). Hybrid is best — fuses keyword precision with semantic recall. Returns scored snippets.',
    inputSchema: {
      type: 'object' as const,
      properties: {
        query: str,
        mode: { type: 'string' as const, enum: ['hybrid', 'lexical', 'semantic'] },
        limit: num,
        offset: num,
      },
      required: ['query'],
    },
  },
  {
    name: 'list_conversations',
    description: 'List conversations, most-recent first (display name, participants, message count, last-message time).',
    inputSchema: { type: 'object' as const, properties: { limit: num } },
  },
  {
    name: 'get_thread',
    description: 'Fetch messages in a conversation. Paginate with `since` (ISO timestamp).',
    inputSchema: {
      type: 'object' as const,
      properties: { conversationId: str, limit: num, since: str },
      required: ['conversationId'],
    },
  },
  {
    name: 'list_contacts',
    description: 'List synced contacts (optionally scoped to a deviceId).',
    inputSchema: { type: 'object' as const, properties: { deviceId: str } },
  },
  {
    name: 'search_contacts_by_name',
    description: 'Find contacts by display name (fuzzy).',
    inputSchema: {
      type: 'object' as const,
      properties: { name: str, limit: num, deviceId: str },
      required: ['name'],
    },
  },
  {
    name: 'list_prospects',
    description:
      'List classified prospects. Filter by tier (BLOCK|PEER|PROSPECT|REGULAR|VIP), heat (HOT|WARM|COLD|DEAD), metro, deviceId.',
    inputSchema: {
      type: 'object' as const,
      properties: {
        tier: { type: 'string' as const, enum: ['BLOCK', 'PEER', 'PROSPECT', 'REGULAR', 'VIP'] },
        heat: { type: 'string' as const, enum: ['HOT', 'WARM', 'COLD', 'DEAD'] },
        metro: str,
        deviceId: str,
        limit: num,
      },
    },
  },
  {
    name: 'get_prospect',
    description: 'Get one prospect by handle (tier, heat, metro, msg counts, flags). Returns null if unknown.',
    inputSchema: {
      type: 'object' as const,
      properties: { handle: str, deviceId: str },
      required: ['handle', 'deviceId'],
    },
  },
  {
    name: 'recent_calls',
    description:
      'Recent calls/FaceTimes (who/when/direction/answered/service), newest first. Use `since` (ISO) for a fresh window — answers "did this lead also call?" for triage.',
    inputSchema: {
      type: 'object' as const,
      properties: {
        since: str,
        direction: { type: 'string' as const, enum: ['incoming', 'outgoing'] },
        callType: str,
        deviceId: str,
        limit: num,
      },
    },
  },
  {
    name: 'enqueue_reply',
    description:
      'Drop a reply into the outbox. mac-sync sends it paced, prioritized, channel-smart (iMessage→SMS fallback) at a humane time. Pass `recvAt` (epoch ms of their last message) for smart scheduling, or pin `notBefore` (ISO). Deduped per day.',
    inputSchema: {
      type: 'object' as const,
      properties: {
        deviceId: str,
        recipient: str,
        body: str,
        channelPref: { type: 'string' as const, enum: ['auto', 'imessage', 'sms'] },
        recvAt: num,
        notBefore: str,
        dedupeKey: str,
        markReadOnSend: bool,
      },
      required: ['deviceId', 'recipient', 'body'],
    },
  },
  {
    name: 'outbox_status',
    description: 'Poll an outbox item by id — status (queued|sending|sent|delivered|failed), channel_used, attempts, error.',
    inputSchema: { type: 'object' as const, properties: { id: str }, required: ['id'] },
  },
  {
    name: 'outbox_stats',
    description: 'Outbox counts by status for a device (queued/sending/sent/failed).',
    inputSchema: { type: 'object' as const, properties: { deviceId: str }, required: ['deviceId'] },
  },
  {
    name: 'mark_conversation_read',
    description:
      "Mark a lead's thread read (logical — macsync's own store, never Apple's chat.db). A later inbound re-surfaces it as unread. Idempotent. Pass `readThrough` (ISO) to mark read only up to an instant; defaults to now.",
    inputSchema: {
      type: 'object' as const,
      properties: { deviceId: str, handle: str, readThrough: str },
      required: ['deviceId', 'handle'],
    },
  },
  {
    name: 'mark_all_replied_read',
    description:
      'Mark read every thread whose latest message is outbound (Quinn has replied) — clears handled leads from the inbox in one call. Returns the handles affected. Idempotent.',
    inputSchema: { type: 'object' as const, properties: { deviceId: str }, required: ['deviceId'] },
  },
  {
    name: 'conversation_read_state',
    description:
      "A handle's read-state: read_through, last inbound time, and whether it's currently unread. Use to check if a lead still needs handling.",
    inputSchema: {
      type: 'object' as const,
      properties: { deviceId: str, handle: str },
      required: ['deviceId', 'handle'],
    },
  },
];

type Args = Record<string, unknown>;
const s = (a: Args, k: string): string | undefined => (typeof a[k] === 'string' ? (a[k] as string) : undefined);
const n = (a: Args, k: string): number | undefined => (typeof a[k] === 'number' ? (a[k] as number) : undefined);
const b = (a: Args, k: string): boolean | undefined => (typeof a[k] === 'boolean' ? (a[k] as boolean) : undefined);

async function dispatch(name: string, a: Args): Promise<unknown> {
  switch (name) {
    case 'search_messages':
      return searchMessages(String(a['query']), s(a, 'mode'), n(a, 'limit'), n(a, 'offset'));
    case 'list_conversations':
      return listConversations(n(a, 'limit'));
    case 'get_thread':
      return getThread(String(a['conversationId']), n(a, 'limit'), s(a, 'since'));
    case 'list_contacts':
      return listContacts(s(a, 'deviceId'));
    case 'search_contacts_by_name':
      return searchContactsByName(String(a['name']), n(a, 'limit'), s(a, 'deviceId'));
    case 'list_prospects':
      return listProspects({
        tier: s(a, 'tier'),
        heat: s(a, 'heat'),
        metro: s(a, 'metro'),
        deviceId: s(a, 'deviceId'),
        limit: n(a, 'limit'),
      });
    case 'get_prospect':
      return getProspect(String(a['handle']), String(a['deviceId']));
    case 'recent_calls':
      return recentCalls({
        since: s(a, 'since'),
        direction: s(a, 'direction'),
        callType: s(a, 'callType'),
        deviceId: s(a, 'deviceId'),
        limit: n(a, 'limit'),
      });
    case 'enqueue_reply':
      return enqueueReply({
        deviceId: String(a['deviceId']),
        recipient: String(a['recipient']),
        body: String(a['body']),
        channelPref: s(a, 'channelPref') as 'auto' | 'imessage' | 'sms' | undefined,
        recvAt: n(a, 'recvAt'),
        notBefore: s(a, 'notBefore'),
        dedupeKey: s(a, 'dedupeKey'),
        markReadOnSend: b(a, 'markReadOnSend'),
      });
    case 'outbox_status':
      return outboxStatus(String(a['id']));
    case 'outbox_stats':
      return outboxStats(String(a['deviceId']));
    case 'mark_conversation_read':
      return markConversationRead(String(a['deviceId']), String(a['handle']), s(a, 'readThrough'));
    case 'mark_all_replied_read':
      return markAllRepliedRead(String(a['deviceId']));
    case 'conversation_read_state':
      return conversationReadState(String(a['deviceId']), String(a['handle']));
    default:
      throw new Error(`unknown tool: ${name}`);
  }
}

const server = new Server({ name: 'mac-sync-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });

server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  const { name, arguments: args } = req.params;
  try {
    const result = await dispatch(name, (args ?? {}) as Args);
    return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
  } catch (err) {
    return {
      content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
      isError: true,
    };
  }
});

const transport = new StdioServerTransport();
await server.connect(transport);
