/**
 * Worker dispatch. Each call runs ONE Han persona as a bounded single-task
 * worker against a local model via `opencode run`. The conductor (flow.ts)
 * owns all sequencing — this layer only knows how to run one agent and return
 * its text. That separation is the whole point: code conducts, the model works.
 */
import { spawn } from 'node:child_process';
import { readFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const HERE = dirname(fileURLToPath(import.meta.url));
const AGENTS_DIR = join(HERE, '..', 'agents');

const OPENCODE_BIN = process.env.CONDUCTOR_OPENCODE_BIN ?? '/home/samkintop/.opencode/bin/opencode';
const MODEL = process.env.CONDUCTOR_MODEL ?? 'llama-swap/qwen3.6-35b-a3b-mxfp4';
// Local web-research personas on a 35B model are slow (many WebFetch round-trips
// + a long structured report) — a strict research-analyst pass routinely runs
// 10+ minutes. Default generously; override per-deployment via env.
const DEFAULT_TIMEOUT_MS = Number(process.env.CONDUCTOR_TIMEOUT_MS ?? 1_500_000);

/** Load a Han agent persona — the markdown body after the YAML frontmatter. */
export async function loadPersona(agent: string): Promise<string> {
  const md = await readFile(join(AGENTS_DIR, `${agent}.md`), 'utf8');
  return md.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '').trim();
}

/**
 * Dispatch one bounded worker. The persona is baked into the prompt and the
 * model is told it is a single-shot worker, so it does exactly one task and
 * returns — it is never asked to spawn or sequence other agents.
 */
export async function dispatchAgent(
  agent: string,
  task: string,
  opts: { timeoutMs?: number } = {},
): Promise<string> {
  const persona = await loadPersona(agent);
  const prompt = [
    persona,
    '',
    '----- YOUR TASK -----',
    'You are operating as a single-shot worker dispatched by an external conductor.',
    'Do ONLY the task below and return your result as your final message.',
    'Do not ask clarifying questions; make reasonable assumptions and state them.',
    '',
    task,
  ].join('\n');
  return runOpencode(prompt, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
}

function runOpencode(prompt: string, timeoutMs: number): Promise<string> {
  return new Promise((resolve, reject) => {
    const child = spawn(OPENCODE_BIN, ['run', '--model', MODEL, prompt], {
      env: { ...process.env },
      stdio: ['ignore', 'pipe', 'pipe'],
    });
    let out = '';
    let err = '';
    const timer = setTimeout(() => {
      child.kill('SIGKILL');
      reject(new Error(`opencode run timed out after ${timeoutMs}ms`));
    }, timeoutMs);
    child.stdout.on('data', (d: Buffer) => (out += d.toString()));
    child.stderr.on('data', (d: Buffer) => (err += d.toString()));
    child.on('error', reject);
    child.on('close', (code: number | null) => {
      clearTimeout(timer);
      if (code !== 0) {
        reject(new Error(`opencode run exited ${code}: ${cleanOutput(err).slice(-600)}`));
        return;
      }
      resolve(cleanOutput(out));
    });
  });
}

/** Strip ANSI, the `> build · model` banner, and tool-progress marker lines,
 *  leaving the worker's actual output. */
export function cleanOutput(raw: string): string {
  return raw
    .replace(/\x1b\[[0-9;]*m/g, '')
    .split('\n')
    .filter((line) => {
      const t = line.trim();
      if (/^>\s.+·/.test(t)) return false; // "> build · qwen…" banner
      if (/^[•✓↻⠀|]/.test(t)) return false; // tool-progress markers
      return true;
    })
    .join('\n')
    .trim();
}
