import { getDb, nowIso, newId } from './db.js';
import { appendAudit } from './runtime.js';
import { publishLiveEvent } from './live-events.js';
import { runWhatsAppOrchestrator } from './whatsapp-agent.js';
import { pollIncomingMessages, sendWhatsAppDocument, sendWhatsAppMessage, setTyping } from './whatsapp.js';

const RATE_LIMIT_MAX = 5;
const RATE_LIMIT_WINDOW_MS = 60_000;
const TYPING_REFRESH_MS = 7000;

// Conversation history (multi-turn context) limits.
const HISTORY_MAX_MESSAGES = 20;     // newest N conversational rows considered
const HISTORY_MAX_CHARS = 12_000;    // total chars across replayed turns
const HISTORY_IDLE_TTL_MS = 6 * 60 * 60 * 1000; // stale context cutoff (6h)
const ARTIFACT_ATTACH_PREFIX = '[artifact attached:'; // outbound rows to exclude
const NEW_SESSION_COMMANDS = new Set(['/new', '/reset']);
const SESSION_RESET_ACK = 'Started a new session — previous context cleared. What would you like to do?';

function startTypingIndicator(chatId, typingFn = setTyping) {
  if (!chatId) return () => {};
  let stopped = false;
  const tick = () => {
    if (stopped) return;
    typingFn(chatId, 'composing').catch(() => {});
  };
  tick();
  const timer = setInterval(tick, TYPING_REFRESH_MS);
  return () => {
    stopped = true;
    clearInterval(timer);
    typingFn(chatId, 'paused').catch(() => {});
  };
}

const recentSenders = new Map();

function listAllowlistChatIds() {
  return new Set(
    getDb().prepare('SELECT chat_id FROM whatsapp_allowlist').all().map(row => row.chat_id),
  );
}

function alreadySeen(messageId) {
  if (!messageId) return false;
  return Boolean(
    getDb().prepare('SELECT id FROM whatsapp_messages WHERE message_id = ? LIMIT 1').get(messageId),
  );
}

