import type { Message as DiscordMessage } from 'discord.js';
import { findUserByIntegrationConfig, readConfigValue } from 'officerdb';
import { sendAndAwait, getSessionModel, setSessionModel } from '../send-and-await';
import { consumePairingCode } from '../pairing';
import { chunkMessage } from './chunker';
import { listChatModels } from '@@/api/chat/list-models';
import { enqueueJob } from '../../queue/init';
import { readJob } from '@@/queue/storage';
import { openUserEmailDb } from '@@/api/email/email-db';
import type { ModelInfo } from '@@/api/chat/types';
import { toShellUsername } from '@@/data-path';

const PAIRING_CODE_PATTERN = /^[A-Z0-9]{6}$/;
const TYPING_INTERVAL_MS = 8_000;

type SendableChannel = { send: (content: string) => Promise<unknown> };

type AccessPolicy = { allowedModels: string[] };
const ACCESS_POLICY_KEY = 'chat-access-policy';

async function getVisibleModels(): Promise<ModelInfo[]> {
  const allModels = await listChatModels();
  const policy = await readConfigValue<AccessPolicy>(ACCESS_POLICY_KEY, { allowedModels: [] });
  const allowed = policy.allowedModels;

  if (allowed.length === 0) return allModels;

  const allowedSet = new Set(allowed);
  const allowedProviderSet = new Set(allowed.map((key) => key.split(':')[0]));

  return allModels.filter((m) => {
    const key = `${m.provider}:${m.id}`;
    const isExplicitlyAllowed = allowedSet.has(key);
    const isFromNewProvider = !allowedProviderSet.has(m.provider);
    return isExplicitlyAllowed || isFromNewProvider;
  });
}

type CommandContext = {
  content: string;
  channel: SendableChannel;
  userId: number;
  email: string;
  discordId: string;
};

async function handleEmailSync(ctx: CommandContext): Promise<void> {
  const { channel, email, userId } = ctx;

  // Count emails before sync
  let countBefore = 0;
  try {
    const db = await openUserEmailDb(email, userId);
    if (db) {
      const row = db.query('SELECT COUNT(*) as count FROM emails WHERE deleted = 0').get() as { count: number };
      countBefore = row.count;
      db.close();
    }
  } catch {
    // DB might not exist yet
  }

  await channel.send('Syncing emails...');

  const job = await enqueueJob({ lane: 'google-api', type: 'gmail-sync', userId: email });

  // Poll until done
  const poll = async (): Promise<'completed' | 'failed' | 'cancelled'> => {
    for (let i = 0; i < 120; i++) {
      await new Promise((r) => setTimeout(r, 3000));
      const current = await readJob(job.id);
      if (!current) return 'failed';
      if (current.status === 'completed' || current.status === 'failed' || current.status === 'cancelled') {
        return current.status;
      }
    }
    return 'failed';
  };

  const status = await poll();

  if (status !== 'completed') {
    await channel.send('Email sync failed. Check the queue dashboard for details.');
    return;
  }

  // Count emails after sync and get newest ones
  try {
    const db = await openUserEmailDb(email, userId);
    if (!db) return;
    const row = db.query('SELECT COUNT(*) as count FROM emails WHERE deleted = 0').get() as { count: number };
    const countAfter = row.count;
    const newCount = countAfter - countBefore;

    if (newCount <= 0) {
      db.close();
      await channel.send('Sync complete — no new emails.');
      return;
    }

    const newest = db
      .query('SELECT from_name, from_address, subject FROM emails WHERE deleted = 0 ORDER BY date DESC LIMIT ?')
      .all(Math.min(newCount, 20)) as Array<{ from_name: string | null; from_address: string; subject: string }>;
    db.close();

    const lines = newest.map((e) => {
      const sender = e.from_name || e.from_address;
      return `- **${sender}**: ${e.subject}`;
    });

    let text = `Sync complete — **${newCount}** new email${newCount !== 1 ? 's' : ''}`;
    if (newCount > 20) text += ` (showing latest 20)`;
    text += `:\n\n${lines.join('\n')}`;

    const chunks = chunkMessage(text);
    for (const chunk of chunks) {
      await channel.send(chunk);
    }
  } catch (err) {
    await channel.send(`Sync complete but failed to read results: ${err instanceof Error ? err.message : String(err)}`);
  }
}

