/**
 * Locale-aware date and time formatting for admin and widget surfaces.
 *
 * Instant values (RFC 3339 / UTC) are interpreted as absolute points in time
 * and shown in the configured site timezone. Wall-clock strings without an
 * offset (e.g. booking `start_local`) are formatted as-is — they already
 * represent local appointment time.
 *
 * @package
 */

export type DateFormatContext = {
	readonly locale?: string;
	readonly timezone?: string;
};

export type DateDisplayMode =
	| 'date'
	| 'time'
	| 'datetime'
	| 'appointment'
	| 'relative';

export type DateTechnicalDetail = {
	readonly label: string;
	readonly value: string;
};

const DEFAULT_LOCALE = 'en-GB';
const DEFAULT_TIMEZONE = 'UTC';

const LOCAL_WALL_CLOCK =
	/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?$/;

function localeOf( ctx: DateFormatContext ): string {
	return ctx.locale?.replace( '_', '-' ) ?? DEFAULT_LOCALE;
}

function timezoneOf( ctx: DateFormatContext ): string {
	return ctx.timezone ?? DEFAULT_TIMEZONE;
}

/**
 * Parse an ISO instant. Returns null when the value cannot be parsed.
 *
 * @param value ISO string.
 */
export function parseInstant( value: string ): Date | null {
	if ( typeof value !== 'string' ) {
		return null;
	}
	const trimmed = value.trim();
	if ( trimmed.length === 0 ) {
		return null;
	}
	const parsed = new Date( trimmed );
	if ( Number.isNaN( parsed.getTime() ) ) {
		return null;
	}
	return parsed;
}

/**
 * Whether a string looks like a wall-clock datetime without a zone suffix.
 *
 * @param value Candidate value.
 */
export function isWallClockDateTime( value: string ): boolean {
	return LOCAL_WALL_CLOCK.test( value.trim() );
}

/**
 * YYYY-MM-DD for an instant in an IANA timezone.
 *
 * @param instant  Point in time.
 * @param timezone IANA timezone name.
 */
export function calendarDayInTimezone(
	instant: Date,
	timezone: string
): string {
	try {
		return new Intl.DateTimeFormat( 'en-CA', {
			timeZone: timezone,
			year: 'numeric',
			month: '2-digit',
			day: '2-digit',
		} ).format( instant );
	} catch {
		return instant.toISOString().slice( 0, 10 );
	}
}

/**
 * Calendar date (YYYY-MM-DD) for an instant or wall-clock string in a zone.
 *
 * @param value    ISO instant or local wall-clock datetime.
 * @param timezone IANA timezone name.
 */
export function calendarDateOf(
	value: string,
	timezone: string
): string | null {
	if ( isWallClockDateTime( value ) ) {
		return value.trim().slice( 0, 10 );
	}
	const instant = parseInstant( value );
	if ( ! instant ) {
		return null;
	}
	return calendarDayInTimezone( instant, timezone );
}

function wallClockParts( value: string ): {
	year: number;
	month: number;
	day: number;
	hour: number;
	minute: number;
} | null {
	const match = LOCAL_WALL_CLOCK.exec( value.trim() );
	if ( ! match ) {
		return null;
	}
	return {
		year: Number.parseInt( match[ 1 ], 10 ),
		month: Number.parseInt( match[ 2 ], 10 ),
		day: Number.parseInt( match[ 3 ], 10 ),
		hour: Number.parseInt( match[ 4 ], 10 ),
		minute: Number.parseInt( match[ 5 ], 10 ),
	};
}

function formatInstantWithOptions(
	instant: Date,
	ctx: DateFormatContext,
	options: Intl.DateTimeFormatOptions
): string {
	try {
		return new Intl.DateTimeFormat( localeOf( ctx ), {
			timeZone: timezoneOf( ctx ),
			...options,
		} ).format( instant );
	} catch {
		return instant.toISOString();
	}
}

function formatWallClockWithOptions(
	value: string,
	ctx: DateFormatContext,
	options: Intl.DateTimeFormatOptions
): string {
	const parts = wallClockParts( value );
	if ( ! parts ) {
		return value;
	}
	const local = new Date(
		parts.year,
		parts.month - 1,
		parts.day,
		parts.hour,
		parts.minute
	);
	try {
		return new Intl.DateTimeFormat( localeOf( ctx ), options ).format(
			local
		);
	} catch {
		return value;
	}
}

