import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Calendar, ChevronLeft, ChevronRight, Clock } from 'lucide-react';
import { DOM_EVENTS, KEYS } from './interactionConstants';
import { APP_LOCALE } from '../../core/constants';

// App-rendered replacements for native <input type="date|time|datetime-local">.
// Native date/time controls are localized by the BROWSER UI language (not the
// page lang attribute), so an English product renders CJK date placeholders for
// users whose browser runs in another language. These components keep the same
// value contracts as the native controls (date: yyyy-MM-dd, time: HH:mm,
// datetime: yyyy-MM-ddTHH:mm) while the app owns every visible string.
//
// Commit model: only a COMPLETE, unambiguous entry commits on the keystroke
// (exact ISO date / H:MM time) so host key handlers (inline-edit Enter-to-save,
// form submit) read settled state — and so no truncation of an existing value
// (Backspace mid-edit) can ever commit a corrupted intermediate. Nonstandard
// but resolvable text (2026/7/1, 9:5) settles only on explicit Enter; on blur
// any unsettled text REVERTS to the last committed value (committing at blur
// would turn abandoned edits and typos into data at the moment a Save button
// steals focus). Clearing the field commits '' on blur/Enter, not per
// keystroke, so non-date placeholder values a host may store (relative-date
// tokens in criteria rows) survive an idle focus/blur pass untouched.

const monthFormatter = new Intl.DateTimeFormat(APP_LOCALE, { month: 'long' });
const weekdayFormatter = new Intl.DateTimeFormat(APP_LOCALE, { weekday: 'short' });
const MONTH_NAMES = Array.from({ length: 12 }, (_, month) =>
    monthFormatter.format(new Date(2001, month, 1)));
// 2001-01-07 is a Sunday; the calendar grid starts weeks on Sunday.
const WEEKDAY_HEADERS = Array.from({ length: 7 }, (_, day) =>
    weekdayFormatter.format(new Date(2001, 0, 7 + day)));

const pad2 = (n: number): string => String(n).padStart(2, '0');
const toIsoDate = (year: number, month1: number, day: number): string =>
    `${year}-${pad2(month1)}-${pad2(day)}`;
// month1 is 1-based; Date(y, m, 0) resolves to the last day of month m.
const daysInMonth = (year: number, month1: number): number =>
    new Date(year, month1, 0).getDate();

const todayIsoDate = (): string => {
    const now = new Date();
    return toIsoDate(now.getFullYear(), now.getMonth() + 1, now.getDate());
};

/**
 * Strict date form for eager commit: exactly YYYY-MM-DD. No shorter form may
 * commit per-keystroke — 1-digit day/month strings are truncations of longer
 * entries (Backspace on 2026-07-10 passes through 2026-07-1), and committing
 * one would corrupt the value if the edit is abandoned.
 */
function parseTypedDateStrict(text: string): string | null {
    if (!/^\d{4}-\d{2}-\d{2}$/.test(text)) return null;
    return parseTypedDateLoose(text);
}

/**
 * Resolvable date forms for explicit Enter: year-first only (2026-07-10,
 * 2026/7/1) — the placeholder advertises YYYY-MM-DD, and day-first vs
 * month-first slash forms are ambiguous across locales, so neither is guessed.
 */
function parseTypedDateLoose(text: string): string | null {
    const match = text.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})$/);
    if (!match) return null;
    const [year, month1, day] = [Number(match[1]), Number(match[2]), Number(match[3])];
    if (month1 < 1 || month1 > 12) return null;
    if (day < 1 || day > daysInMonth(year, month1)) return null;
    return toIsoDate(year, month1, day);
}

/**
 * Strict time form for eager commit: exactly HH:MM. Shorter forms must NOT
 * commit eagerly — a bare hour is the first keystroke of most entries (typing
 * 14:30 would transiently commit 01:00 then 14:00), and a 1-digit hour is the
 * Backspace truncation of a committed 2-digit-hour time (12:30 → 1:30).
 */
function parseTypedTimeStrict(text: string): string | null {
    if (!/^\d{2}:\d{2}$/.test(text)) return null;
    return parseTypedTimeLoose(text);
}

