import { initI18n, t } from "./i18n";
import { initSidebar } from "./sidebar";
import {
  ALL_DENSITIES,
  ALL_PALETTES,
  ALL_RANGE_PRESETS,
  mount,
  type ChartHandle,
  type Density,
  type Palette,
  type RangePreset,
} from "./views/shape-timeline-chart";
import {
  exportCSV,
  exportJSON,
  exportPNG,
  exportPrint,
  exportSVG,
  type ExportContext,
} from "./views/chart-export";

// t-paliad-177 Slice 1 — boot client for the standalone Project Timeline
// / Chart page. Reads the project id from the URL path, loads the
// project metadata (for title + breadcrumb), mounts the SVG renderer
// inside #projects-chart-host. Slice 1 keeps the controls inert; Slice 3
// wires density / palette / zoom against this same surface.
//
// Design ref: docs/design-project-chart-2026-05-09.md §8.2 + §12.

interface Project {
  id: string;
  title: string;
  reference?: string;
  client_matter?: string;
  type?: string;
}

const PROJECT_ID_RE = /^\/projects\/([0-9a-fA-F-]{36})\/chart\/?$/;

function projectIdFromPath(): string | null {
  const match = PROJECT_ID_RE.exec(window.location.pathname);
  return match ? match[1] : null;
}

const PALETTE_SET: ReadonlySet<string> = new Set(ALL_PALETTES);

/** Reads ?palette=... from the URL; returns the default when missing /
 *  unknown so a hostile or stale URL can't break the chart. */
function paletteFromURL(): Palette {
  const raw = new URLSearchParams(window.location.search).get("palette");
  if (raw && PALETTE_SET.has(raw)) return raw as Palette;
  return "default";
}

/** Mirrors paletteFromURL but for writing — pushes a new history entry
 *  so the URL stays bookmarkable / shareable per design §8.2. */
function writePaletteToURL(palette: Palette): void {
  writeParamToURL("palette", palette, "default");
}

const DENSITY_SET: ReadonlySet<string> = new Set(ALL_DENSITIES);

function densityFromURL(): Density {
  const raw = new URLSearchParams(window.location.search).get("density");
  if (raw && DENSITY_SET.has(raw)) return raw as Density;
  return "standard";
}

function writeDensityToURL(density: Density): void {
  writeParamToURL("density", density, "standard");
}

const RANGE_SET: ReadonlySet<string> = new Set(ALL_RANGE_PRESETS);

interface RangeState {
  preset: RangePreset;
  from?: string;
  to?: string;
}

const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;

function rangeFromURL(): RangeState {
  const params = new URLSearchParams(window.location.search);
  const raw = params.get("range");
  const preset: RangePreset = raw && RANGE_SET.has(raw) ? (raw as RangePreset) : "1y";
  if (preset === "custom") {
    const from = params.get("from") || "";
    const to = params.get("to") || "";
    return {
      preset,
      from: ISO_DATE_RE.test(from) ? from : undefined,
      to: ISO_DATE_RE.test(to) ? to : undefined,
    };
  }
  return { preset };
}

function writeRangeToURL(state: RangeState): void {
  const params = new URLSearchParams(window.location.search);
  if (state.preset === "1y") {
    params.delete("range");
  } else {
    params.set("range", state.preset);
  }
  if (state.preset === "custom") {
    if (state.from) params.set("from", state.from);
    else params.delete("from");
    if (state.to) params.set("to", state.to);
    else params.delete("to");
  } else {
    params.delete("from");
    params.delete("to");
  }
  const qs = params.toString();
  const next = window.location.pathname + (qs ? "?" + qs : "");
  window.history.replaceState(null, "", next);
}

/** Read ?lanes=id1,id2 from the URL. Empty / missing → null (show all).
 *  Defence: ids that look hostile (commas embedded, oversized) are dropped
 *  on render via the renderer's allow-set intersection. */
function lanesFromURL(): string[] | null {
  const raw = new URLSearchParams(window.location.search).get("lanes");
  if (!raw) return null;
  const ids = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0 && s.length < 200);
  return ids.length === 0 ? null : ids;
}

function writeLanesToURL(lanes: string[] | null): void {
  const params = new URLSearchParams(window.location.search);
  if (!lanes || lanes.length === 0) {
    params.delete("lanes");
  } else {
    params.set("lanes", lanes.join(","));
  }
  const qs = params.toString();
  const next = window.location.pathname + (qs ? "?" + qs : "");
  window.history.replaceState(null, "", next);
}

