import './bootstrap';
import htmx from 'htmx.org';

const SELECTORS = {
    calendarToggle: '.calendar-toggle',
    calendarViewForm: '#calendar-view',
    calendarExpandToggle: '[data-calendar-expand]',
    colorPicker: '[data-colorpicker]',
    colorPickerColor: '[data-colorpicker-color]',
    colorPickerHex: '[data-colorpicker-hex]',
    colorPickerRandom: '[data-colorpicker-random]',
    eventAllDayToggle: '[data-all-day-toggle]',
    eventStartInput: '[data-event-start]',
    eventEndInput: '[data-event-end]',
    eventTitleInput: 'input[name="title"]',
    eventLocationInput: 'input[name="location"]',
    modalDialog: 'dialog',
    modalContent: '#modal',
    modalClassSource: '[data-modal-class]',
    modalAside: '#modal-aside',
    modalAsideList: '[data-modal-aside-list]',
    modalExpand: '[data-modal-expand]',
    monthDay: '.calendar.month .day',
    monthDayEvent: 'a.event',
    monthDayMore: '[data-day-more]',
    monthDayMoreWrap: '.more-events',
    monthlyMode: '[data-monthly-mode]',
    monthlyDays: '[data-monthly-days]',
    monthlyWeekday: '[data-monthly-weekday]',
    naturalEventInput: '[data-natural-event-input]',
    recurrenceFrequency: '[data-recurrence-frequency]',
    recurrenceInterval: '[data-recurrence-interval]',
    recurrenceUnit: '[data-recurrence-unit]',
    recurrenceSection: '[data-recurrence-section]',
    tabsRoot: '[data-tabs]',
    tabButton: '[role=\"tab\"]',
    tabPanel: '[role=\"tabpanel\"]',
    attendeesRoot: '[data-attendees]',
    attendeesList: '[data-attendees-list]',
    attendeeTemplate: 'template[data-attendee-template]',
    attendeeLookup: '[data-attendee-lookup]',
    attendeeSuggestions: '#attendee-suggestions',
    attendeeAddManual: '[data-attendee-add-manual]',
    attendeePick: '[data-attendee-pick]',
    attendeeRemove: '[data-attendee-remove]',
    attendeeRow: '[data-attendee-row]',
    attendeeRole: '[data-attendee-role]',
    attendeeName: '[data-attendee-name]',
    attendeeOptional: '[data-attendee-optional]',
    attendeeDisplay: '[data-attendee-display]',
    attendeeVerified: '[data-attendee-verified]',
    attendeeEmail: '[data-attendee-email]',
    attendeeUri: '[data-attendee-uri]',
};

function syncModalRootClass(modal) {
    if (!modal) return;

    const previous = (modal.dataset.appliedClass || '')
        .split(/\s+/)
        .filter(Boolean);

    if (previous.length) {
        modal.classList.remove(...previous);
    }

    const source = modal.querySelector(SELECTORS.modalClassSource);
    const next = (source?.dataset?.modalClass || '')
        .split(/\s+/)
        .filter(Boolean);

    if (next.length) {
        modal.classList.add(...next);
    }

    modal.dataset.appliedClass = next.join(' ');
}

function clearModalRootClass(modal) {
    if (!modal) return;

    const previous = (modal.dataset.appliedClass || '')
        .split(/\s+/)
        .filter(Boolean);

    if (previous.length) {
        modal.classList.remove(...previous);
    }

    modal.dataset.appliedClass = '';
}

function renderModalAside(dialog, items = [], show = false) {
    if (!dialog) return;

    const aside = dialog.querySelector(SELECTORS.modalAside);
    if (!aside) return;

    const signature = `${show ? '1' : '0'}|${items.map((item) => `${item.label}:${item.value}`).join('|')}`;
    if (aside.dataset.asideSignature === signature) {
        return;
    }

    const list = aside.querySelector(SELECTORS.modalAsideList);
    if (list) {
        const existing = new Map(
            Array.from(list.querySelectorAll('li[data-aside-key]')).map((li) => [li.dataset.asideKey, li])
        );
        const usedKeys = new Set();

        items.forEach(({ label, value }) => {
            const key = String(label || '').trim().toLowerCase().replace(/\s+/g, '-');
            usedKeys.add(key);

            let li = existing.get(key);
            if (!li) {
                li = document.createElement('li');
                li.dataset.asideKey = key;
                li.classList.add('is-visible');

                const strong = document.createElement('strong');
                const text = document.createElement('span');
                li.appendChild(strong);
                li.appendChild(text);
                list.appendChild(li);

                li.addEventListener('animationend', () => {
                    li.classList.remove('is-visible');
                }, { once: true });
            }

            const strong = li.querySelector('strong');
            const text = li.querySelector('span');
            const nextLabel = `${label}:`;
            const nextValue = ` ${value}`;

            if (strong && strong.textContent !== nextLabel) {
                strong.textContent = nextLabel;
            }
            if (text && text.textContent !== nextValue) {
                text.textContent = nextValue;
            }
        });

        existing.forEach((li, key) => {
            if (!usedKeys.has(key)) {
                li.remove();
            }
        });
    }

    aside.classList.toggle('is-visible', show);
    aside.setAttribute('aria-hidden', show ? 'false' : 'true');
    aside.dataset.asideSignature = signature;
}

function clearModalAside(dialog) {
    renderModalAside(dialog, [], false);
}

/**
 *
 * htmx/global
 */

// make html globally visible to use the devtools and extensions
window.htmx = htmx;

// global htmx config
htmx.config.historyEnabled = true; // HX-Boost back/forward support
htmx.logger = console.log;         // verbose logging during dev

// csrf on htmx requests
document.addEventListener('htmx:configRequest', (evt) => {
    const token = document.querySelector('meta[name="csrf-token"]')?.content
    if (token) evt.detail.headers['X-CSRF-TOKEN'] = token
})

// modal htmx tracking
document.addEventListener('htmx:beforeSwap', (evt) => {
    const target = evt.detail?.target || evt.target;
    if (target && target.id === 'modal') {
        target.dataset.prevUrl = window.location.href;
        syncModalRootClass(target);
    }
});

/**
 *
 * global auth expiry redirect (fetch/axios)
 */

const AUTH_REDIRECT_STATUSES = new Set([401, 419]);
const redirectToLogin = () => {
    if (window.location.pathname !== '/login') {
        window.location.assign('/login');
    }
};

if (window.fetch) {
    const originalFetch = window.fetch.bind(window);
    window.fetch = async (...args) => {
        const response = await originalFetch(...args);
        if (response && AUTH_REDIRECT_STATUSES.has(response.status)) {
            redirectToLogin();
        }
        return response;
    };
}

if (window.axios) {
    window.axios.interceptors.response.use(
        (response) => response,
        (error) => {
            const status = error?.response?.status;
            if (AUTH_REDIRECT_STATUSES.has(status)) {
                redirectToLogin();
            }
            return Promise.reject(error);
        }
    );
}

