import { tryReadConfig, writeConfig } from './config'
import { readdir, readFile } from 'node:fs/promises'
import path from 'node:path'
import { readSuite, writeSuite, upsertSuiteMeta, type TestRow as SuiteRow } from './suite'
import { getRootPath } from '../get-root-path'
import z from 'zod'
import type { WrittenRunResult } from './academy'

export type CsvRow = Record<string, string>

export type TestRow = SuiteRow

export const RunConfigSchema = z.object({
	repeats: z.number().optional(),
	concurrency: z.number().optional(),
	defaultContext: z.record(z.unknown()).optional()
})

export type RunConfig = z.infer<typeof RunConfigSchema>

export interface RunResult {
	success: boolean
	result: string
	evaluation: string
	rendered_prompt: string
	received_prompt: string
	stringified_conversation: string
	received_context: string
	traces: any[]
	errors: string
	logs: string[]
}

export interface TestResult {
	test_id: string
	success_rate_percent?: number
	success_rate?: string
	results: RunResult[]
	report_timestamp: string // ISO
	is_old: boolean
}
export interface TestTable {
	rows: TestRow[]
	headers: string[]
}

// Utility: ensure each row has an id; persist if any are missing.
export async function loadTests(agent: string): Promise<TestTable> {
	const suite = await readSuite(agent)
	// Ensure ids exist; write back if any are missing
	let changed = false
	const rows: TestRow[] = suite.tests.map((r) => {
		const id = (r.id || '').trim()
		if (!id) changed = true
		return { ...r, id: id || globalThis.crypto?.randomUUID?.() || randomUUID() }
	})
	if (changed) {
		await writeSuite(agent, { ...suite, tests: rows })
	}
	const headers = [
		'id',
		'pinned',
		'scenario',
		'author',
		'category',
		'assertion_type',
		'assertion_value',
		'conversation',
		'expected_output',
		'timeout',
		'similarity_threshold',
		'context'
	]
	return { rows, headers }
}

// Utility: update a single row by id and write the CSV
export async function patchTest(
	agent: string,
	id: string,
	patch: Partial<TestRow>
): Promise<TestRow | null> {
	const suite = await readSuite(agent)
	const idx = suite.tests.findIndex((r) => r.id === id)
	if (idx < 0) return null
	const current = suite.tests[idx]
	const updated: TestRow = {
		id: id,
		pinned:
			String(patch.pinned ?? current.pinned ?? 'TRUE').toUpperCase() === 'TRUE' ? 'TRUE' : 'FALSE',
		scenario: String(patch.scenario ?? current.scenario ?? ''),
		author: String(patch.author ?? (current as any).author ?? ''),
		category: String(patch.category ?? (current as any).category ?? ''),
		assertion_type: String(patch.assertion_type ?? current.assertion_type ?? 'llm-rubric'),
		assertion_value: String(patch.assertion_value ?? current.assertion_value ?? ''),
		conversation: String(patch.conversation ?? current.conversation ?? ''),
		expected_output: String(patch.expected_output ?? current.expected_output ?? ''),
		timeout: String(patch.timeout ?? current.timeout ?? '0'),
		similarity_threshold: String(
			patch.similarity_threshold ?? current.similarity_threshold ?? '60'
		),
		context: typeof patch.context === 'string' ? patch.context : (current.context ?? '')
	}
	const nextRows = suite.tests.map((r, i) => (i === idx ? updated : r))
	await writeSuite(agent, { ...suite, tests: nextRows })
	return updated
}

