import { writeFileSync } from 'node:fs';

import type { ExampleResult, Feedback } from './harness-types';
import { DETERMINISTIC_CHECKS } from '../evaluators/binary-checks/checks';
import { LLM_CHECKS } from '../evaluators/binary-checks/llm-checks';

/**
 * Fixed columns that appear first in the CSV (in order).
 */
const FIXED_COLUMNS = [
	'prompt',
	'overall_score',
	'status',
	'gen_latency_ms',
	'gen_input_tokens',
	'gen_output_tokens',
	'node_count',
	'discovery_latency_ms',
	'builder_latency_ms',
	'responder_latency_ms',
] as const;

/**
 * LLM Judge metrics to include (in order).
 * Each metric gets a score column and a _detail column.
 */
const LLM_JUDGE_METRICS = [
	'functionality',
	'connections',
	'expressions',
	'nodeConfiguration',
	'efficiency',
	'dataFlow',
	'maintainability',
	'bestPractices',
] as const;

/**
 * Pairwise evaluator metrics.
 */
const PAIRWISE_METRICS = [
	'pairwise_primary',
	'pairwise_diagnostic',
	'pairwise_judges_passed',
	'pairwise_total_passes',
	'pairwise_total_violations',
] as const;

/**
 * Binary check names derived from the check registries.
 * Order: deterministic checks first, then LLM checks.
 */
const BINARY_CHECK_NAMES = [
	...DETERMINISTIC_CHECKS.map((c) => c.name),
	...LLM_CHECKS.map((c) => c.name),
];

type EvaluationSuite = 'llm-judge' | 'pairwise' | 'binary-checks' | 'unknown';

/**
 * Escape a value for CSV output.
 * Wraps in quotes if contains comma, quote, or newline.
 */
function escapeCsvValue(value: string | number | undefined): string {
	if (value === undefined || value === null) return '';
	const str = String(value);
	if (str.includes(',') || str.includes('"') || str.includes('\n')) {
		return `"${str.replace(/"/g, '""')}"`;
	}
	return str;
}

/**
 * Detect the evaluation suite from feedback.
 */
function detectSuite(feedback: Feedback[]): EvaluationSuite {
	if (feedback.some((f) => f.evaluator === 'llm-judge')) {
		return 'llm-judge';
	}
	if (feedback.some((f) => f.evaluator === 'pairwise')) {
		return 'pairwise';
	}
	if (feedback.some((f) => f.evaluator === 'binary-checks')) {
		return 'binary-checks';
	}
	return 'unknown';
}

/**
 * Extract the detail text from feedback for a given evaluator and metric.
 */
function extractMetricDetail(feedback: Feedback[], evaluator: string, metric: string): string {
	const item = feedback.find((f) => f.evaluator === evaluator && f.metric === metric && f.comment);
	return item?.comment ?? '';
}

/**
 * Extract the score for a given evaluator and metric.
 */
function extractMetricScore(
	feedback: Feedback[],
	evaluator: string,
	metric: string,
): number | undefined {
	const item = feedback.find((f) => f.evaluator === evaluator && f.metric === metric);
	return item?.score;
}

/**
 * Get the number of judges used in pairwise evaluation.
 */
function getJudgeCount(feedback: Feedback[]): number {
	const judgeMetrics = feedback.filter(
		(f) => f.evaluator === 'pairwise' && f.metric.startsWith('judge'),
	);
	return judgeMetrics.length;
}

/**
 * Build CSV row for LLM Judge suite.
 */
function buildLlmJudgeRow(result: ExampleResult): string[] {
	const row: string[] = [];

	// Fixed columns
	row.push(escapeCsvValue(result.prompt));
	row.push(escapeCsvValue(result.score));
	row.push(escapeCsvValue(result.status));
	row.push(escapeCsvValue(result.generationDurationMs));
	row.push(escapeCsvValue(result.generationInputTokens));
	row.push(escapeCsvValue(result.generationOutputTokens));
	row.push(escapeCsvValue(result.subgraphMetrics?.nodeCount));
	row.push(escapeCsvValue(result.subgraphMetrics?.discoveryDurationMs));
	row.push(escapeCsvValue(result.subgraphMetrics?.builderDurationMs));
	row.push(escapeCsvValue(result.subgraphMetrics?.responderDurationMs));

	// LLM Judge metric columns (score + detail pairs)
	for (const metric of LLM_JUDGE_METRICS) {
		row.push(escapeCsvValue(extractMetricScore(result.feedback, 'llm-judge', metric)));
		row.push(escapeCsvValue(extractMetricDetail(result.feedback, 'llm-judge', metric)));
	}

	return row;
}

/**
 * Build CSV row for Pairwise suite.
 */
