import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import type { Sql } from '../db.js';
import type { DeltaEmitter } from '../services/delta-emitter.js';
import { publishJob } from '../services/publish-job.js';
import {
  listEvalSuites,
  getEvalSuite,
  upsertEvalSuite,
  listEvalRuns,
  getEvalResults,
  seedEvalSuites,
} from '../services/eval-suites.js';
import { jsonbArray, jsonbObject } from '../services/jsonb.js';
import { acquireHostAccess } from '../services/host-access.js';
import type { AppContext } from '../app-context.js';

/**
 * Register eval routes.
 *
 * POST /api/eval/suite        — create/update an eval suite
 * GET  /api/eval/suites       — list suites
 * GET  /api/eval/suites/:id   — get suite
 * POST /api/eval/seed         — seed suites from data/ YAML
 * POST /api/eval/run          — start an eval run
 * GET  /api/eval/runs         — list runs
 * GET  /api/eval/runs/:id     — get run + results
 * GET  /api/eval/leaderboard  — per (provider_id, model) aggregate scores
 */
export function registerEvalRoutes(
  app: FastifyInstance,
  ctx: AppContext,
): void {
  const { sql, fleet, emitter } = ctx;
  // Seed suites from data/ YAML on startup (idempotent).
  app.addHook('onReady', async () => {
    await seedEvalSuites(sql).catch((err) => {
      app.log.warn({ err: (err as Error).message }, 'eval: seed failed');
    });
  });

  app.post('/api/eval/suite', async (req: FastifyRequest, reply: FastifyReply) => {
    const body = req.body as Record<string, unknown>;
    const id = (body.id as string) ?? null;
    const name = body.name as string;
    const kind = body.kind as 'chat' | 'code';
    const tasks = body.tasks as unknown[];
    const judgeModel = (body.judgeModel as string) ?? null;
    const metadata = body.metadata as Record<string, unknown> | undefined;

    if (!name || !kind || !tasks?.length) {
      return reply.status(400).send({ error: 'name, kind, and tasks are required' });
    }

    const suiteId = await upsertEvalSuite(sql, id, name, kind, tasks, judgeModel, metadata);
    return reply.status(201).send({ id: suiteId });
  });

  app.get('/api/eval/suites', async (_req: FastifyRequest, reply: FastifyReply) => {
    const suites = await listEvalSuites(sql);
    return reply.send({
      suites: suites.map((s) => ({
        id: s.id,
        name: s.name,
        kind: s.kind,
        version: s.version,
        tasks: jsonbArray(s.tasks),
        judgeModel: s.judge_model,
        judgeModelVersion: s.judge_model_version,
        metadata: jsonbObject(s.metadata) ?? undefined,
        createdAt: s.created_at,
      })),
    });
  });

  app.get('/api/eval/suites/:id', async (req: FastifyRequest, reply: FastifyReply) => {
    const { id } = req.params as { id: string };
    const suite = await getEvalSuite(sql, id);
    if (!suite) {
      return reply.status(404).send({ error: 'suite not found' });
    }
    return reply.send({
      id: suite.id,
      name: suite.name,
      kind: suite.kind,
      version: suite.version,
      tasks: jsonbArray(suite.tasks),
      judgeModel: suite.judge_model,
      judgeModelVersion: suite.judge_model_version,
      metadata: jsonbObject(suite.metadata) ?? undefined,
      createdAt: suite.created_at,
    });
  });

  app.post('/api/eval/seed', async (_req: FastifyRequest, reply: FastifyReply) => {
    await seedEvalSuites(sql);
    return reply.send({ ok: true });
  });

  app.post('/api/eval/run', async (req: FastifyRequest, reply: FastifyReply) => {
    const body = req.body as Record<string, unknown>;
    const suiteId = body.suiteId as string;
    const providerId = body.providerId as string;
    const model = body.model as string;
    const quant = (body.quant as string) ?? null;

    if (!suiteId || !providerId || !model) {
      return reply.status(400).send({ error: 'suiteId, providerId, and model are required' });
    }

    const suite = await getEvalSuite(sql, suiteId);
    if (!suite) {
      return reply.status(404).send({ error: 'suite not found' });
    }

    const grant = await acquireHostAccess(providerId, 'eval');
    if (!grant.ok) {
      return reply.status(409).send({ error: 'host access denied', reason: grant.reason });
    }

    const tasks = jsonbArray(suite.tasks);
    const judgeModel = suite.judge_model;
    const seq = fleet.hosts.get(providerId)?.seq ?? 0;

    void runEvalAsync(
      { suiteId, providerId, model, quant, tasks, judgeModel },
      sql,
      emitter,
      seq,
      app.log,
    );

    return reply.status(202).send({ status: 'queued', suiteId, providerId, model });
  });

  app.get('/api/eval/runs', async (req: FastifyRequest, reply: FastifyReply) => {
    const query = req.query as Record<string, string | undefined>;
    const runs = await listEvalRuns(sql, query.suiteId, query.providerId);
    return reply.send({
      runs: runs.map((r) => ({
        id: r.id,
        suiteId: r.suite_id,
        jobType: r.job_type,
        providerId: r.provider_id,
        model: r.model,
        quant: r.quant,
        status: r.status,
        judgeModel: r.judge_model,
        startedAt: r.started_at,
        finishedAt: r.finished_at,
        totalTasks: r.total_tasks,
        completedTasks: r.completed_tasks,
        aggregate: jsonbObject(r.aggregate),
        error: r.error,
        createdAt: r.created_at,
      })),
    });
  });

  app.get('/api/eval/runs/:id', async (req: FastifyRequest, reply: FastifyReply) => {
    const { id } = req.params as { id: string };
    const runs = await listEvalRuns(sql);
    const run = runs.find((r) => r.id === id);
    if (!run) {
      return reply.status(404).send({ error: 'run not found' });
    }

    const results = await getEvalResults(sql, id);

    return reply.send({
      run: {
        id: run.id,
        suiteId: run.suite_id,
        jobType: run.job_type,
        providerId: run.provider_id,
        model: run.model,
        quant: run.quant,
        status: run.status,
        judgeModel: run.judge_model,
        startedAt: run.started_at,
        finishedAt: run.finished_at,
        totalTasks: run.total_tasks,
        completedTasks: run.completed_tasks,
        aggregate: jsonbObject(run.aggregate),
        error: run.error,
        createdAt: run.created_at,
      },
      results: results.map((r) => ({
        id: r.id,
        taskId: r.task_id,
        taskIndex: r.task_index,
        score: r.score,
        maxScore: r.max_score,
        rationale: r.rationale,
        sandboxExitCode: r.sandbox_exit_code,
        sandboxStderr: r.sandbox_stderr,
        sandboxStdout: r.sandbox_stdout,
        executionMs: r.execution_ms,
        error: r.error,
      })),
    });
  });

  app.get('/api/eval/leaderboard', async (req: FastifyRequest, reply: FastifyReply) => {
    const query = req.query as Record<string, string | undefined>;
    const kind = query.kind as 'chat' | 'code' | undefined;

    // Aggregate scores per (provider_id, model) from completed eval_runs.
    const rows = await sql<{
      provider_id: string;
      model: string;
      quant: string | null;
      suite_kind: string;
      avg_score: number;
      run_count: number;
      latest_run_at: string;
    }[]>`
      SELECT
        er.provider_id,
        er.model,
        er.quant,
        es.kind AS suite_kind,
        AVG(CASE WHEN er.aggregate IS NOT NULL THEN (er.aggregate::jsonb ->> 'avgScore')::float ELSE NULL END) AS avg_score,
        COUNT(DISTINCT er.id) AS run_count,
        MAX(er.finished_at) AS latest_run_at
      FROM eval_runs er
      JOIN eval_suites es ON er.suite_id = es.id
      WHERE er.status = 'completed'
        ${kind ? sql`AND es.kind = ${kind}` : sql`AND 1=1`}
      GROUP BY er.provider_id, er.model, er.quant, es.kind
      ORDER BY avg_score DESC NULLS LAST
    `;

    return reply.send({
      leaderboard: rows.map((r) => ({
        providerId: r.provider_id,
        model: r.model,
        quant: r.quant,
        suiteKind: r.suite_kind,
        avgScore: r.avg_score,
        runCount: r.run_count,
        latestRunAt: r.latest_run_at,
      })),
    });
  });
}

