import { t, getLang } from "../i18n";

// shape-timeline (t-paliad-171 → t-paliad-175) — vertical timeline render
// for the SmartTimeline. Two-column layout (date / event card), "Heute →"
// rule separating past from future, status icon + kind chip per row.
//
// Slice 2 (t-paliad-173) adds:
//   - Kind="projected" rows in three flavours via Status:
//       "predicted"          — fade-grey (future)
//       "court_set"          — dashed border (court-determined)
//       "predicted_overdue"  — amber-faded (past, no anchor yet)
//   - "[Datum setzen]" inline date editor → POST /timeline/anchor.
//     200 → re-fetch + re-render. 409 → render the predecessor_missing
//     payload as inline error with a "Stattdessen <predecessor> erfassen"
//     link that pre-fills the editor for the parent rule.
//   - "Folgt aus: <Name> (<Date|„Datum offen“>)" footer on every row
//     with depends_on_rule_code, plus a "[Pfad anzeigen]" expander that
//     walks the parent chain back to the trigger.
//   - "[+ Mehr anzeigen]" / "[− Weniger]" lookahead toggle after the 7th
//     projected row, cap remembered in localStorage per project.
//
// Slice 4 (t-paliad-175) adds parent-node lane aggregation:
//   - When `lanes.length > 1` (Patent / Litigation / Client view), render
//     a horizontal lane-strip with one column per lane. Time axis stays
//     vertical within each lane; the lane sub-header names the child
//     project. CSS Grid handles the desktop side-by-side and collapses
//     to single-column on mobile (≤640px).
//   - Lane filter chip (multiselect) sits in the timeline header above
//     the strip; selecting a subset dims the others.
//   - Single-column flow stays the default at Case level (lanes mirror
//     tracks one-for-one).
//
// Wire shape: renderSmartTimeline(host, rows, opts). The TimelineEvent
// shape is the wire contract from /api/projects/{id}/timeline.events;
// LaneInfo[] from .lanes drives the lane-grouped layout.
//
// Design ref: docs/design-smart-timeline-2026-05-08.md §3 + §5 + §6 +
// m/paliad#31 layered requirements.

export interface TimelineEvent {
  kind: "deadline" | "appointment" | "milestone" | "projected";
  status:
    | "done"
    | "open"
    | "overdue"
    | "court_set"
    | "predicted"
    | "predicted_overdue"
    | "off_script";
  track: string;
  date?: string | null;
  title: string;
  description?: string;
  rule_code?: string;

  deadline_id?: string;
  appointment_id?: string;
  project_event_id?: string;

  deadline_rule_id?: string;
  deadline_rule_party?: string;

  sub_project_id?: string;
  sub_project_title?: string;

  depends_on_rule_code?: string;
  depends_on_date?: string | null;
  depends_on_rule_name?: string;

  // Slice 4 — parent-node aggregation (t-paliad-175). lane_id buckets
  // the row into one of the columns described by RenderOptions.lanes.
  // Empty / missing is treated as "self" (the legacy single-lane case).
  lane_id?: string;
  bubble_up?: boolean;

  // t-paliad-176 — underlying paliad.project_events.event_type for
  // milestone rows. Empty for deadline / appointment / projected rows.
  // Powers the FilterBar's project_event_kind chip on the Verlauf tab
  // (matched against KnownProjectEventKinds in filter_spec.go).
  project_event_type?: string;
}

export interface LaneInfo {
  id: string;
  label: string;
  project_id?: string;
  primary?: boolean;
}

export interface PredecessorMissingPayload {
  error: "predecessor_missing";
  missing_rule_code: string;
  missing_rule_name_de: string;
  missing_rule_name_en: string;
  requested_rule_code: string;
  requested_rule_name_de: string;
  requested_rule_name_en: string;
  message_de: string;
  message_en: string;
}

export interface RenderOptions {
  // Today's date as ISO YYYY-MM-DD; defaults to "now in browser TZ".
  today?: string;
  // The project the timeline belongs to. Required for anchor / skip
  // POSTs. When undefined, projected rows don't expose "Datum setzen".
  projectId?: string;
  // Language hint — falls back to getLang() when omitted.
  lang?: "de" | "en";
  // Called after a successful anchor write so the host can re-fetch
  // and re-render. Skipped when omitted.
  onChange?: () => void | Promise<void>;
  // Lookahead state for projected rows. Default 7 = backend default.
  lookahead?: number;
  // Total number of future predicted rows the backend knows about
  // (read from X-Projection-Total). When > visible projected count,
  // "Mehr anzeigen" is shown.
  projectedTotal?: number;
  // Called when the user toggles "Mehr / Weniger anzeigen". The host
  // updates state + re-fetches with the new ?lookahead=N.
  onLookaheadChange?: (next: number) => void | Promise<void>;

