#!/usr/bin/env node
/**
 * BooCoder CLI client.
 *
 * Usage:
 *   boocode run "task description" [--agent opencode] [--model claude-opus-4-7] [--project <id>]
 *   boocode ls [--state pending|running|completed|failed]
 *   boocode attach <task-id>
 *   boocode send <task-id> "message"
 */
import { WebSocket } from 'ws';

const BASE_URL = process.env.BOOCODER_URL ?? 'http://100.114.205.53:9502';

function getFlag(args: string[], name: string): string | undefined {
  const idx = args.indexOf(name);
  if (idx === -1 || idx + 1 >= args.length) return undefined;
  return args[idx + 1];
}


async function api(method: string, path: string, body?: unknown): Promise<unknown> {
  const url = `${BASE_URL}${path}`;
  const res = await fetch(url, {
    method,
    headers: body ? { 'Content-Type': 'application/json' } : undefined,
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(`${method} ${path} → ${res.status}: ${text}`);
  }
  return res.json();
}

function streamSession(sessionId: string): void {
  const wsUrl = BASE_URL.replace(/^http/, 'ws') + `/api/ws/sessions/${sessionId}`;
  const ws = new WebSocket(wsUrl);

  ws.on('message', (data) => {
    try {
      const frame = JSON.parse(data.toString()) as { type: string; content?: string; name?: string; arguments?: string };
      if (frame.type === 'delta' && frame.content) {
        process.stdout.write(frame.content);
      } else if (frame.type === 'tool_call') {
        process.stdout.write(`\n[tool: ${frame.name ?? '?'}(${(frame.arguments ?? '').slice(0, 80)})]\n`);
      } else if (frame.type === 'tool_result') {
        process.stdout.write(`[tool_result]\n`);
      } else if (frame.type === 'status' || frame.type === 'chat_status') {
        // Silent
      }
    } catch {
      // Non-JSON frame, ignore
    }
  });

  ws.on('error', (err) => {
    process.stderr.write(`WS error: ${err.message}\n`);
  });

  ws.on('close', () => {
    process.stdout.write('\n');
    process.exit(0);
  });

  process.on('SIGINT', () => {
    ws.close();
    process.exit(0);
  });
}

async function cmdRun(args: string[]): Promise<void> {
  const input = args.find((a) => !a.startsWith('--'));
  if (!input) {
    process.stderr.write('Usage: boocode run "task description" [--agent X] [--model X] [--project X]\n');
    process.exit(1);
  }

  const agent = getFlag(args, '--agent');
  const model = getFlag(args, '--model');
  const project_id = getFlag(args, '--project');

  if (!project_id) {
    process.stderr.write('Error: --project <uuid> is required\n');
    process.exit(1);
  }

  const result = (await api('POST', '/api/tasks', {
    project_id,
    input,
    ...(agent && { agent }),
    ...(model && { model }),
  })) as { id: string; state: string };

  process.stdout.write(`Task created: ${result.id} (state: ${result.state})\n`);

  // Poll until task has session_id, then stream; or poll until terminal state
  const POLL_MS = 2000;
  for (;;) {
    await sleep(POLL_MS);
    const task = (await api('GET', `/api/tasks/${result.id}`)) as {
      id: string; state: string; session_id?: string; output_summary?: string;
    };

    if (task.session_id) {
      process.stdout.write(`Streaming session ${task.session_id}...\n`);
      streamSession(task.session_id);
      return; // streamSession handles exit
    }

    if (task.state === 'completed') {
      process.stdout.write(`\nCompleted: ${task.output_summary ?? '(no summary)'}\n`);
      return;
    }
    if (task.state === 'failed') {
      process.stderr.write(`\nFailed: ${task.output_summary ?? '(no summary)'}\n`);
      process.exit(1);
    }
    if (task.state === 'cancelled') {
      process.stderr.write(`\nCancelled.\n`);
      process.exit(1);
    }
  }
}

async function cmdLs(args: string[]): Promise<void> {
  const state = getFlag(args, '--state');
  const query = state ? `?state=${state}` : '';
  const tasks = (await api('GET', `/api/tasks${query}`)) as Array<{
    id: string; state: string; agent: string | null; input: string; created_at: string;
  }>;

  if (tasks.length === 0) {
    process.stdout.write('No tasks.\n');
    return;
  }

  // Table header
  process.stdout.write(
    pad('ID', 38) + pad('STATE', 12) + pad('AGENT', 14) + pad('INPUT', 52) + 'CREATED\n',
  );
  process.stdout.write('-'.repeat(120) + '\n');

  for (const t of tasks) {
    process.stdout.write(
      pad(t.id, 38) +
      pad(t.state, 12) +
      pad(t.agent ?? '-', 14) +
      pad(t.input.slice(0, 50), 52) +
      (t.created_at?.slice(0, 19) ?? '') + '\n',
    );
  }
}

async function cmdAttach(args: string[]): Promise<void> {
  const taskId = args[0];
  if (!taskId) {
    process.stderr.write('Usage: boocode attach <task-id>\n');
    process.exit(1);
  }

  const task = (await api('GET', `/api/tasks/${taskId}`)) as { session_id?: string };
  if (!task.session_id) {
    process.stderr.write('Task has no session yet (still pending?).\n');
    process.exit(1);
  }

  streamSession(task.session_id);
}

async function cmdSend(args: string[]): Promise<void> {
  const taskId = args[0];
  const message = args[1];
  if (!taskId || !message) {
    process.stderr.write('Usage: boocode send <task-id> "message"\n');
    process.exit(1);
  }

  const task = (await api('GET', `/api/tasks/${taskId}`)) as { session_id?: string };
  if (!task.session_id) {
    process.stderr.write('Task has no session yet.\n');
    process.exit(1);
  }

  // Find active chat
  const sessionId = task.session_id;
  // POST message to the session's chat (the messages route expects session_id in path)
  await api('POST', `/api/sessions/${sessionId}/messages`, { content: message });

  // Then attach to stream the response
  streamSession(sessionId);
}

import { sleep } from './lib/async.js';

function pad(s: string, width: number): string {
  return s.length >= width ? s.slice(0, width) : s + ' '.repeat(width - s.length);
}

const [cmd, ...rest] = process.argv.slice(2);

switch (cmd) {
  case 'run':
    cmdRun(rest).catch(fatal);
    break;
  case 'ls':
    cmdLs(rest).catch(fatal);
    break;
  case 'attach':
    cmdAttach(rest).catch(fatal);
    break;
  case 'send':
    cmdSend(rest).catch(fatal);
    break;
  default:
    process.stdout.write(
      'BooCoder CLI\n\n' +
      'Commands:\n' +
      '  run "task"  [--agent X] [--model X] [--project <id>]   Create and stream a task\n' +
      '  ls          [--state pending|running|completed|failed]   List tasks\n' +
      '  attach      <task-id>                                    Stream a running task\n' +
      '  send        <task-id> "message"                          Send input to a task\n' +
      '\n' +
      `Base URL: ${BASE_URL} (set BOOCODER_URL to override)\n`,
    );
    if (cmd && cmd !== '--help' && cmd !== '-h') process.exit(1);
}

function fatal(err: unknown): void {
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
  process.exit(1);
}
