/**
 * Non-side-effect class export for `<calendar-picker-ui>`.
 *
 * Importing this file gives you the class(es) without auto-registering the tag.
 * Useful for test isolation, subclassing with tag-name override, or selective
 * composition.
 *
 * The auto-register path stays at `@adia-ai/web-components/components/calendar-picker`
 * (which imports this file + calls `defineIfFree()`).
 *
 * @see ../../USAGE.md#registration--auto-vs-explicit
 */

/**
 * <calendar-picker-ui label="Date" placeholder="Select date..." format="short"></calendar-picker-ui>
 *
 * Date picker with trigger button and popover calendar grid.
 * Form-associated via UIFormElement + ElementInternals.
 *
 * Properties:
 *   label       — field label (rendered via CSS ::before)
 *   placeholder — trigger text when no date selected
 *   format      — display format: short | long | iso
 *   min         — minimum selectable date (ISO string)
 *   max         — maximum selectable date (ISO string)
 *   open        — popover visibility
 *
 * Keyboard: Arrow keys navigate days, Enter selects, Escape closes.
 */

import { UIFormElement } from '../../core/form.js';
import { anchorPopover } from '../../core/anchor.js';

const DAYS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
const MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const MONTHS_LONG = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

function toISO(y, m, d) {
  return `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
}

function parseISO(str) {
  if (!str) return null;
  const [y, m, d] = str.split('-').map(Number);
  if (isNaN(y) || isNaN(m) || isNaN(d)) return null;
  return new Date(y, m - 1, d);
}

function sameDay(a, b) {
  return a.getFullYear() === b.getFullYear() &&
    a.getMonth() === b.getMonth() &&
    a.getDate() === b.getDate();
}

export class UICalendarPicker extends UIFormElement {
  static labelDeprecated = false;  // §170 (v0.5.4): label is first-class per calendar-picker.yaml
  // §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
  // consumer markup). Aggregated by installIconLoadersForRegistered()
  // across all defined elements. Audited by check-required-icons.mjs
  // (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
  static requiredIcons = ['calendar', 'caret-left', 'caret-right'];

  static properties = {
    ...UIFormElement.properties,
    label:       { type: String,  default: '', reflect: true },
    placeholder: { type: String,  default: 'Select date...', reflect: true },
    format:      { type: String,  default: 'short', reflect: true },
    min:         { type: String,  default: '', reflect: true },
    max:         { type: String,  default: '', reflect: true },
    open:        { type: Boolean, default: false, reflect: true },
    // Popover placement — yaml documented this as reflect:true since v1;
    // the JS reads it via getAttribute('placement') in the anchorPopover
    // call site. Declaring here so el.placement = 'top' also updates.
    placement:   { type: String,  default: 'bottom', reflect: true },
  };

  static template = () => null;

  #popover = null;
  #trigger = null;
  #anchorCleanup = null;
  #viewYear = new Date().getFullYear();
  #viewMonth = new Date().getMonth();
  #focusedDay = null;
  #popoverClickBound = false;
  #openRaf = null;

  #onPopoverClick = (e) => {
    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); }
  };

  connected() {
    super.connected();
    this.setAttribute('role', 'group');
    this.addEventListener('click', this.#onClick);
    this.addEventListener('keydown', this.#onKey);

    // Initialize view to selected date or today
    const sel = parseISO(this.value);
    if (sel) {
      this.#viewYear = sel.getFullYear();
      this.#viewMonth = sel.getMonth();
    }
  }

  render() {
    // Stamp trigger + popover if not present
    if (!this.querySelector('[slot="trigger"]')) {
      this.innerHTML = `
        <span slot="trigger">
          <span slot="display">${this.#displayText()}</span>
          <icon-ui name="calendar" slot="caret"></icon-ui>
        </span>
      `;
      this.#trigger = this.querySelector('[slot="trigger"]');

      this.#popover = document.createElement('div');
      this.#popover.setAttribute('slot', 'popover');
      this.#popover.setAttribute('popover', 'manual');
      this.appendChild(this.#popover);
      this.#popover.addEventListener('click', this.#onPopoverClick);
      this.#popoverClickBound = true;
    } else {
      const display = this.querySelector('[slot="display"]');
      if (display) display.textContent = this.#displayText();
    }

    if (!this.#popover) this.#popover = this.querySelector('[slot="popover"]');
    if (!this.#trigger) this.#trigger = this.querySelector('[slot="trigger"]');
    if (this.#popover && !this.#popoverClickBound) {
      this.#popover.addEventListener('click', this.#onPopoverClick);
      this.#popoverClickBound = true;
    }

    // Toggle popover
    if (this.open) {
      this.#renderCalendar();
      this.#popover?.showPopover?.();
      // ADR-0034 Rule 2: calendar panel (~330px) >> trigger button (~200px) → center.
      // Consumer can override via placement="bottom-start|top|…" attribute.
      this.#anchorCleanup = anchorPopover(this.#trigger, this.#popover, {
        placement: this.getAttribute('placement') || 'bottom', gap: 4,
      });
      this.#openRaf = requestAnimationFrame(() => {
        this.#openRaf = null;
        if (!this.isConnected || !this.open) return;
        document.addEventListener('pointerdown', this.#onOutside, { once: true });
      });
    } else {
      this.#anchorCleanup?.();
      this.#anchorCleanup = null;
      if (this.#openRaf != null) {
        cancelAnimationFrame(this.#openRaf);
        this.#openRaf = null;
      }
      this.#popover?.hidePopover?.();
      document.removeEventListener('pointerdown', this.#onOutside);
    }
  }

  #displayText() {
    if (!this.value) return this.placeholder;
    const d = parseISO(this.value);
    if (!d) return this.value;
    switch (this.format) {
      case 'long':
        return `${MONTHS_LONG[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
      case 'iso':
        return this.value;
      case 'short':
      default:
        return `${MONTHS_SHORT[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
    }
  }

  #renderCalendar() {
    if (!this.#popover) return;

    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);

    // Header
    let h = `<div data-cal-header>
      <button data-cal-prev type="button" aria-label="Previous month"><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"><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>`;

    // Calendar 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
    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;

      const attrs = [
        'type="button"',
        'data-cal-day',
        `data-date="${iso}"`,
        isToday ? 'data-today' : '',
        isSelected ? 'data-selected' : '',
        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.#popover.innerHTML = h;
    // Events are delegated on #popover (see #onPopoverClick) — no per-element wiring.
  }

  #navigate(delta) {
    this.#viewMonth += delta;
    if (this.#viewMonth > 11) { this.#viewMonth = 0; this.#viewYear++; }
    if (this.#viewMonth < 0) { this.#viewMonth = 11; this.#viewYear--; }
    this.#focusedDay = null;
    this.#renderCalendar();
  }

  #selectDate(iso) {
    this.value = iso;
    this.syncValue(iso);
    this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
    // Re-render grid to reflect new selection; keep popover open until user clicks outside.
    if (this.open) this.#renderCalendar();
  }

  #onClick = (e) => {
    if (this.disabled) return;
    if (this.#popover && e.composedPath().includes(this.#popover)) return;
    this.open = !this.open;
    if (this.open) {
      const sel = parseISO(this.value);
      if (sel) {
        this.#viewYear = sel.getFullYear();
        this.#viewMonth = sel.getMonth();
        this.#focusedDay = sel.getDate();
      } else {
        const today = new Date();
        this.#viewYear = today.getFullYear();
        this.#viewMonth = today.getMonth();
        this.#focusedDay = today.getDate();
      }
    }
  };

  #onOutside = (e) => {
    if (!this.contains(e.target) && !(this.#popover && e.composedPath().includes(this.#popover))) {
      this.open = false;
    }
  };

  #onKey = (e) => {
    if (!this.open) {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        this.open = true;
      }
      return;
    }

    if (e.key === 'Escape') {
      e.preventDefault();
      this.open = false;
      return;
    }

    if (e.key === 'Enter') {
      e.preventDefault();
      if (this.#focusedDay) {
        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.#popover?.querySelector('[data-focused]');
      focused?.focus();
    }
  };

  disconnected() {
    super.disconnected();
    if (this.#openRaf != null) {
      cancelAnimationFrame(this.#openRaf);
      this.#openRaf = null;
    }
    this.removeEventListener('click', this.#onClick);
    this.removeEventListener('keydown', this.#onKey);
    document.removeEventListener('pointerdown', this.#onOutside);
    if (this.#popover && this.#popoverClickBound) {
      this.#popover.removeEventListener('click', this.#onPopoverClick);
    }
    this.#popoverClickBound = false;
    this.#anchorCleanup?.();
    this.#anchorCleanup = null;
    this.#popover?.hidePopover?.();
    this.#popover = null;
    this.#trigger = null;
  }
}
