// date-range-picker.ts — boot client + DOM mount for the symmetric
// date-range picker (t-paliad-248). The picker is a controlled
// component: callers pass `value` + `onChange`, the component renders
// the trigger button + popover scaffold, the popover materialises a
// chip row and (when "Anpassen" is picked) an inline date-pair editor.
//
// The picker reuses the existing `.agenda-chip` styling for chips and
// the `.multi-panel` popover pattern (auto-positioned under a
// `.multi-anchor` wrapper). Both patterns are battle-tested by the
// filter-bar + multi-select widgets — no new design tokens, no new
// dark-mode contrast risk.

import { t } from "./i18n";
import {
  ALL_HORIZONS,
  HORIZON_LABEL_KEY,
  NEXT_HORIZONS,
  PAST_HORIZONS,
  isDefault,
  isValidISODate,
  validateCustomRange,
  type TimeHorizon,
  type TimeSpec,
} from "./date-range-picker-pure";

export interface MountOpts {
  /** Current value. The picker is fully controlled. */
  value: TimeSpec;
  /** Fired on every committed change (chip click or Anwenden). */
  onChange(next: TimeSpec): void;
  /**
   * Which horizon constitutes the "default" for this surface. Used
   * for the non-default indicator dot. Defaults to `"any"`.
   */
  defaultHorizon?: TimeHorizon;
  /**
   * Which chips to render. Order is preserved. Defaults to the full
   * 14-chip fan from ALL_HORIZONS.
   */
  presets?: readonly TimeHorizon[];
  /**
   * Stable surface tag — feeds into the `data-testid` on every DOM
   * node the picker creates so tests can scope. Example: "agenda",
   * "filter-bar.time", "audit-log".
   */
  surface: string;
  /**
   * Optional prefix for the closed-button label. The label always
   * starts with the resolved horizon name (e.g. "Letzte 30 Tage").
   * Surfaces that want a heading prefix ("Zeitraum: Letzte 30 Tage")
   * pass it here.
   */
  labelPrefix?: string;
}

export interface PickerHandle {
  /** Root element — append to the host container. */
  element: HTMLElement;
  /** Read the current value (may have been edited via Anpassen). */
  getValue(): TimeSpec;
  /** Update the value from the host (e.g. after URL change). */
  setValue(next: TimeSpec): void;
  /** Force-close the popover. Safe to call when already closed. */
  close(): void;
  /** Detach event listeners + remove from DOM. */
  destroy(): void;
}

/**
 * Mount a date-range picker. The returned `element` is a single
 * inline node containing both the trigger button and the popover
 * (absolutely positioned via `.multi-anchor` + `.multi-panel`).
 *
 * The popover stays in the DOM permanently; opening/closing toggles
 * the `[hidden]` attribute. This keeps the chip's tab-order stable
 * and matches the multi-select widget's behaviour.
 */