/** Resolvable time forms for explicit Enter: bare/1-digit hour ("9", "9:30") and 1-digit minutes ("9:5"). */
function parseTypedTimeLoose(text: string): string | null {
    const match = text.match(/^(\d{1,2})(?::(\d{1,2}))?$/);
    if (!match) return null;
    const hours = Number(match[1]);
    const minutes = match[2] === undefined ? 0 : Number(match[2]);
    if (hours > 23 || minutes > 59) return null;
    return `${pad2(hours)}:${pad2(minutes)}`;
}

interface PopoverRect {
    top: number;
    left: number;
    openUp: boolean;
}

/**
 * Shared anchored-popover mechanics (mirrors SearchableSelect): portal to body
 * with fixed positioning to escape overflow-clipping ancestors, flip up when
 * short on space, clamp to the right viewport edge, close on outside
 * mousedown, reposition on scroll/resize.
 */
function useAnchoredPopover(estimatedHeight: number, width: number) {
    const [isOpen, setIsOpen] = useState(false);
    const [rect, setRect] = useState<PopoverRect | null>(null);
    const anchorRef = useRef<HTMLDivElement>(null);
    const popoverRef = useRef<HTMLDivElement>(null);

    useEffect(() => {
        if (!isOpen) return;
        const handler = (e: MouseEvent) => {
            const target = e.target as Node;
            if (!anchorRef.current?.contains(target) && !popoverRef.current?.contains(target)) {
                setIsOpen(false);
            }
        };
        document.addEventListener(DOM_EVENTS.MOUSE_DOWN, handler);
        return () => document.removeEventListener(DOM_EVENTS.MOUSE_DOWN, handler);
    }, [isOpen]);

    useLayoutEffect(() => {
        if (!isOpen) {
            setRect(null);
            return;
        }
        const update = () => {
            const el = anchorRef.current;
            if (!el) return;
            const anchor = el.getBoundingClientRect();
            const spaceBelow = window.innerHeight - anchor.bottom;
            const openUp = spaceBelow < estimatedHeight && anchor.top > spaceBelow;
            const next: PopoverRect = {
                top: openUp ? anchor.top : anchor.bottom,
                left: Math.max(8, Math.min(anchor.left, window.innerWidth - width - 8)),
                openUp,
            };
            setRect(prev =>
                prev && prev.top === next.top && prev.left === next.left && prev.openUp === next.openUp
                    ? prev
                    : next);
        };
        update();
        window.addEventListener(DOM_EVENTS.SCROLL, update, true);
        window.addEventListener(DOM_EVENTS.RESIZE, update);
        return () => {
            window.removeEventListener(DOM_EVENTS.SCROLL, update, true);
            window.removeEventListener(DOM_EVENTS.RESIZE, update);
        };
    }, [isOpen, estimatedHeight, width]);

    return { isOpen, setIsOpen, rect, anchorRef, popoverRef };
}

const popoverStyle = (rect: PopoverRect): React.CSSProperties => ({
    position: 'fixed',
    top: rect.openUp ? undefined : rect.top + 4,
    bottom: rect.openUp ? window.innerHeight - rect.top + 4 : undefined,
    left: rect.left,
});

const POPOVER_FRAME_CLASSES =
    'z-[1000] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg';

// Handlers the component owns are excluded from the raw spread and offered as
// first-class composed props instead, so a caller-supplied handler can never
// silently replace the commit/popover machinery.
type NativeInputPassthrough = Omit<
    React.InputHTMLAttributes<HTMLInputElement>,
    | 'value' | 'onChange' | 'type' | 'disabled' | 'className' | 'placeholder'
    | 'onFocus' | 'onBlur' | 'onKeyDown' | 'onClick' | 'inputMode'
>;

interface PickerInputProps extends NativeInputPassthrough {
    value: string;
    onChange: (value: string) => void;
    disabled?: boolean;
    /** Applied to the text input, replacing the default form-control styling. */
    className?: string;
    placeholder?: string;
    /** Composed after the component's own handlers (skipped for consumed keys). */
    onFocus?: React.FocusEventHandler<HTMLInputElement>;
    onBlur?: React.FocusEventHandler<HTMLInputElement>;
    onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
}

