/**
 * Non-side-effect class export for `<calendar-grid-ui>`.
 *
 * The auto-register path stays at `@adia-ai/web-components/components/calendar-grid`
 * (which imports this file + calls `defineIfFree()`).
 *
 * @see ../../USAGE.md#registration--auto-vs-explicit
 */

/**
 * `<calendar-grid-ui>` — Substrate primitive: a single-month calendar grid.
 *
 * Renders ONLY the inline grid (month nav + day-cells + keyboard nav). No
 * trigger, no popover — used as a substrate by <calendar-picker-ui> (trigger
 * + popover wrapper), <date-range-picker-ui> (two side-by-side grids), and
 * <datetime-picker-ui> (one grid + time pane).
 *
 *   <calendar-grid-ui value="2026-05-23" min="2026-01-01" max="2026-12-31">
 *   </calendar-grid-ui>
 *
 * Properties:
 *   value — selected ISO date (YYYY-MM-DD)
 *   min   — minimum selectable date (ISO)
 *   max   — maximum selectable date (ISO)
 *   focus-date — the date that holds the roving tabindex (ISO, defaults to value or today)
 *   disabled / readonly — disables / blocks selection
 *
 * Events:
 *   change — { value: "2026-05-23" } when a date is selected
 *   input  — emitted as the user navigates without committing
 *
 * Keyboard: ArrowUp/Down/Left/Right navigate days; Enter selects;
 *   PageUp/Down jump month; Home/End first/last of month.
 */

import { UIElement } from '../../core/element.js';

const DAYS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
const MONTHS_LONG = [
  'January', 'February', 'March', 'April', 'May', 'June',
  'July', 'August', 'September', 'October', 'November', 'December',
];

function pad(n) { return String(n).padStart(2, '0'); }
function toISO(y, m, d) { return `${y}-${pad(m + 1)}-${pad(d)}`; }
function parseISO(str) {
  if (!str) return null;
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(str).trim());
  if (!m) return null;
  return new Date(+m[1], +m[2] - 1, +m[3]);
}
function sameDay(a, b) {
  return a.getFullYear() === b.getFullYear()
      && a.getMonth()    === b.getMonth()
      && a.getDate()     === b.getDate();
}

export class UICalendarGrid extends UIElement {
  // §154 Phosphor icons stamped without consumer markup.
  static requiredIcons = ['caret-left', 'caret-right'];

  static properties = {
    value:    { type: String,  default: '', reflect: true },
    // Displayed month (`YYYY-MM`) when no `value` is selected — lets a consumer
    // show a specific month independent of selection (e.g. a date-range picker
    // showing its end pane one month ahead of the start pane). Selection wins.
    month:    { type: String,  default: '', reflect: true },
    min:      { type: String,  default: '', reflect: true },
    max:      { type: String,  default: '', reflect: true },
    disabled: { type: Boolean, default: false, reflect: true },
    readonly: { type: Boolean, default: false, reflect: true },
    // Range-aware highlighting — when both rangeStart and rangeEnd are
    // set, day cells whose date falls strictly between them get
    // `[data-in-range]` stamped on them (the endpoints themselves get
    // `[data-selected]` via the `value` prop). Consumers like
    // <date-range-picker-ui> push the same from/to onto both panes so
    // each independently lights up its in-range cells.
    rangeStart: { type: String, default: '', reflect: true, attribute: 'range-start' },
    rangeEnd:   { type: String, default: '', reflect: true, attribute: 'range-end' },
  };

  // No html`` template — we render imperatively via #renderCalendar so we can
  // mutate per-day cells without diffing.
  static template = () => null;

  #viewYear = new Date().getFullYear();
  #viewMonth = new Date().getMonth();
  #focusedDay = null;
  #bound = false;

  // ── Lifecycle ──────────────────────────────────────────────────────

