/**
 * `mcpctl chat-llm <name>` — stateless chat with any registered LLM.
 *
 * Distinct from `mcpctl chat <agent>`:
 *   - No threads, no history, no tools, no project prompts.
 *   - Just an OpenAI chat-completions round-trip per turn.
 *   - Works for both kinds of mcpd-registered LLMs:
 *     * `kind=public` — direct upstream call (existing behavior).
 *     * `kind=virtual` — relayed through the publishing mcplocal's SSE
 *       channel (the v1 virtual-LLM feature).
 *
 * The CLI doesn't need to know which kind the LLM is; mcpd's
 * `/api/v1/llms/:name/infer` route branches on `kind` server-side.
 */
import { Command } from 'commander';
import http from 'node:http';
import https from 'node:https';
import readline from 'node:readline';
import type { ApiClient } from '../api-client.js';
import {
  formatStats,
  installStatusBar,
  newPhase,
  recordDelta,
  STDERR_IS_TTY,
  styleStats,
  type PhaseStats,
  type StatusBar,
} from './chat.js';

const STREAM_TIMEOUT_MS = 600_000;

export interface ChatLlmCommandDeps {
  client: ApiClient;
  baseUrl: string;
  token?: string | undefined;
  log: (...args: unknown[]) => void;
}

export function createChatLlmCommand(deps: ChatLlmCommandDeps): Command {
  return new Command('chat-llm')
    .description('Stateless chat with any registered LLM (public or virtual). No threads, no tools.')
    .argument('<name>', 'LLM name (see `mcpctl get llm`)')
    .option('-m, --message <text>', 'One-shot: send a single message and exit (no REPL)')
    .option('--system <text>', 'Optional system prompt')
    .option('--temperature <n>', 'Sampling temperature (0..2)', parseFloat)
    .option('--max-tokens <n>', 'Maximum tokens in the assistant reply', parseFloatInt)
    .option('--no-stream', 'Disable SSE streaming (single JSON response)')
    .option('--async', 'Enqueue as a durable inference task and print the task id (does not wait for completion). Virtual Llms only. Poll with `mcpctl get task <id>`.')
    .action(async (name: string, opts: ChatLlmOpts) => {
      if (opts.async === true) {
        await runAsync(deps, name, opts);
        return;
      }
      await printHeader(deps, name, opts.system);
      if (opts.message !== undefined) {
        await runOneShot(deps, name, opts);
        return;
      }
      await runRepl(deps, name, opts);
    });
}

interface ChatLlmOpts {
  message?: string;
  system?: string;
  temperature?: number;
  maxTokens?: number;
  stream?: boolean;
  async?: boolean;
}

/**
 * v5: enqueue a durable inference task and print the id. The caller
 * exits without waiting; they can poll with `mcpctl get task <id>` or
 * watch live with the SSE `/stream` endpoint. --async requires either
 * -m or stdin (REPL doesn't make sense in fire-and-forget mode).
 */
async function runAsync(deps: ChatLlmCommandDeps, name: string, opts: ChatLlmOpts): Promise<void> {
  const message = opts.message;
  if (message === undefined || message === '') {
    process.stderr.write('--async requires -m/--message (REPL mode is not supported for async tasks)\n');
    process.exitCode = 1;
    return;
  }
  const messages: Array<{ role: 'system' | 'user'; content: string }> = [];
  if (opts.system !== undefined && opts.system !== '') {
    messages.push({ role: 'system', content: opts.system });
  }
  messages.push({ role: 'user', content: message });
  const request: Record<string, unknown> = { messages };
  if (opts.temperature !== undefined) request.temperature = opts.temperature;
  if (opts.maxTokens !== undefined) request.max_tokens = opts.maxTokens;
  const created = await deps.client.post<{ id: string; status: string; poolName: string; llmName: string }>(
    '/api/v1/inference-tasks',
    { llmName: name, request, streaming: opts.stream !== false },
  );
  // stdout is JUST the id so it's pipeable into other commands.
  // Metadata goes to stderr so `mcpctl chat-llm X --async -m hi | xargs mcpctl describe task` works.
  process.stdout.write(`${created.id}\n`);
  process.stderr.write(`(task ${created.id} enqueued for pool '${created.poolName}', status=${created.status})\n`);
}

