import {
  mount,
  type ChartHandle,
  type Density,
  type Palette,
  type RangePreset,
} from "./shape-timeline-chart";
import type { LaneInfo, TimelineEvent } from "./shape-timeline";
import type { RenderSpec, ViewRow } from "./types";
import { t } from "../i18n";

// shape-timeline-cv (t-paliad-177 Slice 4, faraday-Q7) — Custom Views
// host for the chart renderer.
//
// Adapter contract: ViewRow → TimelineEvent + LaneInfo.
//   - deadline + appointment + project_event rows render as actual marks.
//   - approval_request rows are skipped (no chart-meaningful date).
//   - Lane axis = project_id; the cross-project chart use case (design
//     §10) groups events by their owning project. Rows without a
//     project_id collapse into a synthetic "self" lane.
//   - NO projected rows. ViewService doesn't run the fristenrechner
//     calculator, so the CV chart shows actuals only. The host page
//     ships a one-time caveat tooltip (see C3) explaining this.
//
// Design ref: docs/design-project-chart-2026-05-09.md §8.3 + §11.5 + §13.4.

// Zoom levels in ascending span (t-paliad-211). Width-only — the chart's
// existing range presets already provide three meaningful zoom levels.
// Stored in URL as ?tl_zoom=1y|2y|all.
const ZOOM_LEVELS: RangePreset[] = ["1y", "2y", "all"];
const ZOOM_PARAM = "tl_zoom";

export function renderTimelineShape(
  host: HTMLElement,
  rows: ReadonlyArray<ViewRow>,
  render: RenderSpec,
): ChartHandle {
  // Tear down any previous mount so re-rendering the shape (e.g. shape
  // chip switch on /views/{slug}) doesn't stack SVGs.
  host.innerHTML = "";

  const { events, lanes } = adapt(rows);
  const cfg = render.timeline ?? {};

  // Resolve the initial zoom: URL > render spec > "1y" default.
  const initialZoom = resolveInitialZoom(cfg.range_preset);

  // Toolbar lives above the chart in its own row so it doesn't compete
  // with the date-axis / lane labels for space.
  const toolbar = document.createElement("div");
  toolbar.className = "views-timeline-toolbar";
  host.appendChild(toolbar);

  const chartHost = document.createElement("div");
  chartHost.className = "views-timeline-chart-host-inner";
  host.appendChild(chartHost);

  // The CV adapter has no per-project "id" to fetch live timeline data
  // for — we hand mount() a placeholder projectId and the staticData
  // pre-loaded array so it skips the project endpoint entirely. If the
  // user clicks a mark, the renderer's default click handler still
  // resolves /deadlines/{id} / /appointments/{id} from the adapted
  // event's id field, so deep-links land on the correct entity page.
  const handle = mount(chartHost, {
    projectId: "cv",
    staticData: { events, lanes },
    palette: (cfg.palette as Palette | undefined) ?? "default",
    density: (cfg.density as Density | undefined) ?? "standard",
    rangePreset: initialZoom,
    rangeFrom: cfg.range_from,
    rangeTo: cfg.range_to,
  });

  let currentZoom = initialZoom;
  const setZoom = (next: RangePreset) => {
    if (next === currentZoom) return;
    currentZoom = next;
    handle.setRange(next);
    writeZoomURL(next);
    paintToolbar();
  };

  const paintToolbar = () => {
    toolbar.innerHTML = "";

    const zoomGroup = document.createElement("div");
    zoomGroup.className = "views-timeline-zoom-group";

    const zoomLabel = document.createElement("span");
    zoomLabel.className = "views-timeline-zoom-label";
    zoomLabel.textContent = t("views.timeline.zoom.label");
    zoomGroup.appendChild(zoomLabel);

    const zoomOut = document.createElement("button");
    zoomOut.type = "button";
    zoomOut.className = "btn-secondary btn-small views-timeline-zoom-btn";
    zoomOut.setAttribute("aria-label", t("views.timeline.zoom.out"));
    zoomOut.title = t("views.timeline.zoom.out");
    zoomOut.textContent = "−";
    zoomOut.disabled = currentZoom === ZOOM_LEVELS[ZOOM_LEVELS.length - 1];
    zoomOut.addEventListener("click", () => {
      const idx = ZOOM_LEVELS.indexOf(currentZoom);
      if (idx < ZOOM_LEVELS.length - 1) setZoom(ZOOM_LEVELS[idx + 1]);
    });
    zoomGroup.appendChild(zoomOut);

    // Active-level chips (1y / 2y / all). Clicking jumps directly.
    const chips = document.createElement("div");
    chips.className = "views-timeline-zoom-chips agenda-chip-row";
    for (const level of ZOOM_LEVELS) {
      const chip = document.createElement("button");
      chip.type = "button";
      chip.className = "agenda-chip views-timeline-zoom-chip"
        + (level === currentZoom ? " agenda-chip-active" : "");
      chip.dataset.zoom = level;
      chip.textContent = t(zoomLevelKey(level));
      chip.addEventListener("click", () => setZoom(level));
      chips.appendChild(chip);
    }
    zoomGroup.appendChild(chips);

    const zoomIn = document.createElement("button");
    zoomIn.type = "button";
    zoomIn.className = "btn-secondary btn-small views-timeline-zoom-btn";
    zoomIn.setAttribute("aria-label", t("views.timeline.zoom.in"));
    zoomIn.title = t("views.timeline.zoom.in");
    zoomIn.textContent = "+";
    zoomIn.disabled = currentZoom === ZOOM_LEVELS[0];
    zoomIn.addEventListener("click", () => {
      const idx = ZOOM_LEVELS.indexOf(currentZoom);
      if (idx > 0) setZoom(ZOOM_LEVELS[idx - 1]);
    });
    zoomGroup.appendChild(zoomIn);

    toolbar.appendChild(zoomGroup);
  };

  paintToolbar();

  // Apply the URL zoom if it differed from the spec — mount() already
  // used initialZoom so this is a no-op when URL was empty. But when URL
  // disagreed with the spec, mount() honoured the URL and the toolbar
  // already reflects that, so nothing extra to do here.

  return handle;
}