function buildPairwiseRow(result: ExampleResult, judgeCount: number): string[] {
	const row: string[] = [];

	// Fixed columns
	row.push(escapeCsvValue(result.prompt));
	row.push(escapeCsvValue(result.score));
	row.push(escapeCsvValue(result.status));
	row.push(escapeCsvValue(result.generationDurationMs));
	row.push(escapeCsvValue(result.generationInputTokens));
	row.push(escapeCsvValue(result.generationOutputTokens));
	row.push(escapeCsvValue(result.subgraphMetrics?.nodeCount));
	row.push(escapeCsvValue(result.subgraphMetrics?.discoveryDurationMs));
	row.push(escapeCsvValue(result.subgraphMetrics?.builderDurationMs));
	row.push(escapeCsvValue(result.subgraphMetrics?.responderDurationMs));

	// Pairwise metrics (scores only, no detail)
	for (const metric of PAIRWISE_METRICS) {
		row.push(escapeCsvValue(extractMetricScore(result.feedback, 'pairwise', metric)));
	}

	// Individual judge results (score + violation detail)
	for (let i = 1; i <= judgeCount; i++) {
		const judgeMetric = `judge${i}`;
		row.push(escapeCsvValue(extractMetricScore(result.feedback, 'pairwise', judgeMetric)));
		row.push(escapeCsvValue(extractMetricDetail(result.feedback, 'pairwise', judgeMetric)));
	}

	return row;
}

/**
 * Build CSV row for binary-checks suite.
 */
function buildBinaryChecksRow(result: ExampleResult): string[] {
	const row: string[] = [];

	row.push(escapeCsvValue(result.prompt));
	row.push(escapeCsvValue(result.status));
	row.push(escapeCsvValue(result.generationDurationMs));

	for (const checkName of BINARY_CHECK_NAMES) {
		row.push(escapeCsvValue(extractMetricScore(result.feedback, 'binary-checks', checkName)));
	}

	return row;
}

/**
 * Build CSV header for LLM Judge suite.
 */
function buildLlmJudgeHeader(): string[] {
	const header: string[] = [...FIXED_COLUMNS];

	for (const metric of LLM_JUDGE_METRICS) {
		header.push(metric);
		header.push(`${metric}_detail`);
	}

	return header;
}

/**
 * Build CSV header for Pairwise suite.
 */
function buildPairwiseHeader(judgeCount: number): string[] {
	const header: string[] = [...FIXED_COLUMNS];

	// Pairwise metrics
	for (const metric of PAIRWISE_METRICS) {
		header.push(metric);
	}

	// Individual judge columns
	for (let i = 1; i <= judgeCount; i++) {
		header.push(`judge${i}`);
		header.push(`judge${i}_detail`);
	}

	return header;
}

/**
 * Build CSV header for binary-checks suite.
 */
function buildBinaryChecksHeader(): string[] {
	return ['prompt', 'status', 'gen_latency_ms', ...BINARY_CHECK_NAMES];
}

export interface WriteResultsCsvOptions {
	/** Explicitly specify the evaluation suite. If not provided, auto-detects from feedback. */
	suite?: 'llm-judge' | 'pairwise' | 'binary-checks';
}

/**
 * Write evaluation results to a CSV file.
 * Results are sorted by prompt for consistent ordering across runs.
 * Automatically detects evaluation suite and formats accordingly,
 * unless explicitly specified via options.
 */
export function writeResultsCsv(
	results: ExampleResult[],
	outputPath: string,
	options?: WriteResultsCsvOptions,
): void {
	if (results.length === 0) {
		writeFileSync(outputPath, '', 'utf-8');
		return;
	}

	// Sort by prompt for consistent ordering
	const sorted = [...results].sort((a, b) => a.prompt.localeCompare(b.prompt));

	// Use explicit suite if provided, otherwise detect from feedback
	let suite: EvaluationSuite;
	if (options?.suite) {
		suite = options.suite;
	} else {
		// Detect suite from first result with feedback (excluding runner errors)
		const firstWithFeedback = sorted.find((r) =>
			r.feedback.some((f) => f.evaluator !== 'runner' && f.evaluator !== 'programmatic'),
		);
		suite = firstWithFeedback ? detectSuite(firstWithFeedback.feedback) : 'unknown';
	}

	const lines: string[] = [];

	if (suite === 'pairwise') {
		// Determine max judge count across all results
		const judgeCount = Math.max(...sorted.map((r) => getJudgeCount(r.feedback)), 0);

		// Header
		lines.push(buildPairwiseHeader(judgeCount).join(','));

		// Data rows
		for (const result of sorted) {
			lines.push(buildPairwiseRow(result, judgeCount).join(','));
		}
	} else if (suite === 'binary-checks') {
		// Header
		lines.push(buildBinaryChecksHeader().join(','));

		// Data rows
		for (const result of sorted) {
			lines.push(buildBinaryChecksRow(result).join(','));
		}
	} else {
		// Default to LLM Judge format (also handles unknown)
		// Header
		lines.push(buildLlmJudgeHeader().join(','));

		// Data rows
		for (const result of sorted) {
			lines.push(buildLlmJudgeRow(result).join(','));
		}
	}

	// Write file (overwrites if exists)
	writeFileSync(outputPath, lines.join('\n') + '\n', 'utf-8');
}
