/**
 * Requêtes de consultation : recherche plein texte, facettes, fiches, calendrier.
 *
 * Tout se joue en SQL. La recherche plein texte passe par l'index FTS5 à
 * tokenizer `unicode61 remove_diacritics 2` — « chlordecone » retrouve
 * « chlordécone » — et le classement par `bm25()` pondère les colonnes : un mot
 * dans le titre pèse dix fois plus que dans les points clés.
 *
 * Les facettes sont calculées sur le résultat filtré **par les autres
 * facettes**, pas sur la table entière : cocher « santé » doit recompter les
 * statuts disponibles parmi les textes de santé, sinon les compteurs mentent.
 */
import { base } from '$serveur/db';
import {
	LIBELLES_CONFIANCE,
	LIBELLES_IMPACT,
	LIBELLES_PERTINENCE,
	LIBELLES_STATUT,
	LIBELLES_THEME,
	LIBELLES_TYPE
} from '$lib/libelles';
import type {
	Echeance,
	Facette,
	Filtres,
	Insight,
	Resultats,
	Texte,
	TexteDetaille
} from '$lib/types';

const PAR_PAGE = 20;

/** Pondération bm25 : titre court, titre officiel, résumé, points clés, note, numéro. */
const POIDS_BM25 = '10.0, 5.0, 3.0, 2.0, 2.0, 1.0';

// ─────────────────────────────────────────────────────────────────────────────
// Lecture des filtres depuis l'URL
// ─────────────────────────────────────────────────────────────────────────────
export function lireFiltres(parametres: URLSearchParams): Filtres {
	const multiple = (nom: string) =>
		parametres
			.getAll(nom)
			.flatMap((valeur) => valeur.split(','))
			.map((valeur) => valeur.trim())
			.filter(Boolean);

	const page = Number.parseInt(parametres.get('page') ?? '1', 10);

	return {
		q: (parametres.get('q') ?? '').trim(),
		statut: multiple('statut'),
		theme: multiple('theme'),
		type: multiple('type'),
		impact: multiple('impact'),
		guadeloupe: multiple('guadeloupe'),
		confiance: multiple('confiance'),
		from: parametres.get('from'),
		to: parametres.get('to'),
		tri: parametres.get('tri') === 'date' ? 'date' : 'pertinence',
		page: Number.isFinite(page) && page > 0 ? page : 1
	};
}

/** Reconstruit une URL de recherche en basculant une valeur de facette. */
export function basculerFacette(
	filtres: Filtres,
	champ: keyof Filtres,
	valeur: string
): string {
	const parametres = new URLSearchParams();
	if (filtres.q) parametres.set('q', filtres.q);

	for (const nom of ['statut', 'theme', 'type', 'impact', 'guadeloupe', 'confiance'] as const) {
		let valeurs = [...filtres[nom]];
		if (nom === champ) {
			valeurs = valeurs.includes(valeur)
				? valeurs.filter((v) => v !== valeur)
				: [...valeurs, valeur];
		}
		for (const v of valeurs) parametres.append(nom, v);
	}

	if (filtres.from) parametres.set('from', filtres.from);
	if (filtres.to) parametres.set('to', filtres.to);
	if (filtres.tri !== 'pertinence') parametres.set('tri', filtres.tri);

	const chaine = parametres.toString();
	return chaine ? `/recherche?${chaine}` : '/recherche';
}

// ─────────────────────────────────────────────────────────────────────────────
// Construction du SQL
// ─────────────────────────────────────────────────────────────────────────────
interface Clause {
	sql: string;
	parametres: unknown[];
}

/**
 * Conditions issues des filtres, hors recherche plein texte.
 *
 * `exclure` permet de recalculer les facettes d'un champ en ignorant ses
 * propres sélections — c'est ce qui rend les compteurs honnêtes.
 */