  // Slice 3 — counterclaim parallel tracks. availableTracks lists every
  // track tag present in the response (parsed from X-Projection-Tracks).
  // When the list contains a non-"parent" entry, the [Track ▼] chip
  // surfaces. selectedTrack is the user's filter ("all" = render every
  // available track in parallel; otherwise render only the named tag).
  availableTracks?: string[];
  selectedTrack?: string;
  onTrackChange?: (next: string) => void | Promise<void>;

  // Slice 4 — parent-node lane aggregation. When lanes.length > 1,
  // renderSmartTimeline renders a lane-strip layout (one column per
  // lane) instead of the single-column flow. selectedLanes is the
  // user's lane-filter chip; defaults to all lanes selected. Empty
  // array = nothing rendered (defensible for the user explicitly
  // unchecking every lane).
  lanes?: LaneInfo[];
  selectedLanes?: string[]; // ids; undefined = all lanes selected
  onLaneFilterChange?: (next: string[]) => void | Promise<void>;
}

export function renderSmartTimeline(
  host: HTMLElement,
  rows: TimelineEvent[],
  opts: RenderOptions = {},
): void {
  host.innerHTML = "";
  host.classList.add("smart-timeline");

  // Slice 4 — lane-grouped rendering (t-paliad-175 §5). When the
  // backend reports more than one lane, every event already carries a
  // lane_id and the layout switches from single-column to lane strip.
  // Lane mode takes precedence over Track-mode (the two are different
  // axes — lanes group by *direct child project*, tracks group by
  // CCR-vs-parent on a single Case).
  const lanes = opts.lanes ?? [];
  const isLaneMode = lanes.length > 1;
  if (isLaneMode) {
    host.appendChild(renderLaneStrip(rows, lanes, opts));
    return;
  }

  // Slice 3 — track filtering. The bar header carries the [Track ▼]
  // chip whenever the response advertised more than the default
  // "parent" track; the filter is applied here before any flow render.
  const availableTracks = (opts.availableTracks ?? []).filter((t) => !!t);
  const hasMultipleTracks = availableTracks.length > 1;
  const selectedTrack = opts.selectedTrack ?? "all";
  if (hasMultipleTracks) {
    host.appendChild(renderTrackChip(availableTracks, selectedTrack, opts));
  }

  // Filter rows by the selected track. "all" leaves rows untouched
  // (parallel layout decides per-track partitioning below).
  const filteredRows =
    selectedTrack === "all"
      ? rows
      : rows.filter((r) => (r.track ?? "parent") === selectedTrack);

  if (filteredRows.length === 0) {
    const empty = document.createElement("div");
    empty.className = "smart-timeline-empty";
    empty.textContent = t("projects.detail.smarttimeline.empty");
    host.appendChild(empty);
    return;
  }

  // When the user has selected "all" AND there are multiple tracks
  // present, render parallel columns side-by-side. Otherwise the
  // existing single-column flow serves both single-track projects and
  // an explicit "Nur Hauptverfahren / Nur Widerklage" filter.
  if (selectedTrack === "all" && hasMultipleTracks) {
    host.appendChild(renderParallelTracks(filteredRows, availableTracks, opts));
    return;
  }

  // Single-column flow.
  host.appendChild(renderTimelineFlow(filteredRows, opts));
}

