import type { VNode } from 'vue';
import type { DropdownButtonItemArgs } from '../dropdown-button/dropdown-button-item';
import type { FormItemWrapperArgs, MarginType } from '../form/form-item-wrapper';
import { Prop, toNative } from 'vue-facing-decorator';
import PowerduckState from '../../app/powerduck-state';
import TsxComponent, { Component } from '../../app/vuetsx';
import { capitalize } from '../../common/extensions/string-extensions';
import { MobileModeConfig } from '../../common/mobile-mode-config';
import TemporalUtils from '../../common/utils/temporal-utils';
import Button from '../button/button';
import { ButtonLayout, ButtonSize } from '../button/button-layout';
import FormItemWrapper from '../form/form-item-wrapper';
import Modal, { ModalMobileMode } from '../modal/modal';

import './css/datetime-picker.css';

type ViewMode = 'days' | 'months' | 'years';

interface DatetimePickerArgs extends FormItemWrapperArgs {
	value: Temporal.PlainDateTime;
	disabled?: boolean;
	placeholder?: string;
	showTime?: boolean;
	startDate?: Temporal.PlainDateTime;
	endDate?: Temporal.PlainDateTime;
	changed: (newValue: Temporal.PlainDateTime) => void;
	inline?: boolean;
	mobileModal?: boolean;
}

const MINUTE_STEP = 5;
const HOURS: number[] = [];
for (let i = 0; i < 24; i++) {
	HOURS.push(i);
}
const MINUTES: number[] = [];
for (let i = 0; i < 60; i += MINUTE_STEP) {
	MINUTES.push(i);
}

const _SVG_CALENDAR = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>';
const SVG_LEFT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>';
const SVG_RIGHT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>';
const SVG_X = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';

const pad2 = (n: number): string => {
	return n < 10 ? `0${n}` : `${n}`;
};

interface LocaleDateFormat {
	order: ('day' | 'month' | 'year')[];
	separator: string; // core separator with spaces stripped (e.g. "." for Slovak "dd. mm. yyyy")
}

interface LocaleData {
	months: string[];
	monthsShort: string[];
	weekdaysMin: string[];
	dateFormat: LocaleDateFormat;
}

const localeCache: Record<string, LocaleData> = {};

const getCachedLocaleData = (locale: string): LocaleData => {
	if (localeCache[locale]) {
		return localeCache[locale];
	}

	const months: string[] = [];
	const monthsShort: string[] = [];
	const weekdaysMin: string[] = [];

	for (let i = 0; i < 12; i++) {
		// eslint-disable-next-line no-restricted-syntax -- no Temporal API for locale month names
		const d = new Date(Date.UTC(
			2024,
			i,
			15,
		));
		months.push(d.toLocaleDateString(locale, { month: 'long', timeZone: 'UTC' })[capitalize]());
		monthsShort.push(d.toLocaleDateString(locale, { month: 'short', timeZone: 'UTC' })[capitalize]());
	}

	for (let i = 0; i < 7; i++) {
		// eslint-disable-next-line no-restricted-syntax -- 2024-01-01 is Monday; no Temporal equivalent
		const d = new Date(Date.UTC(
			2024,
			0,
			1 + i,
		));
		let short = d.toLocaleDateString(locale, { weekday: 'short', timeZone: 'UTC' })[capitalize]();
		if (short.length > 2) {
			short = short.substring(0, 2);
		}

		weekdaysMin.push(short);
	}

	// Detect date field order and separator via Intl API

	const fmtParts = new Intl.DateTimeFormat(locale, { day: '2-digit', month: '2-digit', year: 'numeric' })
	// eslint-disable-next-line no-restricted-syntax
		.formatToParts(new Date(Date.UTC(
			2024,
			0,
			15,
		)));
	const order: ('day' | 'month' | 'year')[] = [];
	let separator = '';
	for (const part of fmtParts) {
		if (part.type === 'day' || part.type === 'month' || part.type === 'year') {
			order.push(part.type);
		} else if (part.type === 'literal' && !separator) {
			separator = part.value.replace(/\s/g, '');
		}
	}

	localeCache[locale] = {
		months,
		monthsShort,
		weekdaysMin,
		dateFormat: { order, separator },
	};
	return localeCache[locale];
};

