import { execFile } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { writeFile, unlink } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';

import type { SimpleWorkflow } from '@/types';
import type { ProgrammaticViolationName, SingleEvaluatorResult } from '@/validation/types';

const execFileAsync = promisify(execFile);

interface ExecError extends Error {
	code?: string;
	killed?: boolean;
	stdout?: string;
	stderr?: string;
}

function isExecError(error: unknown): error is ExecError {
	return error instanceof Error;
}

/**
 * Map Python edit types to violation names
 */
function mapEditTypeToViolationName(editType: string): ProgrammaticViolationName {
	const mapping: Record<string, ProgrammaticViolationName> = {
		node_insert: 'workflow-similarity-node-insert',
		node_delete: 'workflow-similarity-node-delete',
		node_substitute: 'workflow-similarity-node-substitute',
		edge_insert: 'workflow-similarity-edge-insert',
		edge_delete: 'workflow-similarity-edge-delete',
		edge_substitute: 'workflow-similarity-edge-substitute',
	};

	return mapping[editType] ?? 'workflow-similarity-node-substitute';
}

interface WorkflowSimilarityResult {
	similarity_score: number; // 0-1
	edit_cost: number;
	max_possible_cost: number;
	top_edits: Array<{
		type:
			| 'node_insert'
			| 'node_delete'
			| 'node_substitute'
			| 'edge_insert'
			| 'edge_delete'
			| 'edge_substitute';
		description: string;
		cost: number;
		priority: 'critical' | 'major' | 'minor';
		node_name?: string;
	}>;
	metadata: {
		generated_nodes: number;
		ground_truth_nodes: number;
		generated_nodes_after_filter?: number;
		ground_truth_nodes_after_filter?: number;
		config_name: string;
		config_description?: string;
	};
}

function isWorkflowSimilarityResult(value: unknown): value is WorkflowSimilarityResult {
	if (typeof value !== 'object' || value === null) {
		return false;
	}

	const obj = value as Record<string, unknown>;

	return (
		typeof obj.similarity_score === 'number' &&
		typeof obj.edit_cost === 'number' &&
		typeof obj.max_possible_cost === 'number' &&
		Array.isArray(obj.top_edits) &&
		typeof obj.metadata === 'object' &&
		obj.metadata !== null
	);
}

/**
 * Evaluate workflow similarity using Python graph edit distance algorithm.
 * Compares against a single reference workflow.
 *
 * @param generatedWorkflow - Workflow generated by AI
 * @param groundTruthWorkflow - Reference workflow to compare against
 * @param configPreset - Built-in preset to use ('strict' | 'standard' | 'lenient')
 * @param customConfigPath - Optional path to custom configuration file
 * @returns SingleEvaluatorResult with violations and score
 */
