import * as path from 'path';
import type {
  AxisScore,
  BenchTask,
  ProgrammaticConstraint,
  RawJobResult,
} from './types.js';

function clamp01(v: number): number {
  return Math.max(0, Math.min(1, v));
}

function avg(xs: number[]): number {
  if (xs.length === 0) return 0;
  return xs.reduce((a, b) => a + b, 0) / xs.length;
}

/**
 * Axis A — tool calling correctness.
 *
 * Components:
 *   - +1 for each must_use tool actually called (averaged)
 *   - -1 for each forbidden_tool used
 *   - -1 for each forbidden_tool_for_ext violation (e.g. Read on .xlsx)
 */
export function gradeTools(task: BenchTask, raw: RawJobResult): AxisScore {
  const must = task.expected.must_use_tools ?? [];
  const forbidden = task.expected.forbidden_tools ?? [];
  const forbiddenForExt = task.expected.forbidden_tool_for_ext ?? {};

  const used = new Set(raw.toolCalls.map((c) => c.name));
  const details: string[] = [];

  let mustHits = 0;
  for (const t of must) {
    if (used.has(t)) {
      mustHits++;
      details.push(`✓ used ${t}`);
    } else {
      details.push(`✗ missing ${t}`);
    }
  }
  let forbiddenViolations = 0;
  for (const t of forbidden) {
    if (used.has(t)) {
      forbiddenViolations++;
      details.push(`✗ forbidden tool used: ${t}`);
    }
  }
  let extViolations = 0;
  for (const [tool, exts] of Object.entries(forbiddenForExt)) {
    for (const call of raw.toolCalls) {
      if (call.name !== tool) continue;
      const fp = call.filePath ?? call.inputSummary;
      const ext = path.extname(fp).toLowerCase();
      if (exts.includes(ext)) {
        extViolations++;
        details.push(`✗ ${tool} on ${ext}: ${fp}`);
      }
    }
  }

  const mustScore = must.length === 0 ? 1 : mustHits / must.length;
  const penalty = (forbiddenViolations + extViolations) * 0.5;
  const score = clamp01(mustScore - penalty);
  return { score, details };
}

/**
 * Axis B — checklist tool usage.
 *
 * +1/3 for CreateChecklist used, +1/3 for GetChecklist used, +1/3 if
 * CheckItem invoked at least min_check_item_calls times.
 */
export function gradeChecklist(task: BenchTask, raw: RawJobResult): AxisScore {
  const cfg = task.checklist;
  if (!cfg) return { score: 1, details: ['(checklist not required)'] };

  const calls = raw.toolCalls.map((c) => c.name);
  const required = cfg.required_tools;
  const checkItemCalls = calls.filter((n) => n === 'CheckItem').length;
  const details: string[] = [];

  let satisfied = 0;
  for (const t of required) {
    if (t === 'CheckItem') {
      if (checkItemCalls >= cfg.min_check_item_calls) {
        satisfied++;
        details.push(`✓ CheckItem ${checkItemCalls}/${cfg.min_check_item_calls}`);
      } else {
        details.push(`✗ CheckItem ${checkItemCalls}/${cfg.min_check_item_calls}`);
      }
      continue;
    }
    if (calls.includes(t)) {
      satisfied++;
      details.push(`✓ ${t}`);
    } else {
      details.push(`✗ missing ${t}`);
    }
  }
  return { score: required.length === 0 ? 1 : satisfied / required.length, details };
}

interface OutputView {
  text: string;
  lines: string[];
  /** Line index keyed by section header (## ...). End is exclusive. */
  sections: Record<string, { start: number; end: number }>;
}

function buildOutputView(text: string): OutputView {
  const lines = text.split('\n');
  const sections: Record<string, { start: number; end: number }> = {};
  let currentHeader: string | null = null;
  let currentStart = 0;
  for (let i = 0; i < lines.length; i++) {
    const m = /^##\s+(.+)\s*$/.exec(lines[i]!);
    if (m) {
      if (currentHeader !== null) {
        sections[currentHeader] = { start: currentStart, end: i };
      }
      currentHeader = m[1]!.trim();
      currentStart = i + 1;
    }
  }
  if (currentHeader !== null) {
    sections[currentHeader] = { start: currentStart, end: lines.length };
  }
  return { text, lines, sections };
}