@Component
class DatetimePickerComponent extends TsxComponent<DatetimePickerArgs> implements DatetimePickerArgs {
	@Prop() label!: string | VNode;
	@Prop() labelButtons!: DropdownButtonItemArgs[];
	@Prop() subtitle!: string;
	@Prop() value!: Temporal.PlainDateTime;
	@Prop() disabled!: boolean;
	@Prop() placeholder!: string;
	@Prop() mandatory!: boolean;
	@Prop() wrap!: boolean;
	@Prop() hint: string;
	@Prop() appendIcon: string;
	@Prop() prependIcon: string;
	@Prop() appendClicked: () => void;
	@Prop() prependClicked: () => void;
	@Prop() maxWidth?: number;
	@Prop() marginType?: MarginType;
	@Prop() changed: (newValue: Temporal.PlainDateTime) => void;
	@Prop() showTime!: boolean;
	@Prop() showClearValueButton!: boolean;
	@Prop() startDate?: Temporal.PlainDateTime;
	@Prop() endDate?: Temporal.PlainDateTime;
	@Prop() inline!: boolean;
	@Prop() mobileModal!: boolean;

	isOpen = false;
	viewMode: ViewMode = 'days';
	viewYear = 2024;
	viewMonth = 1;
	editHour = 0;
	editMinute = 0;

	dropdownVertical: 'down' | 'up' = 'down';

	// Editable input state
	editText = '';
	isEditing = false;

	private outsideClickHandler: ((e: MouseEvent) => void) | null = null;

	/**
	 * MobileModeConfig.shouldDisplayInModal() pinned at open() time. The
	 * modal-vs-dropdown decision must NOT be recomputed while the picker is
	 * open: the viewport checks feeding it can flip mid-interaction (e.g. the
	 * on-screen keyboard collapsing the layout viewport), and a flip would swap
	 * the render branch under the user while close() routes down the wrong path
	 * (hiding a never-shown modal and leaving isOpen stuck). Cleared once the
	 * picker has fully closed so the next open re-evaluates the viewport.
	 */
	private activeDisplayInModal: boolean | null = null;

	get useMobileModal(): boolean {
		return this.mobileModal !== false && (this.activeDisplayInModal ?? MobileModeConfig.shouldDisplayInModal());
	}

	get locale(): string {
		return PowerduckState.getCurrentLanguage();
	}

	get displayText(): string {
		if (this.value == null) {
			return '';
		}

		const d = this.value;
		const ld = getCachedLocaleData(this.locale);
		const { order, separator } = ld.dateFormat;
		const parts: Record<string, string> = {
			day: pad2(d.day),
			month: pad2(d.month),
			year: d.year.toString(),
		};
		let str = order.map(f => parts[f]).join(separator);
		if (this.showTime) {
			str += ` ${pad2(d.hour)}:${pad2(d.minute)}`;
		}

		return str;
	}

	raiseChangeEvent(newValue: Temporal.PlainDateTime) {
		this.populateValidationDeclaration();
		if (this.changed != null) {
			this.changed(newValue);
		}
	}

	scrollTimeIntoView() {
		this.$nextTick(() => {
			const el = this.$el as HTMLElement;
			if (el == null) {
				return;
			}

			const hourEl = el.querySelector('.dtp-time-scroll-item--sel-h');
			const minEl = el.querySelector('.dtp-time-scroll-item--sel-m');
			hourEl?.scrollIntoView({ block: 'nearest' });
			minEl?.scrollIntoView({ block: 'nearest' });
		});
	}

	open() {
		if (this.disabled) {
			return;
		}

		this.activeDisplayInModal = MobileModeConfig.shouldDisplayInModal();
		this.isOpen = true;
		this.viewMode = 'days';
		if (this.value != null) {
			this.viewYear = this.value.year;
			this.viewMonth = this.value.month;
			this.editHour = this.value.hour;
			this.editMinute = this.value.minute;
		} else {
			const now = Temporal.Now.plainDateTimeISO();
			this.viewYear = now.year;
			this.viewMonth = now.month;
			this.editHour = now.hour;
			this.editMinute = 0;
		}

		if (this.useMobileModal) {
			this.$nextTick(() => {
				this.scrollTimeIntoView();
				(this.$refs.dtpModal as any)?.show({
					onHidden: () => this.onModalHidden(),
				});
			});
		} else {
			this.$nextTick(() => {
				this.scrollTimeIntoView();
				this.outsideClickHandler = (e: MouseEvent) => {
					if (this.$el && !this.$el.contains(e.target as Node)) {
						this.close();
					}
				};
				document.addEventListener('mousedown', this.outsideClickHandler);
				this.adjustDropdownPosition();
			});
		}
	}