/**
 * Friendly calendar date, e.g. "Tue, 26 Aug 2026".
 *
 * @param value ISO instant or wall-clock string.
 * @param ctx   Locale and timezone.
 * @param kind  Parsing strategy.
 */
export function formatFriendlyDate(
	value: string,
	ctx: DateFormatContext = {},
	kind: 'instant' | 'local' = 'instant'
): string {
	if ( kind === 'local' || isWallClockDateTime( value ) ) {
		return formatWallClockWithOptions( value, ctx, {
			weekday: 'short',
			day: 'numeric',
			month: 'short',
			year: 'numeric',
		} );
	}
	const instant = parseInstant( value );
	if ( ! instant ) {
		return value;
	}
	return formatInstantWithOptions( instant, ctx, {
		weekday: 'short',
		day: 'numeric',
		month: 'short',
		year: 'numeric',
	} );
}

/**
 * Friendly clock time, e.g. "2:30 pm".
 *
 * @param value ISO instant or wall-clock string.
 * @param ctx   Locale and timezone.
 * @param kind  Parsing strategy.
 */
export function formatFriendlyTime(
	value: string,
	ctx: DateFormatContext = {},
	kind: 'instant' | 'local' = 'instant'
): string {
	if ( kind === 'local' || isWallClockDateTime( value ) ) {
		return formatWallClockWithOptions( value, ctx, {
			hour: 'numeric',
			minute: '2-digit',
		} );
	}
	const instant = parseInstant( value );
	if ( ! instant ) {
		return value;
	}
	return formatInstantWithOptions( instant, ctx, {
		hour: 'numeric',
		minute: '2-digit',
	} );
}

/**
 * Friendly date and time on one line.
 *
 * @param value ISO instant or wall-clock string.
 * @param ctx   Locale and timezone.
 * @param kind  Parsing strategy.
 */
export function formatFriendlyDateTime(
	value: string,
	ctx: DateFormatContext = {},
	kind: 'instant' | 'local' = 'instant'
): string {
	if ( kind === 'local' || isWallClockDateTime( value ) ) {
		return formatWallClockWithOptions( value, ctx, {
			weekday: 'short',
			day: 'numeric',
			month: 'short',
			year: 'numeric',
			hour: 'numeric',
			minute: '2-digit',
		} );
	}
	const instant = parseInstant( value );
	if ( ! instant ) {
		return value;
	}
	return formatInstantWithOptions( instant, ctx, {
		weekday: 'short',
		day: 'numeric',
		month: 'short',
		year: 'numeric',
		hour: 'numeric',
		minute: '2-digit',
	} );
}

/**
 * Appointment-oriented copy: "Today at 2:30 pm", "Wed 26 Aug at 2:30 pm".
 *
 * @param value ISO instant or wall-clock string.
 * @param ctx   Locale and timezone.
 * @param kind  Parsing strategy.
 */
export function formatAppointmentWhen(
	value: string,
	ctx: DateFormatContext = {},
	kind: 'instant' | 'local' = 'instant'
): string {
	const timeLabel = formatFriendlyTime( value, ctx, kind );

	if ( kind === 'local' || isWallClockDateTime( value ) ) {
		const parts = wallClockParts( value );
		if ( ! parts ) {
			return timeLabel;
		}
		const timezone = timezoneOf( ctx );
		const targetKey = `${ parts.year }-${ String( parts.month ).padStart(
			2,
			'0'
		) }-${ String( parts.day ).padStart( 2, '0' ) }`;
		const todayKey = calendarDayInTimezone( new Date(), timezone );

		if ( targetKey === todayKey ) {
			return `Today at ${ timeLabel }`;
		}

		const tomorrow = new Date();
		tomorrow.setDate( tomorrow.getDate() + 1 );
		const tomorrowKey = calendarDayInTimezone( tomorrow, timezone );

		if ( targetKey === tomorrowKey ) {
			return `Tomorrow at ${ timeLabel }`;
		}

		const weekday = formatWallClockWithOptions( value, ctx, {
			weekday: 'long',
		} );
		const dateLabel = formatWallClockWithOptions( value, ctx, {
			day: 'numeric',
			month: 'short',
		} );
		return `${ weekday } ${ dateLabel } at ${ timeLabel }`;
	}

	const instant = parseInstant( value );
	if ( ! instant ) {
		return value;
	}

	const timezone = timezoneOf( ctx );
	const targetDay = calendarDayInTimezone( instant, timezone );
	const todayDay = calendarDayInTimezone( new Date(), timezone );

	if ( targetDay === todayDay ) {
		return `Today at ${ timeLabel }`;
	}

	const tomorrow = new Date();
	tomorrow.setDate( tomorrow.getDate() + 1 );
	const tomorrowDay = calendarDayInTimezone( tomorrow, timezone );

	if ( targetDay === tomorrowDay ) {
		return `Tomorrow at ${ timeLabel }`;
	}

	const weekday = formatInstantWithOptions( instant, ctx, {
		weekday: 'short',
	} );
	const dateLabel = formatInstantWithOptions( instant, ctx, {
		day: 'numeric',
		month: 'short',
	} );
	return `${ weekday } ${ dateLabel } at ${ timeLabel }`;
}