export async function evaluateWorkflowSimilarity(
	generatedWorkflow: SimpleWorkflow,
	groundTruthWorkflow: SimpleWorkflow,
	configPreset: 'strict' | 'standard' | 'lenient' = 'standard',
	customConfigPath?: string,
): Promise<SingleEvaluatorResult> {
	const tmpDir = tmpdir();
	const uniqueId = randomUUID();
	const generatedPath = join(tmpDir, `n8n-workflow-generated-${uniqueId}.json`);
	const groundTruthPath = join(tmpDir, `n8n-workflow-groundtruth-${uniqueId}.json`);

	let stdout = '';
	let stderr = '';

	try {
		// Write workflows to temp files
		await Promise.all([
			writeFile(generatedPath, JSON.stringify(generatedWorkflow)),
			writeFile(groundTruthPath, JSON.stringify(groundTruthWorkflow)),
		]);

		// Build command arguments
		const pythonScriptDir = join(__dirname, '..', 'python');
		const args = [
			'--from',
			pythonScriptDir,
			'python',
			'-m',
			'src.compare_workflows',
			generatedPath,
			groundTruthPath,
			'--output-format',
			'json',
		];

		// Add config argument
		if (customConfigPath) {
			args.push('--config', customConfigPath);
		} else {
			args.push('--preset', configPreset);
		}

		// Run Python script using uvx
		try {
			const result = await execFileAsync('uvx', args, {
				cwd: pythonScriptDir, // Set working directory to Python project
				timeout: 30000, // 30 second timeout
				maxBuffer: 1024 * 1024 * 10, // 10MB buffer
			});
			stdout = result.stdout;
			stderr = result.stderr;
		} catch (execError) {
			// Python script may exit with non-zero code if similarity is below threshold
			// But it still outputs valid JSON, so we should use it
			if (isExecError(execError)) {
				stdout = execError.stdout ?? '';
				stderr = execError.stderr ?? '';
			}

			// Only throw if we don't have valid output
			if (!stdout || stdout.trim() === '') {
				throw execError;
			}
		}

		// Check if stdout is empty
		if (!stdout || stdout.trim() === '') {
			throw new Error(
				`Python script produced no output. stderr: ${stderr || 'none'}. Command: uvx ${args.join(' ')}`,
			);
		}

		// Parse result
		const parsed: unknown = JSON.parse(stdout);

		if (!isWorkflowSimilarityResult(parsed)) {
			throw new Error(
				`Invalid response from Python script. Expected WorkflowSimilarityResult shape but got: ${stdout.slice(0, 200)}`,
			);
		}

		// Convert Python result to SingleEvaluatorResult format
		const violations = parsed.top_edits.map((edit) => ({
			name: mapEditTypeToViolationName(edit.type),
			type: edit.priority,
			description: edit.description,
			pointsDeducted: Math.round(edit.cost),
		}));

		return {
			violations,
			score: parsed.similarity_score,
		};
	} catch (error) {
		// Handle specific error cases
		if (isExecError(error)) {
			if (error.killed) {
				// Timeout error
				throw new Error(
					'Workflow comparison timed out (graphs too complex). Consider using a simpler comparison or increasing timeout.',
				);
			}

			if (error.code === 'ENOENT') {
				throw new Error(
					'uvx command not found. Please install uv: https://docs.astral.sh/uv/getting-started/installation/',
				);
			}

			// Re-throw with more context
			throw new Error(`Workflow similarity evaluation failed: ${error.message}`);
		}

		// Handle non-Error thrown values
		throw new Error(`Workflow similarity evaluation failed: ${String(error)}`);
	} finally {
		// Cleanup temp files (don't throw on cleanup errors)
		await Promise.allSettled([unlink(generatedPath), unlink(groundTruthPath)]);
	}
}

/**
 * Evaluate workflow similarity against multiple reference workflows.
 * Returns the result with the highest similarity score.
 *
 * @param generatedWorkflow - Workflow generated by AI
 * @param referenceWorkflows - Array of reference workflows to compare against
 * @param configPreset - Built-in preset to use ('strict' | 'standard' | 'lenient')
 * @param customConfigPath - Optional path to custom configuration file
 * @returns SingleEvaluatorResult with violations and score from the best match
 */
export async function evaluateWorkflowSimilarityMultiple(
	generatedWorkflow: SimpleWorkflow,
	referenceWorkflows: SimpleWorkflow[],
	configPreset: 'strict' | 'standard' | 'lenient' = 'standard',
	customConfigPath?: string,
): Promise<SingleEvaluatorResult> {
	if (referenceWorkflows.length === 0) {
		throw new Error('At least one reference workflow is required');
	}

	// Compare against all reference workflows in parallel
	const results = await Promise.all(
		referenceWorkflows.map(
			async (refWorkflow) =>
				await evaluateWorkflowSimilarity(
					generatedWorkflow,
					refWorkflow,
					configPreset,
					customConfigPath,
				),
		),
	);

	// Find the result with the highest similarity score
	const bestResult = results.reduce((best, current) =>
		current.score > best.score ? current : best,
	);

	return bestResult;
}