function zoomLevelKey(level: RangePreset): "views.timeline.zoom.1y" | "views.timeline.zoom.2y" | "views.timeline.zoom.all" {
  if (level === "1y") return "views.timeline.zoom.1y";
  if (level === "2y") return "views.timeline.zoom.2y";
  return "views.timeline.zoom.all";
}

function resolveInitialZoom(spec: string | undefined): RangePreset {
  const params = new URLSearchParams(window.location.search);
  const raw = params.get(ZOOM_PARAM);
  if (raw && (ZOOM_LEVELS as string[]).includes(raw)) return raw as RangePreset;
  if (spec && (ZOOM_LEVELS as string[]).includes(spec)) return spec as RangePreset;
  return "1y";
}

function writeZoomURL(zoom: RangePreset): void {
  const url = new URL(window.location.href);
  url.searchParams.set(ZOOM_PARAM, zoom);
  history.replaceState(null, "", url.toString());
}

export interface AdapterResult {
  events: TimelineEvent[];
  lanes: LaneInfo[];
}

/** Exported for tests (shape-timeline-cv.test.ts). Pure — no DOM. */
export function adapt(rows: ReadonlyArray<ViewRow>): AdapterResult {
  const events: TimelineEvent[] = [];
  // Lane order = first-seen order of project_ids in rows, so the user
  // sees lanes in the order their data was returned (typically date-
  // sorted). Deterministic, no surprise re-ordering on re-renders.
  const laneIndex = new Map<string, LaneInfo>();

  for (const row of rows) {
    if (row.kind === "approval_request") {
      // Approval requests have no event_date in the chart sense; they
      // represent pending decisions, not scheduled work. Skip.
      continue;
    }
    const laneId = row.project_id || "self";
    if (!laneIndex.has(laneId)) {
      laneIndex.set(laneId, {
        id: laneId,
        label: row.project_title || row.project_reference || laneLabelFallback(laneId),
        project_id: row.project_id,
      });
    }

    const event: TimelineEvent = {
      kind: toTimelineKind(row.kind),
      status: extractStatus(row),
      track: laneId === "self" ? "parent" : "child:" + laneId,
      date: row.event_date || null,
      title: row.title,
      description: row.subtitle,
      lane_id: laneId,
    };
    // Set the right provenance id so the renderer's click handler can
    // deep-link to /deadlines/{id} / /appointments/{id}.
    switch (row.kind) {
      case "deadline":
        event.deadline_id = row.id;
        break;
      case "appointment":
        event.appointment_id = row.id;
        break;
      case "project_event":
        event.project_event_id = row.id;
        break;
    }
    events.push(event);
  }

  return { events, lanes: [...laneIndex.values()] };
}

function toTimelineKind(kind: ViewRow["kind"]): TimelineEvent["kind"] {
  // ViewRow "project_event" maps to chart "milestone" — they're the
  // same underlying paliad.project_events row, the chart just uses a
  // different name because milestones are the chart-meaningful subset.
  if (kind === "project_event") return "milestone";
  // Defensive: approval_request was filtered earlier, but TS doesn't
  // know that. Default to "milestone" for any unexpected kind.
  if (kind === "deadline" || kind === "appointment") return kind;
  return "milestone";
}

/** Status defaults to "open" — ViewRow doesn't carry chart-status
 *  semantics directly, and the underlying detail json shape varies per
 *  kind. The chart's color saturation maps status → fill / ring style,
 *  so "open" gives every mark a sensible default (filled, full color).
 *  Detail-driven status lookup is a polish job for a future slice. */
function extractStatus(row: ViewRow): TimelineEvent["status"] {
  if (row.kind === "deadline") {
    const d = row.detail as { status?: string };
    if (d.status === "done" || d.status === "overdue") {
      return d.status as TimelineEvent["status"];
    }
  }
  return "open";
}

function laneLabelFallback(id: string): string {
  if (id === "self") return "(ohne Projekt)";
  // Truncated UUID is more useful than a bare 36-char string.
  return id.slice(0, 8);
}
