/**
 * `forge chat` — interactive chat from the terminal.
 *
 * Talks loopback-only to chat-standalone (default 127.0.0.1:8408).
 * For a remote Forge instance, set FORGE_CHAT_URL to a reachable URL
 * (typically the host's chat port over a tunnel).
 *
 * Streaming model:
 *   1. open SSE on /api/sessions/<id>/events
 *   2. wait for the initial `: subscribed` comment so we don't race
 *      the upcoming agent events
 *   3. POST /api/sessions/<id>/messages → 202
 *   4. consume agent events: text_delta → stdout, tool_use/result →
 *      grey aside, turn_done → exit, error → stderr
 *
 * Active session id is persisted in <dataDir>/cli-chat-session.txt so
 * `forge chat "..."` repeated calls continue the same conversation.
 */

import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';

const CHAT_PORT = process.env.CHAT_PORT || '8408';
const BASE = (process.env.FORGE_CHAT_URL || `http://127.0.0.1:${CHAT_PORT}`).replace(/\/+$/, '');

function dataDir(): string {
  return process.env.FORGE_DATA_DIR || join(homedir(), '.forge', 'data');
}
function activeFile(): string { return join(dataDir(), 'cli-chat-session.txt'); }

function loadActive(): string | null {
  try {
    const f = activeFile();
    if (!existsSync(f)) return null;
    const v = readFileSync(f, 'utf-8').trim();
    return v || null;
  } catch { return null; }
}
function saveActive(id: string): void {
  try {
    mkdirSync(dataDir(), { recursive: true });
    writeFileSync(activeFile(), id);
  } catch {}
}
function clearActive(): void {
  try { rmSync(activeFile(), { force: true }); } catch {}
}

async function api(path: string, init?: RequestInit): Promise<any> {
  let res: Response;
  try { res = await fetch(BASE + path, init); }
  catch (e) { throw new Error(`chat service unreachable on ${BASE}: ${(e as Error).message}`); }
  if (!res.ok && res.status !== 202) {
    throw new Error(`HTTP ${res.status}: ${(await res.text().catch(() => '')).slice(0, 200)}`);
  }
  const ct = res.headers.get('content-type') || '';
  if (!ct.includes('application/json')) return null;
  return res.json();
}

interface AgentEvent { type: string; data?: any; message_id?: string }

const isTty = !!process.stdout.isTTY;
const dim = (s: string) => isTty ? `\x1b[2m${s}\x1b[0m` : s;
const red = (s: string) => isTty ? `\x1b[31m${s}\x1b[0m` : s;
const cyan = (s: string) => isTty ? `\x1b[36m${s}\x1b[0m` : s;

async function openSse(sessionId: string): Promise<ReadableStreamDefaultReader<Uint8Array>> {
  const res = await fetch(`${BASE}/api/sessions/${encodeURIComponent(sessionId)}/events`, {
    headers: { accept: 'text/event-stream' },
  });
  if (!res.ok) throw new Error(`SSE open failed: HTTP ${res.status}`);
  if (!res.body) throw new Error('SSE response has no body');
  const reader = res.body.getReader();
  // Wait for the initial `: subscribed` comment so the server-side
  // subscription is registered before the caller fires the POST.
  await reader.read();
  return reader;
}

async function consumeSse(reader: ReadableStreamDefaultReader<Uint8Array>): Promise<void> {
  const decoder = new TextDecoder();
  let buf = '';
  let inText = false;

  while (true) {
    const { value, done } = await reader.read();
    if (done) return;
    buf += decoder.decode(value, { stream: true });

    let idx: number;
    while ((idx = buf.indexOf('\n\n')) >= 0) {
      const frame = buf.slice(0, idx);
      buf = buf.slice(idx + 2);

      const dataLine = frame.split('\n').find((l) => l.startsWith('data: '));
      if (!dataLine) continue;
      let evt: AgentEvent;
      try { evt = JSON.parse(dataLine.slice(6)); } catch { continue; }

      switch (evt.type) {
        case 'text_delta': {
          inText = true;
          process.stdout.write(String(evt.data?.delta || ''));
          break;
        }
        case 'tool_use': {
          if (inText) { process.stdout.write('\n'); inText = false; }
          const name = String(evt.data?.name || '?');
          let inp = '';
          try { inp = JSON.stringify(evt.data?.input ?? {}); } catch { inp = ''; }
          if (inp.length > 60) inp = inp.slice(0, 60) + '…';
          process.stdout.write(dim(`  → ${name}(${inp}) …\n`));
          break;
        }
        case 'tool_result': {
          const name = String(evt.data?.name || '?');
          const r = evt.data?.result;
          const text = typeof r === 'string' ? r : (r?.content ?? JSON.stringify(r ?? ''));
          const head = String(text).split('\n', 1)[0] || '';
          const short = head.length > 80 ? head.slice(0, 80) + '…' : head;
          process.stdout.write(dim(`  ← ${name}: ${short}\n`));
          break;
        }
        case 'turn_done': {
          if (inText) process.stdout.write('\n');
          try { await reader.cancel(); } catch {}
          return;
        }
        case 'error': {
          if (inText) process.stdout.write('\n');
          process.stderr.write(red(`error: ${String(evt.data?.error || 'unknown')}\n`));
          try { await reader.cancel(); } catch {}
          return;
        }
        // 'message_saved' is authoritative state used by GUI clients;
        // CLI already streamed the text, so we ignore it.
      }
    }
  }
}

