import * as datePickerMachine from "@zag-js/date-picker";
import { CalendarDate, createCalendar, toCalendar, type CalendarIdentifier, type DateValue } from "@internationalized/date";
import type { MachineInput } from "marko-zag";
import { buttonVariants } from "../button/variants.ts";
import Icon from "../icon/icon.marko";
import { cn } from "#lib/utils.ts";
import { stripOwnProps } from "#lib/native-attrs.ts";
import { calendar as styles } from "./classes.ts";

/** Plain, serializable stand-in for a `DateValue` — the calendar-system id is a string, never the `Calendar` class instance itself. */
export interface PlainDate {
  year: number;
  month: number;
  day: number;
  /**
   * `@internationalized/date` calendar identifier (e.g. `"gregory"`,
   * `"persian"`, `"islamic-umalqura"`) resolved via `createCalendar()`.
   * Omit for the default Gregorian calendar.
   */
  calendar?: CalendarIdentifier;
}

// Calendar is an inline, always-open date-picker: `open`/`inline`/
// `closeOnSelect` are fixed by this component, not exposed to callers.
// `value`/`valueChange`/`focusedValue`/`defaultFocusedValue` use plain
// { year, month, day, calendar? } objects instead of Zag's DateValue so
// consumers don't need @internationalized/date, AND so a non-Gregorian
// date never crosses the SSR->hydration wire as a live `DateValue` class
// instance (its `.calendar` field is itself a class instance with methods
// — Marko's tag-input serializer rejects it outright: "Unable to
// serialize ... Values referenced in the browser must be serializable").
// The real DateValue is reconstructed from the plain primitive inside
// machineProps's closure below, independently on server and client, the
// same way value/min/max already are.
export type Input = Omit<
  MachineInput<"div", datePickerMachine.Props>,
  "open" | "inline" | "closeOnSelect" | "value" | "min" | "max" | "focusedValue" | "defaultFocusedValue"
> & {
  /** Selected date(s) as plain objects — one entry per selected date (single mode uses a one-element array). */
  value?: { year: number; month: number; day: number }[];
  /** Fires with the full selection as plain date objects whenever it changes; enables `value:=` two-way binding. */
  valueChange?: (value: { year: number; month: number; day: number }[]) => void;
  /** Earliest selectable date (inclusive); days before it render disabled. */
  min?: { year: number; month: number; day: number };
  /** Latest selectable date (inclusive); days after it render disabled. */
  max?: { year: number; month: number; day: number };
  /** Controlled visible-month focus, as a plain (optionally non-Gregorian) date. */
  focusedValue?: PlainDate;
  /**
   * Initial visible-month focus, as a plain (optionally non-Gregorian)
   * date. Pins the SSR-rendered month instead of falling back to the
   * browser's wall-clock "today" (`getTodayDate`), which would otherwise
   * make SSR and hydrated markup diverge near midnight or across
   * timezones.
   */
  defaultFocusedValue?: PlainDate;
  /**
   * Caption presentation. `"label"` (default) shows the visible range as a
   * plain view-trigger button; `"dropdown"` swaps it for native month and year
   * `<select>` controls. Component-owned — not forwarded to the machine.
   */
  captionLayout?: "label" | "dropdown";
};

static function toDateValue(date: { year: number; month: number; day: number }): DateValue {
  return new CalendarDate(date.year, date.month, date.day);
}

static function toPlain(date: DateValue) {
  return { year: date.year, month: date.month, day: date.day };
}

// Reconstructs a real (possibly non-Gregorian) DateValue from a PlainDate
// every time it's called — never stored as a static const or passed by
// reference, so server and client each independently derive the same
// value from the same serializable primitive instead of one instance
// resuming across the SSR boundary (see PlainDate's doc comment above).
static function toFocusedDateValue(date: PlainDate): DateValue {
  const calendar = date.calendar ? createCalendar(date.calendar) : undefined;
  const gregorian = new CalendarDate(date.year, date.month, date.day);
  return calendar ? toCalendar(gregorian, calendar) : gregorian;
}

<zag/api=() => datePickerMachine from=input props=(picked) => ({
  ...picked,
  value: input.value?.map(toDateValue),
  min: input.min ? toDateValue(input.min) : undefined,
  max: input.max ? toDateValue(input.max) : undefined,
  focusedValue: input.focusedValue ? toFocusedDateValue(input.focusedValue) : undefined,
  defaultFocusedValue: input.defaultFocusedValue ? toFocusedDateValue(input.defaultFocusedValue) : undefined,
})
  open=true
  inline=true
  closeOnSelect=false
  onValueChange(details: datePickerMachine.ValueChangeDetails) {
    input.onValueChange?.(details);
    input.valueChange?.(details.value.map(toPlain));
  }
/>