// renderLaneStrip builds the parent-node aggregated layout (Slice 4).
// One column per lane, each column shows the lane's own past/today/
// future flow. Lane filter chip (multiselect) sits above the strip.
// Lanes the user has unchecked render dimmed but still take up the
// column slot — this preserves the time-axis alignment across lanes.
function renderLaneStrip(
  rows: TimelineEvent[],
  lanes: LaneInfo[],
  opts: RenderOptions,
): HTMLElement {
  const wrap = document.createElement("div");
  wrap.className = "smart-timeline-lanes-wrap";

  // Lane filter chip (Slice 4) — multiselect with "alle" / "keine".
  // Sits above the strip.
  wrap.appendChild(renderLaneFilterChip(lanes, opts));

  const selected = new Set(opts.selectedLanes ?? lanes.map((l) => l.id));

  const grid = document.createElement("div");
  grid.className = "smart-timeline-lanes";
  grid.style.setProperty("--smart-timeline-lane-count", String(lanes.length));

  // Group rows by lane_id. Rows without a lane_id default to the first
  // lane id so they don't disappear. For lane mode the backend always
  // sets lane_id explicitly; this fallback is defensive.
  const byLane = new Map<string, TimelineEvent[]>();
  for (const l of lanes) byLane.set(l.id, []);
  for (const r of rows) {
    const id = r.lane_id || lanes[0].id;
    if (!byLane.has(id)) byLane.set(id, []);
    byLane.get(id)!.push(r);
  }

  for (const lane of lanes) {
    const col = document.createElement("div");
    col.className = "smart-timeline-lane";
    if (!selected.has(lane.id)) {
      col.classList.add("smart-timeline-lane--dimmed");
    }
    if (lane.primary) {
      col.classList.add("smart-timeline-lane--primary");
    }

    const header = document.createElement("h4");
    header.className = "smart-timeline-lane-header";
    if (lane.project_id) {
      const link = document.createElement("a");
      link.href = `/projects/${encodeURIComponent(lane.project_id)}`;
      link.textContent = lane.label;
      link.className = "smart-timeline-lane-header-link";
      header.appendChild(link);
    } else {
      header.textContent = lane.label;
    }
    col.appendChild(header);

    const laneRows = byLane.get(lane.id) ?? [];
    if (laneRows.length === 0) {
      const empty = document.createElement("div");
      empty.className = "smart-timeline-lane-empty";
      empty.textContent = t("projects.detail.smarttimeline.lane.empty");
      col.appendChild(empty);
    } else {
      col.appendChild(renderTimelineFlow(laneRows, opts));
    }
    grid.appendChild(col);
  }

  wrap.appendChild(grid);
  return wrap;
}

// renderLaneFilterChip — multiselect chip-row for the lane filter.
// Defaults to all lanes selected; user toggles individual chips. The
// "Alle" pseudo-chip resets to all selected.
function renderLaneFilterChip(
  lanes: LaneInfo[],
  opts: RenderOptions,
): HTMLElement {
  const wrap = document.createElement("div");
  wrap.className = "smart-timeline-lane-filter";

  const label = document.createElement("span");
  label.className = "smart-timeline-lane-filter-label";
  label.textContent = t("projects.detail.smarttimeline.lane.filter.label");
  wrap.appendChild(label);

  const selected = new Set(opts.selectedLanes ?? lanes.map((l) => l.id));

  const allBtn = document.createElement("button");
  allBtn.type = "button";
  allBtn.className = "smart-timeline-lane-chip smart-timeline-lane-chip--all";
  if (selected.size === lanes.length) {
    allBtn.classList.add("is-active");
  }
  allBtn.textContent = t("projects.detail.smarttimeline.lane.filter.all");
  allBtn.addEventListener("click", () => {
    if (opts.onLaneFilterChange) void opts.onLaneFilterChange(lanes.map((l) => l.id));
  });
  wrap.appendChild(allBtn);

  for (const lane of lanes) {
    const chip = document.createElement("button");
    chip.type = "button";
    chip.className = "smart-timeline-lane-chip";
    if (selected.has(lane.id)) chip.classList.add("is-active");
    chip.textContent = lane.label;
    chip.addEventListener("click", () => {
      const next = new Set(selected);
      if (next.has(lane.id)) {
        next.delete(lane.id);
      } else {
        next.add(lane.id);
      }
      if (opts.onLaneFilterChange) void opts.onLaneFilterChange(Array.from(next));
    });
    wrap.appendChild(chip);
  }

  return wrap;
}

// renderParallelTracks builds a CSS-grid wrapper with one column per
// track. Each column is a self-contained smart-timeline-flow with its
// own past / today / future sections, plus a sub-header that names the
// track ("Hauptverfahren" / "Widerklage — <CCR title>" / "Hauptverfahren
// (Kontext)" for the parent_context view on a CCR child).
//
// Mobile collapse (≤640px) is owned by CSS via .smart-timeline-tracks
// and a media query — the grid switches to a single column there with
// each sub-header preserved so the user knows which track they're on.
function renderParallelTracks(
  rows: TimelineEvent[],
  availableTracks: string[],
  opts: RenderOptions,
): HTMLElement {
  const grid = document.createElement("div");
  grid.className = "smart-timeline-tracks";
  grid.style.setProperty("--smart-timeline-track-count", String(availableTracks.length));

  // Group rows by track. Rows with no track default to "parent".
  const byTrack = new Map<string, TimelineEvent[]>();
  for (const tr of availableTracks) byTrack.set(tr, []);
  for (const r of rows) {
    const key = r.track && byTrack.has(r.track) ? r.track : "parent";
    if (!byTrack.has(key)) byTrack.set(key, []);
    byTrack.get(key)!.push(r);
  }

  for (const trackTag of availableTracks) {
    const trackRows = byTrack.get(trackTag) ?? [];
    const col = document.createElement("div");
    col.className = `smart-timeline-track ${trackClassFor(trackTag)}`;

    const header = document.createElement("h4");
    header.className = "smart-timeline-track-header";
    header.textContent = trackHeaderLabel(trackTag, trackRows);
    col.appendChild(header);

    if (trackRows.length === 0) {
      const empty = document.createElement("div");
      empty.className = "smart-timeline-track-empty";
      empty.textContent = t("projects.detail.smarttimeline.empty");
      col.appendChild(empty);
    } else {
      col.appendChild(renderTimelineFlow(trackRows, opts));
    }
    grid.appendChild(col);
  }

  return grid;
}

