import { formatMCDate } from './date';
import { m } from '$lib/paraglide/messages.js';

/**
 * Precision of a document's date — mirrors the backend {@code DatePrecision} enum
 * and the import normalizer's seven values verbatim.
 *
 * DRIFT RISK: this is a hand-maintained mirror of the Java {@code DatePrecision}
 * enum, NOT an OpenAPI-generated type. It must be updated manually whenever the
 * Java enum changes, and must NOT be migrated to the generated API type — the
 * generated enum is request/response-shaped, while this drives the shared
 * client-side formatter (used by both documents and the timeline façade). Keep
 * the two in lockstep by hand.
 */
export type DatePrecision = 'DAY' | 'MONTH' | 'SEASON' | 'YEAR' | 'RANGE' | 'APPROX' | 'UNKNOWN';

/**
 * Renders a document date at exactly the precision the data claims — never finer.
 *
 * Every structured part (month name, day-of-month text, season word, prefixes)
 * is rendered in the active `locale` — DAY, MONTH and RANGE all go through
 * `Intl.DateTimeFormat(locale, …)` and the localized words through Paraglide —
 * so an `en`/`es` reader never sees a German month name. The `T12:00:00`
 * UTC-safety convention is kept via {@link noon}.
 *
 * The label is the SINGLE SOURCE OF TRUTH shared with the Java
 * {@code DocumentTitleFormatter}: both are asserted against
 * `docs/date-label-fixtures.json` so they cannot drift. The untrusted `raw`
 * cell is only used to derive a season word (a known German season token) — it
 * is never displayed and never interpolated into HTML here.
 *
 * @param iso the sort/filter anchor day (`YYYY-MM-DD`), nullable for UNKNOWN rows
 * @param precision descriptive precision metadata
 * @param end the RANGE end day; null means an open-ended range
 * @param raw the verbatim spreadsheet cell, used only for the SEASON word
 * @param locale BCP 47 tag for the localized structured parts (default `de-DE`)
 */
export function formatDocumentDate(
	iso: string | null | undefined,
	precision: DatePrecision,
	end?: string | null,
	raw?: string | null,
	locale: string = 'de-DE'
): string {
	if (precision === 'UNKNOWN' || !iso) {
		return m.date_precision_unknown(undefined, { locale: messageLocale(locale) });
	}

	const year = iso.slice(0, 4);

	switch (precision) {
		case 'DAY':
			return longDate(iso, locale);
		case 'MONTH':
			return monthYear(iso, locale);
		case 'SEASON':
			return seasonLabel(iso, raw, locale, year);
		case 'YEAR':
			return year;
		case 'APPROX':
			return `${m.date_precision_approx_prefix(undefined, { locale: messageLocale(locale) })} ${year}`;
		case 'RANGE':
			return rangeLabel(iso, end, locale);
		default:
			return m.date_precision_unknown(undefined, { locale: messageLocale(locale) });
	}
}

/**
 * Formats one nullable date at the precision the data claims, delegating all
 * rendering to {@link formatDocumentDate}. Returns '' for a missing date; a
 * missing precision falls back to YEAR — pre-precision rows knew only a year,
 * and a bare year is the only safe rendering without precision metadata.
 *
 * This is the shared core of {@link formatLifeDate} (person birth/death) and the
 * relationship from/to formatter. Range-level glyphs and dashes belong in those
 * domain wrappers, never here.
 */
export function formatDatePart(
	date: string | null | undefined,
	precision: DatePrecision | null | undefined,
	locale?: string
): string {
	if (!date) {
		return '';
	}
	return formatDocumentDate(date, precision ?? 'YEAR', null, null, locale);
}

// ─── precision branches ──────────────────────────────────────────────────────

function longDate(iso: string, locale: string): string {
	return new Intl.DateTimeFormat(locale, {
		day: 'numeric',
		month: 'long',
		year: 'numeric'
	}).format(noon(iso));
}

function monthYear(iso: string, locale: string): string {
	return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(noon(iso));
}

function seasonLabel(
	iso: string,
	raw: string | null | undefined,
	locale: string,
	year: string
): string {
	const month = Number(iso.slice(5, 7));
	// Prefer the season named in the raw cell; fall back to deriving it from the
	// anchor month. Either way the WORD is localized (Decision 4).
	const season = seasonFromRaw(raw) ?? seasonOfMonth(month);
	return `${seasonWord(season, locale)} ${year}`;
}

function rangeLabel(iso: string, end: string | null | undefined, locale: string): string {
	if (!end) {
		return `${m.date_range_open_prefix(undefined, { locale: messageLocale(locale) })} ${formatMCDate(iso, locale)}`;
	}
	if (end === iso) {
		return formatMCDate(iso, locale);
	}
	const start = noon(iso);
	const finish = noon(end);
	if (start.getFullYear() === finish.getFullYear()) {
		return sameYearRange(end, start, finish, locale);
	}
	return `${formatMCDate(iso, locale)} – ${formatMCDate(end, locale)}`;
}

function sameYearRange(end: string, start: Date, finish: Date, locale: string): string {
	if (start.getMonth() === finish.getMonth()) {
		// Collapse the shared month/year: only the end carries "DD. Mon. YYYY".
		return `${start.getDate()}.–${formatMCDate(end, locale)}`;
	}
	const startNoYear = new Intl.DateTimeFormat(locale, { day: 'numeric', month: 'short' }).format(
		start
	);
	return `${startNoYear} – ${formatMCDate(end, locale)}`;
}

// ─── season helpers ──────────────────────────────────────────────────────────

type Season = 'spring' | 'summer' | 'autumn' | 'winter';

/** Quarter buckets; matches the normalizer's representative months (4/7/10/1). */
function seasonOfMonth(month: number): Season {
	if (month >= 3 && month <= 5) return 'spring';
	if (month >= 6 && month <= 8) return 'summer';
	if (month >= 9 && month <= 11) return 'autumn';
	return 'winter';
}

function seasonWord(season: Season, locale: string): string {
	const opts = { locale: messageLocale(locale) };
	switch (season) {
		case 'spring':
			return m.date_season_spring(undefined, opts);
		case 'summer':
			return m.date_season_summer(undefined, opts);
		case 'autumn':
			return m.date_season_autumn(undefined, opts);
		case 'winter':
			return m.date_season_winter(undefined, opts);
	}
}

/** Maps a German season token at the start of the raw cell to a Season, else null. */
function seasonFromRaw(raw: string | null | undefined): Season | null {
	if (!raw) return null;
	const token = raw.trim().split(/\s+/)[0].toLowerCase();
	const byToken: Record<string, Season> = {
		frühling: 'spring',
		frühjahr: 'spring',
		sommer: 'summer',
		herbst: 'autumn',
		winter: 'winter'
	};
	return byToken[token] ?? null;
}

// ─── shared utilities ────────────────────────────────────────────────────────

function noon(iso: string): Date {
	return new Date(iso + 'T12:00:00');
}

/** Paraglide expects a registered locale tag; map `de-DE` → `de` etc. */
function messageLocale(locale: string): 'de' | 'en' | 'es' {
	const base = locale.slice(0, 2);
	if (base === 'en') return 'en';
	if (base === 'es') return 'es';
	return 'de';
}