	close() {
		if (this.isEditing) {
			this.commitEditText();
			this.isEditing = false;
		}

		if (this.useMobileModal) {
			// Let the modal animate out; onModalHidden sets isOpen = false after animation
			(this.$refs.dtpModal as any)?.hide();
			return;
		}

		this.isOpen = false;
		this.activeDisplayInModal = null;
		if (this.outsideClickHandler) {
			document.removeEventListener('mousedown', this.outsideClickHandler);
			this.outsideClickHandler = null;
		}
	}

	onModalHidden() {
		this.isOpen = false;
		this.activeDisplayInModal = null;
	}

	toggle() {
		if (this.isOpen) {
			this.close();
		} else {
			this.open();
		}
	}

	adjustDropdownPosition() {
		const root = this.$el as HTMLElement;
		const dropdown = root?.querySelector('.dtp-dropdown') as HTMLElement;
		if (!dropdown || this.inline) {
			return;
		}

		dropdown.style.left = '';
		dropdown.style.right = '';
		const rect = dropdown.getBoundingClientRect();
		if (rect.right > window.innerWidth) {
			dropdown.style.left = 'auto';
			dropdown.style.right = '0';
		}

		// Measure space within the intersection of the clipping ancestor and
		// the viewport — a clipper that extends past the viewport bottom must
		// not be trusted as available space.
		const SAFETY_MARGIN_PX = 8;
		const wrapRect = root.getBoundingClientRect();
		const dropdownHeight = dropdown.offsetHeight || 320;
		const clipper = this.findClippingAncestor(root);
		const clipperRect = clipper ? clipper.getBoundingClientRect() : null;
		const effectiveBottom = clipperRect
			? Math.min(clipperRect.bottom, window.innerHeight)
			: window.innerHeight;
		const effectiveTop = clipperRect
			? Math.max(clipperRect.top, 0)
			: 0;
		const spaceBelow = effectiveBottom - wrapRect.bottom;
		const spaceAbove = wrapRect.top - effectiveTop;
		const requiredHeight = dropdownHeight + SAFETY_MARGIN_PX;
		const fitsBelow = requiredHeight <= spaceBelow;
		const fitsAbove = requiredHeight <= spaceAbove;

		if (fitsAbove && (!fitsBelow || spaceAbove > spaceBelow)) {
			this.dropdownVertical = 'up';
		} else if (fitsBelow) {
			this.dropdownVertical = 'down';
		} else {
			this.dropdownVertical = spaceAbove > spaceBelow ? 'up' : 'down';
		}
	}

	private findClippingAncestor(start: HTMLElement): HTMLElement | null {
		let el = start.parentElement;

		while (el && el !== document.body && el !== document.documentElement) {
			const cs = getComputedStyle(el);

			if (cs.overflowY === 'hidden' || cs.overflowY === 'auto' || cs.overflowY === 'scroll' || cs.overflowY === 'clip') {
				return el;
			}

			el = el.parentElement;
		}

		return null;
	}

	// ── Editable input methods ───────────────────────────────────────