/**
 *
 * calendar ui improvements
 */

// progressive enhancement on html form with no JS
document.addEventListener('change', (event) => {
    const target = event.target;

    if (target?.matches(SELECTORS.calendarToggle)) {
        const slug = target.value;
        const show = target.checked;

        document
            .querySelectorAll(`[data-calendar="${slug}"]`)
            .forEach(el => el.classList.toggle('hidden', !show));
        return;
    }

    const form = target?.form;
    if (!form || form.id !== 'calendar-view') return;
    if (target.name !== 'view') return;

    form.requestSubmit();
});

// close event modal on back/forward navigation
window.addEventListener('popstate', () => {
    if (!document.querySelector('article#calendar')) return;

    const dialog = document.querySelector('dialog');
    if (!dialog?.open) return;

    const modal = dialog.querySelector('#modal');
    if (!modal?.querySelector('[data-modal-kind="event"]')) return;

    dialog.close();
});

/**
 *
 * event form all-day toggle
 *
 * converts the datetime-local inputs into regular date inputs, maintaining the date
 */
function initEventAllDayToggles(root = document) {
    const toDate = (value) => {
        if (!value) return '';
        return value.split('T')[0];
    };

    const withTime = (date, time) => {
        if (!date) return '';
        return `${date}T${time}`;
    };

    root.querySelectorAll(SELECTORS.eventAllDayToggle).forEach((toggleHost) => {
        const toggle = toggleHost.matches('input[type="checkbox"]')
            ? toggleHost
            : toggleHost.querySelector('input[type="checkbox"]');

        if (!toggle || toggle.__allDayWired) return;
        toggle.__allDayWired = true;

        const form = toggle.closest('form') || toggleHost.closest('form');
        if (!form) return;

        const start = form.querySelector(SELECTORS.eventStartInput);
        const end = form.querySelector(SELECTORS.eventEndInput);
        if (!start || !end) return;

        const apply = () => {
            if (toggle.checked) {
                if (start.type === 'datetime-local') {
                    start.dataset.datetimeValue = start.value;
                }
                if (end.type === 'datetime-local') {
                    end.dataset.datetimeValue = end.value;
                }

                const startDate = toDate(start.value);
                const endDate = toDate(end.value);

                start.type = 'date';
                end.type = 'date';

                if (startDate) start.value = startDate;
                if (endDate) end.value = endDate;

                if (start.value && end.value && end.value < start.value) {
                    end.value = start.value;
                }
            } else {
                const startDate = toDate(start.value);
                const endDate = toDate(end.value);
                const startTime = (start.dataset.datetimeValue || '').split('T')[1] || '09:00';
                const endTime = (end.dataset.datetimeValue || '').split('T')[1] || '10:00';

                start.type = 'datetime-local';
                end.type = 'datetime-local';

                start.value = start.dataset.datetimeValue || withTime(startDate, startTime);
                end.value = end.dataset.datetimeValue || withTime(endDate || startDate, endTime);
            }
        };

        toggle.addEventListener('change', apply);
        apply();
    });
}

/**
 *
 * recurrence preset selector
 */
function initRecurrenceControls(root = document) {
    const sections = root.querySelectorAll(SELECTORS.recurrenceSection);
    if (!sections.length) return;

    const select = root.querySelector(SELECTORS.recurrenceFrequency);
    const intervalRow = root.querySelector(SELECTORS.recurrenceInterval);
    const intervalUnit = root.querySelector(SELECTORS.recurrenceUnit);
    const monthModes = root.querySelectorAll(SELECTORS.monthlyMode);
    const monthDays = root.querySelector(SELECTORS.monthlyDays);
    const monthWeekday = root.querySelector(SELECTORS.monthlyWeekday);

    if (!select) return;

    const unitMap = {
        daily: 'day',
        weekly: 'week',
        monthly: 'month',
        yearly: 'year',
    };

    const applyMonthlyMode = () => {
        if (!monthDays || !monthWeekday) return;
        const modeInput = Array.from(monthModes).find((input) => input.checked);
        const mode = modeInput?.value || 'days';

        if (mode === 'weekday') {
            monthDays.classList.add('hidden');
            monthWeekday.classList.remove('hidden');
        } else {
            monthDays.classList.remove('hidden');
            monthWeekday.classList.add('hidden');
        }
    };

    const apply = () => {
        const value = select.value;
        const show = value !== '';

        sections.forEach((section) => {
            const type = section.getAttribute('data-recurrence-section');
            if (!show) {
                section.classList.add('hidden');
                return;
            }
            section.classList.toggle('hidden', type !== value);
        });

        if (intervalRow) {
            intervalRow.classList.toggle('hidden', !show);
        }

        if (intervalUnit) {
            const unit = unitMap[value] || 'day';
            intervalUnit.textContent = unit ? `${unit}(s)` : '';
        }

        if (value === 'monthly') {
            applyMonthlyMode();
        }
    };

    select.addEventListener('change', apply);
    monthModes.forEach((input) => input.addEventListener('change', applyMonthlyMode));

    apply();
}

/**
 *
 * natural-language event parser (create modal header input)
 */