// renderTimelineFlow renders the past / today / future / undated flow
// for the given row set into a fresh container. Extracted from the
// pre-Slice-3 renderSmartTimeline so it can be reused as a per-track
// column in the parallel layout.
function renderTimelineFlow(rows: TimelineEvent[], opts: RenderOptions): HTMLElement {
  const todayISO = opts.today ?? todayLocalISO();
  const past: TimelineEvent[] = [];
  const todays: TimelineEvent[] = [];
  const future: TimelineEvent[] = [];
  const undated: TimelineEvent[] = [];
  for (const r of rows) {
    const iso = dateOnlyISO(r.date);
    if (!iso) {
      undated.push(r);
      continue;
    }
    if (iso < todayISO) past.push(r);
    else if (iso === todayISO) todays.push(r);
    else future.push(r);
  }
  past.sort(byDateAsc);
  todays.sort(byDateAsc);
  future.sort(byDateAsc);

  const wrap = document.createElement("div");
  wrap.className = "smart-timeline-flow";

  if (past.length > 0) {
    const section = document.createElement("section");
    section.className = "smart-timeline-section smart-timeline-section--past";
    const heading = document.createElement("h3");
    heading.className = "smart-timeline-heading";
    heading.textContent = t("projects.detail.smarttimeline.section.past");
    section.appendChild(heading);
    for (const ev of past) section.appendChild(renderRow(ev, opts));
    wrap.appendChild(section);
  }

  const todayRule = document.createElement("div");
  todayRule.className = "smart-timeline-today-rule";
  const todayLabel = document.createElement("span");
  todayLabel.className = "smart-timeline-today-label";
  todayLabel.textContent = `${t("projects.detail.smarttimeline.today")} (${formatDateOnly(todayISO)})`;
  todayRule.appendChild(todayLabel);
  wrap.appendChild(todayRule);

  if (todays.length > 0) {
    const section = document.createElement("section");
    section.className = "smart-timeline-section smart-timeline-section--today";
    for (const ev of todays) section.appendChild(renderRow(ev, opts));
    wrap.appendChild(section);
  }

  if (future.length > 0) {
    const section = document.createElement("section");
    section.className = "smart-timeline-section smart-timeline-section--future";
    const heading = document.createElement("h3");
    heading.className = "smart-timeline-heading";
    heading.textContent = t("projects.detail.smarttimeline.section.future");
    section.appendChild(heading);
    for (const ev of future) section.appendChild(renderRow(ev, opts));
    section.appendChild(renderLookaheadToggle(future, opts));
    wrap.appendChild(section);
  } else {
    const lookaheadHost = renderLookaheadToggle(future, opts);
    if (lookaheadHost.childElementCount > 0) {
      wrap.appendChild(lookaheadHost);
    }
  }

  if (undated.length > 0) {
    const section = document.createElement("section");
    section.className = "smart-timeline-section smart-timeline-section--undated";
    const heading = document.createElement("h3");
    heading.className = "smart-timeline-heading";
    heading.textContent = t("projects.detail.smarttimeline.section.undated");
    section.appendChild(heading);
    for (const ev of undated) section.appendChild(renderRow(ev, opts));
    wrap.appendChild(section);
  }

  return wrap;
}