/** Shared URL writer — omits the param when it equals its default, so the
 *  canonical URL stays short and dedupable. */
function writeParamToURL(name: string, value: string, defaultValue: string): void {
  const params = new URLSearchParams(window.location.search);
  if (value === defaultValue) {
    params.delete(name);
  } else {
    params.set(name, value);
  }
  const qs = params.toString();
  const next = window.location.pathname + (qs ? "?" + qs : "");
  window.history.replaceState(null, "", next);
}

async function loadProject(id: string): Promise<Project | null> {
  try {
    const resp = await fetch(`/api/projects/${encodeURIComponent(id)}`);
    if (!resp.ok) return null;
    return (await resp.json()) as Project;
  } catch {
    return null;
  }
}

function formatMeta(p: Project): string {
  const parts: string[] = [];
  if (p.reference) parts.push(p.reference);
  if (p.client_matter) parts.push(p.client_matter);
  return parts.join(" • ");
}

async function boot(): Promise<void> {
  initI18n();
  initSidebar();

  const loadingEl = document.getElementById("projects-chart-loading");
  const notfoundEl = document.getElementById("projects-chart-notfound");
  const bodyEl = document.getElementById("projects-chart-body");
  const titleEl = document.getElementById("projects-chart-title");
  const metaEl = document.getElementById("projects-chart-meta");
  const backLink = document.getElementById("projects-chart-back-link") as HTMLAnchorElement | null;
  const host = document.getElementById("projects-chart-host");
  const undatedHint = document.getElementById("projects-chart-undated");

  const id = projectIdFromPath();
  if (!id || !host || !bodyEl || !loadingEl || !notfoundEl) {
    if (loadingEl) loadingEl.style.display = "none";
    if (notfoundEl) notfoundEl.style.display = "block";
    return;
  }

  const project = await loadProject(id);
  if (!project) {
    loadingEl.style.display = "none";
    notfoundEl.style.display = "block";
    return;
  }

  // Wire back-link to the Verlauf tab specifically — projects-detail.ts
  // reads the /history sub-path on init and switches to that tab. Going
  // back to the bare /projects/{id} also lands on Verlauf today, but the
  // /history form is explicit + survives a future default-tab change.
  if (backLink) backLink.href = `/projects/${encodeURIComponent(id)}/history`;

  if (titleEl) titleEl.textContent = project.title || t("projects.chart.title");
  if (metaEl) metaEl.textContent = formatMeta(project);

  loadingEl.style.display = "none";
  bodyEl.style.display = "";

  const initialPalette = paletteFromURL();
  const initialDensity = densityFromURL();
  const initialRange = rangeFromURL();
  const initialLanes = lanesFromURL();
  let handle: ChartHandle | null = null;
  // Module-scope mirrors so the chip click handlers (rendered later)
  // can reach the live state without threading it through callbacks.
  moduleVisibleLanes = initialLanes;
  try {
    handle = mount(host, {
      projectId: id,
      palette: initialPalette,
      density: initialDensity,
      rangePreset: initialRange.preset,
      rangeFrom: initialRange.from,
      rangeTo: initialRange.to,
      visibleLanes: initialLanes,
      onDataLoaded: ({ lanes }) => {
        renderLaneFilter(lanes);
      },
    });
  } catch (err) {
    console.error("chart mount failed", err);
    host.textContent = t("projects.chart.error.mount");
    return;
  }
  moduleHandleRef = handle;

  // Wire the palette picker. Reflect the URL-decoded initial value, then
  // re-write the URL + flip the data-palette attribute on every change.
  const paletteSel = document.getElementById("projects-chart-palette") as HTMLSelectElement | null;
  if (paletteSel) {
    paletteSel.value = initialPalette;
    paletteSel.addEventListener("change", () => {
      const next = paletteSel.value;
      if (!PALETTE_SET.has(next)) return;
      const p = next as Palette;
      handle!.setPalette(p);
      writePaletteToURL(p);
    });
  }

  // Density picker — same URL-state pattern. Density triggers a repaint
  // (lane height + mark radius change), palette is a pure CSS swap.
  const densitySel = document.getElementById("projects-chart-density") as HTMLSelectElement | null;
  if (densitySel) {
    densitySel.value = initialDensity;
    densitySel.addEventListener("change", () => {
      const next = densitySel.value;
      if (!DENSITY_SET.has(next)) return;
      const d = next as Density;
      handle!.setDensity(d);
      writeDensityToURL(d);
    });
  }

  // Range chips — 4-option select plus a custom date-pair that shows
  // only when preset === "custom". Per design §8.2 + faraday-Q8 default.
  const rangeSel = document.getElementById("projects-chart-range") as HTMLSelectElement | null;
  const rangeCustomWrap = document.getElementById("projects-chart-range-custom");
  const rangeFromInput = document.getElementById("projects-chart-range-from") as HTMLInputElement | null;
  const rangeToInput = document.getElementById("projects-chart-range-to") as HTMLInputElement | null;
  if (rangeSel && rangeCustomWrap && rangeFromInput && rangeToInput) {
    rangeSel.value = initialRange.preset;
    if (initialRange.preset === "custom") {
      rangeCustomWrap.style.display = "";
      if (initialRange.from) rangeFromInput.value = initialRange.from;
      if (initialRange.to) rangeToInput.value = initialRange.to;
    }
    const applyRange = () => {
      const preset = rangeSel.value;
      if (!RANGE_SET.has(preset)) return;
      const p = preset as RangePreset;
      rangeCustomWrap.style.display = p === "custom" ? "" : "none";
      const from = rangeFromInput.value || undefined;
      const to = rangeToInput.value || undefined;
      handle!.setRange(p, from, to);
      writeRangeToURL({ preset: p, from, to });
    };
    rangeSel.addEventListener("change", applyRange);
    rangeFromInput.addEventListener("change", applyRange);
    rangeToInput.addEventListener("change", applyRange);
  }

  // Export menu. Each button maps to one chart-export function; the
  // handle exposes the live SVG + last-fetched data needed to compose
  // an ExportContext. Errors land in the host's message area so the
  // user gets feedback instead of a silent failure.
  function ctxNow(): ExportContext {
    const data = handle!.getData();
    return {
      projectId: id,
      projectTitle: project.title || t("projects.chart.title"),
      svgEl: handle!.getSVGElement(),
      events: data.events,
      lanes: data.lanes,
    };
  }
  function runExport(fn: (ctx: ExportContext) => void | Promise<void>): void {
    void Promise.resolve()
      .then(() => fn(ctxNow()))
      .catch((err) => {
        console.error("export failed", err);
        if (host) {
          host.setAttribute("data-export-error", "1");
        }
      });
  }
  wirePermalinkCopy("projects-chart-copylink");

  wireExport("projects-chart-export-svg", () => runExport(exportSVG));
  wireExport("projects-chart-export-png", () => runExport(exportPNG));
  wireExport("projects-chart-export-csv", () => runExport(exportCSV));
  wireExport("projects-chart-export-json", () => runExport(exportJSON));
  wireExport("projects-chart-export-print", () => exportPrint());
  // iCal goes server-side so it reuses the existing caldav_ical formatter
  // (faraday-Q6 / m's pick: deadlines + appointments only — no projected).
  wireExport("projects-chart-export-ics", () => {
    window.location.href = `/api/projects/${encodeURIComponent(id)}/timeline.ics`;
  });

  // After the first paint, surface the undated hint when the renderer
  // reports clipped/undated rows. Re-checked on resize-debounced repaint.
  const checkUndated = () => {
    if (!undatedHint || !handle) return;
    const layout = handle.getLayout();
    if (!layout) return;
    if (layout.undatedCount > 0) {
      undatedHint.style.display = "";
      undatedHint.textContent = `${layout.undatedCount} Ereignis(se) ohne Datum (links angeheftet).`;
    } else {
      undatedHint.style.display = "none";
    }
  };
  // Poll once after the initial fetch settles. mount() kicks the fetch
  // synchronously; layout becomes available after the network round-trip.
  setTimeout(checkUndated, 400);
  setTimeout(checkUndated, 1500);
}