  connected() {
    this.setAttribute('role', 'group');
    if (!this.hasAttribute('aria-label')) this.setAttribute('aria-label', 'Calendar');

    // Initialize view to selected date, else the [month] hint, else today.
    const sel = parseISO(this.value);
    if (sel) {
      this.#viewYear = sel.getFullYear();
      this.#viewMonth = sel.getMonth();
    } else {
      const vm = /^(\d{4})-(\d{2})$/.exec(String(this.month || '').trim());
      if (vm) { this.#viewYear = +vm[1]; this.#viewMonth = +vm[2] - 1; }
    }

    if (!this.#bound) {
      this.#bound = true;
      this.addEventListener('click', this.#onClick);
      this.addEventListener('keydown', this.#onKey);
    }
  }

  disconnected() {
    this.removeEventListener('click', this.#onClick);
    this.removeEventListener('keydown', this.#onKey);
    this.#bound = false;
  }

  render() {
    // If a selected value changed externally, re-sync the visible month so it
    // contains the selection (without clobbering the user's mid-flight nav).
    const sel = parseISO(this.value);
    if (sel && (sel.getFullYear() !== this.#viewYear || sel.getMonth() !== this.#viewMonth)) {
      this.#viewYear = sel.getFullYear();
      this.#viewMonth = sel.getMonth();
    }
    this.#renderCalendar();
  }

  // ── Public API ─────────────────────────────────────────────────────

  /** Move the visible month by ±N months. */
  navigate(delta) {
    this.#viewMonth += delta;
    while (this.#viewMonth > 11) { this.#viewMonth -= 12; this.#viewYear++; }
    while (this.#viewMonth < 0)  { this.#viewMonth += 12; this.#viewYear--; }
    this.#focusedDay = null;
    this.#renderCalendar();
  }

  /** Programmatically focus a day (1-based) inside the visible month. */
  focusDay(day) {
    const daysInMonth = new Date(this.#viewYear, this.#viewMonth + 1, 0).getDate();
    if (day < 1 || day > daysInMonth) return;
    this.#focusedDay = day;
    this.#renderCalendar();
    this.querySelector('[data-cal-day][data-focused]')?.focus?.();
  }

  // ── Internal — render the grid ────────────────────────────────────

  #renderCalendar() {
    const year  = this.#viewYear;
    const month = this.#viewMonth;
    const today = new Date();
    const selected = parseISO(this.value);
    const minDate  = parseISO(this.min);
    const maxDate  = parseISO(this.max);
    // Range endpoints — only honor when BOTH are set + valid + ordered.
    const rs = parseISO(this.rangeStart);
    const re = parseISO(this.rangeEnd);
    const rangeFrom = (rs && re && rs <= re) ? rs : null;
    const rangeTo   = (rs && re && rs <= re) ? re : null;

    // Header — Prev / Title / Next
    let h = `<div data-cal-header>
      <button data-cal-prev type="button" aria-label="Previous month" tabindex="-1"><icon-ui name="caret-left"></icon-ui></button>
      <span data-cal-title>${MONTHS_LONG[month]} ${year}</span>
      <button data-cal-next type="button" aria-label="Next month" tabindex="-1"><icon-ui name="caret-right"></icon-ui></button>
    </div>`;

    // Day-of-week header
    h += `<div data-cal-weekdays>${DAYS.map((d) => `<span>${d}</span>`).join('')}</div>`;

    // Day grid
    const firstDay     = new Date(year, month, 1).getDay();
    const daysInMonth  = new Date(year, month + 1, 0).getDate();
    const daysInPrev   = new Date(year, month, 0).getDate();

    h += '<div data-cal-grid>';

    // Previous-month trailing days (outside, disabled)
    for (let i = firstDay - 1; i >= 0; i--) {
      const day = daysInPrev - i;
      h += `<button type="button" data-cal-day data-outside disabled tabindex="-1">${day}</button>`;
    }

    // Current-month days
    for (let d = 1; d <= daysInMonth; d++) {
      const date       = new Date(year, month, d);
      const iso        = toISO(year, month, d);
      const isToday    = sameDay(date, today);
      const isSelected = selected && sameDay(date, selected);
      const isDisabled = (minDate && date < minDate) || (maxDate && date > maxDate);
      const isFocused  = this.#focusedDay === d;
      // In-range: strictly between rangeStart and rangeEnd (endpoints
      // themselves render as `[data-selected]` via the `value` prop;
      // marking them in-range too would double-up the styling).
      const isInRange = !!(rangeFrom && rangeTo
        && date > rangeFrom && date < rangeTo);
      // Range endpoint flags — let CSS render the start/end cell with
      // half-pill caps so the in-range strip reads as a continuous
      // band when displayed next to the in-range cells.
      const isRangeStart = !!(rangeFrom && sameDay(date, rangeFrom));
      const isRangeEnd   = !!(rangeTo   && sameDay(date, rangeTo));

      const attrs = [
        'type="button"',
        'data-cal-day',
        `data-date="${iso}"`,
        isToday      ? 'data-today' : '',
        isSelected   ? 'data-selected' : '',
        isInRange    ? 'data-in-range' : '',
        isRangeStart ? 'data-range-start' : '',
        isRangeEnd   ? 'data-range-end' : '',
        isDisabled   ? 'disabled' : '',
        isFocused    ? 'data-focused' : '',
        `tabindex="${isFocused ? '0' : '-1'}"`,
      ].filter(Boolean).join(' ');

      h += `<button ${attrs}>${d}</button>`;
    }

    // Next-month leading days
    const totalCells = firstDay + daysInMonth;
    const remaining  = (7 - (totalCells % 7)) % 7;
    for (let d = 1; d <= remaining; d++) {
      h += `<button type="button" data-cal-day data-outside disabled tabindex="-1">${d}</button>`;
    }

    h += '</div>';

    this.innerHTML = h;
  }

  // ── Selection ─────────────────────────────────────────────────────

  #selectDate(iso) {
    if (this.disabled || this.readonly) return;
    this.value = iso;
    // Re-render to reflect new selection.
    this.#renderCalendar();
    this.dispatchEvent(new CustomEvent('change', {
      bubbles: true,
      detail: { value: iso },
    }));
  }

  // ── Event handlers ────────────────────────────────────────────────

  #onClick = (e) => {
    if (this.disabled) return;
    const target = e.target instanceof Element ? e.target : null;
    if (!target) return;
    const prev = target.closest('[data-cal-prev]');
    if (prev) { e.stopPropagation(); this.navigate(-1); return; }
    const next = target.closest('[data-cal-next]');
    if (next) { e.stopPropagation(); this.navigate(1); return; }
    const day = target.closest('[data-cal-day][data-date]:not([disabled])');
    if (day) { e.stopPropagation(); this.#selectDate(day.dataset.date); }
  };

  #onKey = (e) => {
    if (this.disabled) return;

    if (e.key === 'Enter' || e.key === ' ') {
      if (this.#focusedDay) {
        e.preventDefault();
        const iso = toISO(this.#viewYear, this.#viewMonth, this.#focusedDay);
        const date = new Date(this.#viewYear, this.#viewMonth, this.#focusedDay);
        const minDate = parseISO(this.min);
        const maxDate = parseISO(this.max);
        if ((!minDate || date >= minDate) && (!maxDate || date <= maxDate)) {
          this.#selectDate(iso);
        }
      }
      return;
    }

    const arrows = { ArrowUp: -7, ArrowDown: 7, ArrowLeft: -1, ArrowRight: 1 };
    if (arrows[e.key] !== undefined) {
      e.preventDefault();
      const daysInMonth = new Date(this.#viewYear, this.#viewMonth + 1, 0).getDate();
      let day = (this.#focusedDay || 1) + arrows[e.key];

      if (day < 1) {
        this.navigate(-1);
        const prevDays = new Date(this.#viewYear, this.#viewMonth + 1, 0).getDate();
        this.#focusedDay = prevDays + day;
      } else if (day > daysInMonth) {
        this.navigate(1);
        this.#focusedDay = day - daysInMonth;
      } else {
        this.#focusedDay = day;
      }

      this.#renderCalendar();
      const focused = this.querySelector('[data-cal-day][data-focused]');
      focused?.focus?.();
      this.dispatchEvent(new CustomEvent('input', {
        bubbles: true,
        detail: { focusDate: toISO(this.#viewYear, this.#viewMonth, this.#focusedDay) },
      }));
      return;
    }

    if (e.key === 'PageUp') {
      e.preventDefault();
      this.navigate(-1);
      return;
    }
    if (e.key === 'PageDown') {
      e.preventDefault();
      this.navigate(1);
      return;
    }
    if (e.key === 'Home') {
      e.preventDefault();
      this.#focusedDay = 1;
      this.#renderCalendar();
      this.querySelector('[data-cal-day][data-focused]')?.focus?.();
      return;
    }
    if (e.key === 'End') {
      e.preventDefault();
      this.#focusedDay = new Date(this.#viewYear, this.#viewMonth + 1, 0).getDate();
      this.#renderCalendar();
      this.querySelector('[data-cal-day][data-focused]')?.focus?.();
      return;
    }
  };
}