// Batch update multiple rows in a single read/write to avoid race conditions
export async function patchTestsBatch(
	agent: string,
	updates: Array<{ id: string; patch: Partial<TestRow> }>
): Promise<TestRow[]> {
	if (!Array.isArray(updates) || updates.length === 0) return []
	const suite = await readSuite(agent)
	const byId = new Map(suite.tests.map((r) => [r.id, r]))
	const updatedRows: TestRow[] = []
	for (const { id, patch } of updates) {
		const current = byId.get(id)
		if (!current) continue
		const updated: TestRow = {
			id: id,
			pinned:
				String(patch.pinned ?? current.pinned ?? 'TRUE').toUpperCase() === 'TRUE'
					? 'TRUE'
					: 'FALSE',
			scenario: String(patch.scenario ?? current.scenario ?? ''),
			author: String(patch.author ?? (current as any).author ?? ''),
			category: String(patch.category ?? (current as any).category ?? ''),
			assertion_type: String(patch.assertion_type ?? current.assertion_type ?? 'llm-rubric'),
			assertion_value: String(patch.assertion_value ?? current.assertion_value ?? ''),
			conversation: String(patch.conversation ?? current.conversation ?? ''),
			expected_output: String(patch.expected_output ?? current.expected_output ?? ''),
			timeout: String(patch.timeout ?? current.timeout ?? '0'),
			similarity_threshold: String(
				patch.similarity_threshold ?? current.similarity_threshold ?? '60'
			),
			context: typeof patch.context === 'string' ? patch.context : (current.context ?? '')
		}
		byId.set(id, updated)
		updatedRows.push(updated)
	}
	const nextRows: TestRow[] = suite.tests.map((r) => byId.get(r.id) ?? r)
	await writeSuite(agent, { ...suite, tests: nextRows })
	return updatedRows
}

// Add a new row at the top
export async function addTest(agent: string, seed?: Partial<TestRow>): Promise<TestRow> {
	const suite = await readSuite(agent).catch(() => ({ version: 1, tests: [] }))
	const id = globalThis.crypto?.randomUUID?.() || randomUUID()
	const row: TestRow = {
		id,
		pinned:
			((seed?.pinned || 'TRUE') as any).toString().toUpperCase() === 'TRUE' ? 'TRUE' : 'FALSE',
		scenario: seed?.scenario ?? 'New scenario',
		author: seed?.author ?? '',
		category: seed?.category ?? '',
		assertion_type: seed?.assertion_type ?? 'llm-rubric',
		assertion_value: seed?.assertion_value ?? '',
		conversation: seed?.conversation ?? 'user: ...',
		expected_output: seed?.expected_output ?? '',
		timeout: String((seed as any)?.timeout ?? '0'),
		similarity_threshold: String((seed as any)?.similarity_threshold ?? '60'),
		context: typeof seed?.context === 'string' ? (seed?.context as string) : '{}'
	}
	const nextRows = [row, ...suite.tests]
	await writeSuite(agent, { ...(suite as any), tests: nextRows })
	return row
}

export async function deleteTest(agent: string, id: string): Promise<boolean> {
	const suite = await readSuite(agent)
	const filtered = suite.tests.filter((r) => r.id !== id)
	if (filtered.length === suite.tests.length) return false
	await writeSuite(agent, { ...suite, tests: filtered })
	return true
}

export async function reorderTests(agent: string, order: string[]): Promise<void> {
	const suite = await readSuite(agent)
	const map = new Map(suite.tests.map((r) => [r.id, r]))
	const next: TestRow[] = []
	for (const id of order) {
		const r = map.get(id)
		if (r) {
			next.push(r)
			map.delete(id)
		}
	}
	// Append any rows that were not included in order (safety)
	for (const r of map.values()) next.push(r)
	await writeSuite(agent, { ...suite, tests: next })
}

export async function loadConfig(agent: string): Promise<RunConfig> {
	const cfg = (await tryReadConfig(agent)) || {}
	// Always source defaultContext from the suite meta, not config
	let defaultContext: Record<string, unknown> = {}
	try {
		const suite = await readSuite(agent)
		defaultContext = (suite.meta?.defaultContext as any) || {}
	} catch {
		// ignore
	}
	return {
		repeats: typeof (cfg as any).repeats === 'number' ? (cfg as any).repeats : 1,
		concurrency: typeof (cfg as any).concurrency === 'number' ? (cfg as any).concurrency : 1,
		defaultContext
	}
}

export async function saveConfig(agent: string, cfg: RunConfig): Promise<void> {
	// Merge patch into existing config, persisting only repeats/concurrency
	const current = (await tryReadConfig(agent)) || {}
	const next = {
		repeats: typeof cfg.repeats === 'number' ? cfg.repeats : (current as any).repeats,
		concurrency:
			typeof cfg.concurrency === 'number' ? cfg.concurrency : (current as any).concurrency
	}
	await writeConfig(agent, next)
	// Persist defaultContext exclusively in the suite meta (do not write to config)
	if (cfg && typeof cfg.defaultContext !== 'undefined') {
		await upsertSuiteMeta(agent, { defaultContext: cfg.defaultContext })
	}
}