const DEFAULT_INPUT_CLASSES =
    'w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:text-gray-500 dark:disabled:bg-gray-800 dark:disabled:text-gray-500';

/**
 * Shared editing state machine for DateInput/TimeInput: draft text while
 * typing, eager commit of strictly-valid entries, loose commit on blur/Enter,
 * revert of unparseable text, and native-like Enter/Escape layering (consume
 * for the popover first, then delegate to the host's handler).
 */
function usePickerEditing(opts: {
    value: string;
    onChange: (value: string) => void;
    parseStrict: (text: string) => string | null;
    parseLoose: (text: string) => string | null;
    isOpen: boolean;
    setIsOpen: (open: boolean) => void;
    onOpen: () => void;
    onFocus?: React.FocusEventHandler<HTMLInputElement>;
    onBlur?: React.FocusEventHandler<HTMLInputElement>;
    onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
}) {
    const {
        value, onChange, parseStrict, parseLoose,
        isOpen, setIsOpen, onOpen, onFocus, onBlur, onKeyDown,
    } = opts;
    const [draft, setDraft] = useState<string | null>(null);

    const commit = (next: string) => {
        if (next !== value) onChange(next);
    };

    /** Loose-parse the draft; returns undefined when there is nothing to do. */
    const resolveDraft = (): string | null | undefined => {
        if (draft === null) return undefined;
        const trimmed = draft.trim();
        if (trimmed === '') return '';
        return parseLoose(trimmed);
    };

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const text = e.target.value;
        setDraft(text);
        const trimmed = text.trim();
        // Emptying the box is NOT an eager clear — it is the first step of
        // retyping. An intentional clear settles on blur/Enter.
        if (trimmed === '') return;
        const parsed = parseStrict(trimmed);
        if (parsed !== null) commit(parsed);
    };

    const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
        setDraft(value);
        onOpen();
        e.target.select();
        onFocus?.(e);
    };

    const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
        const resolved = resolveDraft();
        // Only an explicit clear commits at blur. Strict entries committed on
        // their keystroke; loose-only text ("9:5") and garbage REVERT — blur is
        // often a Save-button mousedown, and committing here would turn typos
        // and abandoned edits into data at the moment of saving.
        if (resolved === '') commit('');
        setDraft(null);
        setIsOpen(false);
        onBlur?.(e);
    };

    const handleClick = () => {
        if (!isOpen) onOpen();
    };

    const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
        if (e.key === KEYS.ENTER) {
            const resolved = resolveDraft();
            if (resolved === null) {
                // Unparseable text: revert and consume so the host doesn't
                // save/submit while the field shows discarded text.
                e.preventDefault();
                setDraft(null);
                setIsOpen(false);
                return;
            }
            if (resolved !== undefined && resolved !== value) {
                // Loose-only entry (e.g. "9:5") or an explicit clear settles on
                // this Enter; the next Enter reaches the host with the parent
                // state already re-rendered from this commit.
                e.preventDefault();
                commit(resolved);
                setDraft(null);
                setIsOpen(false);
                return;
            }
            // Nothing pending: close the (focus-auto-opened) popover but let
            // the SAME Enter through — hosts rely on single-press Enter for
            // form submit and inline-edit save, and the committed state is
            // already settled.
            setDraft(null);
            setIsOpen(false);
            onKeyDown?.(e);
            return;
        }
        if (e.key === KEYS.ESCAPE) {
            if (isOpen) {
                // Consume so a host modal's document-level Escape handler does
                // not also fire and close the whole modal.
                e.preventDefault();
                e.stopPropagation();
                setDraft(null);
                setIsOpen(false);
                return;
            }
            onKeyDown?.(e);
            return;
        }
        onKeyDown?.(e);
    };

    return { draft, setDraft, handleChange, handleFocus, handleBlur, handleClick, handleKeyDown };
}