// renderTrackChip builds the [Track ▼] selector. Options are derived
// from the response's available_tracks header — i18n keys translate
// each option label, with the sub-project title surfacing for CCR
// tracks ("Widerklage — <title>"). Persists the user's selection via
// the host through opts.onTrackChange.
function renderTrackChip(
  availableTracks: string[],
  selected: string,
  opts: RenderOptions,
): HTMLElement {
  const wrap = document.createElement("div");
  wrap.className = "smart-timeline-track-chip";

  const label = document.createElement("label");
  label.className = "smart-timeline-track-chip-label";
  label.textContent = t("projects.detail.smarttimeline.track.label");
  wrap.appendChild(label);

  const select = document.createElement("select");
  select.className = "smart-timeline-track-chip-select";

  const allOpt = document.createElement("option");
  allOpt.value = "all";
  allOpt.textContent = t("projects.detail.smarttimeline.track.both");
  select.appendChild(allOpt);

  for (const trackTag of availableTracks) {
    const opt = document.createElement("option");
    opt.value = trackTag;
    opt.textContent = trackOnlyLabel(trackTag);
    select.appendChild(opt);
  }

  select.value = selected;
  select.addEventListener("change", () => {
    if (opts.onTrackChange) void opts.onTrackChange(select.value);
  });
  wrap.appendChild(select);
  return wrap;
}

// trackClassFor maps a track tag to its CSS modifier so the column
// gets the appropriate visual treatment (lime for parent, light shade
// for counterclaim, faded for parent_context).
function trackClassFor(trackTag: string): string {
  if (trackTag === "parent") return "smart-timeline-track--parent";
  if (trackTag.startsWith("counterclaim:")) return "smart-timeline-track--counterclaim";
  if (trackTag.startsWith("parent_context:")) return "smart-timeline-track--parent-context";
  return "smart-timeline-track--other";
}

// trackHeaderLabel picks the column sub-header. For CCR tracks pulls
// the sub_project_title from the first row in the track so the user
// sees "Widerklage — <child title>". Falls back to a generic label
// when the title is empty.
function trackHeaderLabel(trackTag: string, rows: TimelineEvent[]): string {
  if (trackTag === "parent") {
    return t("projects.detail.smarttimeline.track.header.parent");
  }
  const firstWithTitle = rows.find((r) => r.sub_project_title);
  const subTitle = firstWithTitle?.sub_project_title ?? "";
  if (trackTag.startsWith("counterclaim:")) {
    const base = t("projects.detail.smarttimeline.track.header.counterclaim");
    return subTitle ? `${base} — ${subTitle}` : base;
  }
  if (trackTag.startsWith("parent_context:")) {
    const base = t("projects.detail.smarttimeline.track.header.parent_context");
    return subTitle ? `${base} — ${subTitle}` : base;
  }
  return trackTag;
}

// trackOnlyLabel is the chip dropdown label for "show only this track".
function trackOnlyLabel(trackTag: string): string {
  if (trackTag === "parent") {
    return t("projects.detail.smarttimeline.track.only.parent");
  }
  if (trackTag.startsWith("counterclaim:")) {
    return t("projects.detail.smarttimeline.track.only.counterclaim");
  }
  if (trackTag.startsWith("parent_context:")) {
    return t("projects.detail.smarttimeline.track.only.parent_context");
  }
  return trackTag;
}

function renderLookaheadToggle(
  futureRows: TimelineEvent[],
  opts: RenderOptions,
): HTMLElement {
  const wrap = document.createElement("div");
  wrap.className = "smart-timeline-lookahead";
  const total = opts.projectedTotal ?? 0;
  const projectedShown = futureRows.filter((r) => r.kind === "projected").length;
  const cur = opts.lookahead ?? 7;

  if (total > projectedShown && opts.onLookaheadChange) {
    const more = document.createElement("button");
    more.type = "button";
    more.className = "smart-timeline-lookahead-btn";
    more.textContent = t("projects.detail.smarttimeline.lookahead.more");
    more.setAttribute(
      "aria-label",
      `${t("projects.detail.smarttimeline.lookahead.more")} (${total - projectedShown})`,
    );
    more.addEventListener("click", () => {
      const next = Math.min(50, cur + 7);
      void opts.onLookaheadChange?.(next);
    });
    wrap.appendChild(more);
  }
  if (cur > 7 && opts.onLookaheadChange) {
    const less = document.createElement("button");
    less.type = "button";
    less.className = "smart-timeline-lookahead-btn smart-timeline-lookahead-btn--less";
    less.textContent = t("projects.detail.smarttimeline.lookahead.less");
    less.addEventListener("click", () => {
      void opts.onLookaheadChange?.(7);
    });
    wrap.appendChild(less);
  }
  return wrap;
}

