import type { Sql } from '../db.js';
import type { DeltaEmitter } from './delta-emitter.js';
import { publishJob } from './publish-job.js';
import { recordEvalResult, completeEvalRun } from './eval-suites.js';
import { resolveProviderBaseUrl } from './llama-providers.js';

export interface JudgeEvalParams {
  runId: string;
  providerId: string;
  model: string;
  quant: string | null;
  tasks: Array<Record<string, unknown>>;
  judgeModel: string | null;
}

export interface JudgeProgress {
  completedTasks: number;
}

export interface JudgeResult {
  error: string | null;
}

/**
 * Run a judge-based eval (chat quality, rubric scoring).
 *
 * Judge requests go through llama-swap with:
 * - temperature 0
 * - judge model + version pinned per run
 * - X-Boo-Source: control-eval
 * - BARE wire model id
 *
 * Rubric scoring: each criterion gets a score, weighted average produces the task score.
 * Rationale is captured per criterion.
 */
export async function runJudgeEval(
  params: JudgeEvalParams,
  sql: Sql,
  emitter: DeltaEmitter,
  seq: number,
  logger: import('fastify').FastifyBaseLogger,
  onProgress: (progress: JudgeProgress) => void,
): Promise<JudgeResult> {
  const { runId, providerId, model, tasks, judgeModel, quant: _quant } = params;

  // Resolve the target model's base URL.
  const baseUrl = resolveProviderBaseUrl(providerId);
  if (!baseUrl) {
    const err = `no base URL for provider ${providerId}`;
    await completeEvalRun(sql, runId, 0, null, err).catch(() => {});
    return { error: err };
  }

  // Determine judge model: suite default -> strongest local model.
  const judgeModelId = judgeModel ?? resolveDefaultJudgeModel();
  const judgeModelVersion = `${judgeModelId}@${Date.now()}`;

  logger.info(
    { runId, judgeModel: judgeModelId, targetModel: model, taskCount: tasks.length },
    'eval: judge run started',
  );

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

  for (let i = 0; i < tasks.length; i++) {
    const task = tasks[i];
    if (!task) continue;
    const taskId = (task.id as string) ?? `task_${i}`;
    const prompt = (task.prompt as string) ?? '';
    const rubric = (task.rubric as { criteria: Array<{ criterion: string; description: string; weight: number }>; max_score: number }) ?? null;

    const startTime = Date.now();

    try {
      // Generate the response from the target model.
      const response = await generateResponse(baseUrl, model, prompt);

      // Score the response.
      let score: number | null = null;
      let maxScore: number | null = null;
      let rationale: string | null = null;

      if (rubric) {
        const scoring = await scoreWithRubric(
          baseUrl,
          judgeModelId,
          prompt,
          response,
          rubric,
        );
        score = scoring.score;
        maxScore = scoring.maxScore;
        rationale = scoring.rationale;
      } else {
        // Simple pass/fail for tasks without rubric.
        score = response.trim().length > 0 ? 1 : 0;
        maxScore = 1;
        rationale = response.trim().length > 0 ? 'Response generated' : 'Empty response';
      }

      const executionMs = Date.now() - startTime;

      await recordEvalResult(
        sql,
        runId,
        taskId,
        i,
        score,
        maxScore,
        rationale,
        null,
        null,
        null,
        executionMs,
        null,
      );

      completedTasks++;
      onProgress({ completedTasks });

      publishJob(emitter, {
        seq,
        jobType: 'eval',
        jobId: runId,
        status: 'running',
        detail: {
          completedTasks,
          totalTasks: tasks.length,
          taskId,
          score,
        },
      });
    } catch (err) {
      const msg = (err as Error).message ?? String(err);
      logger.warn({ taskId, err: msg }, 'eval: judge task failed');

      await recordEvalResult(
        sql,
        runId,
        taskId,
        i,
        null,
        null,
        null,
        null,
        null,
        null,
        Date.now() - startTime,
        msg,
      ).catch(() => {});

      completedTasks++;
      onProgress({ completedTasks });
    }
  }

  return { error };
}

/**
 * Generate a response from the target model through llama-swap.
 */
async function generateResponse(
  baseUrl: string,
  model: string,
  prompt: string,
): Promise<string> {
  const res = await fetch(`${baseUrl}/v1/chat/completions`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Boo-Source': 'control-eval',
    },
    body: JSON.stringify({
      model,
      messages: [{ role: 'user', content: prompt }],
      // Design S8: temperature 0 everywhere in the eval pipeline -- response
      // generation must be as reproducible as the judging (audit B1).
      temperature: 0,
      max_tokens: 2048,
    }),
    signal: AbortSignal.timeout(120_000),
  });

  if (!res.ok) {
    const body = await res.text().catch(() => '');
    throw new Error(`model response failed: ${res.status} ${body.slice(0, 200)}`);
  }

  const data = await res.json() as { choices?: Array<{ message?: { content?: string } }> };
  return data.choices?.[0]?.message?.content ?? '';
}

/**
 * Score a response using a rubric via LLM-as-judge.
 */
async function scoreWithRubric(
  baseUrl: string,
  judgeModelId: string,
  prompt: string,
  response: string,
  rubric: { criteria: Array<{ criterion: string; description: string; weight: number }>; max_score: number },
): Promise<{ score: number; maxScore: number; rationale: string }> {
  const criteriaText = rubric.criteria
    .map((c, i) => `${i + 1}. **${c.criterion}** (weight: ${c.weight}): ${c.description}`)
    .join('\n');

  const judgePrompt = `You are an evaluation judge. Score the following response against the given prompt using the rubric criteria.

**Prompt:**
${prompt}

**Response:**
${response}

**Rubric Criteria (score each 0-3, then compute weighted average):**
${criteriaText}

**Max Score:** ${rubric.max_score}

Return your evaluation in JSON format:
{
  "criterion_scores": {
    "criterion_name": { "score": 0-3, "rationale": "explanation" }
  },
  "weighted_score": <number>,
  "overall_rationale": "<summary>"
}`;

  const res = await fetch(`${baseUrl}/v1/chat/completions`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Boo-Source': 'control-eval',
    },
    body: JSON.stringify({
      model: judgeModelId,
      messages: [{ role: 'user', content: judgePrompt }],
      temperature: 0,
      max_tokens: 1024,
      response_format: { type: 'json_object' },
    }),
    signal: AbortSignal.timeout(120_000),
  });

  if (!res.ok) {
    const body = await res.text().catch(() => '');
    throw new Error(`judge failed: ${res.status} ${body.slice(0, 200)}`);
  }

  const data = await res.json() as { choices?: Array<{ message?: { content?: string } }> };
  const content = data.choices?.[0]?.message?.content ?? '{}';

  let parsed: { weighted_score?: number; overall_rationale?: string };
  try {
    parsed = JSON.parse(content);
  } catch {
    // Fallback: try to extract JSON from markdown code blocks.
    const match = content.match(/```(?:json)?\s*([\s\S]*?)```/);
    if (match && match[1]) {
      parsed = JSON.parse(match[1]);
    } else {
      parsed = {};
    }
  }

  const score = parsed.weighted_score ?? 0;
  const rationale = parsed.overall_rationale ?? 'No rationale provided';

  return {
    score: Math.min(score, rubric.max_score),
    maxScore: rubric.max_score,
    rationale,
  };
}

/**
 * Resolve the default judge model.
 * Strongest local model by default -- configurable via config.
 */
function resolveDefaultJudgeModel(): string {
  return process.env.EVAL_JUDGE_MODEL ?? 'qwen2.5-72b-instruct';
}