function initNaturalEventParser(root = document) {
    const monthMap = {
        jan: 0, january: 0,
        feb: 1, february: 1,
        mar: 2, march: 2,
        apr: 3, april: 3,
        may: 4,
        jun: 5, june: 5,
        jul: 6, july: 6,
        aug: 7, august: 7,
        sep: 8, sept: 8, september: 8,
        oct: 9, october: 9,
        nov: 10, november: 10,
        dec: 11, december: 11,
    };

    const pad = (value) => String(value).padStart(2, '0');

    const toLocalDateInputValue = (date) => (
        `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
    );

    const toLocalDatetimeInputValue = (date) => (
        `${toLocalDateInputValue(date)}T${pad(date.getHours())}:${pad(date.getMinutes())}`
    );

    const readDateInputValue = (value) => {
        if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
        const [year, month, day] = value.split('-').map((n) => parseInt(n, 10));
        if ([year, month, day].some((n) => Number.isNaN(n))) return null;
        return new Date(year, month - 1, day, 0, 0, 0, 0);
    };

    const readDatetimeInputValue = (value) => {
        if (!value || !value.includes('T')) return null;
        const [datePart, timePart] = value.split('T');
        if (!datePart || !timePart) return null;
        const [year, month, day] = datePart.split('-').map((n) => parseInt(n, 10));
        const [hours, minutes] = timePart.split(':').map((n) => parseInt(n, 10));
        if ([year, month, day, hours, minutes].some((n) => Number.isNaN(n))) return null;
        return new Date(year, month - 1, day, hours, minutes, 0, 0);
    };

    const formatDayLong = (date) => (
        date.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' })
    );

    const formatTimeShort = (date) => (
        date
            .toLocaleTimeString(undefined, {
                hour: 'numeric',
                minute: date.getMinutes() === 0 ? undefined : '2-digit',
            })
            .toLowerCase()
            .replace(/\./g, '')
            .replace(/\s+/g, '')
    );

    const formatDateSummary = (startDate, endDate, allDay) => {
        if (!startDate) return '';

        const startDay = formatDayLong(startDate);

        if (allDay) {
            if (endDate && endDate.toDateString() !== startDate.toDateString()) {
                return `${startDay} to ${formatDayLong(endDate)}, all day`;
            }
            return `${startDay}, all day`;
        }

        const startTime = formatTimeShort(startDate);
        if (!endDate) {
            return `${startDay}, ${startTime}`;
        }

        const endTime = formatTimeShort(endDate);
        if (endDate.toDateString() === startDate.toDateString()) {
            return `${startDay}, ${startTime} to ${endTime}`;
        }

        return `${startDay}, ${startTime} to ${formatDayLong(endDate)}, ${endTime}`;
    };

    const parseDurationMinutes = (text) => {
        const match = text.match(/\bfor\s+(\d+)\s*(minutes?|mins?|hours?|hrs?)\b/i);
        if (!match) return 60;

        const count = parseInt(match[1], 10);
        const unit = (match[2] || '').toLowerCase();
        if (Number.isNaN(count) || count <= 0) return 60;

        if (unit.startsWith('hour') || unit.startsWith('hr')) {
            return count * 60;
        }

        return count;
    };

    const parseDateToken = (text, now) => {
        const lower = text.toLowerCase();

        const todayMatch = lower.match(/\btoday\b/);
        if (todayMatch) {
            const date = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
            return { date, index: todayMatch.index ?? 0 };
        }

        const tomorrowMatch = lower.match(/\btomorrow\b/);
        if (tomorrowMatch) {
            const date = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0, 0);
            return { date, index: tomorrowMatch.index ?? 0 };
        }

        const monthMatch = text.match(/\b(?:on\s+)?([A-Za-z]{3,9})\.?\s+(\d{1,2})(?:,\s*(\d{4}))?\b/);
        if (monthMatch) {
            const monthToken = (monthMatch[1] || '').toLowerCase();
            const month = monthMap[monthToken];
            const day = parseInt(monthMatch[2], 10);
            const explicitYear = monthMatch[3] ? parseInt(monthMatch[3], 10) : null;

            if (month !== undefined && !Number.isNaN(day) && day >= 1 && day <= 31) {
                const thisYear = now.getFullYear();
                let date = new Date(explicitYear || thisYear, month, day, 0, 0, 0, 0);
                if (!explicitYear) {
                    const today = new Date(thisYear, now.getMonth(), now.getDate(), 0, 0, 0, 0);
                    if (date < today) {
                        date = new Date(thisYear + 1, month, day, 0, 0, 0, 0);
                    }
                }
                return { date, index: monthMatch.index ?? 0 };
            }
        }

        const numericMatch = text.match(/\b(?:on\s+)?(\d{1,2})\/(\d{1,2})(?:\/(\d{2,4}))?\b/);
        if (numericMatch) {
            const month = parseInt(numericMatch[1], 10) - 1;
            const day = parseInt(numericMatch[2], 10);
            const yearToken = numericMatch[3];
            const thisYear = now.getFullYear();
            let year = thisYear;

            if (yearToken) {
                year = parseInt(yearToken, 10);
                if (year < 100) year += 2000;
            }

            if (!Number.isNaN(month) && !Number.isNaN(day)) {
                let date = new Date(year, month, day, 0, 0, 0, 0);
                if (!yearToken) {
                    const today = new Date(thisYear, now.getMonth(), now.getDate(), 0, 0, 0, 0);
                    if (date < today) {
                        date = new Date(thisYear + 1, month, day, 0, 0, 0, 0);
                    }
                }
                return { date, index: numericMatch.index ?? 0 };
            }
        }

        return null;
    };

    const parseTimeToken = (text) => {
        const lowered = text.toLowerCase();
        const noonIndex = lowered.indexOf(' at noon');
        if (noonIndex !== -1) {
            return { hours: 12, minutes: 0, index: noonIndex };
        }

        const midnightIndex = lowered.indexOf(' at midnight');
        if (midnightIndex !== -1) {
            return { hours: 0, minutes: 0, index: midnightIndex };
        }

        const match = text.match(/\bat\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\b/i);
        if (!match) return null;

        let hours = parseInt(match[1], 10);
        const minutes = parseInt(match[2] || '0', 10);
        const meridiem = (match[3] || '').toLowerCase();

        if (Number.isNaN(hours) || Number.isNaN(minutes)) return null;

        if (meridiem) {
            if (hours === 12 && meridiem === 'am') hours = 0;
            if (hours < 12 && meridiem === 'pm') hours += 12;
        } else if (hours > 23) {
            return null;
        }

        return {
            hours,
            minutes,
            index: match.index ?? 0,
        };
    };

    const parseRelativeToken = (text, now) => {
        const addOffset = (days, minutes) => {
            const date = new Date(now.getTime());
            date.setMinutes(date.getMinutes() + (days * 24 * 60) + minutes);
            return date;
        };

        let match = text.match(/\bin\s+(\d+)\s+days?(?:\s+and\s+(\d+)\s+minutes?)?\b/i);
        if (match) {
            const days = parseInt(match[1], 10);
            const minutes = parseInt(match[2] || '0', 10);
            if (!Number.isNaN(days) && !Number.isNaN(minutes)) {
                return {
                    date: addOffset(days, minutes),
                    index: match.index ?? 0,
                };
            }
        }

        match = text.match(/\b(\d+)\s+days?(?:\s+and\s+(\d+)\s+minutes?)?\s+from\s+now\b/i);
        if (match) {
            const days = parseInt(match[1], 10);
            const minutes = parseInt(match[2] || '0', 10);
            if (!Number.isNaN(days) && !Number.isNaN(minutes)) {
                return {
                    date: addOffset(days, minutes),
                    index: match.index ?? 0,
                };
            }
        }

        match = text.match(/\bin\s+(\d+)\s+minutes?\b/i);
        if (match) {
            const minutes = parseInt(match[1], 10);
            if (!Number.isNaN(minutes)) {
                return {
                    date: addOffset(0, minutes),
                    index: match.index ?? 0,
                };
            }
        }

        match = text.match(/\b(\d+)\s+minutes?\s+from\s+now\b/i);
        if (match) {
            const minutes = parseInt(match[1], 10);
            if (!Number.isNaN(minutes)) {
                return {
                    date: addOffset(0, minutes),
                    index: match.index ?? 0,
                };
            }
        }

        return null;
    };

    const weekdayCodeMap = {
        su: 'SU', sun: 'SU', sunday: 'SU', sundays: 'SU',
        mo: 'MO', mon: 'MO', monday: 'MO', mondays: 'MO',
        tu: 'TU', tue: 'TU', tues: 'TU', tuesday: 'TU', tuesdays: 'TU',
        we: 'WE', wed: 'WE', wednesday: 'WE', wednesdays: 'WE',
        th: 'TH', thu: 'TH', thur: 'TH', thurs: 'TH', thursday: 'TH', thursdays: 'TH',
        fr: 'FR', fri: 'FR', friday: 'FR', fridays: 'FR',
        sa: 'SA', sat: 'SA', saturday: 'SA', saturdays: 'SA',
    };

    const weekdayNameMap = {
        SU: 'Sunday',
        MO: 'Monday',
        TU: 'Tuesday',
        WE: 'Wednesday',
        TH: 'Thursday',
        FR: 'Friday',
        SA: 'Saturday',
    };

    const weekdayCodeFromDate = (date) => ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'][date.getDay()];

    const parseRecurrenceToken = (text) => {
        const repeatMatch = text.match(/\brepeats?\b([\s\S]*)$/i);
        if (!repeatMatch) return null;

        const tail = (repeatMatch[1] || '').trim().toLowerCase();
        const recurrence = {
            frequency: null,
            interval: 1,
            weekdays: [],
            index: repeatMatch.index ?? 0,
        };

        const everyNumbered = tail.match(/\bevery\s+(\d+)\s*(day|week|month|year)s?\b/i);
        if (everyNumbered) {
            const count = parseInt(everyNumbered[1], 10);
            const unit = everyNumbered[2].toLowerCase();
            if (!Number.isNaN(count) && count > 0) {
                recurrence.interval = count;
            }
            recurrence.frequency = {
                day: 'daily',
                week: 'weekly',
                month: 'monthly',
                year: 'yearly',
            }[unit] || null;
        } else if (/\b(daily|every day)\b/i.test(tail)) {
            recurrence.frequency = 'daily';
        } else if (/\b(weekly|every week)\b/i.test(tail)) {
            recurrence.frequency = 'weekly';
        } else if (/\b(monthly|every month)\b/i.test(tail)) {
            recurrence.frequency = 'monthly';
        } else if (/\b(yearly|every year)\b/i.test(tail)) {
            recurrence.frequency = 'yearly';
        }

        if (!recurrence.frequency) {
            return null;
        }

        if (recurrence.frequency === 'weekly') {
            const onMatch = tail.match(/\bon\s+(.+)$/i);
            if (onMatch) {
                const weekdays = [];
                const tokens = onMatch[1]
                    .replace(/[,/]/g, ' ')
                    .replace(/\band\b/g, ' ')
                    .split(/\s+/)
                    .map((token) => token.trim().toLowerCase().replace(/[^a-z]/g, ''))
                    .filter(Boolean);

                tokens.forEach((token) => {
                    const code = weekdayCodeMap[token];
                    if (code && !weekdays.includes(code)) {
                        weekdays.push(code);
                    }
                });

                recurrence.weekdays = weekdays;
            }
        }

        return recurrence;
    };

    const parseLocationToken = (text) => {
        const lower = text.toLowerCase();
        const index = lower.lastIndexOf(' at ');
        if (index === -1) return null;

        const candidate = text.slice(index + 4).trim().replace(/[.,;]+$/, '');
        if (!candidate) return null;
        if (/^\d{1,2}(?::\d{2})?\s*(am|pm)?\b/i.test(candidate)) return null;
        if (/\bfor\s+\d+\s*(minutes?|mins?|hours?|hrs?)\b/i.test(candidate)) return null;

        return candidate;
    };

    const parseTitleToken = (text, dateToken, timeToken, relativeToken, recurrenceToken, locationToken) => {
        const boundaries = [];
        if (dateToken) boundaries.push(dateToken.index);
        if (timeToken) boundaries.push(timeToken.index);
        if (relativeToken) boundaries.push(relativeToken.index);
        if (recurrenceToken) boundaries.push(recurrenceToken.index);

        const lower = text.toLowerCase();
        const durationIndex = lower.indexOf(' for ');
        if (durationIndex !== -1) boundaries.push(durationIndex);

        if (locationToken) {
            const locationIndex = lower.lastIndexOf(' at ');
            if (locationIndex !== -1) boundaries.push(locationIndex);
        }

        if (!boundaries.length) {
            return text.trim();
        }

        const end = Math.min(...boundaries);
        return text.slice(0, end).trim().replace(/[,:;.\-]+$/, '').trim();
    };

    const parseNaturalEventText = (text, baseDate) => {
        const trimmed = (text || '').trim();
        if (!trimmed) return null;

        const relativeToken = parseRelativeToken(trimmed, baseDate);
        const recurrenceToken = parseRecurrenceToken(trimmed);
        const dateToken = parseDateToken(trimmed, baseDate);
        const timeToken = parseTimeToken(trimmed);
        const durationMinutes = parseDurationMinutes(trimmed);
        const location = parseLocationToken(trimmed);
        const title = parseTitleToken(trimmed, dateToken, timeToken, relativeToken, recurrenceToken, location);
        const allDay = /\ball(?:\s|-)?day\b/i.test(trimmed);

        return {
            title,
            location,
            dateToken,
            timeToken,
            relativeToken,
            recurrenceToken,
            durationMinutes,
            allDay,
        };
    };

    const findFormFromNaturalInput = (input) => {
        const dialog = input.closest('dialog');
        if (dialog) {
            return dialog.querySelector('#event-form');
        }

        const modal = input.closest(SELECTORS.modalContent);
        if (modal) {
            return modal.querySelector('#event-form');
        }

        return document.querySelector('#event-form');
    };

    const enableNaturalCollapsedMode = (input) => {
        const dialog = input.closest('dialog');
        const modal = dialog?.querySelector(SELECTORS.modalContent);
        if (!dialog || !modal || modal.classList.contains('natural-expanded')) return;

        modal.classList.add('natural-collapsed');
        renderModalAside(dialog, [], true);
    };

    const clearRecurrenceInputs = (form) => {
        const frequencyInput = form.querySelector('select[name="repeat_frequency"]');
        const intervalInput = form.querySelector('input[name="repeat_interval"]');
        const weekdayInputs = Array.from(form.querySelectorAll('input[name="repeat_weekdays[]"]'));
        const monthlyModeInputs = Array.from(form.querySelectorAll('input[name="repeat_monthly_mode"]'));
        const monthDayInputs = Array.from(form.querySelectorAll('input[name="repeat_month_days[]"]'));

        if (frequencyInput) {
            frequencyInput.value = '';
            frequencyInput.dispatchEvent(new Event('change', { bubbles: true }));
        }
        if (intervalInput) {
            intervalInput.value = '1';
        }
        weekdayInputs.forEach((input) => {
            input.checked = false;
        });
        monthlyModeInputs.forEach((input) => {
            input.checked = input.value === 'days';
        });
        monthDayInputs.forEach((input) => {
            input.checked = false;
        });
    };

    const applyRecurrenceInputs = (form, recurrenceToken, anchorDate) => {
        const frequencyInput = form.querySelector('select[name="repeat_frequency"]');
        const intervalInput = form.querySelector('input[name="repeat_interval"]');
        const weekdayInputs = Array.from(form.querySelectorAll('input[name="repeat_weekdays[]"]'));
        const monthlyModeInputs = Array.from(form.querySelectorAll('input[name="repeat_monthly_mode"]'));
        const monthDayInputs = Array.from(form.querySelectorAll('input[name="repeat_month_days[]"]'));

        if (!frequencyInput || !recurrenceToken?.frequency) {
            return;
        }

        frequencyInput.value = recurrenceToken.frequency;
        frequencyInput.dispatchEvent(new Event('change', { bubbles: true }));

        if (intervalInput) {
            intervalInput.value = String(recurrenceToken.interval || 1);
        }

        if (recurrenceToken.frequency === 'weekly') {
            let weekdays = recurrenceToken.weekdays || [];
            if (!weekdays.length && anchorDate) {
                weekdays = [weekdayCodeFromDate(anchorDate)];
            }
            weekdayInputs.forEach((input) => {
                input.checked = weekdays.includes(input.value);
            });
        } else {
            weekdayInputs.forEach((input) => {
                input.checked = false;
            });
        }

        if (recurrenceToken.frequency === 'monthly') {
            monthlyModeInputs.forEach((input) => {
                input.checked = input.value === 'days';
            });

            const dayOfMonth = anchorDate ? anchorDate.getDate() : null;
            monthDayInputs.forEach((input) => {
                input.checked = dayOfMonth ? Number(input.value) === dayOfMonth : false;
            });
        } else {
            monthDayInputs.forEach((input) => {
                input.checked = false;
            });
        }
    };

    const recurrenceSummary = (recurrenceToken, anchorDate) => {
        if (!recurrenceToken?.frequency) return '';

        const interval = recurrenceToken.interval || 1;

        if (recurrenceToken.frequency === 'daily') {
            return interval === 1 ? 'Daily' : `Every ${interval} days`;
        }

        if (recurrenceToken.frequency === 'weekly') {
            const days = (recurrenceToken.weekdays || []).length
                ? recurrenceToken.weekdays
                : (anchorDate ? [weekdayCodeFromDate(anchorDate)] : []);
            const dayLabels = days.map((code) => weekdayNameMap[code] || code);
            const head = interval === 1 ? 'Every week' : `Every ${interval} weeks`;
            return dayLabels.length ? `${head} on ${dayLabels.join(' and ')}` : head;
        }

        if (recurrenceToken.frequency === 'monthly') {
            return interval === 1 ? 'Monthly' : `Every ${interval} months`;
        }

        if (recurrenceToken.frequency === 'yearly') {
            return interval === 1 ? 'Yearly' : `Every ${interval} years`;
        }

        return '';
    };

    const applyParsedData = (input) => {
        const dialog = input.closest('dialog');
        const modal = dialog?.querySelector(SELECTORS.modalContent);
        const form = findFormFromNaturalInput(input);
        if (!form) return;

        const titleInput = form.querySelector(SELECTORS.eventTitleInput);
        const locationInput = form.querySelector(SELECTORS.eventLocationInput);
        const startInput = form.querySelector(SELECTORS.eventStartInput);
        const endInput = form.querySelector(SELECTORS.eventEndInput);
        const allDayToggleHost = form.querySelector(SELECTORS.eventAllDayToggle);
        const allDayToggle = allDayToggleHost?.matches('input[type="checkbox"]')
            ? allDayToggleHost
            : allDayToggleHost?.querySelector('input[type="checkbox"]');

        if (!titleInput || !startInput || !endInput) return;

        const parsed = parseNaturalEventText(input.value, new Date());
        if (!parsed) {
            if (modal?.classList.contains('natural-collapsed')) {
                renderModalAside(dialog, [], true);
            }
            return;
        }

        if (parsed.title) {
            titleInput.value = parsed.title;
        }

        if (parsed.location && locationInput) {
            locationInput.value = parsed.location;
        }

        const existingStart = startInput.type === 'date'
            ? readDateInputValue(startInput.value)
            : readDatetimeInputValue(startInput.value);
        const baseDate = parsed.relativeToken?.date
            ? new Date(parsed.relativeToken.date.getTime())
            : parsed.dateToken?.date
                ? new Date(parsed.dateToken.date.getTime())
                : (existingStart ? new Date(existingStart.getTime()) : new Date());

        if (parsed.allDay) {
            if (allDayToggle && !allDayToggle.checked) {
                allDayToggle.checked = true;
                allDayToggle.dispatchEvent(new Event('change', { bubbles: true }));
            }

            const dateOnly = toLocalDateInputValue(baseDate);
            startInput.value = dateOnly;
            endInput.value = dateOnly;
        } else if (parsed.dateToken || parsed.timeToken || parsed.relativeToken) {
            if (allDayToggle?.checked) {
                allDayToggle.checked = false;
                allDayToggle.dispatchEvent(new Event('change', { bubbles: true }));
            }

            const hours = parsed.timeToken?.hours ?? (existingStart ? existingStart.getHours() : 9);
            const minutes = parsed.timeToken?.minutes ?? (existingStart ? existingStart.getMinutes() : 0);
            baseDate.setHours(hours, minutes, 0, 0);

            const end = new Date(baseDate.getTime());
            end.setMinutes(end.getMinutes() + parsed.durationMinutes);

            startInput.value = toLocalDatetimeInputValue(baseDate);
            endInput.value = toLocalDatetimeInputValue(end);
        }

        if (parsed.recurrenceToken) {
            applyRecurrenceInputs(form, parsed.recurrenceToken, baseDate);
            input.dataset.nlRecurrenceApplied = '1';
        } else if (input.dataset.nlRecurrenceApplied === '1') {
            clearRecurrenceInputs(form);
            input.dataset.nlRecurrenceApplied = '0';
        }

        const summaryItems = [];
        if (parsed.title) {
            summaryItems.push({ label: 'Title', value: parsed.title });
        }
        if (parsed.location) {
            summaryItems.push({ label: 'Location', value: parsed.location });
        }
        if (parsed.dateToken || parsed.timeToken || parsed.relativeToken || parsed.allDay) {
            const summaryStart = startInput.type === 'date'
                ? readDateInputValue(startInput.value)
                : readDatetimeInputValue(startInput.value);
            const summaryEnd = endInput.type === 'date'
                ? readDateInputValue(endInput.value)
                : readDatetimeInputValue(endInput.value);
            const allDayState = Boolean(allDayToggle?.checked);
            const summaryDate = formatDateSummary(summaryStart, summaryEnd, allDayState);

            if (summaryDate) {
                summaryItems.push({ label: 'Date', value: summaryDate });
            }
        }
        if (parsed.recurrenceToken) {
            const repeat = recurrenceSummary(parsed.recurrenceToken, baseDate);
            if (repeat) {
                summaryItems.push({ label: 'Repeat', value: repeat });
            }
        }

        if (modal?.classList.contains('natural-collapsed')) {
            renderModalAside(dialog, summaryItems, true);
        }
    };

    if (!document.__naturalEventDelegated) {
        document.__naturalEventDelegated = true;

        document.addEventListener('input', (event) => {
            const input = event.target?.closest?.(SELECTORS.naturalEventInput);
            if (!input) return;
            applyParsedData(input);
        });

        document.addEventListener('change', (event) => {
            const input = event.target?.closest?.(SELECTORS.naturalEventInput);
            if (!input) return;
            applyParsedData(input);
        });

        document.addEventListener('click', (event) => {
            const button = event.target?.closest?.(SELECTORS.modalExpand);
            if (!button) return;

            const dialog = button.closest('dialog');
            const modal = dialog?.querySelector(SELECTORS.modalContent);
            if (!dialog || !modal) return;

            modal.classList.remove('natural-collapsed');
            modal.classList.add('natural-expanded');
            clearModalAside(dialog);
        });
    }

    root.querySelectorAll(SELECTORS.naturalEventInput).forEach((input) => {
        enableNaturalCollapsedMode(input);

        if (input.value?.trim()) {
            applyParsedData(input);
        }
    });
}

/**
 *
 * modal behaviors (backdrop close + url restore)
 */
function initModalHandlers(root = document) {
    const dialog = root.querySelector(SELECTORS.modalDialog);
    if (!dialog || dialog.__modalWired) return;
    dialog.__modalWired = true;

    dialog.addEventListener('click', (event) => {
        if (event.target === dialog) {
            dialog.close();
        }
    });

    dialog.addEventListener('close', () => {
        const modal = dialog.querySelector(SELECTORS.modalContent);
        if (!modal) return;

        const isEvent = modal.querySelector('[data-modal-kind="event"]');
        const prevUrl = modal.dataset.prevUrl;
        modal.classList.remove('natural-collapsed', 'natural-expanded');
        modal.innerHTML = '';
        clearModalRootClass(modal);
        clearModalAside(dialog);

        if (isEvent && prevUrl) {
            history.replaceState({}, '', prevUrl);
        }
    });
}

/**
 * tabs (simple modal panels)
 */
function initTabs(root = document) {
    root.querySelectorAll(SELECTORS.tabsRoot).forEach((tabs) => {
        if (tabs.__tabsWired) return;
        tabs.__tabsWired = true;

        const tabEls = Array.from(tabs.querySelectorAll(SELECTORS.tabButton));
        const panels = Array.from(tabs.querySelectorAll(SELECTORS.tabPanel));
        if (!tabEls.length || !panels.length) return;

        const getPanelForTab = (tab, index) => {
            const controls = tab.getAttribute('aria-controls');

            if (controls) {
                const panelById = tabs.querySelector(`[role="tabpanel"]#${controls}`);
                if (panelById) return panelById;
            }

            return panels[index] || null;
        };

        const panelByTab = new Map();
        tabEls.forEach((tab, index) => {
            const panel = getPanelForTab(tab, index);
            if (panel) {
                panelByTab.set(tab, panel);
            }

            // <li role="tab"> is not focusable by default.
            if (!tab.hasAttribute('tabindex')) {
                tab.setAttribute('tabindex', '-1');
            }
        });

        const activate = (activeTab, moveFocus = false) => {
            tabEls.forEach((tab) => {
                const isActive = tab === activeTab;
                tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
                tab.setAttribute('tabindex', isActive ? '0' : '-1');
            });

            panels.forEach((panel) => {
                panel.hidden = true;
            });

            const panel = panelByTab.get(activeTab);
            if (panel) {
                panel.hidden = false;
            }

            if (moveFocus && typeof activeTab.focus === 'function') {
                activeTab.focus();
            }
        };

        tabs.addEventListener('click', (event) => {
            const tab = event.target.closest(SELECTORS.tabButton);
            if (!tab || !tabs.contains(tab)) return;

            event.preventDefault();
            activate(tab, false);
        });

        tabs.addEventListener('keydown', (event) => {
            const currentTab = event.target.closest(SELECTORS.tabButton);
            if (!currentTab || !tabs.contains(currentTab)) return;

            const currentIndex = tabEls.indexOf(currentTab);
            if (currentIndex === -1) return;

            let nextIndex = null;
            const horizontal = ['ArrowLeft', 'ArrowRight'];
            const vertical = ['ArrowUp', 'ArrowDown'];

            if (event.key === 'Home') nextIndex = 0;
            if (event.key === 'End') nextIndex = tabEls.length - 1;
            if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
                nextIndex = (currentIndex - 1 + tabEls.length) % tabEls.length;
            }
            if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
                nextIndex = (currentIndex + 1) % tabEls.length;
            }

            const orientation = tabs.querySelector('[role="tablist"]')?.getAttribute('aria-orientation') || 'horizontal';
            if (orientation === 'vertical' && horizontal.includes(event.key)) return;
            if (orientation !== 'vertical' && vertical.includes(event.key)) return;

            if (nextIndex === null) return;

            event.preventDefault();
            activate(tabEls[nextIndex], true);
        });

        const current = tabs.querySelector('[role="tab"][aria-selected="true"]') || tabEls[0];
        if (current) activate(current, false);
    });
}

