/**
 * Parser for trueref.json / context7.json repository configuration files
 * (TRUEREF-0013).
 *
 * Parsing rules:
 *  - `trueref.json` takes precedence over `context7.json`.
 *  - Invalid field types are skipped with a warning (not a hard error).
 *  - Array fields are truncated to their maximum allowed size.
 *  - String fields are truncated to their maximum allowed length.
 *  - The entire file must be a JSON object; anything else is a hard error.
 */

import type { TrueRefConfig } from '$lib/types';
import { CONFIG_CONSTRAINTS } from './trueref-config.schema.js';

// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------

export interface ParsedConfig {
	/** Validated and sanitised configuration values. */
	config: TrueRefConfig;
	/** Which config file was found: trueref.json takes precedence. */
	source: 'trueref.json' | 'context7.json';
	/** Non-fatal issues discovered during parsing. */
	warnings: string[];
}

// ---------------------------------------------------------------------------
// Domain error
// ---------------------------------------------------------------------------

export class ConfigParseError extends Error {
	constructor(message: string) {
		super(message);
		this.name = 'ConfigParseError';
	}
}

// ---------------------------------------------------------------------------
// Public parser
// ---------------------------------------------------------------------------

/**
 * Parse and sanitise the content of a `trueref.json` or `context7.json` file.
 *
 * The function is intentionally lenient: unknown keys are silently ignored,
 * mistyped values are skipped with a warning, and oversized strings/arrays are
 * truncated. Only structural problems (not valid JSON, not an object) throw.
 *
 * @param content  - Raw file content as a UTF-8 string.
 * @param filename - File name used in error messages and to determine `source`.
 * @returns ParsedConfig with sanitised config, source, and any warnings.
 * @throws ConfigParseError on invalid JSON or non-object root.
 */