async function createSession(): Promise<string> {
  const r = await api('/api/sessions', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ title: 'CLI chat' }),
  });
  const id = r?.session?.id;
  if (!id) throw new Error('create session: no id in response');
  return id;
}

export async function chatCommand(args: string[]): Promise<void> {
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
    return printHelp();
  }

  const sub = args[0];
  if (sub === 'sessions' || sub === 'ls') return cmdSessions();
  if (sub === 'history' || sub === 'h') return cmdHistory(args[1]);
  if (sub === 'watch') return cmdWatch(args[1]);
  if (sub === 'new') return cmdNew();
  if (sub === 'rm') return cmdRm(args[1]);
  if (sub === 'use') return cmdUse(args[1]);

  // Otherwise treat the args as a message to send.
  let forceNew = false;
  let explicit: string | null = null;
  const parts: string[] = [];
  for (let i = 0; i < args.length; i++) {
    const a = args[i]!;
    if (a === '-n' || a === '--new') { forceNew = true; continue; }
    if (a === '-s' || a === '--session') { explicit = args[++i] || null; continue; }
    parts.push(a);
  }
  const text = parts.join(' ').trim();
  if (!text) {
    console.error('No message provided. Try: forge chat "your message"');
    process.exit(1);
  }

  let sessionId = explicit;
  if (!sessionId && !forceNew) sessionId = loadActive();
  if (!sessionId) {
    sessionId = await createSession();
    process.stderr.write(dim(`(new session ${sessionId.slice(0, 8)})\n`));
  }
  saveActive(sessionId);

  // Open SSE first, wait for subscribe, then POST so we don't miss events.
  const reader = await openSse(sessionId);
  await api(`/api/sessions/${encodeURIComponent(sessionId)}/messages`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ text }),
  });
  await consumeSse(reader);
}

async function cmdSessions(): Promise<void> {
  const j = await api('/api/sessions?limit=50');
  if (!j?.sessions?.length) { console.log('(no sessions)'); return; }
  const active = loadActive();
  for (const s of j.sessions) {
    const marker = s.id === active ? '*' : ' ';
    const ts = new Date(s.updated_at || s.created_at).toISOString().slice(0, 19).replace('T', ' ');
    console.log(`${marker} ${cyan(s.id.slice(0, 8))}  ${dim(ts)}  ${s.title || '(untitled)'}`);
  }
}

async function cmdHistory(id?: string): Promise<void> {
  const sessionId = id || loadActive();
  if (!sessionId) { console.error('No session. Pick one with: forge chat sessions'); process.exit(1); }
  const j = await api(`/api/sessions/${encodeURIComponent(sessionId)}`);
  if (!j?.messages?.length) { console.log('(empty)'); return; }
  for (const m of j.messages) {
    const role = m.role === 'user' ? 'U' : 'A';
    for (const b of m.blocks || []) {
      if (b.type === 'text' && b.text) {
        console.log(`[${role}] ${b.text}`);
      } else if (b.type === 'tool_use') {
        let inp = '';
        try { inp = JSON.stringify(b.input ?? {}); } catch {}
        if (inp.length > 80) inp = inp.slice(0, 80) + '…';
        console.log(dim(`    → ${b.name}(${inp})`));
      } else if (b.type === 'tool_result') {
        const head = String(b.content || '').split('\n', 1)[0] || '';
        console.log(dim(`    ← ${head.slice(0, 100)}`));
      }
    }
  }
}

async function cmdWatch(id?: string): Promise<void> {
  const sessionId = id || loadActive();
  if (!sessionId) { console.error('No session. Pick one with: forge chat sessions'); process.exit(1); }
  process.stderr.write(dim(`watching ${sessionId} (Ctrl+C to exit)\n`));
  const reader = await openSse(sessionId);
  await consumeSse(reader);
}

async function cmdNew(): Promise<void> {
  const id = await createSession();
  saveActive(id);
  console.log(id);
}

async function cmdRm(id?: string): Promise<void> {
  if (!id) { console.error('Usage: forge chat rm <id>'); process.exit(1); }
  await api(`/api/sessions/${encodeURIComponent(id)}`, { method: 'DELETE' });
  if (loadActive() === id) clearActive();
  console.log('deleted');
}

async function cmdUse(id?: string): Promise<void> {
  if (!id) { console.error('Usage: forge chat use <id>'); process.exit(1); }
  // Verify it exists
  await api(`/api/sessions/${encodeURIComponent(id)}`);
  saveActive(id);
  console.log(`active: ${id}`);
}

function printHelp(): void {
  console.log(`forge chat — chat with your Forge agent from the terminal

Usage:
  forge chat "<prompt>"           Send to active session (creates one if none).
  forge chat -n "<prompt>"        Force a fresh session for this turn.
  forge chat -s <id> "<prompt>"   Send to a specific session.

  forge chat sessions             List recent sessions ('*' = active).
  forge chat history [<id>]       Print message history (default: active).
  forge chat watch [<id>]         Subscribe to the live event stream.
  forge chat new                  Create a fresh session and set as active.
  forge chat use <id>             Set <id> as the active session.
  forge chat rm <id>              Delete a session.

Env:
  FORGE_CHAT_URL=http://127.0.0.1:8408   chat-standalone endpoint
  CHAT_PORT=8408                         shorthand for the port`);
}