async function handleCommand(ctx: CommandContext): Promise<boolean> {
  const { content, channel, discordId } = ctx;
  const lower = content.toLowerCase();

  const helpSections: Record<string, string> = {
    models:
      '**Models:**\n' +
      '`!model` — show current model\n' +
      '`!model <id>` — switch model\n' +
      '`!models` — list available models',
    email: '**Email:**\n' + '`!email sync` — sync Gmail and show new emails',
  };

  if (lower === '!help' || lower.startsWith('!help ')) {
    const topic = content.slice('!help'.length).trim().toLowerCase();
    if (topic && topic in helpSections) {
      await channel.send(helpSections[topic]!);
      return true;
    }
    if (topic) {
      await channel.send(
        `Unknown topic: \`${topic}\`\nAvailable: ${Object.keys(helpSections)
          .map((k) => `\`${k}\``)
          .join(', ')}`,
      );
      return true;
    }
    const full = Object.values(helpSections).join('\n\n');
    await channel.send(full + '\n\n`!help <topic>` — show commands for a topic');
    return true;
  }

  if (lower === '!models') {
    const models = await getVisibleModels();
    if (models.length === 0) {
      await channel.send('No models available.');
      return true;
    }
    const current = getSessionModel('discord', ctx.userId, discordId);
    const grouped = new Map<string, string[]>();
    for (const m of models) {
      const list = grouped.get(m.provider) ?? [];
      list.push(m.id === current ? `**${m.id}** (current)` : m.id);
      grouped.set(m.provider, list);
    }
    let text = '**Available models:**\n';
    for (const [provider, ids] of grouped) {
      text += `\n__${provider}__\n${ids.map((id) => `  ${id}`).join('\n')}\n`;
    }
    text += '\nUse `!model <id>` to switch.';
    await channel.send(text);
    return true;
  }

  if (lower === '!model') {
    const current = getSessionModel('discord', ctx.userId, discordId);
    await channel.send(
      current
        ? `Current model: **${current}**`
        : 'No active session yet — the default model will be used on your next message.',
    );
    return true;
  }

  if (lower.startsWith('!model ')) {
    const requested = content.slice('!model '.length).trim();
    if (!requested) {
      const current = getSessionModel('discord', ctx.userId, discordId);
      await channel.send(current ? `Current model: **${current}**` : 'No active session yet.');
      return true;
    }
    const models = await getVisibleModels();
    const match = models.find((m) => m.id === requested || m.name === requested);
    if (!match) {
      await channel.send(`Model not found: \`${requested}\`\nUse \`!models\` to see available models.`);
      return true;
    }
    setSessionModel('discord', ctx.userId, discordId, match.id);
    await channel.send(`Model switched to **${match.id}**. The new model will be used on your next message.`);
    return true;
  }

  if (lower === '!email sync') {
    await handleEmailSync(ctx);
    return true;
  }

  // Not a recognized command — pass through to PI
  return false;
}

export async function handleDiscordMessage(message: DiscordMessage): Promise<void> {
  // Ignore bots and non-DM messages
  if (message.author.bot) return;
  if (!message.channel.isDMBased() || !('send' in message.channel)) return;

  const channel = message.channel;
  const discordId = message.author.id;
  const content = message.content.trim();
  if (!content) return;

  // Look up linked Officer user
  const linked = await findUserByIntegrationConfig('discord', 'discordId', discordId);

  if (!linked) {
    // Check if this is a pairing code
    if (PAIRING_CODE_PATTERN.test(content.toUpperCase())) {
      const result = await consumePairingCode(content.toUpperCase(), discordId);
      if (result) {
        await channel.send('Account linked! You can now chat with me.');
        return;
      }
      await channel.send('Invalid or expired pairing code. Please generate a new one from Officer Settings.');
      return;
    }

    await channel.send(
      "I don't recognize your Discord account. To link it:\n" +
        '1. Go to Officer Settings → Integrations → Discord\n' +
        '2. Click "Link Discord" to get a pairing code\n' +
        '3. Send the 6-character code to me here',
    );
    return;
  }

  // Handle commands
  if (content.startsWith('!')) {
    const handled = await handleCommand({
      content,
      channel,
      userId: linked.user.id,
      email: linked.user.email,
      discordId,
    });
    if (handled) return;
  }

  // Start typing indicator with keep-alive
  const sendTyping = () => {
    if ('sendTyping' in channel) {
      (channel as { sendTyping: () => Promise<void> }).sendTyping().catch(() => {});
    }
  };
  const typingInterval = setInterval(sendTyping, TYPING_INTERVAL_MS);
  sendTyping();

  try {
    const result = await sendAndAwait({
      userId: linked.user.id,
      email: linked.user.email,
      username: toShellUsername(linked.user.username ?? '', linked.user.email),
      prompt: content,
      context: 'discord',
      contextId: discordId,
    });

    clearInterval(typingInterval);

    const signature = `\`${result.model}\`\n`;
    const chunks = chunkMessage(result.text);
    for (let i = 0; i < chunks.length; i++) {
      await channel.send(i === 0 ? signature + chunks[i]! : chunks[i]!);
    }
  } catch (err) {
    clearInterval(typingInterval);
    console.error('[discord] Error handling message:', err);
    await channel.send('Sorry, something went wrong processing your message.').catch(() => {});
  }
}
