import { randomUUID } from 'node:crypto';
import { readFileSync, readdirSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { load as loadYaml } from 'js-yaml';
import type { Sql } from '../db.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

export interface CodeTask {
  id: string;
  prompt: string;
  test_code: string;
  expected_output: string;
  language: string;
}

export interface RubricCriterion {
  criterion: string;
  description: string;
  weight: number;
}

export interface ChatTask {
  id: string;
  prompt: string;
  prompt_template?: string;
  context_generator?: string;
  rubric: {
    criteria: RubricCriterion[];
    max_score: number;
  };
}

export interface EvalSuiteData {
  id: string;
  name: string;
  kind: 'chat' | 'code';
  version: number;
  description?: string;
  judge_model: string | null;
  tasks: (CodeTask | ChatTask)[];
}

export interface EvalSuiteRow {
  id: string;
  name: string;
  kind: string;
  version: number;
  tasks: string;
  judge_model: string | null;
  judge_model_version: string | null;
  metadata: string | null;
  created_at: string;
}

const DATA_DIR = resolve(dirname(__filename), '../../data');

/**
 * Load all eval suite YAML files from the data/ directory.
 */
export function loadEvalSuitesFromData(): EvalSuiteData[] {
  const suites: EvalSuiteData[] = [];
  try {
    const files = readdirSync(DATA_DIR).filter((f) => f.startsWith('suite-') && f.endsWith('.yaml'));
    for (const file of files) {
      const path = resolve(DATA_DIR, file);
      const content = readFileSync(path, 'utf8');
      const parsed = loadYaml(content) as Record<string, unknown>;
      const tasks = parsed.tasks as (CodeTask | ChatTask)[] | undefined;
      if (!tasks || !Array.isArray(tasks)) continue;

      const chatTasks: ChatTask[] = [];
      const codeTasks: CodeTask[] = [];

      for (const task of tasks) {
        const t = task as unknown as Record<string, unknown>;
        if (t.rubric) {
          const rubric = t.rubric as Record<string, unknown>;
          chatTasks.push({
            id: t.id as string,
            prompt: t.prompt as string,
            prompt_template: (t.prompt_template as string) ?? undefined,
            context_generator: (t.context_generator as string) ?? undefined,
            rubric: {
              criteria: normalizeCriteria(rubric),
              max_score: (rubric.max_score as number) ?? 7,
            },
          });
        } else if (t.test_code) {
          codeTasks.push({
            id: t.id as string,
            prompt: t.prompt as string,
            test_code: t.test_code as string,
            expected_output: t.expected_output as string,
            language: t.language as string,
          });
        }
      }

      suites.push({
        id: parsed.id as string,
        name: parsed.name as string,
        kind: parsed.kind as 'chat' | 'code',
        version: (parsed.version as number) ?? 1,
        description: (parsed.description as string) ?? undefined,
        judge_model: (parsed.judge_model as string) ?? null,
        tasks: [...codeTasks, ...chatTasks],
      });
    }
  } catch (err) {
    console.warn({ err: (err as Error).message }, 'eval: failed to load suites from data/');
  }
  return suites;
}

function normalizeCriteria(rubric: Record<string, unknown>): RubricCriterion[] {
  const criteria = rubric.criteria as RubricCriterion[] | undefined;
  if (criteria && Array.isArray(criteria)) {
    return criteria.filter((c) => c.criterion && c.weight);
  }
  const maxScore = rubric.max_score as number | undefined;
  const entries = Object.entries(rubric);
  const result: RubricCriterion[] = [];
  let totalWeight = 0;
  for (const [key, val] of entries) {
    if (key === 'max_score' || key === 'criteria') continue;
    const entry = val as { criterion?: string; description?: string; weight?: number };
    if (entry.weight && entry.description) {
      result.push({ criterion: key, description: entry.description, weight: entry.weight });
      totalWeight += entry.weight;
    }
  }
  if (result.length === 0) {
    for (const [key, val] of entries) {
      if (key === 'max_score' || key === 'criteria') continue;
      result.push({ criterion: key, description: String(val), weight: 1 });
    }
  }
  if (maxScore && totalWeight > 0) {
    const scale = maxScore / totalWeight;
    for (const c of result) {
      c.weight = Math.round(c.weight * scale * 10) / 10;
    }
  }
  return result;
}

/**
 * Seed eval suites from data/ YAML files into the database.
 * Uses INSERT ... ON CONFLICT DO NOTHING for idempotency.
 */
export async function seedEvalSuites(sql: Sql): Promise<void> {
  const suites = loadEvalSuitesFromData();
  for (const suite of suites) {
    await sql`
      INSERT INTO eval_suites (id, name, kind, version, tasks, judge_model, judge_model_version, metadata)
      VALUES (
        ${suite.id},
        ${suite.name},
        ${suite.kind},
        ${suite.version},
        ${sql.json(suite.tasks as never)},
        ${suite.judge_model},
        NULL,
        ${suite.description ? sql.json({ description: suite.description } as never) : sql`NULL::jsonb`}
      )
      ON CONFLICT (id) DO NOTHING
    `;
  }
}

/**
 * List all eval suites.
 */
export async function listEvalSuites(sql: Sql): Promise<EvalSuiteRow[]> {
  return await sql<EvalSuiteRow[]>`
    SELECT id, name, kind, version, tasks, judge_model, judge_model_version, metadata, created_at
    FROM eval_suites
    ORDER BY created_at DESC
  `;
}

/**
 * Get a single eval suite by ID.
 */
export async function getEvalSuite(sql: Sql, id: string): Promise<EvalSuiteRow | null> {
  const rows = await sql<EvalSuiteRow[]>`
    SELECT id, name, kind, version, tasks, judge_model, judge_model_version, metadata, created_at
    FROM eval_suites WHERE id = ${id}
  `;
  return rows[0] ?? null;
}

/**
 * Create or update an eval suite.
 */
export async function upsertEvalSuite(
  sql: Sql,
  id: string | null,
  name: string,
  kind: 'chat' | 'code',
  tasks: unknown[],
  judgeModel: string | null,
  metadata?: Record<string, unknown>,
): Promise<string> {
  const suiteId = id ?? randomUUID();
  const existing = await getEvalSuite(sql, suiteId);
  const version = existing ? existing.version + 1 : 1;

  await sql`
    INSERT INTO eval_suites (id, name, kind, version, tasks, judge_model, judge_model_version, metadata)
    VALUES (
      ${suiteId},
      ${name},
      ${kind},
      ${version},
      ${sql.json(tasks as never)},
      ${judgeModel},
      NULL,
      ${metadata ? sql.json(metadata as never) : sql`NULL::jsonb`}
    )
    ON CONFLICT (id) DO UPDATE SET
      name = EXCLUDED.name,
      kind = EXCLUDED.kind,
      version = EXCLUDED.version,
      tasks = EXCLUDED.tasks,
      judge_model = EXCLUDED.judge_model,
      metadata = EXCLUDED.metadata
  `;
  return suiteId;
}

/**
 * Create a new eval run record.
 */
export async function createEvalRun(
  sql: Sql,
  suiteId: string,
  providerId: string,
  model: string,
  quant: string | null,
  judgeModel: string | null,
  judgeModelVersion: string | null,
  totalTasks: number,
): Promise<string> {
  const runId = `eval_${Date.now()}_${randomUUID().slice(0, 8)}`;
  await sql`
    INSERT INTO eval_runs (id, suite_id, job_type, provider_id, model, quant, status, judge_model, judge_model_version, started_at, total_tasks)
    VALUES (
      ${runId}, ${suiteId}, 'eval', ${providerId}, ${model}, ${quant},
      'running', ${judgeModel}, ${judgeModelVersion},
      clock_timestamp(), ${totalTasks}
    )
  `;
  return runId;
}

/**
 * Record a single eval result.
 */
export async function recordEvalResult(
  sql: Sql,
  runId: string,
  taskId: string,
  taskIndex: number,
  score: number | null,
  maxScore: number | null,
  rationale: string | null,
  sandboxExitCode: number | null,
  sandboxStderr: string | null,
  sandboxStdout: string | null,
  executionMs: number | null,
  error: string | null,
): Promise<void> {
  await sql`
    INSERT INTO eval_results (run_id, task_id, task_index, score, max_score, rationale, sandbox_exit_code, sandbox_stderr, sandbox_stdout, execution_ms, error)
    VALUES (
      ${runId}, ${taskId}, ${taskIndex}, ${score}, ${maxScore},
      ${rationale}, ${sandboxExitCode}, ${sandboxStderr}, ${sandboxStdout},
      ${executionMs}, ${error}
    )
  `;
}

/**
 * Update eval run completion.
 */
export async function completeEvalRun(
  sql: Sql,
  runId: string,
  completedTasks: number,
  aggregate: Record<string, unknown> | null,
  error: string | null,
): Promise<void> {
  await sql`
    UPDATE eval_runs
    SET status = ${error ? 'failed' : 'completed'},
        finished_at = clock_timestamp(),
        completed_tasks = ${completedTasks},
        aggregate = ${aggregate ? sql.json(aggregate as never) : sql`NULL::jsonb`},
        error = ${error}
    WHERE id = ${runId}
  `;
}

/**
 * List eval runs with optional filters.
 */
export async function listEvalRuns(
  sql: Sql,
  suiteId?: string,
  providerId?: string,
): Promise<Array<{
  id: string;
  suite_id: string;
  job_type: string;
  provider_id: string;
  model: string;
  quant: string | null;
  status: string;
  judge_model: string | null;
  started_at: string | null;
  finished_at: string | null;
  total_tasks: number;
  completed_tasks: number;
  aggregate: string | null;
  error: string | null;
  created_at: string;
}>> {
  let query = sql<EvalSuiteRow[]>`
    SELECT id, suite_id, job_type, provider_id, model, quant, status, judge_model,
      started_at, finished_at, total_tasks, completed_tasks, aggregate, error, created_at
    FROM eval_runs
    WHERE 1=1
  `;

  if (suiteId) {
    query = sql`${query} AND suite_id = ${suiteId}`;
  }
  if (providerId) {
    query = sql`${query} AND provider_id = ${providerId}`;
  }

  query = sql`${query} ORDER BY created_at DESC LIMIT 200`;
  return query as unknown as Array<{
    id: string;
    suite_id: string;
    job_type: string;
    provider_id: string;
    model: string;
    quant: string | null;
    status: string;
    judge_model: string | null;
    started_at: string | null;
    finished_at: string | null;
    total_tasks: number;
    completed_tasks: number;
    aggregate: string | null;
    error: string | null;
    created_at: string;
  }>;
}

/**
 * Get eval results for a run.
 */
export async function getEvalResults(
  sql: Sql,
  runId: string,
): Promise<Array<{
  id: number;
  task_id: string;
  task_index: number;
  score: number | null;
  max_score: number | null;
  rationale: string | null;
  sandbox_exit_code: number | null;
  sandbox_stderr: string | null;
  sandbox_stdout: string | null;
  execution_ms: number | null;
  error: string | null;
}>> {
  return await sql<Array<{
    id: number;
    task_id: string;
    task_index: number;
    score: number | null;
    max_score: number | null;
    rationale: string | null;
    sandbox_exit_code: number | null;
    sandbox_stderr: string | null;
    sandbox_stdout: string | null;
    execution_ms: number | null;
    error: string | null;
  }>>`
    SELECT id, task_id, task_index, score, max_score, rationale,
      sandbox_exit_code, sandbox_stderr, sandbox_stdout, execution_ms, error
    FROM eval_results WHERE run_id = ${runId}
    ORDER BY task_index
  `;
}