interface LlmInfo {
  name: string;
  type: string;
  model: string;
  kind: 'public' | 'virtual';
  status: 'active' | 'inactive' | 'hibernating';
}

async function printHeader(deps: ChatLlmCommandDeps, name: string, systemPrompt?: string): Promise<void> {
  let info: LlmInfo;
  try {
    info = await deps.client.get<LlmInfo>(`/api/v1/llms/${encodeURIComponent(name)}`);
  } catch (err) {
    process.stderr.write(`(could not fetch LLM metadata: ${(err as Error).message})\n`);
    return;
  }
  const sep = '─'.repeat(60);
  const out = (s: string): void => { process.stderr.write(`${styleStats(s)}\n`); };
  out(sep);
  out(`LLM: ${info.name}  ${info.type} → ${info.model}`);
  out(`Kind: ${info.kind}    Status: ${info.status}`);
  if (systemPrompt !== undefined) {
    out(`System: ${systemPrompt.slice(0, 120)}${systemPrompt.length > 120 ? '…' : ''}`);
  }
  out(sep);
}

async function runOneShot(deps: ChatLlmCommandDeps, name: string, opts: ChatLlmOpts): Promise<void> {
  const messages = buildMessages([], opts.system, opts.message ?? '');
  const bar = opts.stream === false ? null : installStatusBar();
  try {
    if (opts.stream === false) {
      const reply = await postNonStream(deps, name, messages, opts);
      process.stdout.write(`${reply}\n`);
    } else {
      await streamOnce(deps, name, messages, opts, bar);
    }
  } finally {
    bar?.teardown();
  }
}

async function runRepl(deps: ChatLlmCommandDeps, name: string, opts: ChatLlmOpts): Promise<void> {
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  const ask = (q: string): Promise<string> => new Promise((resolve) => rl.question(q, resolve));
  const history: Array<{ role: 'user' | 'assistant'; content: string }> = [];

  const bar = opts.stream === false ? null : installStatusBar();
  process.stderr.write(`Stateless chat with LLM '${name}'. Ctrl-D to exit.\n`);

  try {
    while (true) {
      let line: string;
      try { line = await ask('> '); } catch { break; }
      if (line === '') continue;

      const messages = buildMessages(history, opts.system, line);
      try {
        let reply: string;
        if (opts.stream === false) {
          reply = await postNonStream(deps, name, messages, opts);
          process.stdout.write(`${reply}\n`);
        } else {
          reply = await streamOnce(deps, name, messages, opts, bar);
          process.stdout.write('\n');
        }
        history.push({ role: 'user', content: line });
        history.push({ role: 'assistant', content: reply });
      } catch (err) {
        process.stderr.write(`error: ${(err as Error).message}\n`);
      }
    }
    rl.close();
  } finally {
    bar?.teardown();
  }
}

function buildMessages(
  history: Array<{ role: 'user' | 'assistant'; content: string }>,
  system: string | undefined,
  user: string,
): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
  const out: Array<{ role: 'system' | 'user' | 'assistant'; content: string }> = [];
  if (system !== undefined && system !== '') out.push({ role: 'system', content: system });
  out.push(...history);
  out.push({ role: 'user', content: user });
  return out;
}

async function postNonStream(
  deps: ChatLlmCommandDeps,
  name: string,
  messages: Array<{ role: string; content: string }>,
  opts: ChatLlmOpts,
): Promise<string> {
  const body: Record<string, unknown> = { messages };
  if (opts.temperature !== undefined) body['temperature'] = opts.temperature;
  if (opts.maxTokens !== undefined) body['max_tokens'] = opts.maxTokens;
  const res = await deps.client.post<{
    choices?: Array<{ message?: { content?: string } }>;
  }>(`/api/v1/llms/${encodeURIComponent(name)}/infer`, body);
  return res.choices?.[0]?.message?.content ?? '';
}