/** Render the lane-filter chip group once the renderer has lanes from
 *  the server. One toggle button per lane; clicking flips inclusion in
 *  the visible-lane allow-set. Hidden when there's only one lane (or
 *  none) — the filter is pointless on a single-track render. */
function renderLaneFilter(lanes: ReadonlyArray<{ id: string; label: string }>): void {
  const container = document.getElementById("projects-chart-lanes-filter");
  if (!container) return;
  // Hide and bail when the filter wouldn't add value.
  if (lanes.length < 2) {
    container.innerHTML = "";
    container.style.display = "none";
    return;
  }
  container.style.display = "";
  container.innerHTML = "";
  const titleEl = document.createElement("span");
  titleEl.className = "smart-timeline-chart-lanes-label";
  titleEl.textContent = "Spuren:";
  container.appendChild(titleEl);
  for (const lane of lanes) {
    const btn = document.createElement("button");
    btn.type = "button";
    btn.className = "smart-timeline-chart-lane-chip";
    const isVisible = laneIsVisible(lane.id);
    btn.setAttribute("aria-pressed", isVisible ? "true" : "false");
    btn.dataset.laneId = lane.id;
    btn.textContent = lane.label || lane.id;
    btn.addEventListener("click", () => {
      toggleLane(lane.id, lanes);
      // Reflect new state immediately on this button + siblings.
      for (const sibling of container.querySelectorAll<HTMLButtonElement>("button[data-lane-id]")) {
        const sid = sibling.dataset.laneId || "";
        sibling.setAttribute("aria-pressed", laneIsVisible(sid) ? "true" : "false");
      }
    });
    container.appendChild(btn);
  }
}