// We no longer migrate legacy reports automatically. Please update old reports manually.

export async function loadLatestResultsByTest(agent: string): Promise<Record<string, TestResult>> {
	const projectRoot = path.dirname(await getRootPath())
	const outDir = path.join(projectRoot, 'data', 'test_runs')
	let entries: string[] = []
	try {
		entries = await readdir(outDir)
	} catch {
		return {}
	}
	const prefix = `${agent}.`
	const suffix = `.report.json`
	const files = entries.filter((n) => n.startsWith(prefix) && n.endsWith(suffix)).sort()
	if (!files.length) return {}

	const latestFile = files[files.length - 1]
	const latestTs = latestFile
		.slice(prefix.length, latestFile.length - suffix.length)
		.replace(/-/g, ':')

	const results: Record<string, TestResult> = {}
	for (const name of files) {
		const full = path.join(outDir, name)
		const raw = await readFile(full, 'utf8')
		const parsed = JSON.parse(raw) as any
		const rows = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.rows) ? parsed.rows : []
		const ts = name.slice(prefix.length, name.length - suffix.length).replace(/-/g, ':')
		for (const r of rows) {
			const test_id = (r as any).test_id
			if (!test_id) {
				throw new Error(
					`Missing test_id in report ${name}. Please add 'test_id' and provenance columns manually.`
				)
			}
			const par = String((r as any).pinned_at_run ?? 'TRUE').toUpperCase()
			if (par === 'FALSE') continue
			// Always prefer newer timestamp
			const prev = results[test_id]
			const rec: TestResult = {
				test_id,
				success_rate_percent: (r as any).success_rate_percent,
				success_rate: (r as any).success_rate,
				results: Array.isArray((r as any).results) ? ((r as any).results as RunResult[]) : [],
				report_timestamp: ts,
				is_old: ts < latestTs
			}
			if (!prev || prev.report_timestamp < ts) {
				results[test_id] = rec
			}
		}
	}
	return results
}

// Fallback UUID for Node environments without global crypto
function randomUUID() {
	const rnd = () =>
		Math.floor(Math.random() * 0xffffffff)
			.toString(16)
			.padStart(8, '0')
	return `${rnd()}-${rnd().slice(0, 4)}-${rnd().slice(0, 4)}-${rnd().slice(0, 4)}-${rnd()}${rnd()}`
}

async function getRun(agent: string, ts?: string) {
	const projectRoot = path.dirname(await getRootPath())
	const outDir = path.join(projectRoot, 'data', 'test_runs')
	const file = `${agent}.${(ts || '').replace(/:/g, '-')}.report.json`
	const reportPath = ts ? path.join(outDir, file) : null

	async function readLatestPath(): Promise<string> {
		const entries = await readdir(outDir)
		const prefix = `${agent}.`
		const suffix = `.report.json`
		const candidates = entries.filter((n) => n.startsWith(prefix) && n.endsWith(suffix)).sort()
		if (!candidates.length) throw new Error('No reports found')
		return path.join(outDir, candidates[candidates.length - 1])
	}

	const chosen = reportPath || (await readLatestPath())
	const raw = await readFile(chosen, 'utf8')
	const parsed = JSON.parse(raw) as WrittenRunResult
	return parsed
}

type ResultKey = keyof WrittenRunResult['rows'][0]['results'][0]

export async function getResultEntry(
	agent: string,
	testId: string,
	resultKey: ResultKey,
	index = 0,
	ts?: string
) {
	const result = await getRun(agent, ts)

	const row = result.rows.find((r) => r.test_id === testId)
	if (!row) throw new Error('Row not found in report')

	const run = row.results[index]
	if (!run) throw new Error(`Run not found for test ${testId}, index: ${index}`)

	const entryContent = run[resultKey]

	return Array.isArray(entryContent) ? entryContent.join('\n') : String(entryContent)
}