export function DateInput({
    value,
    onChange,
    disabled = false,
    className,
    placeholder = 'YYYY-MM-DD',
    onFocus,
    onBlur,
    onKeyDown,
    ...inputPassthrough
}: PickerInputProps) {
    const { isOpen, setIsOpen, rect, anchorRef, popoverRef } = useAnchoredPopover(340, 256);
    const [todayIso, setTodayIso] = useState(todayIsoDate);

    const deriveView = (iso: string) => {
        const source = /^\d{4}-\d{2}-\d{2}$/.test(iso) ? iso : todayIsoDate();
        return { year: Number(source.slice(0, 4)), month1: Number(source.slice(5, 7)) };
    };
    const [view, setView] = useState(() => deriveView(value));

    const openPopover = () => {
        setTodayIso(todayIsoDate());
        setView(deriveView(value));
        setIsOpen(true);
    };

    const editing = usePickerEditing({
        value,
        onChange: (next) => {
            onChange(next);
            if (isOpen && /^\d{4}-\d{2}-\d{2}$/.test(next)) setView(deriveView(next));
        },
        parseStrict: parseTypedDateStrict,
        parseLoose: parseTypedDateLoose,
        isOpen,
        setIsOpen,
        onOpen: openPopover,
        onFocus,
        onBlur,
        onKeyDown,
    });

    const selectDay = (iso: string) => {
        if (iso !== value) onChange(iso);
        editing.setDraft(null);
        setIsOpen(false);
    };

    const shiftMonth = (delta: number) => {
        setView(({ year, month1 }) => {
            const zeroBased = month1 - 1 + delta;
            return {
                year: year + Math.floor(zeroBased / 12),
                month1: ((zeroBased % 12) + 12) % 12 + 1,
            };
        });
    };

    const grid = useMemo(() => {
        const leadingBlanks = new Date(view.year, view.month1 - 1, 1).getDay();
        const total = daysInMonth(view.year, view.month1);
        const cells: Array<number | null> = Array.from({ length: leadingBlanks }, () => null);
        for (let day = 1; day <= total; day++) cells.push(day);
        return cells;
    }, [view.year, view.month1]);

    return (
        <div ref={anchorRef} className="relative">
            <input
                type="text"
                autoComplete="off"
                value={editing.draft ?? value}
                placeholder={placeholder}
                disabled={disabled}
                className={`${className ?? DEFAULT_INPUT_CLASSES} pr-9`}
                onFocus={editing.handleFocus}
                onChange={editing.handleChange}
                onBlur={editing.handleBlur}
                onClick={editing.handleClick}
                onKeyDown={editing.handleKeyDown}
                {...inputPassthrough}
            />
            <button
                type="button"
                tabIndex={-1}
                disabled={disabled}
                aria-label="Open calendar"
                // Keep focus in the input: without this, mousedown blurs the
                // input, blur closes the popover, and the click reopens it —
                // the toggle would never close.
                onMouseDown={(e) => e.preventDefault()}
                onClick={() => (isOpen ? setIsOpen(false) : openPopover())}
                className="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 rounded text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-50"
            >
                <Calendar className="w-4 h-4" />
            </button>

            {isOpen && rect && createPortal(
                <div
                    ref={popoverRef}
                    // Mouse-only affordance: focus stays in the input (typed
                    // entry is the keyboard/screen-reader path), so the popover
                    // is hidden from the a11y tree rather than announced as a
                    // dialog no keyboard can reach. A focus-managed keyboard
                    // calendar is a deliberate follow-up.
                    aria-hidden="true"
                    style={popoverStyle(rect)}
                    className={`${POPOVER_FRAME_CLASSES} p-2 w-64`}
                    // Keep focus in the text input so popover clicks don't blur it.
                    onMouseDown={(e) => e.preventDefault()}
                >
                    <div className="flex items-center justify-between mb-1">
                        <button
                            type="button"
                            tabIndex={-1}
                            aria-label="Previous month"
                            onClick={() => shiftMonth(-1)}
                            className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400"
                        >
                            <ChevronLeft className="w-4 h-4" />
                        </button>
                        <span className="text-sm font-medium text-gray-900 dark:text-white">
                            {MONTH_NAMES[view.month1 - 1]} {view.year}
                        </span>
                        <button
                            type="button"
                            tabIndex={-1}
                            aria-label="Next month"
                            onClick={() => shiftMonth(1)}
                            className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400"
                        >
                            <ChevronRight className="w-4 h-4" />
                        </button>
                    </div>

                    <div className="grid grid-cols-7 mb-1">
                        {WEEKDAY_HEADERS.map(day => (
                            <div key={day} className="text-center text-xs font-medium text-gray-400 py-1">
                                {day}
                            </div>
                        ))}
                    </div>
                    <div className="grid grid-cols-7">
                        {grid.map((day, index) => {
                            if (day === null) {
                                return <div key={`blank-${index}`} />;
                            }
                            const iso = toIsoDate(view.year, view.month1, day);
                            const isSelected = iso === value;
                            const isToday = iso === todayIso;
                            return (
                                <button
                                    key={iso}
                                    type="button"
                                    tabIndex={-1}
                                    onClick={() => selectDay(iso)}
                                    className={`h-8 text-sm rounded-md transition-colors ${isSelected
                                        ? 'bg-blue-600 text-white font-medium'
                                        : `hover:bg-blue-50 dark:hover:bg-blue-900/30 text-gray-700 dark:text-gray-200 ${isToday ? 'ring-1 ring-inset ring-blue-400 font-medium' : ''}`
                                        }`}
                                >
                                    {day}
                                </button>
                            );
                        })}
                    </div>

                    <div className="flex items-center justify-between mt-2 pt-2 border-t border-gray-100 dark:border-gray-700">
                        <button
                            type="button"
                            tabIndex={-1}
                            onClick={() => selectDay(todayIsoDate())}
                            className="text-xs font-medium text-blue-600 dark:text-blue-400 hover:underline px-1"
                        >
                            Today
                        </button>
                        <button
                            type="button"
                            tabIndex={-1}
                            onClick={() => { onChange(''); editing.setDraft(null); setIsOpen(false); }}
                            className="text-xs font-medium text-gray-500 dark:text-gray-400 hover:underline px-1"
                        >
                            Clear
                        </button>
                    </div>
                </div>,
                document.body
            )}
        </div>
    );
}