function conditions(filtres: Filtres, exclure?: string): Clause {
	const morceaux: string[] = [];
	const parametres: unknown[] = [];

	if (filtres.statut.length && exclure !== 'statut') {
		// `saisie_cc` n'est pas seulement un statut : c'est aussi l'état des
		// textes dont une affaire est en instance devant le Conseil. La vue
		// `v_textes` expose cette lecture combinée sous `devant_cc`.
		const statutsSimples = filtres.statut.filter((s) => s !== 'saisie_cc');
		const clauses: string[] = [];
		if (statutsSimples.length) {
			clauses.push(`statut IN (${statutsSimples.map(() => '?').join(', ')})`);
			parametres.push(...statutsSimples);
		}
		if (filtres.statut.includes('saisie_cc')) clauses.push('devant_cc = 1');
		morceaux.push(`(${clauses.join(' OR ')})`);
	}

	if (filtres.theme.length && exclure !== 'theme') {
		const clauses = filtres.theme.map(
			() => `EXISTS (SELECT 1 FROM json_each(themes) WHERE json_each.value = ?)`
		);
		morceaux.push(`(${clauses.join(' OR ')})`);
		parametres.push(...filtres.theme);
	}

	if (filtres.type.length && exclure !== 'type') {
		morceaux.push(`type IN (${filtres.type.map(() => '?').join(', ')})`);
		parametres.push(...filtres.type);
	}

	if (filtres.guadeloupe.length && exclure !== 'guadeloupe') {
		morceaux.push(
			`guadeloupe_pertinence IN (${filtres.guadeloupe.map(() => '?').join(', ')})`
		);
		parametres.push(...filtres.guadeloupe);
	}

	if (filtres.confiance.length && exclure !== 'confiance') {
		morceaux.push(`confiance IN (${filtres.confiance.map(() => '?').join(', ')})`);
		parametres.push(...filtres.confiance);
	}

	if (filtres.impact.length && exclure !== 'impact') {
		// Un filtre d'impact s'écrit « public:sens », par exemple
		// « associations:negatif ». Les deux moitiés doivent correspondre
		// ensemble, d'où l'accès direct par clé JSON.
		const clauses = filtres.impact.map(() => `json_extract(impacts, ?) = ?`);
		morceaux.push(`(${clauses.join(' OR ')})`);
		for (const valeur of filtres.impact) {
			const [pub, sens] = valeur.split(':');
			parametres.push(`$.${pub}.sens`, sens);
		}
	}

	if (filtres.from) {
		morceaux.push(`COALESCE(date_promulgation, date_adoption, statut_date) >= ?`);
		parametres.push(filtres.from);
	}
	if (filtres.to) {
		morceaux.push(`COALESCE(date_promulgation, date_adoption, statut_date) <= ?`);
		parametres.push(filtres.to);
	}

	return { sql: morceaux.length ? morceaux.join(' AND ') : '1 = 1', parametres };
}

/**
 * Traduit une saisie libre en requête FTS5.
 *
 * Chaque terme est cité pour neutraliser la syntaxe FTS (`NEAR`, `*`, `:`)
 * qu'un visiteur ne saisit pas volontairement, et suffixé d'une étoile pour
 * accepter les préfixes : « chlord » trouve « chlordécone ».
 */