export function parseConfigFile(content: string, filename: string): ParsedConfig {
	// ---- 1. JSON parse -------------------------------------------------------
	let raw: unknown;

	try {
		raw = JSON.parse(content);
	} catch (e) {
		throw new ConfigParseError(`${filename} is not valid JSON: ${(e as Error).message}`);
	}

	// ---- 2. Root must be an object ------------------------------------------
	if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
		throw new ConfigParseError(`${filename} must be a JSON object, got ${Array.isArray(raw) ? 'array' : typeof raw}`);
	}

	const input = raw as Record<string, unknown>;
	const validated: TrueRefConfig = {};
	const warnings: string[] = [];

	// ---- 3. projectTitle ----------------------------------------------------
	if (input.projectTitle !== undefined) {
		if (typeof input.projectTitle !== 'string') {
			warnings.push('projectTitle must be a string — ignoring');
		} else {
			const { maxLength } = CONFIG_CONSTRAINTS.projectTitle;
			if (input.projectTitle.length > maxLength) {
				validated.projectTitle = input.projectTitle.slice(0, maxLength);
				warnings.push(`projectTitle truncated to ${maxLength} characters`);
			} else {
				validated.projectTitle = input.projectTitle;
			}
		}
	}

	// ---- 4. description -----------------------------------------------------
	if (input.description !== undefined) {
		if (typeof input.description !== 'string') {
			warnings.push('description must be a string — ignoring');
		} else {
			const { maxLength } = CONFIG_CONSTRAINTS.description;
			validated.description = input.description.slice(0, maxLength);
			if (input.description.length > maxLength) {
				warnings.push(`description truncated to ${maxLength} characters`);
			}
		}
	}

	// ---- 5. folders / excludeFolders / excludeFiles -------------------------
	for (const field of ['folders', 'excludeFolders', 'excludeFiles'] as const) {
		if (input[field] === undefined) continue;

		if (!Array.isArray(input[field])) {
			warnings.push(`${field} must be an array — ignoring`);
			continue;
		}

		const maxItems =
			field === 'excludeFiles'
				? CONFIG_CONSTRAINTS.excludeFiles.maxItems
				: CONFIG_CONSTRAINTS.folders.maxItems;

		const maxLength =
			field === 'excludeFiles'
				? CONFIG_CONSTRAINTS.excludeFiles.maxLength
				: CONFIG_CONSTRAINTS.folders.maxLength;

		const arr = input[field] as unknown[];

		const sanitised = arr
			.filter((item): item is string => {
				if (typeof item !== 'string') {
					warnings.push(`${field} entry must be a string — skipping: ${JSON.stringify(item)}`);
					return false;
				}
				return true;
			})
			.map((item) => {
				if (item.length > maxLength) {
					warnings.push(`${field} entry truncated to ${maxLength} characters: "${item.slice(0, 40)}..."`);
					return item.slice(0, maxLength);
				}
				return item;
			})
			.slice(0, maxItems);

		if (arr.length > maxItems) {
			warnings.push(`${field} has more than ${maxItems} entries — extras dropped`);
		}

		validated[field] = sanitised;
	}

	// ---- 6. rules -----------------------------------------------------------
	if (input.rules !== undefined) {
		if (!Array.isArray(input.rules)) {
			warnings.push('rules must be an array — ignoring');
		} else {
			const { maxItems, minLength, maxLength } = CONFIG_CONSTRAINTS.rules;
			const arr = input.rules as unknown[];

			const sanitised = arr
				.filter((r): r is string => {
					if (typeof r !== 'string') {
						warnings.push(`rules entry must be a string — skipping: ${JSON.stringify(r)}`);
						return false;
					}
					if (r.length < minLength) {
						warnings.push(
							`rules entry too short (< ${minLength} chars) — skipping: "${r}"`
						);
						return false;
					}
					return true;
				})
				.map((r) => {
					if (r.length > maxLength) {
						warnings.push(`rules entry truncated to ${maxLength} characters`);
						return r.slice(0, maxLength);
					}
					return r;
				})
				.slice(0, maxItems);

			if (arr.length > maxItems) {
				warnings.push(`rules has more than ${maxItems} entries — extras dropped`);
			}

			validated.rules = sanitised;
		}
	}

	// ---- 7. previousVersions ------------------------------------------------
	if (input.previousVersions !== undefined) {
		if (!Array.isArray(input.previousVersions)) {
			warnings.push('previousVersions must be an array — ignoring');
		} else {
			const { maxItems } = CONFIG_CONSTRAINTS.previousVersions;
			const arr = input.previousVersions as unknown[];

			const sanitised = arr
				.filter((v): v is { tag: string; title: string } => {
					if (typeof v !== 'object' || v === null || Array.isArray(v)) {
						warnings.push(
							`previousVersions entry must be an object — skipping: ${JSON.stringify(v)}`
						);
						return false;
					}
					const obj = v as Record<string, unknown>;
					if (typeof obj.tag !== 'string') {
						warnings.push(
							`previousVersions entry missing string "tag" — skipping: ${JSON.stringify(v)}`
						);
						return false;
					}
					if (typeof obj.title !== 'string') {
						warnings.push(
							`previousVersions entry missing string "title" — skipping: ${JSON.stringify(v)}`
						);
						return false;
					}
					return true;
				})
				.slice(0, maxItems);

			if (arr.length > maxItems) {
				warnings.push(`previousVersions has more than ${maxItems} entries — extras dropped`);
			}

			validated.previousVersions = sanitised;
		}
	}

	// ---- 8. Determine source ------------------------------------------------
	const source: ParsedConfig['source'] = filename.toLowerCase().startsWith('trueref')
		? 'trueref.json'
		: 'context7.json';

	return { config: validated, source, warnings };
}

// ---------------------------------------------------------------------------
// Multi-file resolver
// ---------------------------------------------------------------------------

/**
 * Given an array of (filename, content) pairs found in a repo root, parse
 * whichever config file takes precedence (`trueref.json` > `context7.json`).
 *
 * Returns `null` if neither file is found in the list.
 *
 * @param candidates - Array of `{ filename, content }` objects to consider.
 */
export function resolveConfig(
	candidates: Array<{ filename: string; content: string }>
): ParsedConfig | null {
	// Prefer trueref.json over context7.json.
	const ordered = [
		candidates.find((c) => c.filename.toLowerCase() === 'trueref.json'),
		candidates.find((c) => c.filename.toLowerCase() === 'context7.json')
	].filter(Boolean) as Array<{ filename: string; content: string }>;

	if (ordered.length === 0) return null;

	const winner = ordered[0];
	return parseConfigFile(winner.content, winner.filename);
}