function renderRow(ev: TimelineEvent, opts: RenderOptions): HTMLElement {
  const li = document.createElement("article");
  li.className =
    `smart-timeline-row smart-timeline-row--${ev.kind} ` +
    `smart-timeline-row--${ev.status}`;
  if (ev.deadline_rule_party) {
    li.classList.add(`smart-timeline-row--party-${ev.deadline_rule_party}`);
  }

  const dateCol = document.createElement("div");
  dateCol.className = "smart-timeline-date";
  dateCol.textContent = ev.date ? formatDateOnly(dateOnlyISO(ev.date) ?? "") : "—";
  li.appendChild(dateCol);

  const body = document.createElement("div");
  body.className = "smart-timeline-body";

  const head = document.createElement("div");
  head.className = "smart-timeline-row-head";

  const icon = document.createElement("span");
  icon.className = "smart-timeline-status-icon";
  icon.textContent = statusGlyph(ev.status);
  icon.setAttribute("aria-label", t(statusKey(ev.status)));
  head.appendChild(icon);

  const titleEl = document.createElement("span");
  titleEl.className = "smart-timeline-title";
  const href = deepLinkHref(ev);
  if (href) {
    const a = document.createElement("a");
    a.className = "smart-timeline-link";
    a.href = href;
    a.textContent = ev.title;
    titleEl.appendChild(a);
  } else {
    titleEl.textContent = ev.title;
  }
  head.appendChild(titleEl);

  const kindChip = document.createElement("span");
  kindChip.className = `smart-timeline-kind-chip smart-timeline-kind-chip--${ev.kind}`;
  kindChip.textContent = t(kindKey(ev.kind));
  head.appendChild(kindChip);

  if (ev.rule_code) {
    const ruleChip = document.createElement("span");
    ruleChip.className = "smart-timeline-rule-chip";
    ruleChip.textContent = ev.rule_code;
    head.appendChild(ruleChip);
  }

  // "voraussichtlich" / "vom Gericht" / "überfällig" status pill on
  // projected rows so the user reads the row's nature at a glance.
  if (ev.kind === "projected") {
    const statusPill = document.createElement("span");
    statusPill.className = `smart-timeline-status-pill smart-timeline-status-pill--${ev.status}`;
    statusPill.textContent = t(statusKey(ev.status));
    head.appendChild(statusPill);
  }

  body.appendChild(head);

  if (ev.description) {
    const desc = document.createElement("div");
    desc.className = "smart-timeline-desc";
    desc.textContent = ev.description;
    body.appendChild(desc);
  }

  // Depends-on footer (#31 layer 2) — surface the parent rule + its
  // date right under the title so the user reads the dependency at a
  // glance. "[Pfad anzeigen]" expands the full chain on demand.
  if (ev.depends_on_rule_code) {
    body.appendChild(renderDependsOn(ev));
  }

  // Click-to-anchor affordance (Slice 2 §6.2) — projected rows expose
  // "[Datum setzen]" inline editor; actuals from rules expose a
  // "[Datum ändern]" variant that PATCHes via the same endpoint.
  if (ev.kind === "projected" && ev.deadline_rule_id && opts.projectId) {
    body.appendChild(renderAnchorAction(ev, opts));
  }

  li.appendChild(body);

  // Row-level navigation — same pattern as .entity-event (t-paliad-103).
  if (href) {
    li.classList.add("smart-timeline-row--clickable");
    li.addEventListener("click", (e) => {
      const target = e.target as HTMLElement;
      if (target.closest("a") || target.closest("button") || target.closest("input")) return;
      window.location.href = href;
    });
  }

  return li;
}

function renderDependsOn(ev: TimelineEvent): HTMLElement {
  const wrap = document.createElement("div");
  wrap.className = "smart-timeline-depends-on";
  const code = ev.depends_on_rule_code ?? "";
  const name = ev.depends_on_rule_name || code;
  const dateText = ev.depends_on_date
    ? formatDateOnly(dateOnlyISO(ev.depends_on_date) ?? "")
    : t("projects.detail.smarttimeline.depends_on.date_open");
  const prefix = t("projects.detail.smarttimeline.depends_on.prefix");
  const txt = document.createElement("span");
  txt.textContent = `${prefix}: ${name} (${code}, ${dateText})`;
  wrap.appendChild(txt);

  const expand = document.createElement("button");
  expand.type = "button";
  expand.className = "smart-timeline-depends-on-expand";
  expand.textContent = t("projects.detail.smarttimeline.depends_on.show_path");
  expand.addEventListener("click", () => {
    if (wrap.classList.contains("smart-timeline-depends-on--expanded")) {
      wrap.classList.remove("smart-timeline-depends-on--expanded");
      const list = wrap.querySelector(".smart-timeline-depends-on-path");
      if (list) list.remove();
      expand.textContent = t("projects.detail.smarttimeline.depends_on.show_path");
      return;
    }
    wrap.classList.add("smart-timeline-depends-on--expanded");
    const list = document.createElement("div");
    list.className = "smart-timeline-depends-on-path";
    // The walked chain isn't pre-computed server-side beyond the
    // immediate parent; the backend annotation gives one hop. Future
    // slice can deepen this — for v1 we surface the immediate parent
    // (already in the prefix line) and a hint that the user can click
    // the parent's row to see its own dependency.
    const hint = document.createElement("span");
    hint.className = "smart-timeline-depends-on-hint";
    hint.textContent = t("projects.detail.smarttimeline.depends_on.path_hint");
    list.appendChild(hint);
    wrap.appendChild(list);
    expand.textContent = t("projects.detail.smarttimeline.depends_on.hide_path");
  });
  wrap.appendChild(expand);
  return wrap;
}