/**
 * Relative phrasing for activity and audit timestamps.
 *
 * @param value ISO instant.
 * @param ctx   Locale and timezone.
 */
export function formatRelativeWhen(
	value: string,
	ctx: DateFormatContext = {}
): string {
	const instant = parseInstant( value );
	if ( ! instant ) {
		return value;
	}

	const diffMs = instant.getTime() - Date.now();
	const absMs = Math.abs( diffMs );

	if ( absMs < 60_000 ) {
		return 'Just now';
	}

	const locale = localeOf( ctx );

	try {
		const rtf = new Intl.RelativeTimeFormat( locale, {
			numeric: 'auto',
		} );
		const minutes = Math.round( diffMs / 60_000 );
		if ( absMs < 3_600_000 ) {
			return rtf.format( minutes, 'minute' );
		}
		const hours = Math.round( diffMs / 3_600_000 );
		if ( absMs < 86_400_000 ) {
			return rtf.format( hours, 'hour' );
		}
		const days = Math.round( diffMs / 86_400_000 );
		if ( absMs < 7 * 86_400_000 ) {
			return rtf.format( days, 'day' );
		}
	} catch {
		// Fall through to absolute formatting.
	}

	return formatFriendlyDateTime( value, ctx, 'instant' );
}

/**
 * Format using the requested display mode.
 *
 * @param value ISO instant or wall-clock string.
 * @param ctx   Locale and timezone.
 * @param mode  Output style.
 * @param kind  Parsing strategy.
 */
export function formatDateValue(
	value: string,
	ctx: DateFormatContext = {},
	mode: DateDisplayMode = 'datetime',
	kind: 'instant' | 'local' = 'instant'
): string {
	switch ( mode ) {
		case 'date':
			return formatFriendlyDate( value, ctx, kind );
		case 'time':
			return formatFriendlyTime( value, ctx, kind );
		case 'appointment':
			return formatAppointmentWhen( value, ctx, kind );
		case 'relative':
			return formatRelativeWhen( value, ctx );
		case 'datetime':
		default:
			return formatFriendlyDateTime( value, ctx, kind );
	}
}

/**
 * Technical detail rows for admin detail popovers.
 *
 * @param value Raw stored value.
 * @param ctx   Locale and timezone.
 * @param kind  Parsing strategy.
 */
export function dateTechnicalDetails(
	value: string,
	ctx: DateFormatContext = {},
	kind: 'instant' | 'local' = 'instant'
): readonly DateTechnicalDetail[] {
	const timezone = timezoneOf( ctx );
	const rows: DateTechnicalDetail[] = [
		{
			label: 'Readable',
			value: formatFriendlyDateTime( value, ctx, kind ),
		},
	];

	if ( kind === 'local' || isWallClockDateTime( value ) ) {
		rows.push(
			{ label: 'Timezone', value: timezone },
			{ label: 'Stored value', value: value.trim() }
		);
		return rows;
	}

	const instant = parseInstant( value );
	if ( ! instant ) {
		return [ { label: 'Stored value', value: value.trim() } ];
	}

	rows.push(
		{
			label: 'Site timezone',
			value: `${ timezone } · ${ formatInstantWithOptions( instant, ctx, {
				weekday: 'long',
				day: 'numeric',
				month: 'long',
				year: 'numeric',
				hour: 'numeric',
				minute: '2-digit',
				timeZoneName: 'short',
			} ) }`,
		},
		{
			label: 'UTC',
			value: formatInstantWithOptions(
				instant,
				{ locale: 'en-GB' },
				{
					weekday: 'short',
					day: 'numeric',
					month: 'short',
					year: 'numeric',
					hour: 'numeric',
					minute: '2-digit',
					timeZone: 'UTC',
					timeZoneName: 'short',
				}
			),
		},
		{ label: 'Stored value', value: value.trim() }
	);

	return rows;
}