	parseInputText(text: string): Temporal.PlainDateTime | null {
		if (!text?.trim()) {
			return null;
		}

		text = text.trim();

		const ld = getCachedLocaleData(this.locale);
		const { order, separator } = ld.dateFormat;
		let match: RegExpMatchArray | null;

		// Locale-aware format: build regex from Intl-detected order and separator
		// Allow optional spaces around separator (e.g. "15. 01. 2024" or "15.01.2024" both work)
		const escapedSep = separator.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
		const sepPat = `\\s*${escapedSep}\\s*`;
		// Optional trailing separator (e.g. Hungarian "2024. 01. 15.")
		const trailPat = `(?:\\s*${escapedSep})?`;
		const localeRegex = new RegExp(`^(\\d{1,4})${sepPat}(\\d{1,2})${sepPat}(\\d{1,4})${trailPat}(?:\\s+(\\d{1,2}):(\\d{2}))?$`);
		match = text.match(localeRegex);
		if (match) {
			const fields: Record<string, number> = {};
			order.forEach((field, i) => {
				fields[field] = +match![i + 1];
			});

			return this.buildDate(
				fields.year,
				fields.month,
				fields.day,
				match[4] ? +match[4] : undefined,
				match[5] ? +match[5] : undefined,
			);
		}

		// ISO fallback: YYYY-MM-DD or YYYY-MM-DDTHH:MM (always supported)
		match = text.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T\s](\d{1,2}):(\d{2}))?$/);
		if (match) {
			return this.buildDate(
				+match[1],
				+match[2],
				+match[3],
				match[4] ? +match[4] : undefined,
				match[5] ? +match[5] : undefined,
			);
		}

		return null;
	}

	private buildDate(
		year: number,
		month: number,
		day: number,
		hour?: number,
		minute?: number,
	): Temporal.PlainDateTime | null {
		if (month < 1 || month > 12 || day < 1 || day > 31 || year < 1900 || year > 2100) {
			return null;
		}

		const h = hour ?? (this.showTime ? this.editHour : 0);
		const m = minute ?? (this.showTime ? this.editMinute : 0);
		if (h < 0 || h > 23 || m < 0 || m > 59) {
			return null;
		}

		try {
			return TemporalUtils.fromString(`${year}-${pad2(month)}-${pad2(day)}T${pad2(h)}:${pad2(m)}:00`);
		} catch {
			return null;
		}
	}

	commitEditText() {
		if (!this.editText?.trim()) {
			this.raiseChangeEvent(null);
			return;
		}

		const parsed = this.parseInputText(this.editText);
		if (parsed != null) {
			this.viewYear = parsed.year;
			this.viewMonth = parsed.month;
			if (this.showTime) {
				this.editHour = parsed.hour;
				this.editMinute = parsed.minute;
			}

			this.raiseChangeEvent(parsed);
		}
		// If parsing failed, silently revert (displayText will show on next render)
	}

	handleInputFocus(e: FocusEvent) {
		if (this.useMobileModal) {
			return;
		}

		// `isEditing` represents "user is typing freeform text", not "input has focus".
		// Setting it on focus alone caused the input to render the stale `editText` ("")
		// while the user was picking via the calendar in Safari (where `<button>`s don't
		// take focus on mouse click, so no blur fires to reset the flag). We now flip
		// `isEditing` only in `handleInputInput` when the user actually types.
		this.editText = this.displayText;
		if (!this.isOpen) {
			this.open();
		}

		setTimeout(() => {
			(e.target as HTMLInputElement)?.select();
		}, 0);
	}

	handleInputBlur(e: FocusEvent) {
		if (this.useMobileModal) {
			return;
		}

		// Always commit edit text on blur (handles clearing via empty input)
		if (this.isEditing) {
			this.commitEditText();
			this.isEditing = false;
		}

		// If focus moved to another element inside this component (e.g. calendar day), keep open
		const relatedTarget = e.relatedTarget as Node;
		if (relatedTarget && this.$el?.contains(relatedTarget)) {
			return;
		}

		// Focus left the component — close
		this.close();
	}

	handleInputKeydown(e: KeyboardEvent) {
		if (e.key === 'Enter') {
			e.preventDefault();
			// Enter must behave exactly like the OK button
			this.close();
			(e.target as HTMLInputElement)?.blur();
		} else if (e.key === 'Escape') {
			e.preventDefault();
			this.isEditing = false;
			this.close();
			(e.target as HTMLInputElement)?.blur();
		}
	}

	handleInputInput(e: Event) {
		if (!this.isEditing) {
			this.isEditing = true;
			if (!this.isOpen) {
				this.open();
			}
		}

		this.editText = (e.target as HTMLInputElement).value;
	}

	// ── Original methods ─────────────────────────────────────────────

	isDisabledDate(
		year: number,
		month: number,
		day: number,
	): boolean {
		if (this.startDate != null) {
			if (year < this.startDate.year) {
				return true;
			}

			if (year === this.startDate.year && month < this.startDate.month) {
				return true;
			}

			if (year === this.startDate.year && month === this.startDate.month && day < this.startDate.day) {
				return true;
			}
		}

		if (this.endDate != null) {
			if (year > this.endDate.year) {
				return true;
			}

			if (year === this.endDate.year && month > this.endDate.month) {
				return true;
			}

			if (year === this.endDate.year && month === this.endDate.month && day > this.endDate.day) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Explicit picks (day / time item / Today / Clear) supersede any in-progress freeform
	 * typing in the input, so we exit editing mode here. The input then renders the latest
	 * `displayText`, and `close() → commitEditText()` becomes a no-op instead of overwriting
	 * the pick with stale `editText`.
	 */
	private exitEditingMode() {
		this.isEditing = false;
		this.editText = '';
	}

	selectDay(
		year: number,
		month: number,
		day: number,
	) {
		if (this.isDisabledDate(
			year,
			month,
			day,
		)) {
			return;
		}

		const h = this.showTime ? this.editHour : 0;
		const m = this.showTime ? this.editMinute : 0;
		const val = TemporalUtils.fromString(`${year}-${pad2(month)}-${pad2(day)}T${pad2(h)}:${pad2(m)}:00`);
		this.viewYear = year;
		this.viewMonth = month;
		this.exitEditingMode();
		this.raiseChangeEvent(val);
		if (!this.showTime && !this.inline) {
			this.close();
		}
	}

	selectHour(h: number) {
		this.editHour = h;
		this.exitEditingMode();
		this.emitTimeChange();
		this.scrollTimeIntoView();
	}

	selectMinute(m: number) {
		this.editMinute = m;
		this.exitEditingMode();
		this.emitTimeChange();
		this.scrollTimeIntoView();
	}

	emitTimeChange() {
		if (this.value == null) {
			return;
		}

		const v = this.value;
		const val = TemporalUtils.fromString(`${v.year}-${pad2(v.month)}-${pad2(v.day)}T${pad2(this.editHour)}:${pad2(this.editMinute)}:00`);
		this.raiseChangeEvent(val);
	}

	selectToday() {
		const now = Temporal.Now.plainDateTimeISO();
		if (this.showTime) {
			this.editHour = now.hour;
			this.editMinute = Math.floor(now.minute / MINUTE_STEP) * MINUTE_STEP;
		}

		this.viewYear = now.year;
		this.viewMonth = now.month;
		this.viewMode = 'days';
		this.selectDay(
			now.year,
			now.month,
			now.day,
		);
	}

	clearValue() {
		this.exitEditingMode();
		this.raiseChangeEvent(null);
	}

	prevMonth() {
		if (this.viewMonth === 1) {
			this.viewMonth = 12;
			this.viewYear--;
		} else {
			this.viewMonth--;
		}
	}

	nextMonth() {
		if (this.viewMonth === 12) {
			this.viewMonth = 1;
			this.viewYear++;
		} else {
			this.viewMonth++;
		}
	}

	mounted() {
		if (this.inline && this.value != null) {
			this.viewYear = this.value.year;
			this.viewMonth = this.value.month;
			this.editHour = this.value.hour;
			this.editMinute = this.value.minute;
		}
	}

	beforeUnmount() {
		if (this.outsideClickHandler) {
			document.removeEventListener('mousedown', this.outsideClickHandler);
			this.outsideClickHandler = null;
		}
	}

	render() {
		const showDropdown = this.isOpen || this.inline;
		const ld = getCachedLocaleData(this.locale);
		const today = Temporal.Now.plainDateISO();

		// ── Calendar content (days / months / years) ──────────────────────
		let calContent: VNode;

		if (this.viewMode === 'days') {
			const year = this.viewYear;
			const month = this.viewMonth;
			const ym = Temporal.PlainYearMonth.from({ year, month });
			const dim = ym.daysInMonth;
			const firstDow = Temporal.PlainDate.from({ year, month, day: 1 }).dayOfWeek;
			const offset = firstDow - 1;

			const prevMonth = month === 1 ? 12 : month - 1;
			const prevYear = month === 1 ? year - 1 : year;
			const prevDim = Temporal.PlainYearMonth.from({ year: prevYear, month: prevMonth }).daysInMonth;
			const nextMonth = month === 12 ? 1 : month + 1;
			const nextYear = month === 12 ? year + 1 : year;

			const dayButtons: VNode[] = [];

			for (let i = offset - 1; i >= 0; i--) {
				const d = prevDim - i;
				const disabled = this.isDisabledDate(
					prevYear,
					prevMonth,
					d,
				);
				const btn = (
					<button
						class={`dtp-cal-day dtp-cal-day--other${disabled ? ' dtp-cal-day--disabled' : ''}`}
						onClick={() => {
							if (!disabled) {
								this.selectDay(
									prevYear,
									prevMonth,
									d,
								);
							}
						}}
					>
						{d}
					</button>
				);
				dayButtons.push(btn);
			}

			for (let d = 1; d <= dim; d++) {
				const isToday = today.year === year && today.month === month && today.day === d;
				const isSelected = this.value != null
					&& this.value.year === year
					&& this.value.month === month
					&& this.value.day === d;
				const disabled = this.isDisabledDate(
					year,
					month,
					d,
				);
				const cls = ['dtp-cal-day'];
				if (isToday) {
					cls.push('dtp-cal-day--today');
				}

				if (isSelected) {
					cls.push('dtp-cal-day--selected');
				}

				if (disabled) {
					cls.push('dtp-cal-day--disabled');
				}

				const btn = (
					<button
						class={cls.join(' ')}
						onClick={() => {
							if (!disabled) {
								this.selectDay(
									year,
									month,
									d,
								);
							}
						}}
					>
						{d}
					</button>
				);
				dayButtons.push(btn);
			}

			const totalCells = offset + dim;
			const remaining = totalCells % 7 === 0 ? 0 : 7 - (totalCells % 7);
			for (let d = 1; d <= remaining; d++) {
				const disabled = this.isDisabledDate(
					nextYear,
					nextMonth,
					d,
				);
				const btn = (
					<button
						class={`dtp-cal-day dtp-cal-day--other${disabled ? ' dtp-cal-day--disabled' : ''}`}
						onClick={() => {
							if (!disabled) {
								this.selectDay(
									nextYear,
									nextMonth,
									d,
								);
							}
						}}
					>
						{d}
					</button>
				);
				dayButtons.push(btn);
			}

			// Year range for dropdown
			const minYear = 1900;
			const maxYear = Math.max(year, today.year) + 10;
			const yearOptions: VNode[] = [];
			for (let y = minYear; y <= maxYear; y++) {
				yearOptions.push(<option value={y}>{y}</option>);
			}

			calContent = (
				<div>
					<div class="dtp-cal-header">
						<div class="dtp-cal-nav">
							<button class="dtp-cal-nav-btn" onClick={() => this.prevMonth()} innerHTML={SVG_LEFT} />
						</div>
						<div class="dtp-cal-selectors">
							<span class="dtp-select-wrapper">
								<span class="dtp-select-display">
									{ld.months[month - 1]}
									<span class="dtp-select-arrow" />
								</span>
								<select
									class="dtp-select-native"
									value={month}
									onChange={(e: Event) => { this.viewMonth = Number.parseInt((e.target as HTMLSelectElement).value); }}
								>
									{ld.months.map((name, i) => (
										<option value={i + 1}>{name}</option>
									))}
								</select>
							</span>
							<span class="dtp-select-wrapper">
								<span class="dtp-select-display">
									{year}
									<span class="dtp-select-arrow" />
								</span>
								<select
									class="dtp-select-native"
									value={year}
									onChange={(e: Event) => { this.viewYear = Number.parseInt((e.target as HTMLSelectElement).value); }}
								>
									{yearOptions}
								</select>
							</span>
						</div>
						<div class="dtp-cal-nav">
							<button class="dtp-cal-nav-btn" onClick={() => this.nextMonth()} innerHTML={SVG_RIGHT} />
						</div>
					</div>
					<div class="dtp-cal-weekdays">
						{ld.weekdaysMin.map(w => <div class="dtp-cal-weekday">{w}</div>)}
					</div>
					<div class="dtp-cal-days">
						{dayButtons}
					</div>
				</div>
			);
		} else if (this.viewMode === 'months') {
			const monthCells = ld.monthsShort.map((name, i) => {
				const isCurrent = today.month === i + 1 && today.year === this.viewYear;
				const isSelected = this.viewMonth === i + 1;
				const cls = ['dtp-grid-cell'];
				if (isCurrent) {
					cls.push('dtp-grid-cell--current');
				}

				if (isSelected) {
					cls.push('dtp-grid-cell--selected');
				}

				return (
					<button
						class={cls.join(' ')}
						onClick={() => {
							this.viewMonth = i + 1;
							this.viewMode = 'days';
						}}
					>
						{name}
					</button>
				);
			});

			calContent = (
				<div>
					<div class="dtp-cal-header">
						<div class="dtp-cal-nav">
							<button class="dtp-cal-nav-btn" onClick={() => { this.viewYear--; }} innerHTML={SVG_LEFT} />
						</div>
						<button class="dtp-cal-title" onClick={() => { this.viewMode = 'years'; }}>{this.viewYear}</button>
						<div class="dtp-cal-nav">
							<button class="dtp-cal-nav-btn" onClick={() => { this.viewYear++; }} innerHTML={SVG_RIGHT} />
						</div>
					</div>
					<div class="dtp-grid-view">{monthCells}</div>
				</div>
			);
		} else {
			const startYear = this.viewYear - 5;
			const yearCells: VNode[] = [];
			for (let y = startYear; y < startYear + 12; y++) {
				const isCurrent = today.year === y;
				const isSelected = this.viewYear === y;
				const cls = ['dtp-grid-cell'];
				if (isCurrent) {
					cls.push('dtp-grid-cell--current');
				}

				if (isSelected) {
					cls.push('dtp-grid-cell--selected');
				}

				const btn = (
					<button
						class={cls.join(' ')}
						onClick={() => {
							this.viewYear = y;
							this.viewMode = 'months';
						}}
					>
						{y}
					</button>
				);
				yearCells.push(btn);
			}

			calContent = (
				<div>
					<div class="dtp-cal-header">
						<div class="dtp-cal-nav">
							<button class="dtp-cal-nav-btn" onClick={() => { this.viewYear -= 12; }} innerHTML={SVG_LEFT} />
						</div>
						<span class="dtp-cal-title dtp-cal-title--static">
							{startYear}
							{' '}
							–
							{' '}
							{startYear + 11}
						</span>
						<div class="dtp-cal-nav">
							<button class="dtp-cal-nav-btn" onClick={() => { this.viewYear += 12; }} innerHTML={SVG_RIGHT} />
						</div>
					</div>
					<div class="dtp-grid-view">{yearCells}</div>
				</div>
			);
		}

		// ── Time panel (desktop — scrollable columns on right) ────────────
		let timePanel: VNode = null;
		let timeMobile: VNode = null;

		if (this.showTime && showDropdown) {
			const hourItems = HOURS.map(h => (
				<button
					class={`dtp-time-scroll-item${h === this.editHour ? ' dtp-time-scroll-item--selected dtp-time-scroll-item--sel-h' : ''}`}
					onClick={() => this.selectHour(h)}
				>
					{pad2(h)}
				</button>
			));

			const minuteItems = MINUTES.map(m => (
				<button
					class={`dtp-time-scroll-item${m === this.editMinute ? ' dtp-time-scroll-item--selected dtp-time-scroll-item--sel-m' : ''}`}
					onClick={() => this.selectMinute(m)}
				>
					{pad2(m)}
				</button>
			));

			timePanel = (
				<div class="dtp-time-panel">
					<div class="dtp-time-panel-header">{PowerduckState.getResourceValue('datePickerTime')}</div>
					<div class="dtp-time-columns">
						<div class="dtp-time-scroll">
							<div class="dtp-time-column-header">HH</div>
							<div class="dtp-time-scroll-list">
								{hourItems}
							</div>
						</div>
						<div class="dtp-time-colon">:</div>
						<div class="dtp-time-scroll">
							<div class="dtp-time-column-header">MM</div>
							<div class="dtp-time-scroll-list">
								{minuteItems}
							</div>
						</div>
					</div>
				</div>
			);

			timeMobile = (
				<div class="dtp-time-mobile">
					<div class="dtp-time-mobile-row">
						<span class="dtp-time-mobile-label">HH</span>
						<input
							class="dtp-time-mobile-slider"
							type="range"
							min={0}
							max={23}
							value={this.editHour}
							onInput={(e: Event) => this.selectHour(Number.parseInt((e.target as HTMLInputElement).value))}
						/>
						<span class="dtp-time-mobile-value">{pad2(this.editHour)}</span>
					</div>
					<div class="dtp-time-mobile-row">
						<span class="dtp-time-mobile-label">MM</span>
						<input
							class="dtp-time-mobile-slider"
							type="range"
							min={0}
							max={55}
							step={MINUTE_STEP}
							value={this.editMinute}
							onInput={(e: Event) => this.selectMinute(Number.parseInt((e.target as HTMLInputElement).value))}
						/>
						<span class="dtp-time-mobile-value">{pad2(this.editMinute)}</span>
					</div>
				</div>
			);
		}

		// ── Footer with Today + OK ────────────────────────────────────────
		const footer = showDropdown
			? (
					<div class="dtp-footer">
						<button class="dtp-footer-btn dtp-footer-btn--ghost" onClick={() => this.selectToday()}>
							{PowerduckState.getResourceValue('datePickerToday')}
						</button>
						<div class="dtp-footer-right">
							{this.useMobileModal && this.value != null && (
								<button
									class="dtp-footer-btn dtp-footer-btn--ghost dtp-footer-btn--clear"
									onClick={() => {
										this.clearValue();
										this.close();
									}}
								>
									{SVG_X ? <span class="dtp-footer-clear-icon" innerHTML={SVG_X} /> : null}
									{PowerduckState.getResourceValue('datePickerClear')}
								</button>
							)}
							{!this.inline && (
								<Button
									layout={ButtonLayout.Primary}
									size={ButtonSize.Small}
									text="OK"
									clicked={() => this.close()}
								/>
							)}
						</div>
					</div>
				)
			: null;

		// ── Dropdown (desktop) ───────────────────────────────────────────
		const dropdown = showDropdown && !this.useMobileModal
			? (
					<div
						class={`dtp-dropdown${this.inline ? ' dtp-dropdown--inline' : ''}${this.dropdownVertical === 'up' ? ' dtp-dropdown--up' : ''}`}
						onMousedown={(e: MouseEvent) => {
							// Safari does not focus <button>s on mouse click on desktop. Without this guard
							// the input would blur on mousedown with `relatedTarget === null`, `handleInputBlur`
							// would treat that as "focus left the picker" and call `close()` — unmounting the
							// dropdown before the click on the day/time/footer button could land. Preventing
							// default on mousedown suppresses the focus shift in every browser while still
							// letting `click` fire normally. Native form controls (`<select>` for month/year
							// navigation) need their default focus to open their native popups, so we exempt them.
							const target = e.target as HTMLElement | null;
							if (target == null) {
								return;
							}

							if (target.closest('select, input, textarea') != null) {
								return;
							}

							e.preventDefault();
						}}
					>
						<div class="dtp-body">
							<div class="dtp-calendar-section">
								{calContent}
								{timeMobile}
							</div>
							{timePanel}
						</div>
						{footer}
					</div>
				)
			: null;

		// ── Mobile modal ─────────────────────────────────────────────────
		let modalPicker: VNode = null;
		if (this.useMobileModal) {
			modalPicker = (
				<Modal
					ref="dtpModal"
					mobileMode={ModalMobileMode.BottomSheetModal}
					title={this.label}
				>
					{showDropdown && (
						<div class="modal-body dtp-modal-body">
							<div class="dtp-body">
								<div class="dtp-calendar-section">
									{calContent}
									{timeMobile}
								</div>
								{timePanel}
							</div>
							{footer}
						</div>
					)}
				</Modal>
			);
		}

		// ── Input field ───────────────────────────────────────────────────
		let inputSection: VNode = null;
		if (!this.inline) {
			const containerCls = ['dtp-input-container'];
			if (this.isOpen) {
				containerCls.push('dtp-input-container--focused');
			}

			if (this.disabled) {
				containerCls.push('dtp-input-container--disabled');
			}

			const isMobile = this.useMobileModal;

			inputSection = (
				<div class={containerCls.join(' ')}>
					<input
						class="dtp-input-field"
						type="text"
						readOnly={isMobile}
						placeholder={this.placeholder || PowerduckState.getResourceValue('datePickerPlaceholder')}
						value={this.isEditing ? this.editText : this.displayText}
						disabled={this.disabled}
						onClick={() => {
							if (!this.isOpen) {
								this.open();
							}
						}}
						onFocus={(e: FocusEvent) => this.handleInputFocus(e)}
						onBlur={(e: FocusEvent) => this.handleInputBlur(e)}
						onKeydown={(e: KeyboardEvent) => this.handleInputKeydown(e)}
						onInput={(e: Event) => this.handleInputInput(e)}
					/>
					{this.showClearValueButton && this.value != null && (
						<button
							class="dtp-clear-btn"
							tabindex={-1}
							onClick={(e: Event) => {
								e.stopPropagation();
								this.clearValue();
							}}
							innerHTML={SVG_X}
						/>
					)}
				</div>
			);
		}

		// ── Wrapper ───────────────────────────────────────────────────────
		return (
			<FormItemWrapper
				label={this.label}
				mandatory={this.mandatory}
				wrap={this.wrap}
				appendIcon={this.appendIcon}
				prependIcon={this.prependIcon}
				hint={this.hint}
				appendClicked={this.appendClicked}
				prependClicked={this.prependClicked}
				marginType={this.marginType}
				maxWidth={this.maxWidth}
				validationState={this.validationState}
				labelButtons={this.labelButtons}
				subtitle={this.subtitle}
				showClearValueButton={false}
			>
				<div class="dtp-wrap">
					{inputSection}
					{dropdown}
					{modalPicker}
				</div>
			</FormItemWrapper>
		);
	}
}

const DatetimePicker = toNative(DatetimePickerComponent);
export default DatetimePicker;