function renderAnchorAction(ev: TimelineEvent, opts: RenderOptions): HTMLElement {
  const wrap = document.createElement("div");
  wrap.className = "smart-timeline-anchor";

  const trigger = document.createElement("button");
  trigger.type = "button";
  trigger.className = "smart-timeline-anchor-btn";
  trigger.textContent = t("projects.detail.smarttimeline.anchor.set");
  wrap.appendChild(trigger);

  trigger.addEventListener("click", () => {
    if (wrap.classList.contains("smart-timeline-anchor--editing")) return;
    wrap.classList.add("smart-timeline-anchor--editing");
    trigger.style.display = "none";
    wrap.appendChild(buildAnchorEditor(ev, opts, wrap));
  });

  return wrap;
}

function buildAnchorEditor(
  ev: TimelineEvent,
  opts: RenderOptions,
  wrap: HTMLElement,
): HTMLElement {
  const editor = document.createElement("form");
  editor.className = "smart-timeline-anchor-form";
  editor.setAttribute("aria-label", t("projects.detail.smarttimeline.anchor.set"));
  editor.addEventListener("submit", (e) => e.preventDefault());

  const dateInput = document.createElement("input");
  dateInput.type = "date";
  dateInput.className = "smart-timeline-anchor-date";
  dateInput.required = true;
  if (ev.date) dateInput.value = dateOnlyISO(ev.date) ?? "";
  editor.appendChild(dateInput);

  const submit = document.createElement("button");
  submit.type = "submit";
  submit.className = "smart-timeline-anchor-submit";
  submit.textContent = t("projects.detail.smarttimeline.anchor.save");
  editor.appendChild(submit);

  const cancel = document.createElement("button");
  cancel.type = "button";
  cancel.className = "smart-timeline-anchor-cancel";
  cancel.textContent = t("projects.detail.smarttimeline.anchor.cancel");
  cancel.addEventListener("click", () => {
    wrap.innerHTML = "";
    const trig = document.createElement("button");
    trig.type = "button";
    trig.className = "smart-timeline-anchor-btn";
    trig.textContent = t("projects.detail.smarttimeline.anchor.set");
    wrap.classList.remove("smart-timeline-anchor--editing");
    wrap.appendChild(trig);
    trig.addEventListener("click", () => {
      wrap.innerHTML = "";
      wrap.appendChild(buildAnchorEditor(ev, opts, wrap));
      wrap.classList.add("smart-timeline-anchor--editing");
    });
  });
  editor.appendChild(cancel);

  const msg = document.createElement("div");
  msg.className = "smart-timeline-anchor-msg";
  editor.appendChild(msg);

  editor.addEventListener("submit", async () => {
    if (!opts.projectId) return;
    if (!ev.rule_code) return;
    const date = dateInput.value;
    if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
      msg.textContent = t("projects.detail.smarttimeline.anchor.invalid_date");
      msg.classList.add("smart-timeline-anchor-msg--error");
      return;
    }
    submit.disabled = true;
    cancel.disabled = true;
    msg.classList.remove("smart-timeline-anchor-msg--error");
    msg.textContent = t("projects.detail.smarttimeline.anchor.saving");
    try {
      const resp = await fetch(
        `/api/projects/${encodeURIComponent(opts.projectId)}/timeline/anchor`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            rule_code: ev.rule_code,
            actual_date: date,
          }),
        },
      );
      if (resp.ok) {
        msg.textContent = t("projects.detail.smarttimeline.anchor.saved");
        if (opts.onChange) await opts.onChange();
        return;
      }
      if (resp.status === 409) {
        const payload = (await resp.json()) as PredecessorMissingPayload;
        renderPredecessorError(msg, payload, ev, opts, dateInput, submit, cancel);
        return;
      }
      msg.textContent = t("projects.detail.smarttimeline.anchor.error");
      msg.classList.add("smart-timeline-anchor-msg--error");
    } catch {
      msg.textContent = t("projects.detail.smarttimeline.anchor.error");
      msg.classList.add("smart-timeline-anchor-msg--error");
    } finally {
      submit.disabled = false;
      cancel.disabled = false;
    }
  });

  return editor;
}