/**
 *
 * auto-scroll time views to 8am on load (when daytime hours are disabled)
 */
function initTimeViewAutoScroll(root = document)
{
    // make sure we're on a time calendar
    const calendar = root.querySelector('.calendar.time');
    if (!calendar) return;

    // get out if we're autoscrolled or daytime hours is set
    if (calendar.dataset.autoscrolled === '1') return;
    if (calendar.dataset.daytimeHoursEnabled === '1') return;

    // find the target minute (8:00am)
    const target = calendar.querySelector('[data-slot-minutes="480"]');
    if (!target) return;

    // get the scroll container and offset
    const container = calendar.closest('article') || document.querySelector('article#calendar');
    if (!container) return;
    const header = container.querySelector('header');
    const headerOffset = header ? header.offsetHeight : 0;
    const containerRect = container.getBoundingClientRect();
    const targetRect = target.getBoundingClientRect();
    const top = targetRect.top - containerRect.top + container.scrollTop - headerOffset - 12;

    // scroll
    container.scrollTo({ top: Math.max(top, 0), behavior: 'auto' });
    calendar.dataset.autoscrolled = '1';
}

/**
 *
 * attendees form controls (contact lookup + email fallback)
 */
function initAttendeeControls(root = document) {
    const normalizeEmail = (value) => String(value || '').trim().toLowerCase();
    const isValidEmail = (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizeEmail(value));

    const formatDisplay = (name, email) => {
        const n = String(name || '').trim();
        const e = normalizeEmail(email);
        if (!e) return '';
        return n ? `${n} <${e}>` : e;
    };

    root.querySelectorAll(SELECTORS.attendeesRoot).forEach((container) => {
        if (container.__attendeesWired) return;
        container.__attendeesWired = true;

        const list = container.querySelector(SELECTORS.attendeesList);
        const template = container.querySelector(SELECTORS.attendeeTemplate);
        const lookup = container.querySelector(SELECTORS.attendeeLookup);
        const suggestions = container.querySelector(SELECTORS.attendeeSuggestions);
        const addManual = container.querySelector(SELECTORS.attendeeAddManual);
        if (!list || !template || !lookup) return;

        const nextIndex = () => {
            const current = parseInt(container.dataset.nextIndex || '0', 10);
            const safe = Number.isNaN(current) ? 0 : current;
            container.dataset.nextIndex = String(safe + 1);
            return safe;
        };

        const updateRole = (row) => {
            const optionalInput = row.querySelector(SELECTORS.attendeeOptional);
            const roleInput = row.querySelector(SELECTORS.attendeeRole);
            if (!optionalInput || !roleInput) return;
            roleInput.value = optionalInput.checked ? 'OPT-PARTICIPANT' : 'REQ-PARTICIPANT';
        };

        const wireRow = (row) => {
            if (!row || row.__attendeeRowWired) return;
            row.__attendeeRowWired = true;

            const optionalInput = row.querySelector(SELECTORS.attendeeOptional);
            if (optionalInput) {
                optionalInput.addEventListener('change', () => updateRole(row));
            }

            updateRole(row);
        };

        const hasEmail = (email) => {
            const normalized = normalizeEmail(email);
            if (!normalized) return true;

            return Array.from(list.querySelectorAll(`${SELECTORS.attendeeRow} ${SELECTORS.attendeeEmail}`))
                .some((input) => normalizeEmail(input.value) === normalized);
        };

        const createRow = () => {
            const index = nextIndex();
            const html = template.innerHTML.replaceAll('__INDEX__', String(index)).trim();
            if (!html) return null;

            const fragment = document.createElement('div');
            fragment.innerHTML = html;
            return fragment.firstElementChild;
        };

        const addAttendee = ({ email, name = '', attendeeUri = '', verified = false }) => {
            const normalizedEmail = normalizeEmail(email);
            if (!isValidEmail(normalizedEmail)) return false;
            if (hasEmail(normalizedEmail)) return false;

            const row = createRow();
            if (!row) return false;

            const uriInput = row.querySelector(SELECTORS.attendeeUri);
            const emailInput = row.querySelector(SELECTORS.attendeeEmail);
            const nameInput = row.querySelector(SELECTORS.attendeeName);
            const display = row.querySelector(SELECTORS.attendeeDisplay);
            const verifiedEl = row.querySelector(SELECTORS.attendeeVerified);

            if (uriInput) uriInput.value = attendeeUri || `mailto:${normalizedEmail}`;
            if (emailInput) emailInput.value = normalizedEmail;
            if (nameInput) nameInput.value = String(name || '').trim();
            if (display) display.textContent = formatDisplay(name, normalizedEmail);
            if (verifiedEl) {
                verifiedEl.classList.toggle('hidden', !verified);
            }

            list.appendChild(row);
            wireRow(row);
            return true;
        };

        const addFromLookupIfEmail = () => {
            const raw = lookup.value;
            if (!isValidEmail(raw)) return false;

            const ok = addAttendee({
                email: raw,
                attendeeUri: `mailto:${normalizeEmail(raw)}`,
                verified: false,
            });

            if (ok) {
                lookup.value = '';
                if (suggestions) suggestions.innerHTML = '';
            }

            return ok;
        };

        container.addEventListener('click', (event) => {
            const removeButton = event.target.closest(SELECTORS.attendeeRemove);
            if (removeButton && container.contains(removeButton)) {
                event.preventDefault();
                const row = removeButton.closest(SELECTORS.attendeeRow);
                row?.remove();
                return;
            }

            const pickButton = event.target.closest(SELECTORS.attendeePick);
            if (pickButton && container.contains(pickButton)) {
                event.preventDefault();
                const email = pickButton.dataset.attendeeEmail || '';
                const name = pickButton.dataset.attendeeName || '';
                const attendeeUri = pickButton.dataset.attendeeUri || '';
                const verified = pickButton.dataset.attendeeVerified === '1';

                const ok = addAttendee({ email, name, attendeeUri, verified });
                if (ok) {
                    lookup.value = '';
                    if (suggestions) suggestions.innerHTML = '';
                }
                return;
            }

            if (addManual && event.target.closest(SELECTORS.attendeeAddManual)) {
                event.preventDefault();
                addFromLookupIfEmail();
            }
        });

        lookup.addEventListener('keydown', (event) => {
            if (event.key !== 'Enter') return;
            event.preventDefault();
            addFromLookupIfEmail();
        });

        list.querySelectorAll(SELECTORS.attendeeRow).forEach(wireRow);
    });
}

