import type { TRPCRootObject } from '@trpc/server/unstable-core-do-not-import'
import { z } from 'zod'
import { access, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'

import {
	addTest,
	deleteTest,
	getResultEntry,
	loadConfig,
	loadLatestResultsByTest,
	loadTests,
	patchTest,
	patchTestsBatch,
	reorderTests,
	RunConfigSchema,
	saveConfig
} from './data'
import { readSuite, TestRowSchema } from './suite'
import { cancelPromptTests, runPromptTestsManaged } from './academy'
import { analyzeRunQuotes } from './analyze'
import { findPromptIssues } from './prompt-issues'
import type { TrpcContext } from '../lib/trpc-context'
import { getRootPath } from '../lib/get-root-path'
import { type BosunAgentMeta } from '../lib/get-agent-factories'
import { getProjectBosunApi } from '../lib/project-api'

async function fileExists(p: string): Promise<boolean> {
	try {
		await access(p)
		return true
	} catch {
		return false
	}
}

async function suiteOrConfigExists(baseDir: string, suiteKey: string): Promise<boolean> {
	const jsonl = path.resolve(baseDir, `${suiteKey}.tests.jsonl`)
	const legacyJson = path.resolve(baseDir, `${suiteKey}.tests.json`)
	const cfg = path.resolve(baseDir, `${suiteKey}.config.json`)
	return (await fileExists(jsonl)) || (await fileExists(legacyJson)) || (await fileExists(cfg))
}

async function migrateLegacySuiteToDefaultRegion(
	baseDir: string,
	agent: string,
	meta: BosunAgentMeta | null
): Promise<void> {
	if (!meta || meta.availableRegions.length === 0) return

	const defaultRegion =
		meta.defaultRegion && meta.availableRegions.includes(meta.defaultRegion)
			? meta.defaultRegion
			: null
	if (!defaultRegion) return

	const defaultSuiteKey = `${agent}.${defaultRegion}`
	if (await suiteOrConfigExists(baseDir, defaultSuiteKey)) return
	if (!(await suiteOrConfigExists(baseDir, agent))) return

	// Non-trivial: one-time migration so the first region-aware run does NOT silently fall back to the
	// legacy suite/config (which would incorrectly show the same tests for every region).
	const legacyJsonl = path.resolve(baseDir, `${agent}.tests.jsonl`)
	const legacyJson = path.resolve(baseDir, `${agent}.tests.json`)
	const legacyCfg = path.resolve(baseDir, `${agent}.config.json`)

	const targetJsonl = path.resolve(baseDir, `${defaultSuiteKey}.tests.jsonl`)
	const targetCfg = path.resolve(baseDir, `${defaultSuiteKey}.config.json`)

	await mkdir(baseDir, { recursive: true })

	if (!(await fileExists(targetJsonl)) && (await fileExists(legacyJsonl))) {
		await copyFile(legacyJsonl, targetJsonl)
	} else if (!(await fileExists(targetJsonl)) && (await fileExists(legacyJson))) {
		// Convert legacy JSON suite to JSONL so the new file matches the expected layout.
		const raw = await readFile(legacyJson, 'utf8')
		const parsed = JSON.parse(raw) as any
		const metaObj = parsed?.meta && typeof parsed.meta === 'object' ? parsed.meta : {}
		const tests = Array.isArray(parsed?.tests) ? parsed.tests : []
		const header = { version: 1, meta: metaObj }
		const lines = [JSON.stringify(header), ...tests.map((t: any) => JSON.stringify(t))]
		await writeFile(targetJsonl, lines.join('\n') + '\n', 'utf8')
	}

	if (!(await fileExists(targetCfg)) && (await fileExists(legacyCfg))) {
		await copyFile(legacyCfg, targetCfg)
	}
}

type SuiteSelection = {
	suiteKey: string
	agentId: string
	region?: string
}

async function resolveSuiteSelection(
	agentId: string,
	region?: string | null
): Promise<SuiteSelection> {
	// Convention:
	// - `agentId` is the canonical factory key (workflow export key).
	// - `suiteKey` is the filesystem/report key:
	//    - non-regional projects: `${agentId}`
	//    - regional projects: `${agentId}.${region}`
	// We never substitute legacy tests for the wrong region.
	const baseDir = path.dirname(await getRootPath())

	const bosunApi = await getProjectBosunApi()
	const meta = bosunApi ? await bosunApi.getAgentMeta(agentId) : null
	const isRegionScoped = meta?.testScope === 'region'
	if (!isRegionScoped) return { suiteKey: agentId, agentId }

	await migrateLegacySuiteToDefaultRegion(baseDir, agentId, meta)

	if (!bosunApi) {
		throw new Error(`Project does not export bosunApi, cannot resolve suite for '${agentId}'.`)
	}

	const resolved = await bosunApi.resolveTestSuite({ agentId, region })
	return {
		suiteKey: resolved.suiteKey,
		agentId,
		region: resolved.resolvedRegion ?? undefined
	}
}

export function createTestsRouter(t: TRPCRootObject<TrpcContext, any, any>) {
	return t.router({
		list: t.procedure
			.input(z.object({ agent: z.string(), region: z.string().nullish() }))
			.query(async ({ input }) => {
				const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
				return loadTests(suiteKey)
			}),
		config: t.procedure
			.input(z.object({ agent: z.string(), region: z.string().nullish() }))
			.query(async ({ input }) => {
				const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
				return loadConfig(suiteKey)
			}),
		updateConfig: t.procedure
			.input(z.object({ agent: z.string(), region: z.string().nullish(), cfg: RunConfigSchema }))
			.mutation(async ({ input }) => {
				const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
				return saveConfig(suiteKey, input.cfg)
			}),
		latestResult: t.procedure
			.input(z.object({ agent: z.string(), region: z.string().nullish() }))
			.query(async ({ input }) => {
				const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
				return loadLatestResultsByTest(suiteKey)
			}),
		resultEntry: t.procedure
			.input(
				z.object({
					agent: z.string(),
					region: z.string().nullish(),
					testId: z.string(),
					// TODO resultKey should be keyof of ResultEntry
					resultKey: z.string(),
					index: z.number().optional(),
					timestamp: z.string().optional()
				})
			)
			.query(async ({ input }) => {
				const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
				return getResultEntry(
					suiteKey,
					input.testId,
					input.resultKey as any,
					input.index,
					input.timestamp
				)
			}),
		analyzeRun: t.procedure
			.input(
				z.object({
					agent: z.string(),
					region: z.string().nullish(),
					testId: z.string(),
					index: z.number().int().nonnegative(),
					timestamp: z.string().optional()
				})
			)
			.mutation(async ({ input }) => {
				const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
				return analyzeRunQuotes({
					agent: suiteKey,
					testId: input.testId,
					index: input.index,
					timestamp: input.timestamp
				})
			}),
		findPromptIssues: t.procedure
			.input(
				z.object({
					agent: z.string(),
					step: z.string(),
					prompt: z.string()
				})
			)
			.mutation(async ({ input }) => {
				return findPromptIssues(input)
			}),
		suite: t.procedure
			.input(z.object({ agent: z.string(), region: z.string().nullish() }))
			.query(async ({ input }) => {
				const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
				return readSuite(suiteKey)
			}),
		addTest: t.procedure
			.input(
				z.object({
					agent: z.string(),
					region: z.string().nullish(),
					mergeContexts: z.boolean(),
					seed: TestRowSchema.partial()
				})
			)
			.mutation(async ({ input }) => {
				const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
				return addTest(suiteKey, input.mergeContexts, input.seed)
			}),
		patchTest: t.procedure
			.input(
				z.object({
					agent: z.string(),
					region: z.string().nullish(),
					id: z.string(),
					patch: TestRowSchema.partial()
				})
			)
			.mutation(async ({ input }) => {
				const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
				return patchTest(suiteKey, input.id, input.patch)
			}),
		patchTestsBatch: t.procedure
			.input(
				z.object({
					agent: z.string(),
					region: z.string().nullish(),
					updates: z.array(z.object({ id: z.string(), patch: TestRowSchema.partial() }))
				})
			)
			.mutation(async ({ input }) => {
				const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
				return patchTestsBatch(suiteKey, input.updates)
			}),
		deleteTest: t.procedure
			.input(z.object({ agent: z.string(), region: z.string().nullish(), id: z.string() }))
			.mutation(async ({ input }) => {
				const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
				return deleteTest(suiteKey, input.id)
			}),
		reorder: t.procedure
			.input(
				z.object({ agent: z.string(), region: z.string().nullish(), order: z.array(z.string()) })
			)
			.mutation(async ({ input }) => {
				const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
				return reorderTests(suiteKey, input.order)
			}),
		runTests: t.procedure
			.input(
				z.union([
					z
						.object({
							agent: z.string(),
							region: z.string().nullish(),
							scope: z.enum(['pinned', 'all']).optional()
						})
						.strict(),
					z
						.object({
							agent: z.string(),
							region: z.string().nullish(),
							testId: z.string()
						})
						.strict()
				])
			)
			.mutation(async ({ input }) => {
				const selection = await resolveSuiteSelection(input.agent, input.region)
				if ('testId' in input) {
					return runPromptTestsManaged(selection, { testId: input.testId })
				}
				return runPromptTestsManaged(selection, { scope: input.scope ?? 'pinned' })
			}),
		cancelRun: t.procedure
			.input(z.object({ agent: z.string(), region: z.string().nullish() }))
			.mutation(async ({ input }) => {
				const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
				return cancelPromptTests(suiteKey)
			})
	})
}