const TIME_OPTIONS: string[] = Array.from({ length: 48 }, (_, i) =>
    `${pad2(Math.floor(i / 2))}:${i % 2 === 0 ? '00' : '30'}`);

export function TimeInput({
    value,
    onChange,
    disabled = false,
    className,
    placeholder = 'HH:MM',
    onFocus,
    onBlur,
    onKeyDown,
    ...inputPassthrough
}: PickerInputProps) {
    const { isOpen, setIsOpen, rect, anchorRef, popoverRef } = useAnchoredPopover(260, 112);

    const editing = usePickerEditing({
        value,
        onChange,
        parseStrict: parseTypedTimeStrict,
        parseLoose: parseTypedTimeLoose,
        isOpen,
        setIsOpen,
        onOpen: () => setIsOpen(true),
        onFocus,
        onBlur,
        onKeyDown,
    });

    return (
        <div ref={anchorRef} className="relative">
            <input
                type="text"
                autoComplete="off"
                value={editing.draft ?? value}
                placeholder={placeholder}
                disabled={disabled}
                className={`${className ?? DEFAULT_INPUT_CLASSES} pr-9`}
                onFocus={editing.handleFocus}
                onChange={editing.handleChange}
                onBlur={editing.handleBlur}
                onClick={editing.handleClick}
                onKeyDown={editing.handleKeyDown}
                {...inputPassthrough}
            />
            <button
                type="button"
                tabIndex={-1}
                disabled={disabled}
                aria-label="Open time list"
                // See the calendar icon button: prevent the mousedown-blur so
                // the toggle can actually close an open popover.
                onMouseDown={(e) => e.preventDefault()}
                onClick={() => setIsOpen(!isOpen)}
                className="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 rounded text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-50"
            >
                <Clock className="w-4 h-4" />
            </button>

            {isOpen && rect && createPortal(
                <div
                    ref={popoverRef}
                    // Mouse-only affordance — see the DateInput popover note.
                    aria-hidden="true"
                    style={popoverStyle(rect)}
                    className={`${POPOVER_FRAME_CLASSES} w-28 max-h-60 overflow-y-auto`}
                    onMouseDown={(e) => e.preventDefault()}
                >
                    {TIME_OPTIONS.map(option => (
                        <button
                            key={option}
                            type="button"
                            tabIndex={-1}
                            onClick={() => {
                                if (option !== value) onChange(option);
                                editing.setDraft(null);
                                setIsOpen(false);
                            }}
                            className={`w-full text-left px-3 py-1.5 text-sm transition-colors ${option === value
                                ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 font-medium'
                                : 'hover:bg-blue-50 dark:hover:bg-blue-900/30 text-gray-700 dark:text-gray-200'
                                }`}
                        >
                            {option}
                        </button>
                    ))}
                </div>,
                document.body
            )}
        </div>
    );
}