function evaluateConstraint(
  constraint: ProgrammaticConstraint,
  outputs: Record<string, string>,
): { passed: boolean; detail: string } {
  const file = (constraint as { file: string }).file;
  const text = outputs[file];
  if (text === undefined) {
    return { passed: false, detail: `[${constraint.type}] file missing: ${file}` };
  }
  const view = buildOutputView(text);

  switch (constraint.type) {
    case 'file_first_line_equals': {
      const ok = (view.lines[0] ?? '').trim() === constraint.line.trim();
      return { passed: ok, detail: `[first_line] ${file}: ${ok ? '✓' : `got "${view.lines[0]}"`}` };
    }
    case 'file_must_contain_in_order': {
      let cursor = 0;
      const missing: string[] = [];
      for (const sec of constraint.sections) {
        const idx = view.text.indexOf(sec, cursor);
        if (idx === -1) missing.push(sec);
        else cursor = idx + sec.length;
      }
      return {
        passed: missing.length === 0,
        detail: `[order] ${file}: ${missing.length === 0 ? '✓' : `missing/out-of-order: ${missing.join(', ')}`}`,
      };
    }
    case 'file_line_starts_with': {
      const range = constraint.section ? view.sections[constraint.section] : { start: 0, end: view.lines.length };
      if (!range) {
        return { passed: false, detail: `[starts_with] section "${constraint.section}" not found` };
      }
      const target = view.lines.slice(range.start, range.end).filter((l) => l.trim().length > 0);
      const matched = target.filter((l) => l.trim().startsWith(constraint.prefix)).length;
      const ok = matched >= constraint.min_lines;
      return {
        passed: ok,
        detail: `[starts_with "${constraint.prefix}"] ${constraint.section ?? file}: ${matched}/${constraint.min_lines}`,
      };
    }
    case 'file_line_max_chars': {
      const range = constraint.section ? view.sections[constraint.section] : { start: 0, end: view.lines.length };
      if (!range) {
        return { passed: false, detail: `[max_chars] section "${constraint.section}" not found` };
      }
      const target = view.lines.slice(range.start, range.end).filter((l) => l.trim().length > 0);
      const violations = target.filter((l) => [...l.trim()].length > constraint.max);
      return {
        passed: violations.length === 0,
        detail: `[max_chars ${constraint.max}] ${constraint.section ?? file}: ${violations.length === 0 ? '✓' : `${violations.length} violations`}`,
      };
    }
    case 'file_section_max_lines': {
      const range = view.sections[constraint.section];
      if (!range) {
        return { passed: false, detail: `[max_lines] section "${constraint.section}" not found` };
      }
      const nonEmpty = view.lines.slice(range.start, range.end).filter((l) => l.trim().length > 0).length;
      return {
        passed: nonEmpty <= constraint.max,
        detail: `[max_lines ${constraint.max}] ${constraint.section}: ${nonEmpty} lines`,
      };
    }
    case 'file_no_pattern': {
      const re = new RegExp(constraint.pattern, 'm');
      const ok = !re.test(view.text);
      return { passed: ok, detail: `[no_pattern /${constraint.pattern}/] ${file}: ${ok ? '✓' : '✗'}` };
    }
  }
}

/**
 * Axis C — instruction adherence (programmatic part).
 *
 * Combines:
 *   - must_produce_files coverage
 *   - each programmatic.constraints check
 *   - completion_status acceptance
 */
export function gradeInstructionsProgrammatic(task: BenchTask, raw: RawJobResult): AxisScore {
  const details: string[] = [];
  const checks: boolean[] = [];

  const acceptable = task.expected.completion_status ?? ['succeeded'];
  const statusOk = acceptable.includes(raw.status as 'succeeded');
  checks.push(statusOk);
  details.push(`[status] ${raw.status} ${statusOk ? '✓' : `(expected one of ${acceptable.join(',')})`}`);

  const mustFiles = task.expected.must_produce_files ?? [];
  for (const f of mustFiles) {
    const ok = raw.outputFiles[f] !== undefined && raw.outputFiles[f].length > 0;
    checks.push(ok);
    details.push(`[file] ${f} ${ok ? '✓' : '✗ (empty/missing)'}`);
  }

  for (const c of task.grading?.programmatic?.constraints ?? []) {
    const r = evaluateConstraint(c, raw.outputFiles);
    checks.push(r.passed);
    details.push(r.detail);
  }

  return { score: checks.length === 0 ? 1 : checks.filter(Boolean).length / checks.length, details };
}

/**
 * Combine programmatic + judge scores for axis C / D, weighted by config.
 */
export function combineAxisScores(programmatic: AxisScore, judge: AxisScore | null, judgeWeight: number): AxisScore {
  if (!judge) return programmatic;
  const w = clamp01(judgeWeight);
  return {
    score: (1 - w) * programmatic.score + w * judge.score,
    details: [...programmatic.details, ...judge.details.map((d) => `[judge] ${d}`)],
  };
}

/**
 * Total score in 0..100 with the described axis weights.
 *
 *   tools 30 / checklist 15 / instructions 30 / reasoning 25
 */
export function computeTotal(axes: {
  tools: AxisScore;
  checklist: AxisScore;
  instructions: AxisScore;
  reasoning: AxisScore;
}): number {
  const sum = axes.tools.score * 30 + axes.checklist.score * 15 + axes.instructions.score * 30 + axes.reasoning.score * 25;
  return Math.round(sum);
}

export { avg, clamp01 };