export function mountDateRangePicker(opts: MountOpts): PickerHandle {
  const presets = opts.presets ?? ALL_HORIZONS;
  const defaultHorizon = opts.defaultHorizon ?? "any";
  let value: TimeSpec = normalize(opts.value);

  // Cached drafts for the "Anpassen" editor — preserved across
  // open/close so the user doesn't lose their typing if they peek
  // away. Seeded from the live value when the editor opens.
  let customFromDraft = value.horizon === "custom" ? (value.from ?? "") : "";
  let customToDraft = value.horizon === "custom" ? (value.to ?? "") : "";
  let customEditorOpen = value.horizon === "custom";

  const root = document.createElement("div");
  root.className = "date-range-anchor multi-anchor";
  root.dataset.testid = `${opts.surface}.date-range-picker`;

  const trigger = document.createElement("button");
  trigger.type = "button";
  trigger.className = "date-range-trigger";
  trigger.setAttribute("aria-haspopup", "dialog");
  trigger.setAttribute("aria-expanded", "false");
  trigger.dataset.testid = `${opts.surface}.date-range-trigger`;

  const panel = document.createElement("div");
  panel.className = "date-range-panel multi-panel";
  panel.setAttribute("role", "dialog");
  panel.setAttribute("aria-label", t("date_range.dialog.label"));
  panel.hidden = true;
  panel.dataset.testid = `${opts.surface}.date-range-panel`;

  root.appendChild(trigger);
  root.appendChild(panel);

  renderTrigger();
  renderPanel();

  // Open/close wiring. Click outside the root collapses the popover;
  // Esc inside it bubbles up to the same handler via keydown delegate.
  const onDocClick = (e: MouseEvent) => {
    if (panel.hidden) return;
    if (e.target instanceof Node && root.contains(e.target)) return;
    closePopover();
  };
  const onKeydown = (e: KeyboardEvent) => {
    if (panel.hidden) return;
    if (e.key === "Escape") {
      e.stopPropagation();
      closePopover();
      trigger.focus();
    }
  };

  trigger.addEventListener("click", () => {
    if (panel.hidden) openPopover();
    else closePopover();
  });

  document.addEventListener("mousedown", onDocClick);
  document.addEventListener("keydown", onKeydown);

  function openPopover(): void {
    panel.hidden = false;
    trigger.setAttribute("aria-expanded", "true");
    // Re-render to reflect the very latest value (host may have
    // patched via setValue between open/close).
    renderPanel();
    // Move keyboard focus into the panel so Esc works without a
    // prior click. The first chip is the natural landing spot.
    const firstChip = panel.querySelector<HTMLButtonElement>(".date-range-chip");
    firstChip?.focus({ preventScroll: true });
  }

  function closePopover(): void {
    panel.hidden = true;
    trigger.setAttribute("aria-expanded", "false");
  }

  function commit(next: TimeSpec, closeAfter: boolean): void {
    value = normalize(next);
    customEditorOpen = value.horizon === "custom";
    if (value.horizon === "custom") {
      customFromDraft = value.from ?? "";
      customToDraft = value.to ?? "";
    }
    renderTrigger();
    renderPanel();
    opts.onChange(value);
    if (closeAfter) {
      closePopover();
      trigger.focus({ preventScroll: true });
    }
  }

  function renderTrigger(): void {
    trigger.replaceChildren();
    if (!isDefault(value, defaultHorizon)) {
      const dot = document.createElement("span");
      dot.className = "date-range-trigger-dot";
      dot.setAttribute("aria-hidden", "true");
      trigger.appendChild(dot);
    }
    const labelSpan = document.createElement("span");
    labelSpan.className = "date-range-trigger-label";
    labelSpan.textContent = labelFor(value, opts.labelPrefix);
    trigger.appendChild(labelSpan);

    const chev = document.createElement("span");
    chev.className = "date-range-trigger-chev";
    chev.setAttribute("aria-hidden", "true");
    chev.textContent = "▾";
    trigger.appendChild(chev);
  }

  function renderPanel(): void {
    panel.replaceChildren();

    // Three vertical columns: Past (closest→farthest top→bottom),
    // NOW (Heute + Alles), Future (closest→farthest). The grid
    // visualises time as space around NOW — each column's top is
    // closest to the current moment, bottom is furthest away.
    const grid = document.createElement("div");
    grid.className = "date-range-grid";

    // Past column: PAST_HORIZONS registry is outermost→innermost
    // (past_all → past_1d); reverse for closeness-to-NOW ordering
    // (past_1d at top, past_all at bottom).
    const pastCol = renderColumn(
      "past",
      t("date_range.fan.past.label"),
      [...PAST_HORIZONS].reverse().filter((h) => presets.includes(h)),
    );
    const nowCol = renderNowColumn();
    // Future column: NEXT_HORIZONS registry is already in closeness
    // order (next_1d → next_all). next_1d moves to the NOW column as
    // "Heute" (semantically just-today, single-day window), so the
    // future column skips it.
    const futureCol = renderColumn(
      "future",
      t("date_range.fan.future.label"),
      NEXT_HORIZONS.filter((h) => h !== "next_1d" && presets.includes(h)),
    );

    if (pastCol) grid.appendChild(pastCol);
    if (nowCol) grid.appendChild(nowCol);
    if (futureCol) grid.appendChild(futureCol);

    panel.appendChild(grid);

    // Custom-range section ("Anpassen"). Toggle button + collapsible
    // date-pair editor below.
    if (presets.includes("custom")) {
      panel.appendChild(renderCustomSection());
    }
  }

  function renderColumn(
    side: "past" | "future",
    heading: string,
    horizons: readonly TimeHorizon[],
  ): HTMLElement | null {
    if (horizons.length === 0) return null;
    const col = document.createElement("div");
    col.className = `date-range-col date-range-col--${side}`;
    col.setAttribute("role", "group");
    col.setAttribute("aria-label", heading);

    const head = document.createElement("div");
    head.className = "date-range-col-heading";
    head.textContent = heading;
    col.appendChild(head);

    for (const h of horizons) {
      col.appendChild(makeChip(h));
    }
    return col;
  }

  function renderNowColumn(): HTMLElement | null {
    const showHeute = presets.includes("next_1d");
    const showAlles = presets.includes("any");
    if (!showHeute && !showAlles) return null;

    const col = document.createElement("div");
    col.className = "date-range-col date-range-col--now";
    col.setAttribute("role", "group");
    col.setAttribute("aria-label", t("date_range.center.label"));

    const glyph = document.createElement("div");
    glyph.className = "date-range-col-heading date-range-col-heading--glyph";
    glyph.setAttribute("aria-hidden", "true");
    glyph.textContent = "⌖"; // ⌖ POSITION INDICATOR
    col.appendChild(glyph);

    if (showHeute) col.appendChild(makeChip("next_1d"));
    if (showAlles) {
      const allesChip = makeChip("any");
      // Legacy "all" horizon also lights up Alles for back-compat
      // with saved Custom Views that store the bidirectional-unbounded
      // value (Q26 — parser preserves it, picker surfaces it here).
      if (value.horizon === "all") {
        allesChip.classList.add("agenda-chip-active");
        allesChip.setAttribute("aria-pressed", "true");
      }
      col.appendChild(allesChip);
    }
    return col;
  }

  function makeChip(h: TimeHorizon): HTMLButtonElement {
    const chip = document.createElement("button");
    chip.type = "button";
    chip.className = "agenda-chip date-range-chip";
    if (value.horizon === h) chip.classList.add("agenda-chip-active");
    chip.setAttribute("aria-pressed", String(value.horizon === h));
    chip.textContent = t(HORIZON_LABEL_KEY[h]);
    chip.dataset.testid = `${opts.surface}.date-range-chip.${h}`;
    chip.addEventListener("click", () => {
      commit({ horizon: h }, /*closeAfter*/ true);
    });
    return chip;
  }

  function renderCustomSection(): HTMLElement {
    const section = document.createElement("div");
    section.className = "date-range-custom";

    const toggleBtn = document.createElement("button");
    toggleBtn.type = "button";
    toggleBtn.className = "agenda-chip date-range-chip date-range-chip--custom";
    if (value.horizon === "custom") toggleBtn.classList.add("agenda-chip-active");
    toggleBtn.setAttribute("aria-expanded", String(customEditorOpen));
    toggleBtn.dataset.testid = `${opts.surface}.date-range-chip.custom`;
    toggleBtn.textContent = t("date_range.horizon.custom");
    toggleBtn.addEventListener("click", () => {
      customEditorOpen = !customEditorOpen;
      renderPanel();
      if (customEditorOpen) {
        // Focus the first input on expand.
        panel.querySelector<HTMLInputElement>(".date-range-custom-from")?.focus();
      }
    });
    section.appendChild(toggleBtn);

    if (!customEditorOpen) return section;

    const editor = document.createElement("div");
    editor.className = "date-range-custom-editor";

    const fromWrap = document.createElement("label");
    fromWrap.className = "date-range-custom-field";
    const fromLbl = document.createElement("span");
    fromLbl.className = "date-range-custom-label";
    fromLbl.textContent = t("date_range.custom.from");
    const fromInput = document.createElement("input");
    fromInput.type = "date";
    fromInput.lang = "de";
    fromInput.className = "date-range-custom-from";
    fromInput.value = customFromDraft;
    fromInput.dataset.testid = `${opts.surface}.date-range-custom-from`;
    fromInput.addEventListener("input", () => {
      customFromDraft = fromInput.value;
      refreshValidity();
    });
    fromWrap.appendChild(fromLbl);
    fromWrap.appendChild(fromInput);

    const toWrap = document.createElement("label");
    toWrap.className = "date-range-custom-field";
    const toLbl = document.createElement("span");
    toLbl.className = "date-range-custom-label";
    toLbl.textContent = t("date_range.custom.to");
    const toInput = document.createElement("input");
    toInput.type = "date";
    toInput.lang = "de";
    toInput.className = "date-range-custom-to";
    toInput.value = customToDraft;
    toInput.dataset.testid = `${opts.surface}.date-range-custom-to`;
    toInput.addEventListener("input", () => {
      customToDraft = toInput.value;
      refreshValidity();
    });
    toWrap.appendChild(toLbl);
    toWrap.appendChild(toInput);

    const applyBtn = document.createElement("button");
    applyBtn.type = "button";
    applyBtn.className = "date-range-custom-apply";
    applyBtn.textContent = t("date_range.custom.apply");
    applyBtn.dataset.testid = `${opts.surface}.date-range-custom-apply`;
    applyBtn.addEventListener("click", () => {
      const err = validateCustomRange(customFromDraft, customToDraft);
      if (err !== null) {
        showError(err);
        return;
      }
      commit(
        { horizon: "custom", from: customFromDraft, to: customToDraft },
        /*closeAfter*/ true,
      );
    });

    const cancelBtn = document.createElement("button");
    cancelBtn.type = "button";
    cancelBtn.className = "date-range-custom-cancel";
    cancelBtn.textContent = t("date_range.custom.cancel");
    cancelBtn.addEventListener("click", () => {
      customEditorOpen = false;
      // Restore drafts from live value so a re-open shows the
      // committed state rather than the abandoned typing.
      customFromDraft = value.horizon === "custom" ? (value.from ?? "") : "";
      customToDraft = value.horizon === "custom" ? (value.to ?? "") : "";
      renderPanel();
    });

    const errEl = document.createElement("div");
    errEl.className = "date-range-custom-error";
    errEl.hidden = true;
    errEl.dataset.testid = `${opts.surface}.date-range-custom-error`;

    editor.appendChild(fromWrap);
    editor.appendChild(toWrap);
    editor.appendChild(applyBtn);
    editor.appendChild(cancelBtn);
    editor.appendChild(errEl);
    section.appendChild(editor);

    refreshValidity();

    function refreshValidity(): void {
      const err = validateCustomRange(customFromDraft, customToDraft);
      if (err === null) {
        applyBtn.disabled = false;
        errEl.hidden = true;
        errEl.textContent = "";
        return;
      }
      applyBtn.disabled = true;
      // Only surface the *content* error (`invalid` = inverted range)
      // while the user is typing. Empty / format errors are visible
      // through the disabled-Anwenden state alone — surfacing them on
      // every keystroke would be noisy.
      if (err === "date_range.custom.invalid") {
        showError(err);
      } else {
        errEl.hidden = true;
      }
    }

    function showError(key: Parameters<typeof t>[0]): void {
      errEl.textContent = t(key);
      errEl.hidden = false;
    }

    return section;
  }

  return {
    element: root,
    getValue: () => normalize(value),
    setValue(next: TimeSpec) {
      value = normalize(next);
      customEditorOpen = value.horizon === "custom";
      if (value.horizon === "custom") {
        customFromDraft = value.from ?? "";
        customToDraft = value.to ?? "";
      }
      renderTrigger();
      renderPanel();
    },
    close: closePopover,
    destroy() {
      document.removeEventListener("mousedown", onDocClick);
      document.removeEventListener("keydown", onKeydown);
      root.remove();
    },
  };
}

function normalize(spec: TimeSpec): TimeSpec {
  if (spec.horizon === "custom") {
    return {
      horizon: "custom",
      from: spec.from && isValidISODate(spec.from) ? spec.from : undefined,
      to: spec.to && isValidISODate(spec.to) ? spec.to : undefined,
    };
  }
  return { horizon: spec.horizon };
}

function labelFor(spec: TimeSpec, prefix?: string): string {
  let body: string;
  if (spec.horizon === "custom") {
    if (spec.from && spec.to) {
      body = t("date_range.button.label.custom_range")
        .replace("{from}", formatISO(spec.from))
        .replace("{to}", formatISO(spec.to));
    } else {
      body = t("date_range.horizon.custom");
    }
  } else {
    body = t(HORIZON_LABEL_KEY[spec.horizon]);
  }
  return prefix ? `${prefix}: ${body}` : body;
}

function formatISO(iso: string): string {
  if (!isValidISODate(iso)) return iso;
  // DE locale: DD.MM.YYYY. The picker is German-first; surfaces in EN
  // can override via labelPrefix or by formatting before commit if
  // they want a different shape.
  const [y, m, d] = iso.split("-");
  return `${d}.${m}.${y}`;
}