/**
 *
 * calendar sidebar expand toggle
 */

document.addEventListener('click', (event) => {
    const toggle = event.target.closest(SELECTORS.calendarExpandToggle);
    if (!toggle) return;

    event.preventDefault();

    const main = toggle.closest('main');
    if (!main) return;

    const isExpanded = main.classList.toggle('expanded');
    toggle.setAttribute('aria-pressed', isExpanded ? 'true' : 'false');
});

/**
 *
 * color picker component
 * native <input type="color"> + hex + random palette)
 */

function initColorPickers(root = document) {
    const isHex = (v) => /^#?[0-9a-fA-F]{6}$/.test((v || '').trim());

    const normalize = (v) => {
        let s = (v || '').trim();
        if (!s) return null;
        if (!s.startsWith('#')) s = '#' + s;
        if (!isHex(s)) return null;
        return s.toUpperCase();
    };

    const pickRandom = (arr) => arr[Math.floor(Math.random() * arr.length)];

    const wire = (el) => {
        // avoid double-binding when htmx swaps
        if (el.__colorpickerWired) return;
        el.__colorpickerWired = true;

        const color = el.querySelector(SELECTORS.colorPickerColor);
        const hex = el.querySelector(SELECTORS.colorPickerHex);
        const btn = el.querySelector(SELECTORS.colorPickerRandom);

        if (!color || !hex) return;

        let palette = [];
        try {
            palette = JSON.parse(el.getAttribute('data-palette') || '[]');
        } catch {
            palette = [];
        }

        const setValue = (val) => {
            const n = normalize(val);
            if (!n) return false;

            color.value = n;
            hex.value = n;

            // bubble input/change for any listeners (htmx, previews, etc.)
            color.dispatchEvent(new Event('input', { bubbles: true }));
            color.dispatchEvent(new Event('change', { bubbles: true }));
            return true;
        };

        // init sync from native input
        hex.value = normalize(color.value) || '#000000';

        // native picker -> hex field
        color.addEventListener('input', () => {
            const n = normalize(color.value);
            if (n) hex.value = n;
        });

        // hex typing -> native picker (on blur + Enter)
        const commitHex = () => {
            const ok = setValue(hex.value);
            if (!ok) hex.value = normalize(color.value) || hex.value;
        };

        hex.addEventListener('blur', commitHex);
        hex.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                commitHex();
            }
        });

        // random button
        if (btn && palette.length) {
            btn.addEventListener('click', (e) => {
                e.preventDefault(); // defensive: never submit, never navigate
                e.stopPropagation();

                let next = pickRandom(palette);
                if (palette.length > 1) {
                    const current = normalize(color.value);
                    // avoid re-rolling the same number if possible
                    while (normalize(next) === current) next = pickRandom(palette);
                }
                setValue(next);
            });
        }
    };

    root.querySelectorAll(SELECTORS.colorPicker).forEach(wire);
}