/**
 * Async eval runner: fire-and-forget.
 * Delegates to judge runner (chat) or sandbox runner (code).
 */
async function runEvalAsync(
  params: {
    suiteId: string;
    providerId: string;
    model: string;
    quant: string | null;
    tasks: unknown[];
    judgeModel: string | null;
  },
  sql: Sql,
  emitter: DeltaEmitter,
  seq: number,
  logger: import('fastify').FastifyBaseLogger,
): Promise<void> {
  const { suiteId, providerId, model, quant, tasks, judgeModel } = params;
  const runId = `eval_${Date.now()}_${crypto.randomUUID().slice(0, 8)}`;

  try {
    await sql`
      INSERT INTO eval_runs (id, suite_id, job_type, provider_id, model, quant, status, judge_model, started_at, total_tasks)
      VALUES (${runId}, ${suiteId}, 'eval', ${providerId}, ${model}, ${quant}, 'running', ${judgeModel}, clock_timestamp(), ${tasks.length})
    `;

    publishJob(emitter, {
      seq,
      jobType: 'eval',
      jobId: runId,
      status: 'running',
      detail: { suiteId, providerId, model, totalTasks: tasks.length },
    });

    // Import runners dynamically to avoid circular deps.
    const suiteKind = tasks[0] as Record<string, unknown>;
    const isCodeSuite = !!(suiteKind && suiteKind.test_code);

    let completed = 0;
    let error: string | null = null;

    if (isCodeSuite) {
      const { runCodeEval } = await import('../services/sandbox-runner.js');
      const result = await runCodeEval(
        { runId, providerId, model, tasks: tasks as Array<Record<string, unknown>>, quant },
        sql,
        emitter,
        seq,
        (progress) => {
          completed = progress.completedTasks;
        },
      );
      if (result.error) error = result.error;
    } else {
      const { runJudgeEval } = await import('../services/judge-runner.js');
      const result = await runJudgeEval(
        { runId, providerId, model, tasks: tasks as Array<Record<string, unknown>>, judgeModel, quant },
        sql,
        emitter,
        seq,
        logger,
        (progress) => {
          completed = progress.completedTasks;
        },
      );
      if (result.error) error = result.error;
    }

    // Compute aggregate.
    const results = await sql<{ score: number | null; max_score: number | null }[]>`
      SELECT score, max_score FROM eval_results WHERE run_id = ${runId}
    `;
    const scores = results.map((r) => r.score).filter((s): s is number => s != null);
    const avgScore = scores.length ? scores.reduce((a, b) => a + b, 0) / scores.length : null;

    await sql`
      UPDATE eval_runs
      SET status = ${error ? 'failed' : 'completed'},
          finished_at = clock_timestamp(),
          completed_tasks = ${completed},
          aggregate = ${avgScore != null ? sql.json({ avgScore, totalTasks: tasks.length, passedTasks: scores.filter((s, i) => { const m = results[i]?.max_score; return m ? s / m >= 0.7 : s != null; }).length } as never) : sql`NULL::jsonb`},
          error = ${error}
      WHERE id = ${runId}
    `;

    publishJob(emitter, {
      seq,
      jobType: 'eval',
      jobId: runId,
      status: error ? 'failed' : 'completed',
      detail: { avgScore, error },
    });
  } catch (err) {
    const msg = (err as Error).message ?? String(err);
    logger.error({ err: msg }, 'eval: run failed');

    await sql`
      UPDATE eval_runs
      SET status = 'failed', finished_at = clock_timestamp(), error = ${msg}
      WHERE id = ${runId}
    `.catch(() => {});

    publishJob(emitter, {
      seq,
      jobType: 'eval',
      jobId: runId,
      status: 'failed',
      detail: { error: msg },
    });
  }
}
