import type { components } from '$lib/generated/api';

/**
 * Pure month-bucket math shared by the document density chart (`lib/document/`)
 * and the global timeline strip (`lib/timeline/`). Reuses the generated
 * `MonthBucket` schema type so both surfaces stay coupled to the backend shape.
 * No I/O, no DOM — relocated here so `lib/timeline/` never imports `lib/document/`.
 */
export type MonthBucket = components['schemas']['MonthBucket'];

export function monthBoundaryFrom(yearMonth: string): string {
	return `${yearMonth}-01`;
}

export function monthBoundaryTo(yearMonth: string): string {
	const [year, month] = yearMonth.split('-').map(Number);
	// Day 0 of `month + 1` rolls back to the last day of `month` — so passing
	// `month` (1-indexed) into `Date.UTC(year, month, 0)` lands on the last day
	// of that month. Handles 28/29/30/31 and leap years without a lookup table.
	const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
	return `${yearMonth}-${String(lastDay).padStart(2, '0')}`;
}

export function buildMonthSequence(minDate: string | null, maxDate: string | null): string[] {
	if (!minDate || !maxDate) return [];

	const [minY, minM] = minDate.split('-').map(Number);
	const [maxY, maxM] = maxDate.split('-').map(Number);

	const sequence: string[] = [];
	let year = minY;
	let month = minM;

	while (year < maxY || (year === maxY && month <= maxM)) {
		sequence.push(`${year}-${String(month).padStart(2, '0')}`);
		month += 1;
		if (month > 12) {
			month = 1;
			year += 1;
		}
	}

	return sequence;
}

export function fillDensityGaps(
	buckets: MonthBucket[],
	minDate: string | null,
	maxDate: string | null
): MonthBucket[] {
	const sequence = buildMonthSequence(minDate, maxDate);
	if (sequence.length === 0) return [];

	const counts = new Map(buckets.map((b) => [b.month, b.count]));
	return sequence.map((month) => ({ month, count: counts.get(month) ?? 0 }));
}

/**
 * Returns only the month buckets whose YYYY-MM falls inside the provided
 * `[fromInclusive, toInclusive]` ISO date range. When either bound is null the
 * input array is returned unchanged. Used by the timeline's zoom-in tool to
 * narrow the visible bars without refetching data.
 *
 * @internal Sole call site is `TimelineDensityFilter.svelte`. Exported so the
 * unit suite (`monthBuckets.spec.ts`) can pin the boundary semantics directly.
 */
export function clipBucketsToRange(
	buckets: MonthBucket[],
	fromInclusive: string | null,
	toInclusive: string | null
): MonthBucket[] {
	if (!fromInclusive || !toInclusive) return buckets;
	const fromMonth = fromInclusive.slice(0, 7);
	const toMonth = toInclusive.slice(0, 7);
	return buckets.filter((b) => b.month >= fromMonth && b.month <= toMonth);
}

/**
 * Aggregates month-granular buckets into one entry per year. Month strings are
 * truncated to "YYYY" and counts are summed. Used when the date span is too
 * long for month-granular bars to render at a clickable size.
 */
export function aggregateToYears(buckets: MonthBucket[]): MonthBucket[] {
	const totals = new Map<string, number>();
	for (const b of buckets) {
		const year = b.month.slice(0, 4);
		totals.set(year, (totals.get(year) ?? 0) + b.count);
	}
	return Array.from(totals.entries())
		.map(([year, count]) => ({ month: year, count }))
		.sort((a, b) => a.month.localeCompare(b.month));
}

/**
 * Boundary helpers for selection. Accept either "YYYY-MM" (month) or "YYYY"
 * (year) and return the matching LocalDate string.
 */
export function selectionBoundaryFrom(label: string): string {
	return label.length === 4 ? `${label}-01-01` : `${label}-01`;
}

export function selectionBoundaryTo(label: string): string {
	if (label.length === 4) return `${label}-12-31`;
	return monthBoundaryTo(label);
}

/**
 * Picks bucket indices that should get an X-axis tick label. The strategy adapts
 * to whether bars are years or months and how many are visible:
 *  - Year bars: pick years divisible by a step that scales with range length
 *    (every 25 yrs for >120 bars, every 20 / 10 / 5 / 1 below).
 *  - Month bars: prefer January boundaries (year breaks). For ≤18 bars (e.g.
 *    one year zoomed in to months), fall back to evenly spaced ticks so we
 *    show ~6 labels even when no January boundary exists.
 */
export function tickIndicesFor(filled: MonthBucket[]): number[] {
	if (filled.length === 0) return [];
	const isYearMode = filled[0].month.length === 4;
	const indices: number[] = [];

	if (isYearMode) {
		const years = filled.length;
		const step =
			years > 120 ? 25 : years > 60 ? 20 : years > 30 ? 10 : years > 12 ? 5 : years > 6 ? 2 : 1;
		for (let i = 0; i < filled.length; i++) {
			const year = parseInt(filled[i].month, 10);
			if (year % step === 0) indices.push(i);
		}
		return indices;
	}

	if (filled.length <= 18) {
		const step = Math.max(1, Math.round(filled.length / 6));
		for (let i = 0; i < filled.length; i += step) indices.push(i);
		return indices;
	}

	// Long month range — pick January boundaries (year breaks).
	for (let i = 0; i < filled.length; i++) {
		if (filled[i].month.endsWith('-01')) indices.push(i);
	}
	// Fallback if there's no January in the visible range (rare): even spacing.
	if (indices.length === 0) {
		const step = Math.max(1, Math.round(filled.length / 6));
		for (let i = 0; i < filled.length; i += step) indices.push(i);
	}
	return indices;
}

/**
 * Formats a bucket month label ("YYYY" or "YYYY-MM") for the X-axis. When
 * `omitYear` is true the year is dropped so a 12-month zoomed view reads as
 * "Jan", "Feb", … without repetition.
 */
export function formatTickLabel(label: string, locale?: string, omitYear = false): string {
	if (label.length === 4) return label;
	const [yearStr, monthStr] = label.split('-');
	const date = new Date(parseInt(yearStr, 10), parseInt(monthStr, 10) - 1, 1);
	const opts: Intl.DateTimeFormatOptions = omitYear
		? { month: 'short' }
		: { month: 'short', year: 'numeric' };
	return new Intl.DateTimeFormat(locale, opts).format(date);
}