/**
 *
 * month view overflow handling (progressive enhancement)
 */

function initMonthOverflow(root = document) {
    const days = root.querySelectorAll(SELECTORS.monthDay);
    days.forEach((day) => updateMonthOverflow(day));
}

function ensureDayMoreButton(dayEl) {
    let wrapper = dayEl.querySelector(SELECTORS.monthDayMoreWrap);
    if (!wrapper) {
        wrapper = document.createElement('div');
        wrapper.className = 'more-events';
        dayEl.appendChild(wrapper);
    }

    let button = wrapper.querySelector(SELECTORS.monthDayMore);
    if (!button) {
        button = document.createElement('button');
        button.type = 'button';
        button.className = 'day-more hidden';
        button.setAttribute('data-day-more', '');
        wrapper.appendChild(button);
    }

    return button;
}

function formatMoreLabel(dayEl, count) {
    const template = dayEl.getAttribute('data-more-label') || ':count more';
    return template.replace(':count', count);
}

function lessLabel(dayEl) {
    return dayEl.getAttribute('data-less-label') || 'Show less';
}

function updateMonthOverflow(dayEl) {
    if (!dayEl) return;

    const events = Array.from(dayEl.querySelectorAll(SELECTORS.monthDayEvent))
        .filter((el) => !el.classList.contains('hidden'));

    const moreButton = ensureDayMoreButton(dayEl);

    if (!events.length) {
        moreButton.textContent = '';
        moreButton.classList.add('hidden');
        moreButton.removeAttribute('aria-expanded');
        dayEl.classList.remove('day--event-overflow');
        dayEl.setAttribute('data-event-visible', '0');
        return;
    }

    if (dayEl.classList.contains('is-expanded')) {
        moreButton.textContent = lessLabel(dayEl);
        moreButton.classList.remove('hidden');
        moreButton.setAttribute('aria-expanded', 'true');
        dayEl.classList.remove('day--event-overflow');
        dayEl.setAttribute('data-event-visible', String(events.length));
        return;
    }

    const wrapper = moreButton.closest(SELECTORS.monthDayMoreWrap);
    let wrapperHeight = wrapper ? wrapper.getBoundingClientRect().height : 0;
    if (wrapperHeight === 0 && wrapper) {
        const wasHidden = moreButton.classList.contains('hidden');
        const prevVisibility = moreButton.style.visibility;

        if (wasHidden) {
            moreButton.classList.remove('hidden');
            moreButton.style.visibility = 'hidden';
        }

        wrapperHeight = wrapper.getBoundingClientRect().height || 0;

        if (wasHidden) {
            moreButton.classList.add('hidden');
            moreButton.style.visibility = prevVisibility;
        }
    }

    const prevVisibility = dayEl.style.visibility;
    dayEl.style.visibility = 'hidden';
    dayEl.removeAttribute('data-event-visible');
    dayEl.classList.remove('day--event-overflow');

    const availableHeight = dayEl.clientHeight - wrapperHeight;
    let hiddenCount = 0;
    events.forEach((eventEl) => {
        const bottom = eventEl.offsetTop + eventEl.offsetHeight;
        if (bottom > availableHeight + 0.5) {
            hiddenCount += 1;
        }
    });

    dayEl.style.visibility = prevVisibility;

    const visibleCount = Math.max(0, events.length - hiddenCount);
    dayEl.setAttribute('data-event-visible', String(visibleCount));

    if (hiddenCount > 0) {
        moreButton.textContent = formatMoreLabel(dayEl, hiddenCount);
        moreButton.classList.remove('hidden');
        moreButton.setAttribute('aria-expanded', 'false');
        dayEl.classList.add('day--event-overflow');
    } else {
        moreButton.textContent = '';
        moreButton.classList.add('hidden');
        moreButton.removeAttribute('aria-expanded');
        dayEl.classList.remove('day--event-overflow');
    }
}

