/**
 * The bridge between a `datetime-local` input and the unix seconds the URL and
 * the API speak.
 *
 * Local wall-clock text is lossy in a way unix seconds are not. Twice a year a
 * fall-back fold gives two instants the same text, and a spring-forward gap
 * gives an hour of text no instant at all. So the text is never the authority:
 * a bound the operator did not touch is carried through as the number it
 * already was, and a bound they did edit is accepted only when it survives a
 * round trip unchanged.
 */

/** Zero-padded to the width the `datetime-local` grammar requires. */
function pad(value: number, width: number): string {
	return String(value).padStart(width, "0");
}

/**
 * Unix seconds → the local wall-clock text a `datetime-local` input holds,
 * always with seconds, because the inputs run at `step={1}`.
 */
export function unixToDatetimeLocal(unix: number): string {
	const date = new Date(unix * 1000);
	const day = `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1, 2)}-${pad(date.getDate(), 2)}`;
	const time = `${pad(date.getHours(), 2)}:${pad(date.getMinutes(), 2)}:${pad(date.getSeconds(), 2)}`;
	return `${day}T${time}`;
}

const DATETIME_LOCAL = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?$/;

/**
 * The text with its seconds spelled out, or undefined when it is not a
 * `datetime-local` value at all. A browser omits `:00` seconds even at
 * `step={1}`, so the canonical form is what a round trip compares against.
 *
 * The grammar allows a fractional part after the seconds and some engines emit
 * one; a bound is a whole second here and on the wire, so it is parsed and then
 * dropped rather than treated as text we do not recognise.
 */
function canonicalize(value: string): string | undefined {
	const match = DATETIME_LOCAL.exec(value);
	if (match === null) return undefined;
	return `${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6] ?? "00"}`;
}

/** Local wall-clock text → unix seconds, or undefined when it names no instant. */
export function datetimeLocalToUnix(value: string): number | undefined {
	const canonical = canonicalize(value);
	if (canonical === undefined) return undefined;
	const ms = new Date(canonical).getTime();
	return Number.isFinite(ms) ? Math.floor(ms / 1000) : undefined;
}

/**
 * One bound of the filter form: what the input shows, what the applied search
 * carried, and whether the operator has touched it since.
 */
export interface DatetimeField {
	text: string;
	/** The applied value this field was seeded from, reused while `dirty` is false. */
	original: number | undefined;
	dirty: boolean;
}

export type DatetimeResolution =
	{ ok: true; value: number | undefined } | { ok: false; reason: "unparseable" | "nonexistent" };

export function datetimeField(original: number | undefined): DatetimeField {
	return { text: original === undefined ? "" : unixToDatetimeLocal(original), original, dirty: false };
}

export function editDatetimeField(field: DatetimeField, text: string): DatetimeField {
	return { ...field, text, dirty: true };
}

/**
 * The unix value this bound applies.
 *
 * An untouched field resolves to the number it was seeded with, never to a
 * re-parse of its own text: the text of a fall-back instant names two of them,
 * and re-parsing would silently move a bound the operator never edited.
 *
 * An edited field is parsed, then formatted back. A wall-clock time inside the
 * spring-forward gap exists on no clock, and `Date` quietly slides it forward
 * an hour; the round trip catches that and the caller reports it instead of
 * filtering on an hour nobody asked for.
 */
export function resolveDatetimeField(field: DatetimeField): DatetimeResolution {
	if (!field.dirty) return { ok: true, value: field.original };
	if (field.text.trim() === "") return { ok: true, value: undefined };
	const unix = datetimeLocalToUnix(field.text);
	if (unix === undefined) return { ok: false, reason: "unparseable" };
	if (unixToDatetimeLocal(unix) !== canonicalize(field.text)) return { ok: false, reason: "nonexistent" };
	return { ok: true, value: unix };
}
