/**
 * Schema bundle loader for the MCP runtime hook.
 *
 * Reads a JSON Schema bundle produced by `@abraca/schema` codegen, compiles
 * one Ajv validator per declared doc-type, and returns a
 * {@link SchemaBundleValidator} that the MCP tool handlers consult before
 * forwarding `meta` writes to the server.
 *
 * Bundle format (see abracadabra-ts/packages/schema/scripts/codegen-json-schema.ts):
 *
 * ```jsonc
 * { "schemaVersion": 1,
 *   "label": "...",
 *   "types": {
 *     "<doc-type>": {
 *       "version": 1,
 *       "meta":     { ...JSON Schema 2020-12... },
 *       "body":     "ytext" | "yarray" | "ymap" | "none",
 *       "children": ["..."]
 *     } } }
 * ```
 *
 * Loader errors at boot are *fatal* — caller is expected to abort startup
 * with a clear message. We do not silently fall back to "no validation".
 */

import { readFileSync } from 'node:fs'
import Ajv2020 from 'ajv/dist/2020.js'
import addFormats from 'ajv-formats'
import type { ValidateFunction } from 'ajv/dist/2020.js'
import type {
	MetaValidationIssue,
	MetaValidationResult,
	SchemaBundleValidator,
} from './validator.ts'

interface BundleDoc {
	version: number
	meta: Record<string, unknown>
	body: 'ytext' | 'yarray' | 'ymap' | 'none'
	children: ReadonlyArray<string>
}

interface Bundle {
	schemaVersion: number
	label?: string
	types: Record<string, BundleDoc>
}

const SUPPORTED_BUNDLE_VERSION = 1

export class SchemaBundleLoadError extends Error {
	constructor(message: string, readonly cause?: unknown) {
		super(message)
		this.name = 'SchemaBundleLoadError'
	}
}

/**
 * Read, parse, and compile a JSON Schema bundle into a runtime validator.
 *
 * @throws {SchemaBundleLoadError} if the file is missing, malformed, has
 *   an unsupported `schemaVersion`, or any per-type schema fails to compile.
 */
export function loadSchemaBundle(path: string): SchemaBundleValidator {
	let raw: string
	try {
		raw = readFileSync(path, 'utf8')
	} catch (err) {
		throw new SchemaBundleLoadError(
			`Failed to read schema bundle from ${path}: ${(err as Error).message}`,
			err,
		)
	}

	let bundle: Bundle
	try {
		bundle = JSON.parse(raw) as Bundle
	} catch (err) {
		throw new SchemaBundleLoadError(
			`Schema bundle at ${path} is not valid JSON: ${(err as Error).message}`,
			err,
		)
	}

	if (bundle.schemaVersion !== SUPPORTED_BUNDLE_VERSION) {
		throw new SchemaBundleLoadError(
			`Schema bundle at ${path} has schemaVersion ${bundle.schemaVersion}; this MCP server supports ${SUPPORTED_BUNDLE_VERSION}`,
		)
	}
	if (!bundle.types || typeof bundle.types !== 'object') {
		throw new SchemaBundleLoadError(
			`Schema bundle at ${path} is missing the "types" object`,
		)
	}

	const ajv = new Ajv2020({
		strict: false,
		allErrors: true,
		// JSON Schema bundles include their own $schema URI; we don't need Ajv to add another.
		validateSchema: false,
	})
	addFormats(ajv)

	const validators = new Map<string, ValidateFunction>()
	for (const [docType, decl] of Object.entries(bundle.types)) {
		try {
			const fn = ajv.compile(decl.meta)
			validators.set(docType, fn)
		} catch (err) {
			throw new SchemaBundleLoadError(
				`Schema bundle at ${path} has an invalid meta schema for "${docType}": ${(err as Error).message}`,
				err,
			)
		}
	}

	const knownTypes = Array.from(validators.keys()).sort()

	return {
		knownTypes,
		validateMeta(docType: string, meta: unknown): MetaValidationResult {
			const fn = validators.get(docType)
			if (!fn) {
				// Unknown doc-type → unconstrained (Rule 4). Existing-app traffic
				// for non-canary types is never rejected by this validator.
				return { ok: true }
			}
			const ok = fn(meta)
			if (ok) return { ok: true }
			const errors: MetaValidationIssue[] = (fn.errors ?? []).map((e) => ({
				path: e.instancePath || '/',
				message: e.message ?? 'validation failed',
				keyword: e.keyword,
			}))
			return { ok: false, errors }
		},
	}
}