function requeteFts(saisie: string): string {
	const termes = saisie
		.replace(/["^*:()]/g, ' ')
		.split(/\s+/)
		.map((terme) => terme.trim())
		.filter((terme) => terme.length > 1);

	if (!termes.length) return '';
	return termes.map((terme) => `"${terme}"*`).join(' AND ');
}

// ─────────────────────────────────────────────────────────────────────────────
// Recherche
// ─────────────────────────────────────────────────────────────────────────────
export function rechercher(filtres: Filtres): Resultats {
	const db = base();
	const where = conditions(filtres);
	const fts = filtres.q ? requeteFts(filtres.q) : '';

	const source = fts
		? `v_textes t JOIN textes_fts f ON f.rowid = t.rowid AND textes_fts MATCH ?`
		: `v_textes t`;
	const parametresSource = fts ? [fts] : [];

	const ordre = fts
		? filtres.tri === 'date'
			? `COALESCE(t.date_promulgation, t.date_adoption, t.statut_date) DESC`
			: `bm25(textes_fts, ${POIDS_BM25})`
		: `COALESCE(t.date_promulgation, t.date_adoption, t.statut_date) DESC, t.id`;

	const total = (
		db
			.prepare(`SELECT COUNT(*) AS n FROM ${source} WHERE ${where.sql}`)
			.get(...parametresSource, ...where.parametres) as { n: number }
	).n;

	const extrait = fts
		? `snippet(textes_fts, -1, '<mark>', '</mark>', '…', 24) AS extrait`
		: `NULL AS extrait`;

	const lignes = db
		.prepare(
			`SELECT t.*, ${extrait}
			   FROM ${source}
			  WHERE ${where.sql}
			  ORDER BY ${ordre}
			  LIMIT ? OFFSET ?`
		)
		.all(
			...parametresSource,
			...where.parametres,
			PAR_PAGE,
			(filtres.page - 1) * PAR_PAGE
		) as Record<string, unknown>[];

	return {
		textes: lignes.map(hydrater),
		total,
		page: filtres.page,
		pages: Math.max(1, Math.ceil(total / PAR_PAGE)),
		facettes: {
			statut: facetteStatut(filtres, fts),
			theme: facetteTheme(filtres, fts),
			type: facetteSimple(filtres, fts, 'type', LIBELLES_TYPE),
			guadeloupe: facetteSimple(
				filtres,
				fts,
				'guadeloupe_pertinence',
				LIBELLES_PERTINENCE,
				'guadeloupe'
			),
			impact: facetteImpact(filtres, fts),
			confiance: facetteSimple(filtres, fts, 'confiance', LIBELLES_CONFIANCE)
		}
	};
}

function sourceEtParametres(fts: string): [string, unknown[]] {
	return fts
		? [`v_textes t JOIN textes_fts f ON f.rowid = t.rowid AND textes_fts MATCH ?`, [fts]]
		: [`v_textes t`, []];
}

function facetteSimple(
	filtres: Filtres,
	fts: string,
	colonne: string,
	libelles: Record<string, string>,
	champFiltre?: keyof Filtres
): Facette[] {
	const champ = (champFiltre ?? colonne) as keyof Filtres;
	const where = conditions(filtres, champ as string);
	const [source, parametresSource] = sourceEtParametres(fts);

	const lignes = base()
		.prepare(
			`SELECT ${colonne} AS valeur, COUNT(*) AS nombre
			   FROM ${source}
			  WHERE ${where.sql} AND ${colonne} IS NOT NULL
			  GROUP BY ${colonne}
			  ORDER BY nombre DESC`
		)
		.all(...parametresSource, ...where.parametres) as { valeur: string; nombre: number }[];

	const selection = filtres[champ] as string[];
	return lignes.map((ligne) => ({
		valeur: ligne.valeur,
		libelle: libelles[ligne.valeur] ?? ligne.valeur,
		nombre: ligne.nombre,
		actif: selection.includes(ligne.valeur)
	}));
}

function facetteStatut(filtres: Filtres, fts: string): Facette[] {
	const facettes = facetteSimple(filtres, fts, 'statut', LIBELLES_STATUT);

	// Facette dérivée : « devant le Conseil constitutionnel » recouvre à la
	// fois le statut `saisie_cc` et les textes ayant une affaire en instance.
	const where = conditions(filtres, 'statut');
	const [source, parametresSource] = sourceEtParametres(fts);
	const nombre = (
		base()
			.prepare(`SELECT COUNT(*) AS n FROM ${source} WHERE ${where.sql} AND devant_cc = 1`)
			.get(...parametresSource, ...where.parametres) as { n: number }
	).n;

	if (nombre > 0 && !facettes.some((f) => f.valeur === 'saisie_cc')) {
		facettes.unshift({
			valeur: 'saisie_cc',
			libelle: LIBELLES_STATUT.saisie_cc,
			nombre,
			actif: filtres.statut.includes('saisie_cc')
		});
	}

	return facettes;
}

function facetteTheme(filtres: Filtres, fts: string): Facette[] {
	const where = conditions(filtres, 'theme');
	const [source, parametresSource] = sourceEtParametres(fts);

	const lignes = base()
		.prepare(
			`SELECT json_each.value AS valeur, COUNT(*) AS nombre
			   FROM ${source}, json_each(t.themes)
			  WHERE ${where.sql}
			  GROUP BY json_each.value
			  ORDER BY nombre DESC`
		)
		.all(...parametresSource, ...where.parametres) as { valeur: string; nombre: number }[];

	return lignes.map((ligne) => ({
		valeur: ligne.valeur,
		libelle: LIBELLES_THEME[ligne.valeur] ?? ligne.valeur,
		nombre: ligne.nombre,
		actif: filtres.theme.includes(ligne.valeur)
	}));
}

function facetteImpact(filtres: Filtres, fts: string): Facette[] {
	const where = conditions(filtres, 'impact');
	const [source, parametresSource] = sourceEtParametres(fts);

	const lignes = base()
		.prepare(
			`SELECT json_each.key || ':' || json_extract(json_each.value, '$.sens') AS valeur,
			        COUNT(*) AS nombre
			   FROM ${source}, json_each(t.impacts)
			  WHERE ${where.sql}
			  GROUP BY valeur
			  ORDER BY nombre DESC`
		)
		.all(...parametresSource, ...where.parametres) as { valeur: string; nombre: number }[];

	return lignes
		.filter((ligne) => !ligne.valeur.endsWith(':neutre'))
		.map((ligne) => ({
			valeur: ligne.valeur,
			libelle: LIBELLES_IMPACT[ligne.valeur] ?? ligne.valeur,
			nombre: ligne.nombre,
			actif: filtres.impact.includes(ligne.valeur)
		}));
}

// ─────────────────────────────────────────────────────────────────────────────
// Fiche détaillée
// ─────────────────────────────────────────────────────────────────────────────
export function texte(slug: string): TexteDetaille | null {
	const db = base();
	const ligne = db.prepare(`SELECT * FROM v_textes WHERE id = ?`).get(slug) as
		| Record<string, unknown>
		| undefined;
	if (!ligne) return null;

	const detaille = hydrater(ligne) as TexteDetaille;

	detaille.sources = db
		.prepare(
			`SELECT url, titre, editeur, date_publication, tier, extrait_verbatim,
			        contexte, confiance, marqueur, fichier_origine
			   FROM sources WHERE texte_id = ?
			  ORDER BY tier ASC, date_publication DESC NULLS LAST, id`
		)
		.all(slug) as TexteDetaille['sources'];

	detaille.evenements = db
		.prepare(
			`SELECT date_evenement, type_etape, description, source_url, previsionnel
			   FROM evenements WHERE texte_id = ?
			  ORDER BY date_evenement ASC, id`
		)
		.all(slug) as TexteDetaille['evenements'];

	detaille.decisions_cc = db
		.prepare(`SELECT * FROM decisions_cc WHERE texte_id = ? ORDER BY numero_affaire`)
		.all(slug) as TexteDetaille['decisions_cc'];

	return detaille;
}

export function tousLesSlugs(): string[] {
	return (base().prepare(`SELECT id FROM textes ORDER BY id`).all() as { id: string }[]).map(
		(l) => l.id
	);
}

// ─────────────────────────────────────────────────────────────────────────────
// Tableau de bord
// ─────────────────────────────────────────────────────────────────────────────
export interface Compteurs {
	total: number;
	parStatut: { valeur: string; libelle: string; nombre: number }[];
	devantCc: number;
	guadeloupeForte: number;
	aVerifier: number;
	sources: number;
}

export function compteurs(): Compteurs {
	const db = base();
	const un = (sql: string) => (db.prepare(sql).get() as { n: number }).n;

	return {
		total: un(`SELECT COUNT(*) AS n FROM textes`),
		parStatut: (
			db
				.prepare(
					`SELECT statut AS valeur, COUNT(*) AS nombre FROM textes
					  GROUP BY statut ORDER BY nombre DESC`
				)
				.all() as { valeur: string; nombre: number }[]
		).map((l) => ({ ...l, libelle: LIBELLES_STATUT[l.valeur] ?? l.valeur })),
		devantCc: un(`SELECT COUNT(*) AS n FROM v_textes WHERE devant_cc = 1`),
		guadeloupeForte: un(
			`SELECT COUNT(*) AS n FROM textes WHERE guadeloupe_pertinence = 'forte'`
		),
		aVerifier: un(`SELECT COUNT(*) AS n FROM textes WHERE a_verifier = 1`),
		sources: un(`SELECT COUNT(*) AS n FROM sources`)
	};
}

/** Échéances des `jours` prochains jours, textes et repères nationaux mêlés. */
export function prochainesEcheances(jours = 60, aPartirDe?: string): Echeance[] {
	const depart = aPartirDe ?? '2026-07-25';
	const fin = new Date(depart);
	fin.setDate(fin.getDate() + jours);
	const limite = fin.toISOString().slice(0, 10);

	const db = base();

	const nationales = db
		.prepare(
			`SELECT * FROM echeances
			  WHERE date_echeance >= ? AND date_echeance <= ?
			  ORDER BY date_echeance`
		)
		.all(depart, limite) as Echeance[];

	const desTextes = db
		.prepare(
			`SELECT NULL AS id, 'texte-' || id AS code, prochaine_echeance AS date_echeance,
			        titre_court AS libelle, prochaine_echeance_label AS description,
			        'texte' AS portee, id AS texte_id,
			        CASE WHEN guadeloupe_pertinence = 'forte' THEN 1 ELSE NULL END
			            AS concerne_guadeloupe,
			        guadeloupe_note AS note_guadeloupe, 1 AS previsionnel,
			        NULL AS source_url, NULL AS source_extrait
			   FROM textes
			  WHERE prochaine_echeance IS NOT NULL
			    AND prochaine_echeance >= ? AND prochaine_echeance <= ?
			  ORDER BY prochaine_echeance`
		)
		.all(depart, limite) as Echeance[];

	return [...nationales, ...desTextes].sort((a, b) =>
		a.date_echeance.localeCompare(b.date_echeance)
	);
}

/** Toutes les échéances, pour la page calendrier. */
export function calendrier(): Echeance[] {
	return prochainesEcheances(400, '2026-07-01');
}

export function decisionsAttendues(): (Echeance & { numero_affaire: string })[] {
	return base()
		.prepare(
			`SELECT d.numero_affaire, d.date_decision_attendue AS date_echeance,
			        COALESCE(t.titre_court, d.resume) AS libelle, d.resume AS description,
			        d.texte_id, t.guadeloupe_pertinence
			   FROM decisions_cc d
			   LEFT JOIN textes t ON t.id = d.texte_id
			  WHERE d.date_decision IS NULL
			  ORDER BY COALESCE(d.date_decision_attendue, '9999'), d.numero_affaire`
		)
		.all() as (Echeance & { numero_affaire: string })[];
}

export function insights(): Insight[] {
	return (
		base().prepare(`SELECT * FROM insights ORDER BY numero`).all() as Record<
			string,
			unknown
		>[]
	).map((ligne) => ({
		numero: ligne.numero as number,
		titre: ligne.titre as string,
		corps: ligne.corps as string,
		implications: ligne.implications as string | null,
		confiance: ligne.confiance as string,
		derive_de: JSON.parse((ligne.derive_de as string) || '[]')
	}));
}

export interface Changement {
	horodatage: string;
	mode: string;
	texte_id: string | null;
	titre: string | null;
	nature: string;
	champ: string | null;
	ancienne_valeur: string | null;
	nouvelle_valeur: string | null;
	description: string | null;
}

/** Derniers changements détectés par le pipeline, pour l'accueil. */
export function derniersChangements(limite = 12): Changement[] {
	return base()
		.prepare(
			`SELECT v.horodatage, v.mode, c.texte_id, t.titre_court AS titre,
			        c.nature, c.champ, c.ancienne_valeur, c.nouvelle_valeur, c.description
			   FROM veille_changements c
			   JOIN veille_log v ON v.id = c.run_id
			   LEFT JOIN textes t ON t.id = c.texte_id
			  ORDER BY v.horodatage DESC, c.id DESC
			  LIMIT ?`
		)
		.all(limite) as Changement[];
}

export interface Run {
	horodatage: string;
	mode: string;
	duree_s: number | null;
	ajouts: number;
	modifications: number;
	echecs: number;
	rapport: string | null;
}

export function dernierRun(): Run | null {
	return (
		(base()
			.prepare(
				`SELECT horodatage, mode, duree_s, ajouts, modifications, echecs, rapport
				   FROM veille_log ORDER BY horodatage DESC, id DESC LIMIT 1`
			)
			.get() as Run | undefined) ?? null
	);
}

// ─────────────────────────────────────────────────────────────────────────────
// Hydratation
// ─────────────────────────────────────────────────────────────────────────────
function hydrater(ligne: Record<string, unknown>): Texte {
	return {
		...(ligne as unknown as Texte),
		themes: JSON.parse((ligne.themes as string) || '[]'),
		impacts: JSON.parse((ligne.impacts as string) || '{}'),
		points_cles: JSON.parse((ligne.points_cles as string) || '[]')
	};
}