// `value`/`min`/`max`/`focusedValue`/`defaultFocusedValue` are deliberately
// remapped to plain objects on this component's public Input (see the notes
// above), so `input` does not satisfy `splitProps`'s `Partial<DatePickerProps>`
// constraint. All five are machine-owned keys that `splitProps` would strip
// from the leftover anyway — drop them first and the call type-checks on the
// keys it actually partitions. `toDateValue()`/`toFocusedDateValue()` are
// what feed real DateValues to the machine.
<const/splitSource=() => {
  const { value, min, max, focusedValue, defaultFocusedValue, ...rest } = input;
  return rest;
}/>
<const/nativeAttrs=() =>
  stripOwnProps(
    datePickerMachine.splitProps(splitSource())[1],
    "class",
    "valueChange",
    "captionLayout",
  )
>

<const/captionLayout=(input.captionLayout ?? "label")/>

// Visible month's short name, matching the month <select>'s option labels.
<const/monthLabel=() => {
  const ui = api();
  const month = ui.visibleRange.start.month;
  return ui.getMonths({ format: "short" }).find((m) => m.value === month)?.label ?? String(month);
}/>

<const/weeks=() =>
  api().weeks.map((week) =>
    week.map((date) => ({
      cell: api().getDayTableCellProps({ value: date }),
      trigger: api().getDayTableCellTriggerProps({ value: date }),
      label: String(date.day),
    })),
  )
/>
<const/navTriggerVariant=buttonVariants({ variant: "ghost", size: "icon-sm" })/>

<div
  ...nativeAttrs()
  ...api().getRootProps()
  data-slot="calendar"
  class=cn(styles.root, input.class)
>
  <div ...api().getContentProps() data-slot="calendar-content" class=styles.content>
    <div class=styles.nav>
      <button type="button"
        ...api().getPrevTriggerProps()
        data-slot="calendar-prev-trigger"
        class=cn(navTriggerVariant, styles.navTrigger)
      >
        <Icon name="ChevronLeft" class=styles.rtlFlip/>
        <span class=styles.srOnly>Previous</span>
      </button>
      <button type="button"
        ...api().getNextTriggerProps()
        data-slot="calendar-next-trigger"
        class=cn(navTriggerVariant, styles.navTrigger)
      >
        <Icon name="ChevronRight" class=styles.rtlFlip/>
        <span class=styles.srOnly>Next</span>
      </button>
    </div>
    <div data-slot="calendar-month-caption" class=styles.monthCaption>
      <if=captionLayout === "dropdown">
        <div
          data-slot="calendar-dropdowns"
          class=styles.dropdowns
        >
          <div
            data-slot="calendar-dropdown-root"
            class=styles.dropdownRoot
          >
            <select
              ...api().getMonthSelectProps()
              data-slot="calendar-month-select"
              class=styles.monthSelect
            >
              <for|month| of=api().getMonths({ format: "short" })>
                <option value=month.value disabled=month.disabled>${month.label}</option>
              </for>
            </select>
            <span
              aria-hidden="true"
              data-slot="calendar-caption-label"
              class=styles.captionLabel
            >
              ${monthLabel()}
              <Icon name="ChevronDown"/>
            </span>
          </div>
          <div
            data-slot="calendar-dropdown-root"
            class=styles.dropdownRoot
          >
            <select
              ...api().getYearSelectProps()
              data-slot="calendar-year-select"
              class=styles.yearSelect
            >
              <for|year| of=api().getYears()>
                <option value=year.value disabled=year.disabled>${year.label}</option>
              </for>
            </select>
            <span
              aria-hidden="true"
              data-slot="calendar-caption-label"
              class=styles.captionLabel
            >
              ${String(api().visibleRange.start.year)}
              <Icon name="ChevronDown"/>
            </span>
          </div>
        </div>
      </if>
      <else>
        <button type="button"
          ...api().getViewTriggerProps()
          data-slot="calendar-view-trigger"
          class=styles.viewTrigger
        >
          ${api().visibleRangeText.formatted}
        </button>
      </else>
    </div>
    <table ...api().getTableProps() data-slot="calendar-table" class=styles.table>
      <thead ...api().getTableHeadProps() data-slot="calendar-table-head">
        <tr class=styles.headerRow>
          <for|weekDay| of=api().weekDays>
            <th class=styles.headerCell>
              ${weekDay.narrow}
              <span class=styles.srOnly>${weekDay.long}</span>
            </th>
          </for>
        </tr>
      </thead>
      <tbody ...api().getTableBodyProps() data-slot="calendar-table-body">
        <for|week, i| of=weeks() by=(_, i) => `week:${i}`>
          <tr class=styles.weekRow>
            <for|day, j| of=week by=(_, j) => `day:${j}`>
              <td
                ...day.cell
                data-slot="calendar-cell"
                class=styles.cell
              >
                <button type="button"
                  ...day.trigger
                  data-slot="calendar-day"
                  class=styles.dayButton
                >
                  ${day.label}
                </button>
              </td>
            </for>
          </tr>
        </for>
      </tbody>
    </table>
  </div>
</div>

<return=api>