interface DateTimeInputProps {
    /** Native datetime-local contract: yyyy-MM-ddTHH:mm or empty string. */
    value: string;
    onChange: (value: string) => void;
    disabled?: boolean;
    className?: string;
    /** Forwarded to both sub-inputs so hosts keep Enter-save/Escape-cancel. */
    onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
    /** Focuses the date sub-input. */
    autoFocus?: boolean;
    required?: boolean;
    id?: string;
    'aria-invalid'?: React.AriaAttributes['aria-invalid'];
    'aria-describedby'?: string;
}

export function DateTimeInput({
    value,
    onChange,
    disabled = false,
    className,
    onKeyDown,
    autoFocus,
    required,
    id,
    'aria-invalid': ariaInvalid,
    'aria-describedby': ariaDescribedBy,
}: DateTimeInputProps) {
    const [datePart, timePart] = useMemo(() => {
        // Anchored: values in other shapes (seconds, Z suffix, relative-date
        // tokens) render as empty sub-fields — the same blank the native
        // datetime-local control showed — instead of being reinterpreted as
        // wall-clock time and silently rewritten on edit.
        const match = value.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})$/);
        return match ? [match[1], match[2]] : ['', ''];
    }, [value]);
    // Holds a time picked before any date is set; one-shot — consumed by the
    // first date commit and discarded whenever the value is cleared.
    const [pendingTime, setPendingTime] = useState('');
    // The handlers below clear it on their own paths, but the parent can also
    // drive the props externally: once any full datetime exists the cache is
    // consumed or superseded, and a disabled control's half-entered state is
    // abandoned (a bulk-edit row being unchecked never sees a value change —
    // the disabled flip is the only observable reset signal). Either way,
    // retire the cache so a later date pick cannot resurrect a stale time.
    useEffect(() => {
        if (datePart || disabled) setPendingTime('');
    }, [datePart, disabled]);

    return (
        <div className="flex gap-2">
            <div className="flex-1 min-w-0">
                <DateInput
                    value={datePart}
                    disabled={disabled}
                    className={className}
                    onKeyDown={onKeyDown}
                    autoFocus={autoFocus}
                    required={required}
                    id={id}
                    aria-invalid={ariaInvalid}
                    aria-describedby={ariaDescribedBy}
                    onChange={(newDate) => {
                        if (newDate === '') {
                            setPendingTime('');
                            onChange('');
                        } else {
                            onChange(`${newDate}T${timePart || pendingTime || '00:00'}`);
                            setPendingTime('');
                        }
                    }}
                />
            </div>
            <div className="w-28 flex-shrink-0">
                <TimeInput
                    value={timePart || pendingTime}
                    disabled={disabled}
                    className={className}
                    onKeyDown={onKeyDown}
                    id={id ? `${id}-time` : undefined}
                    aria-invalid={ariaInvalid}
                    aria-describedby={ariaDescribedBy}
                    onChange={(newTime) => {
                        if (datePart) {
                            onChange(`${datePart}T${newTime || '00:00'}`);
                        } else {
                            setPendingTime(newTime);
                        }
                    }}
                />
            </div>
        </div>
    );
}
