import { initI18n, t, type I18nKey } from "./i18n";
import { initSidebar } from "./sidebar";
import type { FilterSpec, RenderSpec, ViewRunResult, UserView, RenderShape, DataSource } from "./views/types";
import { renderListShape } from "./views/shape-list";
import { renderCardsShape } from "./views/shape-cards";
import { renderCalendarShape } from "./views/shape-calendar";
import { renderTimelineShape } from "./views/shape-timeline-cv";
import type { ChartHandle } from "./views/shape-timeline-chart";
import { mountFilterBar, type BarHandle, type AxisKey } from "./filter-bar";

// /views and /views/{slug} client. Loads the saved or system view, runs
// it via /api/views/{slug}/run, and dispatches to the matching render-
// shape component. Shape-switcher chips toggle the live render without
// re-fetching (the rows are already in memory).
//
// t-paliad-211 — the per-view filter bar (`mountFilterBar`) lives between
// the shape chips and the render hosts. The saved view's filter_spec is
// the baseline; the bar overlays the user's per-session tweaks and POSTs
// `/api/views/{slug}/run` with the effective spec as override (the
// substrate accepts `{filter: ...}` per views.go:283). Axes are picked
// from the spec's data sources so a deadline-only view doesn't expose
// the appointment-type chip cluster and vice versa.

initI18n();
initSidebar();

interface ViewMeta {
  // For saved views: identifies the row for touch/edit/delete.
  user_view_id?: string;
  // Display name + slug.
  name: string;
  slug: string;
  // Filter + render specs (may be overridden by slug detection).
  filter: FilterSpec;
  render: RenderSpec;
  // Whether this is a code-resident SystemView.
  is_system: boolean;
}

let currentMeta: ViewMeta | null = null;
let currentRows: ViewRunResult | null = null;
let currentRender: RenderSpec | null = null;
let bar: BarHandle | null = null;

document.addEventListener("DOMContentLoaded", () => {
  bindShapeChips();
  bindToastClose();
  void hydrate();
});

async function hydrate(): Promise<void> {
  const slug = pathSlug();
  if (!slug) {
    // /views with no slug → empty / onboarding state.
    const onboarding = document.getElementById("views-onboarding");
    const loading = document.getElementById("views-loading");
    if (loading) loading.hidden = true;
    if (onboarding) onboarding.hidden = false;
    return;
  }
  // Resolve the view: try system first, then user.
  const meta = await resolveMeta(slug);
  if (!meta) {
    showError(t("views.error.not_found"));
    return;
  }
  currentMeta = meta;
  currentRender = meta.render;
  document.title = `${meta.name} — Paliad`;
  updateHeader(meta);
  mountBar(meta);
  if (meta.user_view_id) {
    fireAndForget(`/api/user-views/${meta.user_view_id}/touch`, "POST");
  }
}

async function resolveMeta(slug: string): Promise<ViewMeta | null> {
  // Try the system view list first — cheap, code-resident.
  try {
    const r = await fetch("/api/views/system", { credentials: "include" });
    if (r.ok) {
      const list = (await r.json()) as Array<{ Slug: string; Name: string; Filter: FilterSpec; Render: RenderSpec }>;
      const sys = list.find((sv) => sv.Slug === slug);
      if (sys) {
        return { name: sys.Name, slug: sys.Slug, filter: sys.Filter, render: sys.Render, is_system: true };
      }
    }
  } catch (_e) {
    // fall through to user lookup
  }
  // Try a saved user view.
  try {
    const r = await fetch("/api/user-views", { credentials: "include" });
    if (r.ok) {
      const list = (await r.json()) as UserView[];
      const v = list.find((uv) => uv.slug === slug);
      if (v) {
        return {
          user_view_id: v.id,
          name: v.name,
          slug: v.slug,
          filter: v.filter_spec,
          render: v.render_spec,
          is_system: false,
        };
      }
    }
  } catch (_e) { /* noop */ }
  return null;
}