/**
 * Stream a single chat call against /api/v1/llms/:name/infer with stream=true.
 * The response is OpenAI-style SSE (`data: <chat.completion.chunk>`).
 * Returns the assembled assistant content.
 */
function streamOnce(
  deps: ChatLlmCommandDeps,
  name: string,
  messages: Array<{ role: string; content: string }>,
  opts: ChatLlmOpts,
  bar: StatusBar | null,
): Promise<string> {
  const url = new URL(`${deps.baseUrl}/api/v1/llms/${encodeURIComponent(name)}/infer`);
  const reqBody: Record<string, unknown> = { messages, stream: true };
  if (opts.temperature !== undefined) reqBody['temperature'] = opts.temperature;
  if (opts.maxTokens !== undefined) reqBody['max_tokens'] = opts.maxTokens;
  const payload = JSON.stringify(reqBody);
  const stats = { thinking: newPhase(), content: newPhase() } satisfies { thinking: PhaseStats; content: PhaseStats };

  const TICK_MS = 250;
  let timer: NodeJS.Timeout | null = null;
  function startTicker(): void {
    if (timer !== null || bar === null) return;
    timer = setInterval(() => bar.update(formatStats(stats, true)), TICK_MS);
  }
  function stopTicker(): void {
    if (timer !== null) { clearInterval(timer); timer = null; }
  }

  return new Promise<string>((resolve, reject) => {
    let assistant = '';
    const driver = url.protocol === 'https:' ? https : http;
    const req = driver.request({
      hostname: url.hostname,
      port: url.port || (url.protocol === 'https:' ? 443 : 80),
      path: url.pathname + url.search,
      method: 'POST',
      timeout: STREAM_TIMEOUT_MS,
      headers: {
        'Content-Type': 'application/json',
        Accept: 'text/event-stream',
        ...(deps.token !== undefined ? { Authorization: `Bearer ${deps.token}` } : {}),
      },
    }, (res) => {
      const status = res.statusCode ?? 0;
      if (status >= 400) {
        const chunks: Buffer[] = [];
        res.on('data', (c: Buffer) => chunks.push(c));
        res.on('end', () => reject(new Error(`HTTP ${String(status)}: ${Buffer.concat(chunks).toString('utf-8')}`)));
        return;
      }
      let buf = '';
      res.setEncoding('utf-8');
      res.on('data', (chunk: string) => {
        buf += chunk;
        let nl: number;
        while ((nl = buf.indexOf('\n\n')) !== -1) {
          const frame = buf.slice(0, nl);
          buf = buf.slice(nl + 2);
          for (const line of frame.split('\n')) {
            if (!line.startsWith('data: ')) continue;
            const data = line.slice(6);
            if (data === '[DONE]') continue;
            try {
              const parsed = JSON.parse(data) as { choices?: Array<{ delta?: { content?: string } }> };
              const piece = parsed.choices?.[0]?.delta?.content;
              if (typeof piece === 'string' && piece !== '') {
                recordDelta(stats.content, piece);
                process.stdout.write(piece);
                assistant += piece;
                startTicker();
              }
            } catch {
              // ignore malformed frames
            }
          }
        }
      });
      res.on('end', () => {
        stopTicker();
        const final = formatStats(stats, false);
        if (final !== '' && STDERR_IS_TTY) process.stderr.write(`\n${styleStats(`(${final})`)}`);
        else if (final !== '') process.stderr.write(`\n(${final})`);
        if (bar !== null && final !== '') bar.update(final);
        resolve(assistant);
      });
      res.on('error', (err) => { stopTicker(); reject(err); });
    });
    req.on('error', (err) => { stopTicker(); reject(err); });
    req.on('timeout', () => { stopTicker(); req.destroy(); reject(new Error('chat-llm stream timed out')); });
    req.write(payload);
    req.end();
  });
}

function parseFloatInt(value: string): number {
  const n = Number(value);
  if (!Number.isInteger(n)) throw new Error(`expected integer, got '${value}'`);
  return n;
}
