import * as fs from 'fs';
import * as path from 'path';
import { logger } from '../logger.js';
import type { BenchTask, RawJobResult, ToolCallObservation } from './types.js';

const TERMINAL_STATUSES = new Set(['succeeded', 'failed', 'aborted', 'cancelled', 'waiting_human']);

export interface RunnerOptions {
  serverUrl: string;
  benchRoot: string;
  pollIntervalMs?: number;
  /** Substituted into prompt {WEB_PORT}. */
  webPort: number;
}

interface CreateTaskResponse {
  task: { id: number; workspacePath: string | null };
  jobId: string;
}

interface TaskDetailResponse {
  task: {
    id: number;
    workspacePath: string | null;
    latestJob?: {
      id: string;
      status: string;
      contextPromptTokens: number | null;
      contextLimitTokens: number | null;
    } | null;
  };
}

function expandPromptTokens(prompt: string, tokens: Record<string, string>): string {
  return prompt.replace(/\{(\w+)\}/g, (_, key) => tokens[key] ?? `{${key}}`);
}

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

async function getJson(url: string): Promise<unknown> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`GET ${url} → ${res.status}: ${(await res.text()).slice(0, 400)}`);
  return res.json();
}

function buildAttachments(task: BenchTask, benchRoot: string): Array<{ name: string; contentBase64: string }> {
  const attachments: Array<{ name: string; contentBase64: string }> = [];
  for (const fx of task.fixtures ?? []) {
    if (!fx.dest.startsWith('input/')) continue;  // web/ fixtures are served by HTTP server, not uploaded
    const sourcePath = path.resolve(benchRoot, fx.source);
    if (!fs.existsSync(sourcePath)) {
      throw new Error(`Fixture source missing: ${sourcePath}`);
    }
    const name = fx.dest.slice('input/'.length);
    attachments.push({ name, contentBase64: fs.readFileSync(sourcePath).toString('base64') });
  }
  return attachments;
}

function parseToolCalls(activityLog: string): ToolCallObservation[] {
  const out: ToolCallObservation[] = [];
  // Format from summarizeToolInput: "[time] [worker:..] [mode:..] ToolName: arg-summary"
  // tool line shape after metadata strip: "ToolName: <input summary>"
  const lines = activityLog.split('\n').map((l) => l.trim()).filter(Boolean);
  for (const raw of lines) {
    const stripped = raw
      .replace(/^\[[^\]]+\]\s+/, '')
      .replace(/\[worker:[^\]]+\]\s*/g, '')
      .replace(/\[mode:[^\]]+\]\s*/g, '')
      .trim();
    // Skip non-tool lines (preflight, [movement] start/complete/preview, final, ask, context-action)
    if (
      stripped.startsWith('preflight:') ||
      stripped.startsWith('[llm-preflight:') ||
      stripped.startsWith('context-action:') ||
      stripped.startsWith('final:') ||
      stripped.startsWith('ask:') ||
      stripped.startsWith('[')
    ) {
      continue;
    }
    const m = /^([A-Z][A-Za-z0-9_]+):\s*(.+)$/.exec(stripped);
    if (!m) continue;
    const name = m[1]!;
    const inputSummary = m[2]!;
    // Heuristic: extract first quoted/unquoted "input/..." or "output/..." as filePath
    const filePath = /(input|output)\/[\w\-./ ]+/.exec(inputSummary)?.[0];
    out.push({ name, inputSummary, filePath });
  }
  return out;
}

function listOutputFiles(workspacePath: string): Record<string, string> {
  const outDir = path.join(workspacePath, 'output');
  if (!fs.existsSync(outDir)) return {};
  const result: Record<string, string> = {};
  function walk(dir: string, prefix: string): void {
    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
      const abs = path.join(dir, entry.name);
      const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
      if (entry.isDirectory()) {
        walk(abs, rel);
        continue;
      }
      try {
        const buf = fs.readFileSync(abs);
        // Only return small text files (skip binary/huge)
        if (buf.length > 200_000 || buf.includes(0)) continue;
        result[rel] = buf.toString('utf-8');
      } catch {
        /* skip unreadable */
      }
    }
  }
  walk(outDir, '');
  return result;
}

export async function runOneTask(task: BenchTask, opts: RunnerOptions): Promise<RawJobResult> {
  const startedAt = Date.now();
  const promptTokens: Record<string, string> = {
    WEB_PORT: String(opts.webPort),
    ...(task.prompt_tokens ?? {}),
  };
  const expandedPrompt = expandPromptTokens(task.prompt, promptTokens);
  const attachments = buildAttachments(task, opts.benchRoot);

  logger.info(`[bench/runner] submitting task=${task.id} attachments=${attachments.length}`);
  const create = (await postJson(`${opts.serverUrl}/api/local/tasks`, {
    title: `[bench] ${task.title}`,
    body: expandedPrompt,
    piece: task.piece_hint ?? 'chat',
    profile: 'auto',
    outputFormat: 'markdown',
    askPolicy: 'low',
    priority: 'medium',
    visibility: 'private',
    attachments,
  })) as CreateTaskResponse;
  const taskId = create.task.id;
  const jobId = create.jobId;
  logger.info(`[bench/runner] created task=${task.id} taskId=${taskId} jobId=${jobId}`);

  const timeoutMs = (task.timeout_minutes ?? 10) * 60_000;
  const pollMs = opts.pollIntervalMs ?? 3_000;
  let detail: TaskDetailResponse['task'] | null = null;
  while (Date.now() - startedAt < timeoutMs) {
    await new Promise((r) => setTimeout(r, pollMs));
    try {
      const got = (await getJson(`${opts.serverUrl}/api/local/tasks/${taskId}`)) as TaskDetailResponse;
      detail = got.task;
      const status = detail.latestJob?.status ?? 'queued';
      logger.info(`[bench/runner] task=${task.id} status=${status}`);
      if (TERMINAL_STATUSES.has(status)) break;
    } catch (err) {
      logger.warn(`[bench/runner] poll error: ${(err as Error).message}`);
    }
  }
  if (!detail) throw new Error(`Task ${taskId} never reported state`);
  if (!detail.latestJob) throw new Error(`Task ${taskId} has no latestJob`);

  const workspacePath = detail.workspacePath ?? '';
  const activityLogPath = path.join(workspacePath, 'logs', 'activity.log');
  const activityLog = fs.existsSync(activityLogPath) ? fs.readFileSync(activityLogPath, 'utf-8') : '';
  const toolCalls = parseToolCalls(activityLog);
  const outputFiles = listOutputFiles(workspacePath);

  return {
    taskId,
    jobId: detail.latestJob.id,
    status: detail.latestJob.status,
    iterations: null,
    promptTokens: detail.latestJob.contextPromptTokens,
    completionTokens: null,
    workspacePath,
    activityLog,
    toolCalls,
    outputFiles,
    durationMs: Date.now() - startedAt,
  };
}