// show more events in a month calendar day when some are hidden
document.addEventListener('click', (event) => {
    const button = event.target.closest(SELECTORS.monthDayMore);
    if (!button) return;

    const dayEl = button.closest(SELECTORS.monthDay);
    if (!dayEl) return;

    dayEl.classList.toggle('is-expanded');
    updateMonthOverflow(dayEl);
});

// month day resizer
let monthResizeTimer;
window.addEventListener('resize', () => {
    if (!document.querySelector(SELECTORS.monthDay)) return;
    window.clearTimeout(monthResizeTimer);
    monthResizeTimer = window.setTimeout(() => initMonthOverflow(), 100);
});

/**
 *
 * initialization
 */

function initUI() {
    initColorPickers();
    initNaturalEventParser();
    initEventAllDayToggles();
    initRecurrenceControls();
    initAttendeeControls();
    initModalHandlers();
    initTabs();
    initTimeViewAutoScroll();
    initMonthOverflow();
}

// initial bind
document.addEventListener('DOMContentLoaded', initUI);

// rebind in htmx for swapped content
document.addEventListener('htmx:afterSwap', (e) => {
    const target = e.detail?.target || e.target;
    if (target && target.id === 'modal') {
        syncModalRootClass(target);
        target.closest('dialog')?.showModal();
    }

    initColorPickers(e.target);
    initNaturalEventParser(e.target);
    initEventAllDayToggles(e.target);
    initRecurrenceControls(e.target);
    initAttendeeControls(e.target);
    initModalHandlers(e.target);
    initTabs(e.target);
    initTimeViewAutoScroll(e.target);
    initMonthOverflow(e.target);
});