/** Permalink copy. The URL already aggregates every chip's state via the
 *  individual writeParamToURL writers (palette + density + range + lanes),
 *  so window.location.href IS the canonical shareable link. We copy it
 *  to the clipboard and flash a "kopiert" confirmation on the button. */
function wirePermalinkCopy(buttonId: string): void {
  const btn = document.getElementById(buttonId) as HTMLButtonElement | null;
  if (!btn) return;
  const originalLabel = btn.textContent || "";
  let resetTimer: ReturnType<typeof setTimeout> | null = null;
  btn.addEventListener("click", async () => {
    const url = window.location.href;
    const ok = await copyToClipboard(url);
    if (resetTimer) clearTimeout(resetTimer);
    btn.textContent = ok ? "✓ Kopiert" : "⚠ Konnte nicht kopieren";
    btn.classList.add(ok ? "is-success" : "is-error");
    resetTimer = setTimeout(() => {
      btn.textContent = originalLabel;
      btn.classList.remove("is-success", "is-error");
    }, 1800);
  });
}

async function copyToClipboard(text: string): Promise<boolean> {
  // Prefer the async Clipboard API. Falls back to the legacy exec hack
  // for browsers / contexts where it's unavailable (some iframes, file://).
  if (navigator.clipboard && window.isSecureContext) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch {
      // fall through
    }
  }
  try {
    const textarea = document.createElement("textarea");
    textarea.value = text;
    textarea.style.position = "fixed";
    textarea.style.opacity = "0";
    document.body.appendChild(textarea);
    textarea.select();
    const ok = document.execCommand("copy");
    document.body.removeChild(textarea);
    return ok;
  } catch {
    return false;
  }
}

function wireExport(buttonId: string, handler: () => void): void {
  const btn = document.getElementById(buttonId) as HTMLButtonElement | null;
  if (!btn) return;
  btn.addEventListener("click", (e) => {
    e.preventDefault();
    handler();
    // Close the <details> dropdown so the user sees the chart-area
    // update (download notification, print preview, etc).
    const details = btn.closest("details");
    if (details) details.removeAttribute("open");
  });
}

// Lane-filter mutable state lives at module scope so renderLaneFilter
// closures over the same set as toggleLane / laneIsVisible. We can't
// access boot()'s `visibleLanes` from here cleanly, so we mirror it.
let moduleVisibleLanes: string[] | null = null;
let moduleHandleRef: ChartHandle | null = null;

function laneIsVisible(id: string): boolean {
  if (moduleVisibleLanes === null) return true;
  return moduleVisibleLanes.includes(id);
}

function toggleLane(id: string, allLanes: ReadonlyArray<{ id: string }>): void {
  if (moduleVisibleLanes === null) {
    // Currently "show all" — turning a chip off means everyone except this one.
    moduleVisibleLanes = allLanes.map((l) => l.id).filter((l) => l !== id);
  } else if (moduleVisibleLanes.includes(id)) {
    moduleVisibleLanes = moduleVisibleLanes.filter((l) => l !== id);
  } else {
    moduleVisibleLanes = [...moduleVisibleLanes, id];
  }
  // If user toggled every lane back on, collapse to null (show all).
  if (moduleVisibleLanes.length === allLanes.length) {
    moduleVisibleLanes = null;
  }
  // If user toggled every lane off, snap back to null too — an empty
  // chart is never useful, treat as "you didn't mean that, show all".
  if (moduleVisibleLanes !== null && moduleVisibleLanes.length === 0) {
    moduleVisibleLanes = null;
  }
  if (moduleHandleRef) {
    moduleHandleRef.setVisibleLanes(moduleVisibleLanes);
  }
  writeLanesToURL(moduleVisibleLanes);
}

document.addEventListener("DOMContentLoaded", () => {
  void boot();
});