function renderPredecessorError(
  msg: HTMLElement,
  payload: PredecessorMissingPayload,
  _ev: TimelineEvent,
  opts: RenderOptions,
  _dateInput: HTMLInputElement,
  _submit: HTMLButtonElement,
  _cancel: HTMLButtonElement,
): void {
  msg.innerHTML = "";
  msg.classList.add("smart-timeline-anchor-msg--error");
  msg.classList.add("smart-timeline-anchor-msg--predecessor");

  const lang = (opts.lang ?? getLang()) === "en" ? "en" : "de";
  const message = lang === "en" ? payload.message_en : payload.message_de;
  const main = document.createElement("p");
  main.textContent = message;
  msg.appendChild(main);

  // "Stattdessen <predecessor> erfassen" — pre-fills the editor for
  // the missing parent rule, scrolls to its row if present, falls back
  // to a fresh editor in-place.
  const link = document.createElement("button");
  link.type = "button";
  link.className = "smart-timeline-anchor-predecessor-link";
  const predName =
    lang === "en" ? payload.missing_rule_name_en : payload.missing_rule_name_de;
  link.textContent =
    lang === "en"
      ? `Anchor „${predName}“ instead`
      : `Stattdessen „${predName}“ erfassen`;
  link.addEventListener("click", () => {
    // Find the projected row for missing_rule_code and scroll into view;
    // the row's own [Datum setzen] button takes it from there.
    const targetRow = findRowForRuleCode(payload.missing_rule_code);
    if (targetRow) {
      targetRow.scrollIntoView({ behavior: "smooth", block: "center" });
      const btn = targetRow.querySelector<HTMLButtonElement>(
        ".smart-timeline-anchor-btn",
      );
      if (btn) btn.click();
    }
  });
  msg.appendChild(link);
}

function findRowForRuleCode(ruleCode: string): HTMLElement | null {
  const rows = document.querySelectorAll<HTMLElement>(".smart-timeline-row");
  for (const r of Array.from(rows)) {
    const chip = r.querySelector(".smart-timeline-rule-chip");
    if (chip && chip.textContent === ruleCode) return r;
  }
  return null;
}

function deepLinkHref(ev: TimelineEvent): string | null {
  if (ev.kind === "deadline" && ev.deadline_id) {
    return `/deadlines/${ev.deadline_id}`;
  }
  if (ev.kind === "appointment" && ev.appointment_id) {
    return `/appointments/${ev.appointment_id}`;
  }
  return null;
}

function statusGlyph(status: TimelineEvent["status"]): string {
  switch (status) {
    case "done":              return "✓";
    case "open":              return "…";
    case "overdue":           return "!";
    case "court_set":         return "▢";
    case "predicted":         return "░";
    case "predicted_overdue": return "░!";
    case "off_script":        return "⊕";
    default:                  return "·";
  }
}

function statusKey(status: TimelineEvent["status"]) {
  return `projects.detail.smarttimeline.status.${status}` as const;
}

function kindKey(kind: TimelineEvent["kind"]) {
  return `projects.detail.smarttimeline.kind.${kind}` as const;
}

function dateOnlyISO(raw: string | null | undefined): string | null {
  if (!raw) return null;
  if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return raw;
  const d = new Date(raw);
  if (isNaN(d.getTime())) return null;
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${y}-${m}-${day}`;
}

function todayLocalISO(): string {
  const d = new Date();
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${y}-${m}-${day}`;
}

function byDateAsc(a: TimelineEvent, b: TimelineEvent): number {
  const ai = dateOnlyISO(a.date) ?? "";
  const bi = dateOnlyISO(b.date) ?? "";
  if (ai === bi) return a.title.localeCompare(b.title);
  return ai < bi ? -1 : 1;
}

function formatDateOnly(iso: string): string {
  if (!iso) return "—";
  const parts = iso.split("-");
  if (parts.length !== 3) return iso;
  const d = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
  if (isNaN(d.getTime())) return iso;
  return d.toLocaleDateString(getLang() === "de" ? "de-DE" : "en-GB", {
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  });
}