function logMessage({ direction, chat_id, sender, message_id, text, media_path = null, run_id = null }) {
  const id = newId('wamsg');
  getDb().prepare(`
    INSERT INTO whatsapp_messages (id, direction, chat_id, sender, message_id, text, media_path, run_id, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(id, direction, chat_id, sender || null, message_id || null, text || null, media_path, run_id, nowIso());
  return id;
}

// ── Conversation session boundary ───────────────────────────────────────────
// Returns the ISO timestamp from which history should be replayed for a chat.
// Absent row ⇒ treat as epoch (so the idle cutoff below governs); we do NOT
// create a row here on read — only an explicit /new (resetSession) writes one,
// keeping reads side-effect-free.
function getSessionStart(chatId) {
  if (!chatId) return null;
  const row = getDb()
    .prepare('SELECT session_started_at FROM whatsapp_sessions WHERE chat_id = ?')
    .get(String(chatId));
  return row?.session_started_at || null;
}

export function resetSession(chatId) {
  if (!chatId) return;
  const now = nowIso();
  getDb().prepare(`
    INSERT INTO whatsapp_sessions (chat_id, session_started_at, created_at, updated_at)
    VALUES (?, ?, ?, ?)
    ON CONFLICT(chat_id) DO UPDATE SET session_started_at = excluded.session_started_at, updated_at = excluded.updated_at
  `).run(String(chatId), now, now, now);
}

// Reconstruct recent multi-turn context for a chat from the whatsapp_messages
// log. Text-only: tool_use/tool_result blocks are not persisted, so prior turns
// become a clean user/assistant transcript. The current inbound message must NOT
// yet be logged when this is called (caller computes history before logMessage),
// so it is naturally excluded. The agent owns final role-normalization; this
// returns raw turns oldest→newest.
export function buildWhatsAppHistory(chatId, {
  maxMessages = HISTORY_MAX_MESSAGES,
  maxChars = HISTORY_MAX_CHARS,
  idleTtlMs = HISTORY_IDLE_TTL_MS,
  now = Date.now(),
} = {}) {
  if (!chatId) return [];
  const idleCutoff = new Date(now - idleTtlMs).toISOString();
  const sessionStart = getSessionStart(chatId);
  // Replay window starts at the later of: explicit /new boundary, or idle cutoff.
  const sinceIso = sessionStart && sessionStart > idleCutoff ? sessionStart : idleCutoff;

  const rows = getDb().prepare(`
    SELECT direction, sender, text, created_at
    FROM whatsapp_messages
    WHERE chat_id = ? AND created_at >= ?
    ORDER BY created_at DESC
    LIMIT ?
  `).all(String(chatId), sinceIso, Math.max(1, maxMessages * 2));

  // rows are newest→oldest; walk newest-first applying the char budget, then
  // reverse to chronological order for the model.
  const picked = [];
  let chars = 0;
  for (const row of rows) {
    const text = (row.text || '').trim();
    if (!text) continue;
    let role;
    if (row.direction === 'inbound') {
      if (NEW_SESSION_COMMANDS.has(text.toLowerCase())) continue; // commands aren't conversation
      role = 'user';
    } else if (row.direction === 'outbound' && !text.startsWith(ARTIFACT_ATTACH_PREFIX) && text !== SESSION_RESET_ACK) {
      role = 'assistant';
    } else {
      continue; // artifact-attachment rows, reset acks, anything else: not conversation
    }
    if (chars + text.length > maxChars && picked.length) break;
    chars += text.length;
    picked.push({ role, content: text });
    if (picked.length >= maxMessages) break;
  }
  picked.reverse();
  return picked;
}

function snapshotArtifactIds() {
  return new Set(getDb().prepare('SELECT id FROM artifacts').all().map(r => r.id));
}

function newArtifactsSince(snapshot) {
  return getDb().prepare(`
    SELECT id, kind, title, content, path
    FROM artifacts
    ORDER BY created_at ASC, id ASC
  `).all().filter(row => !snapshot.has(row.id));
}

function htmlFilenameFor(artifact) {
  const slug = String(artifact.title || 'artifact')
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 60) || 'artifact';
  return `${slug}.html`;
}

async function sendArtifactAttachments({
  chatId,
  artifacts,
  sendDocument,
  runId,
}) {
  const sent = [];
  for (const artifact of artifacts) {
    // Owner asked for HTML-only attachments. `html` artifacts have HTML in
    // `content` directly; `document` (markdown) artifacts have rendered HTML
    // in `content` (markdown source lives in `source_md`). Other kinds are
    // skipped — they'd need conversion we haven't wired up.
    if (artifact.kind !== 'html' && artifact.kind !== 'document') continue;
    if (!artifact.content || !String(artifact.content).trim()) continue;
    const filename = htmlFilenameFor(artifact);
    try {
      const result = await sendDocument({
        chat_id: chatId,
        content: artifact.content,
        filename,
        mimetype: 'text/html',
        caption: artifact.title || undefined,
      });
      logMessage({
        direction: 'outbound',
        chat_id: chatId,
        sender: 'main_agent',
        message_id: result?.message_id || null,
        text: `[artifact attached: ${artifact.title || artifact.id}]`,
        media_path: artifact.path || null,
        run_id: runId,
      });
      appendAudit({
        type: 'whatsapp.outbound.artifact',
        summary: `Sent artifact "${artifact.title || artifact.id}" via WhatsApp`,
        status: 'sent',
        meta: { chat_id: chatId, artifact_id: artifact.id, filename, run_id: runId },
      });
      sent.push({ artifact_id: artifact.id, filename });
    } catch (err) {
      appendAudit({
        type: 'whatsapp.outbound.artifact_failed',
        summary: `Failed to attach artifact "${artifact.title || artifact.id}": ${err.message}`,
        status: 'error',
        meta: { chat_id: chatId, artifact_id: artifact.id, run_id: runId },
      });
    }
  }
  return sent;
}

function withinRateLimit(sender) {
  if (!sender) return true;
  const now = Date.now();
  const list = (recentSenders.get(sender) || []).filter(ts => now - ts < RATE_LIMIT_WINDOW_MS);
  if (list.length >= RATE_LIMIT_MAX) {
    recentSenders.set(sender, list);
    return false;
  }
  list.push(now);
  recentSenders.set(sender, list);
  return true;
}

async function dispatchOne(msg, {
  send = sendWhatsAppMessage,
  sendDocument = sendWhatsAppDocument,
  runOrchestrator = runWhatsAppOrchestrator,
} = {}) {
  if (!msg?.chat_id) return { skipped: 'no_chat_id' };
  if (alreadySeen(msg.message_id)) return { skipped: 'duplicate' };

  const allow = listAllowlistChatIds();
  if (!allow.has(msg.chat_id)) {
    logMessage({ direction: 'inbound', chat_id: msg.chat_id, sender: msg.sender, message_id: msg.message_id, text: msg.text });
    appendAudit({
      type: 'whatsapp.inbound.dropped',
      summary: 'Dropped inbound WhatsApp message from non-allowlisted chat',
      status: 'dropped',
      meta: { chat_id: msg.chat_id, message_id: msg.message_id },
    });
    return { skipped: 'allowlist' };
  }

  if (!withinRateLimit(msg.sender || msg.chat_id)) {
    logMessage({ direction: 'inbound', chat_id: msg.chat_id, sender: msg.sender, message_id: msg.message_id, text: msg.text });
    appendAudit({
      type: 'whatsapp.inbound.rate_limited',
      summary: 'WhatsApp sender exceeded 5 messages/minute',
      status: 'rate_limited',
      meta: { chat_id: msg.chat_id, sender: msg.sender || null },
    });
    try {
      await send(msg.chat_id, 'Slow down a sec — I can only handle 5 messages per minute. Try again shortly.');
    } catch {}
    return { skipped: 'rate_limit' };
  }

  // Session control commands (/new, /reset) short-circuit the LLM entirely —
  // they clear conversation context, not "create a new thing". Case- and
  // whitespace-insensitive. Logged inbound for audit parity, then we reply and
  // return without invoking the orchestrator.
  if (NEW_SESSION_COMMANDS.has((msg.text || '').trim().toLowerCase())) {
    logMessage({ direction: 'inbound', chat_id: msg.chat_id, sender: msg.sender, message_id: msg.message_id, text: msg.text });
    resetSession(msg.chat_id);
    appendAudit({
      type: 'whatsapp.session.reset',
      summary: 'Owner started a new WhatsApp session',
      status: 'reset',
      meta: { chat_id: msg.chat_id, command: (msg.text || '').trim() },
    });
    const ack = SESSION_RESET_ACK;
    try {
      const sent = await send(msg.chat_id, ack);
      logMessage({ direction: 'outbound', chat_id: msg.chat_id, sender: 'main_agent', message_id: sent?.message_id || null, text: ack });
    } catch {}
    return { ok: true, reset: true };
  }

  // Reconstruct prior conversation context BEFORE logging the current message so
  // the current inbound is not echoed back into its own history.
  const history = buildWhatsAppHistory(msg.chat_id);

  logMessage({ direction: 'inbound', chat_id: msg.chat_id, sender: msg.sender, message_id: msg.message_id, text: msg.text });

  publishLiveEvent({
    type: 'agent_tool_start',
    agent_id: 'main-orchestrator',
    agent_name: 'madarX Main Agent',
    tool: 'whatsapp_inbound',
    input_summary: (msg.text || '').slice(0, 140),
  });

  // Pair every tool_start with a tool_result so the dashboard's bgAgents map
  // can clear the entry. Without this, the Operator Command Center keeps the
  // "whatsapp inbound" row showing "running" forever.
  function publishInboundResult(isError) {
    publishLiveEvent({
      type: 'agent_tool_result',
      agent_id: 'main-orchestrator',
      agent_name: 'madarX Main Agent',
      tool: 'whatsapp_inbound',
      status: isError ? 'failed' : 'done',
      isError: Boolean(isError),
    });
  }

  // Show the WhatsApp "typing…" indicator immediately and keep it refreshed
  // (WhatsApp clears it after ~10-25s without a refresh). Pattern from
  // OpenClaw's extensions/whatsapp/src/inbound/monitor.ts.
  const stopTyping = startTypingIndicator(msg.chat_id);

  const artifactSnapshot = snapshotArtifactIds();
  let run;
  try {
    run = await runOrchestrator({
      prompt: msg.text || '',
      from: msg.sender || msg.chat_id,
      history,
    });
  } catch (err) {
    stopTyping();
    publishInboundResult(true);
    appendAudit({
      type: 'whatsapp.orchestrator.error',
      summary: `Orchestrator error: ${err.message}`,
      status: 'error',
      meta: { chat_id: msg.chat_id, message_id: msg.message_id },
    });
    try {
      await send(msg.chat_id, `Sorry, the main agent hit an error: ${err.message}`);
    } catch {}
    return { error: err.message };
  }
  stopTyping();
  publishInboundResult(false);

  const replyText = (run?.reply_text || 'Done.').toString().slice(0, 4000);
  try {
    const sent = await send(msg.chat_id, replyText);
    logMessage({
      direction: 'outbound',
      chat_id: msg.chat_id,
      sender: 'main_agent',
      message_id: sent?.message_id || null,
      text: replyText,
      run_id: run?.id || null,
    });
    appendAudit({
      type: 'whatsapp.outbound.sent',
      summary: 'Replied to WhatsApp message',
      status: 'sent',
      meta: {
        chat_id: msg.chat_id,
        run_id: run?.id,
        turns_used: run?.turns_used,
        tool_calls: run?.tool_calls,
        hit_cap: run?.hit_cap,
      },
    });

    const newArtifacts = newArtifactsSince(artifactSnapshot);
    const attached = newArtifacts.length
      ? await sendArtifactAttachments({
          chatId: msg.chat_id,
          artifacts: newArtifacts,
          sendDocument,
          runId: run?.id || null,
        })
      : [];
    return { ok: true, run_id: run?.id || null, attached };
  } catch (err) {
    appendAudit({
      type: 'whatsapp.outbound.failed',
      summary: `Failed to send WhatsApp reply: ${err.message}`,
      status: 'error',
      meta: { chat_id: msg.chat_id, run_id: run?.id },
    });
    return { error: err.message };
  }
}

export async function dispatchInboundMessage(msg, deps) {
  return dispatchOne(msg, deps);
}

export async function pollAndDispatch({
  poll = pollIncomingMessages,
  send = sendWhatsAppMessage,
  sendDocument = sendWhatsAppDocument,
} = {}) {
  let messages;
  try {
    messages = await poll();
  } catch (err) {
    return { ok: false, error: err.message, processed: 0 };
  }
  const results = [];
  for (const msg of messages) {
    try {
      results.push(await dispatchOne(msg, { send, sendDocument }));
    } catch (err) {
      results.push({ error: err.message });
    }
  }
  return { ok: true, processed: results.length, results };
}

export async function runRouterLoop({ intervalMs = 1500, signal } = {}) {
  while (!signal?.aborted) {
    await pollAndDispatch();
    await new Promise(resolve => setTimeout(resolve, intervalMs));
  }
}

export function listInboundAllowlist() {
  return getDb().prepare('SELECT id, chat_id, kind, label, created_at FROM whatsapp_allowlist ORDER BY created_at DESC').all();
}

export function addAllowlistEntry({ chat_id, kind = 'dm', label = null }) {
  if (!chat_id) throw new Error('chat_id is required');
  const id = newId('waal');
  getDb().prepare(`
    INSERT INTO whatsapp_allowlist (id, chat_id, kind, label, created_at)
    VALUES (?, ?, ?, ?, ?)
    ON CONFLICT(chat_id) DO UPDATE SET kind = excluded.kind, label = excluded.label
  `).run(id, String(chat_id), String(kind), label ? String(label) : null, nowIso());
  return getDb().prepare('SELECT id, chat_id, kind, label, created_at FROM whatsapp_allowlist WHERE chat_id = ?').get(String(chat_id));
}

export function removeAllowlistEntry(chatId) {
  if (!chatId) return 0;
  return getDb().prepare('DELETE FROM whatsapp_allowlist WHERE chat_id = ?').run(String(chatId)).changes;
}

export function listInboundMessages(limit = 50) {
  return getDb().prepare(`
    SELECT id, direction, chat_id, sender, message_id, text, run_id, created_at
    FROM whatsapp_messages
    ORDER BY created_at DESC
    LIMIT ?
  `).all(Math.max(1, Math.min(200, Number(limit))));
}