// mountBar wires the filter-bar to the view's saved spec. The bar runs
// the spec through `/api/views/{slug}/run` whenever the user tweaks an
// axis, and the onResult callback re-paints into the active shape host.
function mountBar(meta: ViewMeta): void {
  const host = document.getElementById("views-filter-bar");
  const toolbar = document.getElementById("views-toolbar");
  const loading = document.getElementById("views-loading");
  if (loading) loading.hidden = false;
  if (toolbar) toolbar.hidden = false;
  if (host) host.hidden = false;
  if (!host) return;

  // Tear down any prior bar (re-mount on lang change isn't supported
  // here, but a future Phase-2 axis switch may need this).
  if (bar) {
    bar.destroy();
    bar = null;
  }

  const axes = axesForSources(meta.filter.sources);
  // surfaceKey scoped per-view-slug so two views remember their own
  // density/sort prefs independently.
  const surfaceKey = `views.${meta.slug}`;

  bar = mountFilterBar(host, {
    baseFilter: meta.filter,
    baseRender: meta.render,
    axes,
    surfaceKey,
    systemViewSlug: meta.slug,
    // The saved view IS the baseline; "Speichern als Sicht" remains
    // available for users who want to fork.
    showSaveAsView: !meta.is_system,
    userViewId: meta.user_view_id,
    onResult: (result, effective) => {
      if (loading) loading.hidden = true;
      currentRows = result;
      currentRender = effective.render;
      paintRows(result, effective.render);
    },
  });
}

// axesForSources picks the filter-bar axes a saved view's data sources
// support. Universal axes (time / personal_only / sort) always render;
// per-source predicates only render when the view's spec actually
// queries that source — otherwise the chip would be a no-op.
function axesForSources(sources: DataSource[]): AxisKey[] {
  const set = new Set(sources);
  const out: AxisKey[] = ["time"];
  if (set.has("deadline")) out.push("deadline_status");
  if (set.has("appointment")) out.push("appointment_type");
  if (set.has("approval_request")) {
    out.push("approval_viewer_role");
    out.push("approval_status");
    out.push("approval_entity_type");
  }
  if (set.has("project_event")) out.push("project_event_kind");
  out.push("personal_only");
  out.push("sort");
  return out;
}

function paintRows(result: ViewRunResult, render: RenderSpec): void {
  const empty = document.getElementById("views-empty");
  const errorEl = document.getElementById("views-error");
  if (errorEl) errorEl.hidden = true;

  if (result.inaccessible_project_ids && result.inaccessible_project_ids.length > 0) {
    showInaccessibleToast(result.inaccessible_project_ids.length);
  }

  if (result.rows.length === 0) {
    setActiveShape(null);
    if (empty) {
      empty.hidden = false;
      const hint = document.getElementById("views-empty-hint");
      if (hint && currentMeta) hint.textContent = filterSummary(currentMeta.filter);
    }
    return;
  }
  if (empty) empty.hidden = true;

  setActiveShape(render.shape);
  renderShape(render.shape, render, result.rows);
}

function setActiveShape(shape: RenderShape | null): void {
  for (const host of ["views-shape-list", "views-shape-cards", "views-shape-calendar", "views-shape-timeline"]) {
    const el = document.getElementById(host);
    if (el) el.hidden = shape === null ? true : !host.endsWith("-" + shape);
  }
  document.querySelectorAll<HTMLButtonElement>("#views-shape-chips [data-shape]").forEach((btn) => {
    btn.classList.toggle("active", btn.dataset.shape === shape);
  });
}

let timelineHandle: ChartHandle | null = null;

function renderShape(shape: RenderShape, render: RenderSpec, rows: ViewRunResult["rows"]): void {
  const host = document.getElementById(`views-shape-${shape}`);
  if (!host) return;
  // Switching away from timeline → dispose the prior chart handle so we
  // don't leak resize listeners / SVG nodes between shape flips.
  if (shape !== "timeline" && timelineHandle) {
    timelineHandle.dispose();
    timelineHandle = null;
  }
  switch (shape) {
    case "list":
      renderListShape(host, rows, render);
      break;
    case "cards":
      renderCardsShape(host, rows, render);
      break;
    case "calendar":
      renderCalendarShape(host, rows, render);
      break;
    case "timeline": {
      // Tear down any previous chart inside this host before re-mounting
      // (the CV adapter clears chart-host innerHTML on its own, but we
      // need to dispose the prior handle's resize/click listeners too).
      if (timelineHandle) {
        timelineHandle.dispose();
        timelineHandle = null;
      }
      const chartHost = document.getElementById("views-timeline-chart-host");
      if (chartHost) {
        timelineHandle = renderTimelineShape(chartHost, rows, render);
      }
      maybeShowTimelineCaveat();
      break;
    }
  }
}

