import { OBSERVER_STATUSES, type ObservedThread, type ObserverOutput, type ObserverStatus } from "./types.ts";

const MAX_THREADS = 64;
const MAX_REQUEST_LENGTH = 2_000;
const STATUS_SET = new Set<string>(OBSERVER_STATUSES);

const threadProperties = {
	canonicalRequest: { type: "string" },
	status: { type: "string", enum: [...OBSERVER_STATUSES] },
} as const;

export const OBSERVER_JSON_SCHEMA = {
	type: "object",
	additionalProperties: false,
	properties: {
		existing: {
			type: "array",
			items: {
				type: "object",
				additionalProperties: false,
				properties: { id: { type: "string" }, ...threadProperties },
				required: ["id", "canonicalRequest", "status"],
			},
		},
		additions: {
			type: "array",
			items: {
				type: "object",
				additionalProperties: false,
				properties: threadProperties,
				required: ["canonicalRequest", "status"],
			},
		},
	},
	required: ["existing", "additions"],
} as const;

const isRecord = (value: unknown): value is Record<string, unknown> =>
	typeof value === "object" && value !== null && !Array.isArray(value);

function parseThread(value: unknown, requireId: boolean): ObservedThread | undefined {
	if (!isRecord(value)) return undefined;
	const keys = Object.keys(value);
	const allowed = requireId ? new Set(["id", "canonicalRequest", "status"]) : new Set(["canonicalRequest", "status"]);
	if (keys.some((key) => !allowed.has(key))) return undefined;
	if (
		typeof value.canonicalRequest !== "string" ||
		value.canonicalRequest.trim().length === 0 ||
		value.canonicalRequest.length > MAX_REQUEST_LENGTH ||
		typeof value.status !== "string" ||
		!STATUS_SET.has(value.status) ||
		(requireId && typeof value.id !== "string")
	) {
		return undefined;
	}
	return {
		...(requireId ? { id: value.id as string } : {}),
		canonicalRequest: value.canonicalRequest.trim(),
		status: value.status as ObserverStatus,
	};
}

export function parseObserverOutput(
	raw: string,
	knownThreads: ReadonlyMap<string, string>,
	allowUserChanges: boolean,
): ObserverOutput | undefined {
	let parsed: unknown;
	try {
		parsed = JSON.parse(raw);
	} catch {
		return undefined;
	}
	if (!isRecord(parsed) || !Array.isArray(parsed.existing) || !Array.isArray(parsed.additions)) return undefined;
	if (parsed.existing.length > MAX_THREADS || parsed.additions.length > MAX_THREADS) return undefined;
	if (!allowUserChanges && parsed.additions.length > 0) return undefined;
	const existing = parsed.existing.map((value) => parseThread(value, true));
	const additions = parsed.additions.map((value) => parseThread(value, false));
	if (existing.some((thread) => thread === undefined) || additions.some((thread) => thread === undefined)) {
		return undefined;
	}
	const ids = existing.map((thread) => thread!.id!);
	if (new Set(ids).size !== ids.length || ids.some((id) => !knownThreads.has(id))) return undefined;
	if (ids.length !== knownThreads.size || [...knownThreads.keys()].some((id) => !ids.includes(id))) return undefined;
	if (
		!allowUserChanges &&
		(existing as ObservedThread[]).some(
			(thread) => thread.canonicalRequest !== knownThreads.get(thread.id!),
		)
	) {
		return undefined;
	}
	return {
		existing: existing as ObservedThread[],
		additions: additions as ObservedThread[],
	};
}