/** First-open caveat banner. sessionStorage flag means the user sees it
 *  once per browser session — dismissive but not annoying. Design §13.4
 *  documents the limitation; this is the user-facing surface. */
function maybeShowTimelineCaveat(): void {
  const FLAG = "paliad-views-timeline-caveat-dismissed";
  const banner = document.getElementById("views-timeline-caveat");
  const closeBtn = document.getElementById("views-timeline-caveat-close");
  if (!banner) return;
  if (sessionStorage.getItem(FLAG) === "1") {
    banner.hidden = true;
    return;
  }
  banner.hidden = false;
  if (closeBtn && !closeBtn.dataset.bound) {
    closeBtn.addEventListener("click", () => {
      banner.hidden = true;
      try {
        sessionStorage.setItem(FLAG, "1");
      } catch {
        /* sessionStorage may be unavailable in strict modes — silently noop */
      }
    });
    closeBtn.dataset.bound = "1";
  }
}

function bindShapeChips(): void {
  document.querySelectorAll<HTMLButtonElement>("#views-shape-chips [data-shape]").forEach((btn) => {
    btn.addEventListener("click", () => {
      const shape = (btn.dataset.shape ?? "list") as RenderShape;
      if (!currentRows || !currentRender) return;
      // Override the shape transiently — doesn't mutate the saved spec.
      const overrideRender = { ...currentRender, shape };
      currentRender = overrideRender;
      setActiveShape(shape);
      renderShape(shape, overrideRender, currentRows.rows);
    });
  });
}

function updateHeader(meta: ViewMeta): void {
  const heading = document.getElementById("views-heading");
  if (heading) heading.textContent = meta.name;
  const subtitle = document.getElementById("views-subtitle");
  if (subtitle) subtitle.textContent = filterSummary(meta.filter);
  const actions = document.getElementById("views-header-actions");
  if (actions) {
    actions.innerHTML = "";
    if (!meta.is_system && meta.user_view_id) {
      const editLink = document.createElement("a");
      editLink.href = `/views/${encodeURIComponent(meta.slug)}/edit`;
      editLink.className = "btn-secondary btn-small";
      editLink.textContent = t("views.action.edit");
      actions.appendChild(editLink);
    }
  }
}

function filterSummary(filter: FilterSpec): string {
  const parts: string[] = [];
  // Sources
  parts.push(filter.sources.map((s) => t(("views.source." + s) as I18nKey)).join(" + "));
  // Time
  parts.push(t(("views.horizon." + filter.time.horizon) as I18nKey));
  // Scope
  if (filter.scope.personal_only) {
    parts.push(t("views.scope.personal_only"));
  } else if (filter.scope.projects.mode !== "all_visible") {
    parts.push(t(("views.scope." + filter.scope.projects.mode) as I18nKey));
  }
  return parts.join(" · ");
}

function showError(message: string): void {
  const loading = document.getElementById("views-loading");
  const errorEl = document.getElementById("views-error");
  const msg = document.getElementById("views-error-message");
  if (loading) loading.hidden = true;
  if (errorEl) errorEl.hidden = false;
  if (msg) msg.textContent = message;
}

function showInaccessibleToast(count: number): void {
  const toast = document.getElementById("views-toast");
  const text = document.getElementById("views-toast-text");
  if (!toast || !text) return;
  text.textContent = count === 1
    ? t("views.toast.inaccessible_one")
    : t("views.toast.inaccessible_n").replace("{n}", String(count));
  toast.hidden = false;
}

function bindToastClose(): void {
  const close = document.getElementById("views-toast-close");
  const toast = document.getElementById("views-toast");
  if (!close || !toast) return;
  close.addEventListener("click", () => { toast.hidden = true; });
}

function pathSlug(): string | null {
  const m = window.location.pathname.match(/^\/views\/([^\/]+)$/);
  if (!m) return null;
  return decodeURIComponent(m[1]);
}

function fireAndForget(url: string, method: string): void {
  fetch(url, { method, credentials: "include" }).catch(() => { /* noop */ });
}
