import { initI18n, onLangChange, t, tDyn, getLang, translateEvent } from "./i18n";
import { initSidebar } from "./sidebar";
import { initNotes } from "./notes";
import { initProjectTree, refreshProjectTree, rerenderProjectTree } from "./project-tree";
import {
  loadParentCandidates,
  initParentPicker,
  wireTypeChange,
  prefillForm,
  readPayload,
} from "./project-form";
import { mountFilterBar, type BarHandle } from "./filter-bar";
import type { FilterSpec, RenderSpec } from "./views/types";
import { renderSmartTimeline, type TimelineEvent as SmartTimelineEvent, type LaneInfo as SmartTimelineLane } from "./views/shape-timeline";
import { loadAndRenderSubmissions } from "./submissions";

interface Project {
  id: string;
  type: string;
  parent_id?: string | null;
  path: string;
  title: string;
  reference?: string | null;
  description?: string | null;
  status: string;
  client_number?: string | null;
  matter_number?: string | null;
  billing_reference?: string | null;
  netdocuments_url?: string | null;
  industry?: string | null;
  country?: string | null;
  patent_number?: string | null;
  filing_date?: string | null;
  grant_date?: string | null;
  court?: string | null;
  case_number?: string | null;
  updated_at: string;
  created_at: string;
}

interface ProjectTeamMember {
  id: string;
  project_id: string;
  user_id: string;
  // t-paliad-148: per-project responsibility (lead/member/observer/external).
  // The legacy .role field is still set by the server during the
  // deprecation window but the UI ignores it for new code.
  responsibility: string;
  role: string;
  inherited: boolean;
  user_email: string;
  user_display_name: string;
  user_office: string;
  // user_profession is the structured firm tier (partner/of_counsel/…/
  // paralegal). NULL means external collaborator. Read-only here — the
  // value is set on the user's firm profile, not at staffing time.
  user_profession?: string | null;
  inherited_from_id?: string | null;
  inherited_from_title?: string | null;
}

// t-paliad-139 — derived team member from a partner-unit attachment.
// One DerivedMember per user; users in multiple attached units carry one
// DerivedMembership per (unit, role) pair so the Herkunft column can list
// every source (t-paliad-143).
interface DerivedMembership {
  unit_id: string;
  unit_name: string;
  unit_role: string;
}

interface DerivedMember {
  user_id: string;
  user_email: string;
  user_display_name: string;
  user_office: string;
  memberships: DerivedMembership[];
  derive_grants_authority: boolean;
}

// t-paliad-139 — partner unit attached to this project.
interface AttachedUnit {
  project_id: string;
  partner_unit_id: string;
  unit_name: string;
  derive_unit_roles: string[];
  derive_grants_authority: boolean;
  derived_member_count: number;
}

interface ProjectMini {
  id: string;
  type: string;
  title: string;
  reference?: string | null;
  status: string;
}

interface Party {
  id: string;
  project_id: string;
  name: string;
  role?: string;
  representative?: string;
}

interface ProjectEvent {
  id: string;
  project_id: string;
  event_type?: string;
  title: string;
  description?: string;
  created_at: string;
  created_by?: string;
  metadata?: Record<string, unknown>;
  // Populated only when the response was joined to paliad.projects (Verlauf
  // subtree-aggregating queries on /projects/{id}, t-paliad-139). Used to
  // render the attribution chip when the event lives on a descendant.
  project_title?: string;
}

interface Deadline {
  id: string;
  project_id: string;
  title: string;
  due_date: string;
  status: string;
  rule_id?: string;
  rule_code?: string;
  // Populated by the union endpoint (/api/events) which is what the project
  // detail page calls — used for attribution when the row lives on a
  // descendant project (t-paliad-139).
  project_title?: string;
}

interface Appointment {
  id: string;
  project_id?: string;
  title: string;
  start_at: string;
  end_at?: string;
  location?: string;
  appointment_type?: string;
  project_title?: string;
}

interface Me {
  id: string;
  job_title: string | null;
  global_role: string;
  office: string;
}

type TabId =
  | "history"
  | "team"
  | "children"
  | "parties"
  | "deadlines"
  | "appointments"
  | "notes"
  | "checklists"
  | "submissions";

const VALID_TABS: TabId[] = [
  "history",
  "team",
  "children",
  "parties",
  "deadlines",
  "appointments",
  "notes",
  "checklists",
  "submissions",
];

// Legacy German tab slugs that may appear in bookmarked URLs after the
// rename. Mapped to their English successors so old links still land on the
// right tab instead of silently falling back to "history".
const LEGACY_TAB_ALIASES: Record<string, TabId> = {
  verlauf: "history",
  kinder: "children",
  parteien: "parties",
  fristen: "deadlines",
  termine: "appointments",
  notizen: "notes",
  checklisten: "checklists",
};

interface ChecklistInstanceSummary {
  id: string;
  template_slug: string;
  name: string;
  state: Record<string, boolean>;
  created_at: string;
}

interface ChecklistTemplateSummary {
  slug: string;
  titleDE: string;
  titleEN: string;
  itemCount: number;
}

let checklistInstances: ChecklistInstanceSummary[] = [];
let checklistTemplates: Record<string, ChecklistTemplateSummary> = {};

let project: Project | null = null;
let me: Me | null = null;
let parties: Party[] = [];
let events: ProjectEvent[] = [];
let deadlines: Deadline[] = [];
let appointments: Appointment[] = [];
let ancestors: ProjectMini[] = [];
let children: ProjectMini[] = [];
// projects-cards' /projects tree owns rendering inside the Projektbaum
// tab too — see initProjectTreeTab below. Local children[] still feeds
// the empty-state gate + the create-link parent_id pre-fill.
let teamMembers: ProjectTeamMember[] = [];
// t-paliad-139 — additional Team-tab sections.
let descendantStaffed: ProjectTeamMember[] = [];
let derivedMembers: DerivedMember[] = [];
let attachedUnits: AttachedUnit[] = [];
let allUnits: { id: string; name: string; office: string }[] = [];
let userOptions: { id: string; display_name: string; email: string; profession?: string }[] = [];

const EVENTS_PAGE_SIZE = 50;
let eventsHasMore = false;
let eventsLoadingMore = false;

// SmartTimeline (t-paliad-171 / t-paliad-173) — row set + audit-toggle
// + Slice 2 lookahead state. timelineRows is what we render; the count
// of future-projected rows the backend knows about is held separately
// in timelineProjectedTotal so "Mehr anzeigen" can be shown when the
// cap clipped some rows.
let timelineRows: SmartTimelineEvent[] = [];
let timelineAuditFull = parseAuditFullPersisted();
let timelineLookahead = 7; // backend default; overridden from localStorage
let timelineProjectedTotal = 0;

// Slice 3 — counterclaim parallel tracks. timelineAvailableTracks is
// parsed from the X-Projection-Tracks response header; selectedTrack
// is the user's [Track ▼] choice (default "all" → render every track).
let timelineAvailableTracks: string[] = [];
let timelineSelectedTrack = "all";

// Slice 4 — parent-node lane aggregation (t-paliad-175). Lanes come
// from the response envelope's .lanes array. selectedLanes is the
// user's lane-filter state — null = "all selected" (the default);
// set explicitly when the user toggles a chip.
let timelineLanes: SmartTimelineLane[] = [];
let timelineSelectedLanes: string[] | null = null;

// Slice 4 — Client-level "Timeline-Ansicht" toggle. At Client-level
// project pages, the Verlauf tab defaults to the matter-list rendering
// (project tree); flipping the toggle swaps to the SmartTimeline lane
// view. State persists in localStorage per project so navigating away
// and back keeps the user's choice.
let timelineClientShowLanes = false;

// t-paliad-170 / t-paliad-176 — Verlauf FilterBar state.
//
// The bar mounts once, owns the URL params (?time=, ?pe_kind=, ?tl_status=,
// ?tl_track=, …), and drives a client-side filter pass over `timelineRows`
// before render. The SmartTimeline endpoint has no built-in predicate for
// timeline_status / timeline_track / project_event_kind axes — they sit on
// BarState only — so we filter rendered rows in `applyTimelineRowFilters`
// rather than re-fetching on every chip click. The customRunner drains the
// bar's state into `verlaufFilters` and triggers a re-render via onResult.
let verlaufBar: BarHandle | null = null;
interface VerlaufFilters {
  // project_event_kind chip — values from KnownProjectEventKinds (see
  // internal/services/filter_spec.go). Only filters rows whose underlying
  // project_events.event_type is non-empty (deadline / appointment /
  // projected rows pass through unaffected — they have no event_type).
  eventKinds?: Set<string>;
  // timeline_status chip — matches TimelineEvent.status verbatim
  // (done | open | overdue | predicted | predicted_overdue | court_set | off_script).
  timelineStatuses?: Set<string>;
  // timeline_track chip — chip values are "parent" / "counterclaim" /
  // "off_script" but row.track may carry suffixed forms like
  // "counterclaim:<id>" or "parent_context:<id>". Filtering normalises
  // by matching the chip's prefix against the row's track tag.
  timelineTracks?: Set<string>;
  // Bounds are inclusive lower / exclusive upper, matching
  // computeViewSpecBounds in internal/services/view_service.go so the
  // semantics align when this surface eventually moves to the substrate.
  fromDate?: Date;
  toDate?: Date;
}
let verlaufFilters: VerlaufFilters = {};

// applyTimelineRowFilters narrows the SmartTimeline rows to whatever
// the FilterBar's BarState declares. Empty filter → identity passthrough.
// Called from renderTimeline immediately before handing rows to
// renderSmartTimeline (single-column or lane-strip alike).
function applyTimelineRowFilters(rows: SmartTimelineEvent[]): SmartTimelineEvent[] {
  const f = verlaufFilters;
  if (
    !f.eventKinds &&
    !f.timelineStatuses &&
    !f.timelineTracks &&
    !f.fromDate &&
    !f.toDate
  ) {
    return rows;
  }
  return rows.filter((r) => {
    // project_event_kind narrows project_events specifically: deadline /
    // appointment / projected rows pass through unaffected (they carry no
    // project_event_type). A milestone whose project_event_type isn't in
    // the picked subset drops out.
    if (f.eventKinds && r.project_event_type) {
      if (!f.eventKinds.has(r.project_event_type)) return false;
    }
    if (f.timelineStatuses && !f.timelineStatuses.has(r.status)) return false;
    if (f.timelineTracks && !timelineTrackChipMatches(r.track, f.timelineTracks)) return false;
    if (f.fromDate || f.toDate) {
      // Undated rows (court-set decisions, counterclaim-pending) escape
      // the time horizon — same convention as the renderer's "Datum offen"
      // bucket. Otherwise compare the row's date against the bounds.
      if (r.date) {
        const d = new Date(r.date);
        if (f.fromDate && d < f.fromDate) return false;
        if (f.toDate && d >= f.toDate) return false;
      }
    }
    return true;
  });
}

// timelineTrackChipMatches normalises the chip vocabulary against the
// row's track tag — chip "counterclaim" matches both "counterclaim" and
// "counterclaim:<id>"; chip "parent" matches "parent" only (NOT
// "parent_context:<id>", which is a CCR-child-viewing-parent overlay).
function timelineTrackChipMatches(rowTrack: string, chips: Set<string>): boolean {
  const tag = rowTrack || "parent";
  if (chips.has(tag)) return true;
  for (const chip of chips) {
    if (chip === "counterclaim" && tag.startsWith("counterclaim:")) return true;
  }
  return false;
}

// applyVerlaufFilters narrows the legacy /api/projects/{id}/events
// response to the bar's filter state. The render path no longer reads
// this `events` array (the SmartTimeline took over), but loadEvents +
// loadMoreEvents still call it so the cursor pagination state stays
// consistent for any future re-introduction. Keeps the project_event_kind
// + time-horizon filter intact; the SmartTimeline-only axes don't apply
// to the legacy ProjectEvent shape.
function applyVerlaufFilters(rows: ProjectEvent[]): ProjectEvent[] {
  const f = verlaufFilters;
  if (!f.eventKinds && !f.fromDate && !f.toDate) return rows;
  return rows.filter((r) => {
    if (f.eventKinds && !f.eventKinds.has(r.event_type ?? "")) return false;
    const created = new Date(r.created_at);
    if (f.fromDate && created < f.fromDate) return false;
    if (f.toDate && created >= f.toDate) return false;
    return true;
  });
}

// horizonBounds mirrors computeViewSpecBounds in view_service.go for the
// horizons that show up on the Verlauf bar. Forward-looking horizons
// (next_*) are absent on this surface — the timePresets override hides
// them — but the function tolerates them for forward-compatibility with
// the SmartTimeline redesign.
function horizonBounds(horizon: string): { from?: Date; to?: Date } {
  const now = new Date();
  const day = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
  const offset = (days: number) => new Date(day.getTime() + days * 86400000);
  switch (horizon) {
    case "past_7d":  return { from: offset(-7),  to: offset(1) };
    case "past_30d": return { from: offset(-30), to: offset(1) };
    case "past_90d": return { from: offset(-90), to: offset(1) };
    case "next_7d":  return { from: day,         to: offset(7) };
    case "next_30d": return { from: day,         to: offset(30) };
    case "next_90d": return { from: day,         to: offset(90) };
    default:         return {};
  }
}

// Subtree aggregation mode (t-paliad-139). Default true → Fristen, Termine,
// Verlauf show rows from this project AND all descendant projects with an
// attribution chip per non-direct row. URL param `?subtree=false` flips to
// narrow (this project's own rows only).
let subtreeMode: boolean = true;

function parseSubtreeMode(): boolean {
  try {
    const raw = new URLSearchParams(window.location.search).get("subtree");
    return raw !== "false";
  } catch {
    return true;
  }
}

function persistSubtreeMode() {
  try {
    const url = new URL(window.location.href);
    if (subtreeMode) {
      url.searchParams.delete("subtree");
    } else {
      url.searchParams.set("subtree", "false");
    }
    window.history.replaceState({}, "", url.toString());
  } catch {
    // ignore
  }
}

function parseProjectID(): string | null {
  const parts = window.location.pathname.split("/").filter(Boolean);
  if (parts[0] !== "projects" || !parts[1]) return null;
  return parts[1];
}

function parseTab(): TabId {
  const parts = window.location.pathname.split("/").filter(Boolean);
  const candidate = parts[2];
  if (!candidate) return "history";
  if ((VALID_TABS as string[]).includes(candidate)) return candidate as TabId;
  if (LEGACY_TAB_ALIASES[candidate]) return LEGACY_TAB_ALIASES[candidate];
  return "history";
}

async function loadMe() {
  try {
    const resp = await fetch("/api/me");
    if (resp.ok) me = await resp.json();
  } catch {
    /* optional */
  }
}

async function loadProject(id: string): Promise<boolean> {
  try {
    const resp = await fetch(`/api/projects/${id}`);
    if (!resp.ok) return false;
    project = await resp.json();
    return true;
  } catch {
    return false;
  }
}

async function loadParties(id: string) {
  try {
    const resp = await fetch(`/api/projects/${id}/parties`);
    if (resp.ok) parties = (await resp.json()) ?? [];
  } catch {
    parties = [];
  }
}

// Build a query string suffix conveying the current subtree mode. The
// backend defaults to subtree (direct_only=false), so we only emit the
// param when the user has flipped to direct.
function subtreeParam(): string {
  return subtreeMode ? "" : "&direct_only=true";
}

// rawEventsCursor tracks the last *raw* (pre-filter) event ID returned by
// the legacy endpoint so cursor pagination keeps working when filters
// drop most rows from a page. Without it, "Mehr laden" with a tight
// filter could stall because events[] (post-filter) wouldn't reach back
// to the actual pagination boundary.
let rawEventsLastID: string | null = null;
let rawEventsLastPageFull = false;

async function loadEvents(id: string) {
  try {
    const resp = await fetch(
      `/api/projects/${id}/events?limit=${EVENTS_PAGE_SIZE}${subtreeParam()}`,
    );
    if (resp.ok) {
      const raw: ProjectEvent[] = (await resp.json()) ?? [];
      rawEventsLastID = raw.length ? raw[raw.length - 1].id : null;
      rawEventsLastPageFull = raw.length === EVENTS_PAGE_SIZE;
      events = applyVerlaufFilters(raw);
      eventsHasMore = rawEventsLastPageFull;
    } else {
      events = [];
      rawEventsLastID = null;
      rawEventsLastPageFull = false;
      eventsHasMore = false;
    }
  } catch {
    events = [];
    rawEventsLastID = null;
    rawEventsLastPageFull = false;
    eventsHasMore = false;
  }
}

// SmartTimeline (t-paliad-171) — fetches the merged timeline from the
// new /api/projects/{id}/timeline endpoint. Slice 1 returns actuals
// (deadlines + appointments + opted-in project_events); future slices
// add projected rows additively. The audit-full toggle broadens the
// project_events filter to include rows without timeline_kind set.
async function loadTimeline(id: string): Promise<void> {
  const params = new URLSearchParams();
  if (timelineAuditFull) params.set("include", "audit_full");
  if (!subtreeMode) params.set("direct_only", "true");
  if (timelineLookahead && timelineLookahead !== 7) {
    params.set("lookahead", String(timelineLookahead));
  }
  const qs = params.toString();
  const url = `/api/projects/${encodeURIComponent(id)}/timeline${qs ? "?" + qs : ""}`;
  try {
    const resp = await fetch(url);
    if (resp.ok) {
      // Slice 4 (t-paliad-175) — wire shape changed from
      // []TimelineEvent to envelope {events, lanes} so lane metadata
      // can ride alongside the rows. Defensive parse: tolerate both
      // shapes during the rolling deploy window (any cached older
      // backend response is treated as events-only).
      const body = await resp.json();
      if (Array.isArray(body)) {
        timelineRows = body;
        timelineLanes = [];
      } else {
        timelineRows = (body?.events ?? []) as SmartTimelineEvent[];
        timelineLanes = (body?.lanes ?? []) as SmartTimelineLane[];
      }
      // Pull projection meta from headers (Slice 2). When absent (e.g.
      // proxy strips them), fall back to the visible projected count
      // so "Mehr anzeigen" stays hidden — defensible default.
      const totalHdr = resp.headers.get("X-Projection-Total");
      timelineProjectedTotal = totalHdr ? parseInt(totalHdr, 10) || 0 : 0;
      const lookaheadHdr = resp.headers.get("X-Projection-Lookahead");
      if (lookaheadHdr) {
        const n = parseInt(lookaheadHdr, 10);
        if (!isNaN(n) && n > 0) timelineLookahead = n;
      }
      // Slice 3 — track list comes back as comma-separated tags.
      const tracksHdr = resp.headers.get("X-Projection-Tracks");
      timelineAvailableTracks = tracksHdr
        ? tracksHdr.split(",").map((s) => s.trim()).filter((s) => s.length > 0)
        : ["parent"];
      // Drop a previously-selected track if it disappeared from the
      // response (e.g. CCR child was deleted between renders) — fall
      // back to "all" so the user doesn't get an empty pane.
      if (timelineSelectedTrack !== "all" && !timelineAvailableTracks.includes(timelineSelectedTrack)) {
        timelineSelectedTrack = "all";
      }
      // Drop selected lanes that disappeared between renders (e.g. a
      // child case was deleted). null sentinel means "all" so leave it.
      if (timelineSelectedLanes !== null) {
        const laneIds = new Set(timelineLanes.map((l) => l.id));
        timelineSelectedLanes = timelineSelectedLanes.filter((id) => laneIds.has(id));
        if (timelineSelectedLanes.length === 0) {
          timelineSelectedLanes = null;
        }
      }
    } else {
      timelineRows = [];
      timelineProjectedTotal = 0;
      timelineAvailableTracks = [];
      timelineLanes = [];
    }
  } catch {
    timelineRows = [];
    timelineProjectedTotal = 0;
    timelineAvailableTracks = [];
    timelineLanes = [];
  }
}

function renderTimeline() {
  const host = document.getElementById("project-smart-timeline");
  if (!host) return;
  const projectId = project?.id;

  // Slice 4 — Client-level Timeline-Ansicht toggle. At Client-level
  // pages, the Verlauf default is the matter-list (project tree).
  // Flipping the toggle swaps to the SmartTimeline lane view.
  if (project?.type === "client" && !timelineClientShowLanes) {
    renderClientMatterList(host);
    return;
  }

  // t-paliad-176 — apply FilterBar predicates client-side. The
  // SmartTimeline endpoint returns the unfiltered superset; the bar's
  // BarState (timeline_status / timeline_track / project_event_kind /
  // time horizon) narrows what we render. Empty filter → identity.
  const filteredRows = applyTimelineRowFilters(timelineRows);

  renderSmartTimeline(host, filteredRows, {
    projectId,
    lang: getLang() === "en" ? "en" : "de",
    lookahead: timelineLookahead,
    projectedTotal: timelineProjectedTotal,
    availableTracks: timelineAvailableTracks,
    selectedTrack: timelineSelectedTrack,
    lanes: timelineLanes,
    selectedLanes: timelineSelectedLanes ?? undefined,
    onLaneFilterChange: async (next) => {
      // Persist the explicit selection so a re-fetch doesn't reset it.
      // Empty array = user unchecked everything → fall back to "all"
      // so we never render a blank pane.
      timelineSelectedLanes = next.length === 0 ? null : next;
      renderTimeline();
    },
    onTrackChange: async (next) => {
      timelineSelectedTrack = next;
      // Track filter is purely client-side (rows are already loaded);
      // re-render in place without a re-fetch.
      renderTimeline();
    },
    onChange: async () => {
      if (!projectId) return;
      await loadTimeline(projectId);
      renderTimeline();
    },
    onLookaheadChange: async (next) => {
      if (!projectId) return;
      timelineLookahead = next;
      writeLookaheadPersisted(next);
      await loadTimeline(projectId);
      renderTimeline();
    },
  });
}

// renderClientMatterList renders the Client-level default Verlauf view
// — a simple list of direct child litigations with their reference and
// status. This stands in for the existing project-tree component when
// Timeline-Ansicht is OFF (the default at Client level per design §5.1
// + Q12). User can flip the Timeline-Ansicht toggle to see the lane
// SmartTimeline.
function renderClientMatterList(host: HTMLElement) {
  host.innerHTML = "";
  host.classList.add("smart-timeline");

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

  const heading = document.createElement("h3");
  heading.className = "smart-timeline-matter-list-heading";
  heading.textContent = t("projects.detail.smarttimeline.client.matter_list.heading");
  wrap.appendChild(heading);

  const hint = document.createElement("p");
  hint.className = "form-hint";
  hint.textContent = t("projects.detail.smarttimeline.client.matter_list.hint");
  wrap.appendChild(hint);

  // The lane info from the backend already contains the direct child
  // litigations (one entry per child). When empty, the message guides
  // the user to add a litigation first.
  if (timelineLanes.length === 0) {
    const empty = document.createElement("p");
    empty.className = "entity-events-empty";
    empty.textContent = t("projects.detail.smarttimeline.client.matter_list.empty");
    wrap.appendChild(empty);
    host.appendChild(wrap);
    return;
  }

  const list = document.createElement("ul");
  list.className = "smart-timeline-matter-list-items";
  for (const lane of timelineLanes) {
    const li = document.createElement("li");
    li.className = "smart-timeline-matter-list-item";
    if (lane.project_id) {
      const link = document.createElement("a");
      link.href = `/projects/${encodeURIComponent(lane.project_id)}`;
      link.textContent = lane.label;
      li.appendChild(link);
    } else {
      li.textContent = lane.label;
    }
    list.appendChild(li);
  }
  wrap.appendChild(list);
  host.appendChild(wrap);
}

function lookaheadStorageKey(): string {
  const id = project?.id ?? "_";
  return `paliad.smarttimeline.lookahead.${id}`;
}

function writeLookaheadPersisted(n: number) {
  try {
    if (n === 7) localStorage.removeItem(lookaheadStorageKey());
    else localStorage.setItem(lookaheadStorageKey(), String(n));
  } catch {
    // ignore
  }
}

function readLookaheadPersisted(): number {
  try {
    const raw = localStorage.getItem(lookaheadStorageKey());
    if (!raw) return 7;
    const n = parseInt(raw, 10);
    if (isNaN(n) || n < 1 || n > 50) return 7;
    return n;
  } catch {
    return 7;
  }
}

// Audit-full toggle persistence: per-project flag in localStorage so a
// user who flips the legacy view on for one project doesn't see the
// audit clutter on every other project they open.
function auditFullStorageKey(): string {
  const id = project?.id ?? "_";
  return `paliad.smarttimeline.audit_full.${id}`;
}

function parseAuditFullPersisted(): boolean {
  // Project ID isn't known yet at module init; fall back to false here
  // and re-read in initSmartTimelineAuditToggle once project is loaded.
  return false;
}

function readPersistedAuditFull(): boolean {
  try {
    return localStorage.getItem(auditFullStorageKey()) === "1";
  } catch {
    return false;
  }
}

function writePersistedAuditFull(on: boolean) {
  try {
    if (on) localStorage.setItem(auditFullStorageKey(), "1");
    else localStorage.removeItem(auditFullStorageKey());
  } catch {
    // ignore
  }
}

async function loadMoreEvents(id: string) {
  if (eventsLoadingMore || !eventsHasMore || !rawEventsLastID) return;
  const cursor = rawEventsLastID;
  const btn = document.getElementById("project-events-loadmore") as HTMLButtonElement | null;
  eventsLoadingMore = true;
  if (btn) {
    btn.disabled = true;
    btn.textContent = t("projects.detail.verlauf.loadingMore");
  }
  try {
    const resp = await fetch(
      `/api/projects/${id}/events?before=${encodeURIComponent(cursor)}&limit=${EVENTS_PAGE_SIZE}${subtreeParam()}`,
    );
    if (resp.ok) {
      const page: ProjectEvent[] = await resp.json();
      rawEventsLastID = page.length ? page[page.length - 1].id : rawEventsLastID;
      rawEventsLastPageFull = page.length === EVENTS_PAGE_SIZE;
      events = events.concat(applyVerlaufFilters(page));
      eventsHasMore = rawEventsLastPageFull;
    }
  } catch {
    /* swallow — the button re-enables and the user can retry */
  } finally {
    eventsLoadingMore = false;
    if (btn) {
      btn.disabled = false;
      btn.textContent = t("projects.detail.verlauf.loadMore");
    }
    renderTimeline();
  }
}

// Shape returned by /api/events — matches EventListItem in
// frontend/src/client/events.ts. Only the fields projects-detail needs.
interface UnionEvent {
  type: "deadline" | "appointment";
  id: string;
  title: string;
  project_id?: string;
  project_title?: string;
  due_date?: string;
  status?: string;
  rule_id?: string;
  rule_code?: string;
  start_at?: string;
  end_at?: string;
  location?: string;
  appointment_type?: string;
}

async function loadDeadlines(id: string) {
  try {
    // t-paliad-139: switched from /api/projects/{id}/deadlines (legacy
    // narrow path) to the union endpoint, which already aggregates
    // descendants and enriches each row with project_title for the
    // attribution chip.
    const resp = await fetch(
      `/api/events?type=deadline&project_id=${encodeURIComponent(id)}${subtreeParam()}`,
    );
    if (resp.ok) {
      const items: UnionEvent[] = (await resp.json()) ?? [];
      deadlines = items
        .filter((it) => it.type === "deadline")
        .map((it) => ({
          id: it.id,
          project_id: it.project_id ?? "",
          title: it.title,
          due_date: it.due_date ?? "",
          status: it.status ?? "pending",
          rule_id: it.rule_id,
          rule_code: it.rule_code,
          project_title: it.project_title,
        }));
    } else {
      deadlines = [];
    }
  } catch {
    deadlines = [];
  }
}

async function loadAppointments(id: string) {
  try {
    // t-paliad-139: same migration as loadDeadlines.
    const resp = await fetch(
      `/api/events?type=appointment&project_id=${encodeURIComponent(id)}${subtreeParam()}`,
    );
    if (resp.ok) {
      const items: UnionEvent[] = (await resp.json()) ?? [];
      appointments = items
        .filter((it) => it.type === "appointment")
        .map((it) => ({
          id: it.id,
          project_id: it.project_id,
          title: it.title,
          start_at: it.start_at ?? "",
          end_at: it.end_at,
          location: it.location,
          appointment_type: it.appointment_type,
          project_title: it.project_title,
        }));
    } else {
      appointments = [];
    }
  } catch {
    appointments = [];
  }
}

function fmtDateTimeLocal(iso: string): string {
  try {
    const d = new Date(iso);
    return d.toLocaleString(getLang() === "de" ? "de-DE" : "en-GB", {
      year: "numeric",
      month: "2-digit",
      day: "2-digit",
      hour: "2-digit",
      minute: "2-digit",
    });
  } catch {
    return iso;
  }
}

function renderAppointments() {
  const tbody = document.getElementById("project-appointments-body");
  const empty = document.getElementById("project-appointments-empty");
  const wrap = document.getElementById("project-appointments-tablewrap");
  if (!tbody || !empty || !wrap) return;
  if (appointments.length === 0) {
    tbody.innerHTML = "";
    wrap.style.display = "none";
    empty.style.display = "block";
    return;
  }
  wrap.style.display = "";
  empty.style.display = "none";
  tbody.innerHTML = appointments
    .map((tt) => {
      const typeLabel = tt.appointment_type ? tDyn(`appointments.type.${tt.appointment_type}`) || tt.appointment_type : "";
      const typeClass = tt.appointment_type ? `termin-type-${tt.appointment_type}` : "";
      return `<tr class="termin-row" data-id="${esc(tt.id)}">
        <td class="frist-col-check"><span class="termin-dot ${typeClass}" /></td>
        <td>${esc(fmtDateTimeLocal(tt.start_at))}</td>
        <td>${esc(tt.title)}${attributionChip(tt.project_id, tt.project_title)}</td>
        <td>${esc(tt.location ?? "")}</td>
        <td><span class="termin-type-chip ${typeClass}">${esc(typeLabel)}</span></td>
      </tr>`;
    })
    .join("");
  tbody.querySelectorAll<HTMLTableRowElement>(".termin-row").forEach((row) => {
    const id = row.dataset.id!;
    row.addEventListener("click", () => {
      window.location.href = `/appointments/${id}`;
    });
  });
}

function initProjectAppointmentForm() {
  const addBtn = document.getElementById("appointment-add-btn") as HTMLButtonElement | null;
  const form = document.getElementById("project-appointment-form") as HTMLFormElement | null;
  const cancelBtn = document.getElementById("project-appointment-cancel") as HTMLButtonElement | null;
  const msg = document.getElementById("project-appointment-msg");
  if (!addBtn || !form || !cancelBtn || !msg) return;

  addBtn.addEventListener("click", () => {
    form.style.display = "";
    addBtn.style.display = "none";
    (document.getElementById("project-appointment-title") as HTMLInputElement).focus();
  });
  cancelBtn.addEventListener("click", () => {
    form.reset();
    form.style.display = "none";
    addBtn.style.display = "";
    msg.textContent = "";
  });

  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    if (!project) return;
    const title = (document.getElementById("project-appointment-title") as HTMLInputElement).value.trim();
    const start = (document.getElementById("project-appointment-start") as HTMLInputElement).value;
    const end = (document.getElementById("project-appointment-end") as HTMLInputElement).value;
    const type = (document.getElementById("project-appointment-type") as HTMLSelectElement).value;
    const location = (document.getElementById("project-appointment-location") as HTMLInputElement).value.trim();
    if (!title || !start) return;

    const payload: Record<string, unknown> = {
      project_id: project.id,
      title,
      start_at: new Date(start).toISOString(),
    };
    if (end) payload.end_at = new Date(end).toISOString();
    if (type) payload.appointment_type = type;
    if (location) payload.location = location;

    msg.textContent = "";
    const submitBtn = form.querySelector<HTMLButtonElement>("button[type=submit]")!;
    submitBtn.disabled = true;
    try {
      const resp = await fetch("/api/appointments", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      });
      if (resp.ok) {
        form.reset();
        form.style.display = "none";
        addBtn.style.display = "";
        await loadAppointments(project.id);
        renderAppointments();
        await loadTimeline(project.id);
        renderTimeline();
      } else {
        const data = await resp.json().catch(() => ({}) as { error?: string });
        msg.textContent = data.error || t("projects.error.generic");
        msg.className = "form-msg form-msg-error";
      }
    } catch {
      msg.textContent = t("projects.error.generic");
      msg.className = "form-msg form-msg-error";
    } finally {
      submitBtn.disabled = false;
    }
  });
}

function fmtDateOnly(iso: string): string {
  try {
    const d = new Date(iso.slice(0, 10) + "T00:00:00");
    return d.toLocaleDateString(getLang() === "de" ? "de-DE" : "en-GB", {
      year: "numeric",
      month: "2-digit",
      day: "2-digit",
    });
  } catch {
    return iso;
  }
}

function urgencyClass(due: string, status: string): string {
  if (status === "completed") return "frist-urgency-done";
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const d = new Date(due.slice(0, 10) + "T00:00:00");
  const diffDays = Math.floor((d.getTime() - today.getTime()) / 86400000);
  if (diffDays < 0) return "frist-urgency-overdue";
  if (diffDays <= 7) return "frist-urgency-soon";
  return "frist-urgency-later";
}

function renderDeadlines() {
  const tbody = document.getElementById("project-deadlines-body");
  const empty = document.getElementById("project-deadlines-empty");
  const wrap = document.getElementById("project-deadlines-tablewrap");
  if (!tbody || !empty || !wrap) return;
  if (deadlines.length === 0) {
    tbody.innerHTML = "";
    wrap.style.display = "none";
    empty.style.display = "block";
    return;
  }
  wrap.style.display = "";
  empty.style.display = "none";
  tbody.innerHTML = deadlines
    .map((f) => {
      const urgency = urgencyClass(f.due_date, f.status);
      const statusLabel = tDyn(`deadlines.status.${f.status}`) || f.status;
      const checked = f.status === "completed" ? "checked" : "";
      const disabled = f.status === "completed" ? "disabled" : "";
      const titleClass = f.status === "completed" ? "frist-title-done" : "";
      return `<tr class="frist-row" data-id="${esc(f.id)}">
        <td class="frist-col-check">
          <input type="checkbox" class="frist-complete-cb" ${checked} ${disabled}
                 aria-label="${esc(t("deadlines.complete.action"))}" />
        </td>
        <td class="frist-col-due ${urgency}"><span class="frist-due-dot"></span>${fmtDateOnly(f.due_date)}</td>
        <td class="frist-col-title ${titleClass}">${esc(f.title)}${attributionChip(f.project_id, f.project_title)}</td>
        <td class="frist-col-rule">${f.rule_code ? esc(f.rule_code) : "—"}</td>
        <td><span class="entity-status-chip entity-status-${esc(f.status)}">${esc(statusLabel)}</span></td>
      </tr>`;
    })
    .join("");

  tbody.querySelectorAll<HTMLTableRowElement>(".frist-row").forEach((row) => {
    const id = row.dataset.id!;
    row.addEventListener("click", (e) => {
      const target = e.target as HTMLElement;
      if (target.closest(".frist-complete-cb")) return;
      window.location.href = `/deadlines/${id}`;
    });
    const cb = row.querySelector<HTMLInputElement>(".frist-complete-cb");
    if (cb) {
      cb.addEventListener("change", async () => {
        if (!cb.checked || !project) return;
        cb.disabled = true;
        const resp = await fetch(`/api/deadlines/${id}/complete`, { method: "PATCH" });
        if (resp.ok) {
          await loadDeadlines(project.id);
          renderDeadlines();
          await loadTimeline(project.id);
          renderTimeline();
        } else {
          cb.checked = false;
          cb.disabled = false;
        }
      });
    }
  });
}

// attributionChip renders a small inline chip showing which descendant
// project a row actually anchors on, when the row is from an aggregated
// subtree result and not from the project being viewed (t-paliad-139).
// Returns "" when the row's project is the current page or attribution
// data is missing.
function attributionChip(rowProjectID?: string, rowProjectTitle?: string): string {
  if (!project) return "";
  if (!rowProjectID || !rowProjectTitle) return "";
  if (rowProjectID === project.id) return "";
  const label = t("aggregation.attribution.on") || "auf";
  return ` <span class="aggregation-chip" title="${escAttr(rowProjectTitle)}">${esc(label)}: ${esc(rowProjectTitle)}</span>`;
}

function esc(s: string): string {
  const d = document.createElement("div");
  d.textContent = s;
  return d.innerHTML;
}

function escAttr(s: string): string {
  return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
}

function fmtDateTime(iso: string): string {
  try {
    const d = new Date(iso);
    return d.toLocaleString(getLang() === "de" ? "de-DE" : "en-GB", {
      year: "numeric",
      month: "2-digit",
      day: "2-digit",
      hour: "2-digit",
      minute: "2-digit",
    });
  } catch {
    return iso;
  }
}

function renderHeader() {
  if (!project) return;
  (document.getElementById("project-title-display") as HTMLElement).textContent = project.title;
  (document.getElementById("project-ref-display") as HTMLElement).textContent = project.reference || "";

  // t-paliad-177 — link from Verlauf header to standalone chart page.
  // Wired here (not in the TSX shell) because we need the resolved
  // project id, which only exists after the detail fetch settles.
  const chartLink = document.getElementById("smart-timeline-open-chart") as HTMLAnchorElement | null;
  if (chartLink) {
    chartLink.href = `/projects/${encodeURIComponent(project.id)}/chart`;
  }

  const descDisplay = document.getElementById("project-description-display") as HTMLElement;
  const description = project.description ?? "";
  descDisplay.textContent = description;
  const descWrap = document.getElementById("project-description-wrap");
  if (descWrap) {
    // Hide the whole Notizen block when there is no description.
    descWrap.style.display = description ? "" : "none";
  }

  const typeChip = document.getElementById("project-type-chip")!;
  typeChip.className = `entity-type-chip entity-type-${project.type}`;
  typeChip.textContent = tDyn(`projects.type.${project.type}`) || project.type;

  // ClientMatter display. If the project itself has no client_number, walk
  // up the ancestor chain to find an inherited one.
  const cm = document.getElementById("project-clientmatter")!;
  const effectiveClient = project.client_number || inheritedClientNumber();
  const effectiveMatter = project.matter_number || "";
  if (effectiveClient || effectiveMatter) {
    cm.textContent =
      effectiveClient && effectiveMatter
        ? `${effectiveClient}.${effectiveMatter}`
        : effectiveClient || effectiveMatter;
    if (!project.client_number && effectiveClient) {
      cm.classList.add("entity-ref-inherited");
      cm.title = t("projects.detail.clientmatter.inherited") || "inherited";
    } else {
      cm.classList.remove("entity-ref-inherited");
      cm.title = "";
    }
  } else {
    cm.textContent = "";
  }

  const statusChip = document.getElementById("project-status-chip")!;
  statusChip.className = `entity-status-chip entity-status-${project.status}`;
  statusChip.textContent = tDyn(`projects.filter.status.${project.status}`) || project.status;

  const netdocs = document.getElementById("project-netdocs") as HTMLAnchorElement;
  if (project.netdocuments_url) {
    netdocs.href = project.netdocuments_url;
    netdocs.style.display = "";
  } else {
    netdocs.style.display = "none";
  }

  // Delete visibility: partner/admin only
  const deleteWrap = document.getElementById("project-delete-wrap")!;
  if (me && (me.global_role === "global_admin")) {
    deleteWrap.style.display = "";
  } else {
    deleteWrap.style.display = "none";
  }
}

// wrapEventTitleLink — kept for the dashboard activity feed which reuses
// eventDetailHref. The renderEvents() orphan it paired with was removed
// in t-paliad-173; the SmartTimeline (renderTimeline) is now the only
// project-page render path.
function wrapEventTitleLink(e: ProjectEvent, escapedTitle: string): string {
  const href = eventDetailHref(e);
  if (href) {
    return `<a href="${href}" class="entity-event-link">${escapedTitle}</a>`;
  }
  return escapedTitle;
}

// eventDetailHref resolves a ProjectEvent to a deep-link URL, or null if the
// event has no clickable target. Kept separate so the dashboard activity feed
// can reuse the same routing rules without duplicating the wrap logic.
function eventDetailHref(e: ProjectEvent): string | null {
  const meta = e.metadata;
  const evType = e.event_type ?? "";
  if (!meta || typeof meta !== "object") return null;
  const m = meta as Record<string, unknown>;

  if (evType.startsWith("checklist_") && evType !== "checklist_deleted") {
    const id = m["checklist_instance_id"];
    if (typeof id === "string" && id) return `/checklists/instances/${esc(id)}`;
  }
  if (
    evType.startsWith("deadline_") &&
    evType !== "deadline_deleted" &&
    evType !== "deadlines_imported"
  ) {
    const id = m["deadline_id"];
    if (typeof id === "string" && id) return `/deadlines/${esc(id)}`;
  }
  if (evType.startsWith("appointment_") && evType !== "appointment_deleted") {
    const id = m["appointment_id"];
    if (typeof id === "string" && id) return `/appointments/${esc(id)}`;
  }
  if (evType === "note_created") {
    const apptID = m["appointment_id"];
    if (typeof apptID === "string" && apptID) return `/appointments/${esc(apptID)}`;
    const deadlineID = m["deadline_id"];
    if (typeof deadlineID === "string" && deadlineID) return `/deadlines/${esc(deadlineID)}`;
    const projectID = m["project_id"];
    if (typeof projectID === "string" && projectID) return `/projects/${esc(projectID)}`;
  }
  return null;
}

function initEventsLoadMore() {
  const btn = document.getElementById("project-events-loadmore");
  if (!btn) return;
  btn.addEventListener("click", () => {
    if (project) void loadMoreEvents(project.id);
  });
}

// initSmartTimelineAuditToggle — wires the "Audit-Log anzeigen" button
// in the Verlauf tab header. When ON, the next /timeline fetch passes
// ?include=audit_full so every paliad.project_events row surfaces (the
// legacy chronological Verlauf view); OFF only shows rows that opted
// into timeline_kind. State persists in localStorage per project.
function initSmartTimelineAuditToggle(id: string) {
  const btn = document.getElementById("smart-timeline-audit-toggle") as HTMLButtonElement | null;
  if (!btn) return;
  // Re-read from localStorage now that project is loaded.
  timelineAuditFull = readPersistedAuditFull();
  // Slice 2: lookahead state is also project-scoped — same pattern.
  timelineLookahead = readLookaheadPersisted();
  refreshAuditToggleLabel();

  btn.addEventListener("click", async () => {
    timelineAuditFull = !timelineAuditFull;
    writePersistedAuditFull(timelineAuditFull);
    refreshAuditToggleLabel();
    await loadTimeline(id);
    renderTimeline();
  });
}

function refreshAuditToggleLabel() {
  const btn = document.getElementById("smart-timeline-audit-toggle") as HTMLButtonElement | null;
  if (!btn) return;
  btn.setAttribute("aria-pressed", timelineAuditFull ? "true" : "false");
  btn.textContent = timelineAuditFull
    ? t("projects.detail.smarttimeline.audit.toggle.hide")
    : t("projects.detail.smarttimeline.audit.toggle.show");
  btn.classList.toggle("subtree-toggle--active", timelineAuditFull);
}

// Slice 4 — Client-level "Timeline-Ansicht" toggle (t-paliad-175 §5.1
// Q12). Visible only on Client-level projects; default OFF (matter-list
// view). When ON, the SmartTimeline lane view replaces the matter list.
// State persists in localStorage per project.
function clientShowLanesStorageKey(): string {
  const id = project?.id ?? "_";
  return `paliad.smarttimeline.client_show_lanes.${id}`;
}

function readClientShowLanes(): boolean {
  try {
    return localStorage.getItem(clientShowLanesStorageKey()) === "1";
  } catch {
    return false;
  }
}

function writeClientShowLanes(on: boolean) {
  try {
    if (on) localStorage.setItem(clientShowLanesStorageKey(), "1");
    else localStorage.removeItem(clientShowLanesStorageKey());
  } catch {
    // ignore
  }
}

function initSmartTimelineClientToggle(id: string) {
  const btn = document.getElementById("smart-timeline-client-toggle") as HTMLButtonElement | null;
  if (!btn) return;
  // Toggle is markup-rendered always; hide on non-Client projects.
  if (project?.type !== "client") {
    btn.style.display = "none";
    return;
  }
  btn.style.display = "";
  timelineClientShowLanes = readClientShowLanes();
  refreshClientToggleLabel();
  btn.addEventListener("click", async () => {
    timelineClientShowLanes = !timelineClientShowLanes;
    writeClientShowLanes(timelineClientShowLanes);
    refreshClientToggleLabel();
    // Reload to make sure lanes are populated when flipping ON.
    await loadTimeline(id);
    renderTimeline();
  });
}

function refreshClientToggleLabel() {
  const btn = document.getElementById("smart-timeline-client-toggle") as HTMLButtonElement | null;
  if (!btn) return;
  btn.setAttribute("aria-pressed", timelineClientShowLanes ? "true" : "false");
  btn.textContent = timelineClientShowLanes
    ? t("projects.detail.smarttimeline.client.toggle.matter_list")
    : t("projects.detail.smarttimeline.client.toggle.lanes");
  btn.classList.toggle("subtree-toggle--active", timelineClientShowLanes);
}

// initSmartTimelineAddModal — wires the "+ Eintrag" CTA + modal. Only
// the "Eigener Meilenstein" route is fully wired in Slice 1 (writes
// to /api/projects/{id}/timeline/milestone); Frist + Termin are link
// buttons to the existing flows; CCR + R.30 are disabled with a
// "Slice 3" tooltip per the brief.
function initSmartTimelineAddModal(id: string) {
  const cta = document.getElementById("smart-timeline-add-btn") as HTMLButtonElement | null;
  const modal = document.getElementById("smart-timeline-add-modal") as HTMLDivElement | null;
  if (!cta || !modal) return;

  const choices = document.querySelector<HTMLDivElement>(".smart-timeline-add-choices");
  const form = document.getElementById("smart-timeline-milestone-form") as HTMLFormElement | null;
  const milestoneBtn = document.getElementById("smart-timeline-add-milestone") as HTMLButtonElement | null;
  const cancelBtn = document.getElementById("smart-timeline-milestone-cancel") as HTMLButtonElement | null;
  const closeBtn = document.getElementById("smart-timeline-modal-close") as HTMLButtonElement | null;
  const titleInput = document.getElementById("smart-timeline-milestone-title") as HTMLInputElement | null;
  const dateInput = document.getElementById("smart-timeline-milestone-date") as HTMLInputElement | null;
  const descInput = document.getElementById("smart-timeline-milestone-desc") as HTMLTextAreaElement | null;
  const msg = document.getElementById("smart-timeline-milestone-msg") as HTMLDivElement | null;
  const dlLink = document.getElementById("smart-timeline-add-deadline") as HTMLAnchorElement | null;
  const apptLink = document.getElementById("smart-timeline-add-appointment") as HTMLAnchorElement | null;

  if (dlLink) dlLink.href = `/deadlines/new?project=${encodeURIComponent(id)}`;
  if (apptLink) apptLink.href = `/appointments/new?project=${encodeURIComponent(id)}`;

  const open = () => {
    modal.style.display = "";
    if (choices) choices.style.display = "";
    if (form) form.style.display = "none";
    if (msg) {
      msg.textContent = "";
      msg.className = "form-msg";
    }
  };
  const close = () => {
    modal.style.display = "none";
    if (form) form.reset();
  };

  cta.addEventListener("click", open);
  if (closeBtn) closeBtn.addEventListener("click", close);
  if (cancelBtn) cancelBtn.addEventListener("click", close);

  // Click outside the card → close.
  modal.addEventListener("click", (e) => {
    if (e.target === modal) close();
  });

  if (milestoneBtn && form) {
    milestoneBtn.addEventListener("click", () => {
      if (choices) choices.style.display = "none";
      form.style.display = "";
      titleInput?.focus();
    });
  }

  if (form && titleInput) {
    form.addEventListener("submit", async (e) => {
      e.preventDefault();
      const title = titleInput.value.trim();
      if (!title) {
        if (msg) {
          msg.textContent = t("projects.detail.smarttimeline.error.title_required");
          msg.className = "form-msg form-msg-error";
        }
        return;
      }
      const payload: Record<string, unknown> = { title };
      const desc = descInput?.value.trim();
      if (desc) payload.description = desc;
      const date = dateInput?.value;
      if (date) payload.occurred_at = date;
      // Slice 4 — bubble-up checkbox (t-paliad-175 §7.2 Q5). Default OFF
      // for custom_milestone; user opts in to surface this milestone on
      // Patent / Litigation / Client SmartTimelines.
      const bubbleEl = document.getElementById("smart-timeline-milestone-bubble-up") as HTMLInputElement | null;
      if (bubbleEl?.checked) payload.bubble_up = true;

      const submitBtn = form.querySelector<HTMLButtonElement>("button[type=submit]")!;
      submitBtn.disabled = true;
      try {
        const resp = await fetch(`/api/projects/${encodeURIComponent(id)}/timeline/milestone`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(payload),
        });
        if (resp.ok) {
          close();
          await loadTimeline(id);
          renderTimeline();
        } else {
          const data = (await resp.json().catch(() => ({}))) as { error?: string };
          if (msg) {
            msg.textContent = data.error || t("projects.detail.smarttimeline.error.generic");
            msg.className = "form-msg form-msg-error";
          }
        }
      } catch {
        if (msg) {
          msg.textContent = t("projects.detail.smarttimeline.error.generic");
          msg.className = "form-msg form-msg-error";
        }
      } finally {
        submitBtn.disabled = false;
      }
    });
  }

  // Slice 3 — Widerklage (CCR) route: opens an inline form, fetches
  // proceeding types lazily on first open, posts to
  // /api/projects/{id}/counterclaim, navigates to the new child page on
  // success.
  initCounterclaimRoute(id, modal, choices, form);
}

interface ProceedingTypeRow {
  id: number;
  code: string;
  name: string;
  name_en: string;
  jurisdiction?: string;
  is_active: boolean;
}

let proceedingTypesCache: ProceedingTypeRow[] | null = null;

// loadProceedingTypes fetches active proceeding types for the project
// picker. Phase 3 Slice 5 (t-paliad-186) restricts project-binding to
// fristenrechner-category codes (design §3.F + m's Q2 ruling), so the
// picker only ever shows those — never the 7 legacy litigation codes
// (INF / REV / CCR / APM / APP / AMD / ZPO_CIVIL). The matching
// server-side service validation + DB trigger (mig 088) are the
// defence-in-depth backstops for any non-UI writer.
async function loadProceedingTypes(): Promise<ProceedingTypeRow[]> {
  if (proceedingTypesCache) return proceedingTypesCache;
  try {
    const resp = await fetch("/api/proceeding-types-db?category=fristenrechner");
    if (!resp.ok) return [];
    const rows = ((await resp.json()) ?? []) as ProceedingTypeRow[];
    proceedingTypesCache = rows.filter((r) => r.is_active);
    return proceedingTypesCache;
  } catch {
    return [];
  }
}

function initCounterclaimRoute(
  id: string,
  modal: HTMLDivElement,
  choices: HTMLDivElement | null,
  milestoneForm: HTMLFormElement | null,
) {
  const trigger = document.getElementById("smart-timeline-add-counterclaim") as HTMLButtonElement | null;
  const form = document.getElementById("smart-timeline-counterclaim-form") as HTMLFormElement | null;
  const cancel = document.getElementById("smart-timeline-counterclaim-cancel") as HTMLButtonElement | null;
  const procedureSel = document.getElementById("smart-timeline-counterclaim-procedure") as HTMLSelectElement | null;
  const titleInput = document.getElementById("smart-timeline-counterclaim-title") as HTMLInputElement | null;
  const caseNumberInput = document.getElementById("smart-timeline-counterclaim-case-number") as HTMLInputElement | null;
  const flipToggle = document.getElementById("smart-timeline-counterclaim-flip-toggle") as HTMLInputElement | null;
  const msg = document.getElementById("smart-timeline-counterclaim-msg") as HTMLDivElement | null;

  if (!trigger || !form) return;

  const closeModal = () => {
    modal.style.display = "none";
    form.reset();
  };

  trigger.addEventListener("click", async () => {
    if (choices) choices.style.display = "none";
    if (milestoneForm) milestoneForm.style.display = "none";
    form.style.display = "";
    if (msg) {
      msg.textContent = "";
      msg.className = "form-msg";
    }
    // Populate proceeding-type select on first open. Only UPC types
    // make sense for a CCR (Nichtigkeit/CCI); pre-select upc.rev.cfi.
    if (procedureSel && procedureSel.options.length === 0) {
      const types = await loadProceedingTypes();
      const upcTypes = types.filter((t) => (t.jurisdiction ?? "").toUpperCase() === "UPC");
      const langEN = getLang() === "en";
      for (const ty of upcTypes) {
        const opt = document.createElement("option");
        opt.value = String(ty.id);
        opt.textContent = `${ty.code} — ${langEN ? ty.name_en || ty.name : ty.name}`;
        if (ty.code === "upc.rev.cfi") opt.selected = true;
        procedureSel.appendChild(opt);
      }
    }
    titleInput?.focus();
  });

  if (cancel) cancel.addEventListener("click", closeModal);

  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    const submitBtn = form.querySelector<HTMLButtonElement>("button[type=submit]")!;
    submitBtn.disabled = true;
    if (msg) {
      msg.textContent = t("projects.detail.smarttimeline.counterclaim.saving");
      msg.className = "form-msg";
    }

    const payload: Record<string, unknown> = {};
    if (procedureSel && procedureSel.value) {
      const n = parseInt(procedureSel.value, 10);
      if (!isNaN(n)) payload.proceeding_type_id = n;
    }
    const titleVal = titleInput?.value.trim();
    if (titleVal) payload.title = titleVal;
    const caseNum = caseNumberInput?.value.trim();
    if (caseNum) payload.case_number = caseNum;
    // flipToggle CHECKED = "Stimmt nicht?" = do NOT flip our_side.
    // Backend interprets flip_our_side=false as "keep parent's side".
    if (flipToggle && flipToggle.checked) {
      payload.flip_our_side = false;
    }

    try {
      const resp = await fetch(
        `/api/projects/${encodeURIComponent(id)}/counterclaim`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(payload),
        },
      );
      if (resp.ok) {
        const data = (await resp.json()) as { id?: string; url?: string };
        const dest = data.url ?? (data.id ? `/projects/${data.id}` : null);
        if (dest) {
          window.location.href = dest;
          return;
        }
        // No id back? Defensive: just close + reload timeline.
        closeModal();
        await loadTimeline(id);
        renderTimeline();
        return;
      }
      const data = (await resp.json().catch(() => ({}))) as { error?: string };
      if (msg) {
        msg.textContent = data.error || t("projects.detail.smarttimeline.error.generic");
        msg.className = "form-msg form-msg-error";
      }
    } catch {
      if (msg) {
        msg.textContent = t("projects.detail.smarttimeline.error.generic");
        msg.className = "form-msg form-msg-error";
      }
    } finally {
      submitBtn.disabled = false;
    }
  });
}

function renderParties() {
  const tbody = document.getElementById("parties-body")!;
  const empty = document.getElementById("parties-empty")!;
  const tableWrap = tbody.closest<HTMLElement>("table")!;
  if (parties.length === 0) {
    tbody.innerHTML = "";
    tableWrap.style.display = "none";
    empty.style.display = "block";
    return;
  }
  tableWrap.style.display = "";
  empty.style.display = "none";
  tbody.innerHTML = parties
    .map((p) => {
      const roleKey = p.role ? `projects.detail.parteien.role.${p.role}` : "";
      const roleLabel = p.role ? tDyn(roleKey) || p.role : "";
      return `<tr data-id="${esc(p.id)}">
        <td>${esc(p.name)}</td>
        <td>${esc(roleLabel)}</td>
        <td>${esc(p.representative || "")}</td>
        <td class="entity-col-actions">
          <button type="button" class="btn-link-danger party-remove" data-i18n="projects.detail.parteien.remove">Entfernen</button>
        </td>
      </tr>`;
    })
    .join("");
  tbody.querySelectorAll<HTMLButtonElement>(".party-remove").forEach((btn) => {
    btn.textContent = t("projects.detail.parteien.remove");
    btn.addEventListener("click", async () => {
      const row = btn.closest<HTMLTableRowElement>("tr")!;
      const id = row.dataset.id!;
      if (!confirm(t("projects.detail.parteien.remove.confirm"))) return;
      const resp = await fetch(`/api/parties/${id}`, { method: "DELETE" });
      if (resp.ok && project) {
        await loadParties(project.id);
        renderParties();
      }
    });
  });
}

function showTab(tab: TabId) {
  document.querySelectorAll<HTMLElement>(".entity-tab").forEach((el) => {
    el.classList.toggle("active", el.dataset.tab === tab);
  });
  document.querySelectorAll<HTMLElement>(".entity-tab-panel").forEach((el) => {
    el.style.display = el.id === `tab-${tab}` ? "" : "none";
  });
  // Deep-link via pushState so sub-routes stay shareable.
  if (project) {
    const newPath = `/projects/${project.id}/${tab}`;
    if (window.location.pathname !== newPath) {
      window.history.replaceState({}, "", newPath);
    }
  }
  if (tab === "checklists" && project) {
    void loadAndRenderChecklistInstances(project.id);
  }
  if (tab === "submissions" && project) {
    void loadAndRenderSubmissions(project.id);
  }
}

let checklistInstancesInited = false;
async function loadAndRenderChecklistInstances(projectID: string) {
  if (checklistInstancesInited) return;
  checklistInstancesInited = true;
  try {
    const [instResp, tplResp] = await Promise.all([
      fetch(`/api/projects/${projectID}/checklists`),
      fetch(`/api/checklists`),
    ]);
    checklistInstances = instResp.ok ? ((await instResp.json()) ?? []) : [];
    const templates = tplResp.ok ? (((await tplResp.json()) as ChecklistTemplateSummary[]) ?? []) : [];
    checklistTemplates = {};
    for (const tpl of templates) checklistTemplates[tpl.slug] = tpl;
  } catch {
    checklistInstances = [];
  }
  renderChecklistInstances();
}

function renderChecklistInstances() {
  const body = document.getElementById("project-checklists-body");
  const empty = document.getElementById("project-checklists-empty");
  const wrap = document.getElementById("project-checklists-tablewrap");
  if (!body || !empty || !wrap) return;

  if (checklistInstances.length === 0) {
    empty.style.display = "";
    wrap.style.display = "none";
    return;
  }
  empty.style.display = "none";
  wrap.style.display = "";

  const isEN = document.documentElement.lang === "en";
  const fmtDate = (iso: string) => {
    const d = new Date(iso);
    if (isNaN(d.getTime())) return "";
    return d.toLocaleDateString(isEN ? "en-GB" : "de-DE", {
      year: "numeric", month: "2-digit", day: "2-digit",
    });
  };

  body.innerHTML = checklistInstances.map((inst) => {
    const tpl = checklistTemplates[inst.template_slug];
    const tplName = tpl ? (isEN ? tpl.titleEN : tpl.titleDE) : inst.template_slug;
    const total = tpl ? tpl.itemCount : 0;
    const done = Object.values(inst.state || {}).filter(Boolean).length;
    const pct = total === 0 ? 0 : Math.round((done / total) * 100);
    return `<tr class="checklist-instance-row" data-id="${escapeHtml(inst.id)}">
      <td>${escapeHtml(tplName)}</td>
      <td><a href="/checklists/instances/${escapeHtml(inst.id)}" class="checklist-instance-name">${escapeHtml(inst.name)}</a></td>
      <td>
        <div class="checklist-progress-inline">
          <div class="checklist-progress-bar">
            <div class="checklist-progress-fill" style="width:${pct}%"></div>
          </div>
          <span class="checklist-progress-label">${done} / ${total}</span>
        </div>
      </td>
      <td>${escapeHtml(fmtDate(inst.created_at))}</td>
    </tr>`;
  }).join("");

  body.querySelectorAll<HTMLTableRowElement>(".checklist-instance-row").forEach((row) => {
    const id = row.dataset.id!;
    row.addEventListener("click", (e) => {
      if ((e.target as HTMLElement).closest("a")) return;
      window.location.href = `/checklists/instances/${id}`;
    });
  });
}

function escapeHtml(s: string): string {
  const d = document.createElement("div");
  d.textContent = s;
  return d.innerHTML;
}

function initTabs() {
  const id = project?.id ?? parseProjectID();
  document.querySelectorAll<HTMLAnchorElement>(".entity-tab").forEach((tab) => {
    if (id) tab.href = `/projects/${id}/${tab.dataset.tab}`;
    tab.addEventListener("click", (e) => {
      // SPA flow on plain left-click; let the browser handle middle-click,
      // ctrl/meta-click, and "open in new tab" via the real href above.
      if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
      e.preventDefault();
      showTab(tab.dataset.tab as TabId);
    });
  });
}

// Edit modal — full form, same fields as /projects/new but pre-filled and
// PATCH'd back. The shared client/project-form module handles parent-picker
// suggestions, type-driven field visibility, and payload building.
let editFormPrepared = false;

async function prepareEditForm() {
  if (editFormPrepared) return;
  editFormPrepared = true;
  wireTypeChange();
  // Exclude the project itself so users can't accidentally pick themselves
  // as the new parent (server would reject anyway).
  await loadParentCandidates(project?.id);
  initParentPicker();
}

function openEditModal() {
  if (!project) return;
  const modal = document.getElementById("project-edit-modal");
  const msg = document.getElementById("project-edit-msg");
  if (!modal || !msg) return;

  void prepareEditForm().then(() => {
    if (!project) return;
    prefillForm(project as unknown as Record<string, unknown>);
    // Pre-fill the parent picker label from the immediate parent (if any).
    const parentInput = document.getElementById("projekt-parent-input") as HTMLInputElement | null;
    const parentHidden = document.getElementById("projekt-parent-id") as HTMLInputElement | null;
    if (parentInput && parentHidden) {
      if (project.parent_id && ancestors.length > 0) {
        const parent = ancestors[ancestors.length - 1];
        parentHidden.value = parent.id;
        parentInput.value = parent.title;
      } else {
        parentHidden.value = "";
        parentInput.value = "";
      }
    }
    // Re-parenting is out of scope for the edit modal — disable the picker.
    if (parentInput) parentInput.disabled = true;
    // Type changes are allowed (t-paliad-056). Wire the warning that lists
    // which fields will be NULL'd server-side when the user picks a new
    // type.
    const typeSel = document.getElementById("project-type") as HTMLSelectElement | null;
    if (typeSel) {
      typeSel.disabled = false;
      typeSel.onchange = () => {
        // Keep the upstream visibility toggle that wireTypeChange installed.
        renderTypeChangeWarning();
      };
    }
    renderTypeChangeWarning();
  });
  msg.textContent = "";
  msg.className = "form-msg";
  modal.style.display = "flex";
}

// renderTypeChangeWarning compares the type select's current value against
// the loaded project's type. When they differ AND the old type has
// non-NULL type-specific fields on the project record, it surfaces an
// inline warning naming each field that will be cleared on save.
//
// Source of truth for the field map mirrors the server's
// typeSpecificColumns helper. Keep them in sync.
const TYPE_SPECIFIC_FIELDS: Record<string, { key: string; i18n: string }[]> = {
  client: [
    { key: "industry", i18n: "projects.field.industry" },
    { key: "country", i18n: "projects.field.country" },
    { key: "client_number", i18n: "projects.field.client_number" },
  ],
  patent: [
    { key: "patent_number", i18n: "projects.field.patent_number" },
    { key: "filing_date", i18n: "projects.field.filing_date" },
    { key: "grant_date", i18n: "projects.field.grant_date" },
  ],
  case: [
    { key: "court", i18n: "projects.field.court" },
    { key: "case_number", i18n: "projects.field.case_number" },
    { key: "proceeding_type_id", i18n: "projects.field.proceeding_type_id" },
  ],
};

function renderTypeChangeWarning() {
  const wrap = document.getElementById("project-edit-type-warning") as HTMLDivElement | null;
  const fieldsSpan = document.getElementById("project-edit-type-warning-fields") as HTMLSpanElement | null;
  const typeSel = document.getElementById("project-type") as HTMLSelectElement | null;
  if (!wrap || !fieldsSpan || !typeSel || !project) return;

  const newType = typeSel.value;
  if (newType === project.type) {
    wrap.style.display = "none";
    fieldsSpan.textContent = "";
    return;
  }
  const obsolete = TYPE_SPECIFIC_FIELDS[project.type] || [];
  const projectRec = project as unknown as Record<string, unknown>;
  const lost = obsolete
    .filter((f) => {
      const v = projectRec[f.key];
      return v !== null && v !== undefined && v !== "";
    })
    .map((f) => tDyn(f.i18n) || f.key);
  if (lost.length === 0) {
    wrap.style.display = "none";
    fieldsSpan.textContent = "";
    return;
  }
  wrap.style.display = "";
  // Translate the static label too (it has data-i18n but the modal may have
  // opened before the lang change handler re-translated it).
  const titleEl = wrap.querySelector("strong");
  if (titleEl) titleEl.textContent = t("projects.detail.edit.type_change_warning.title") || titleEl.textContent || "";
  fieldsSpan.textContent = " " + lost.join(", ");
}

function closeEditModal() {
  const modal = document.getElementById("project-edit-modal");
  if (modal) modal.style.display = "none";
}

function initEditModal() {
  const editBtn = document.getElementById("project-edit-btn") as HTMLButtonElement | null;
  const modal = document.getElementById("project-edit-modal");
  const closeBtn = document.getElementById("project-edit-modal-close");
  const cancelBtn = document.getElementById("project-edit-cancel");
  const form = document.getElementById("project-edit-form") as HTMLFormElement | null;
  const msg = document.getElementById("project-edit-msg") as HTMLParagraphElement | null;
  if (!editBtn || !modal || !closeBtn || !cancelBtn || !form || !msg) return;

  editBtn.addEventListener("click", openEditModal);
  closeBtn.addEventListener("click", closeEditModal);
  cancelBtn.addEventListener("click", closeEditModal);
  modal.addEventListener("click", (e) => {
    if (e.target === e.currentTarget) closeEditModal();
  });

  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    if (!project) return;
    msg.textContent = "";
    msg.className = "form-msg";

    const payload = readPayload(msg, { omitEmpty: false, mode: "edit" });
    if (!payload) return;
    // Type changes from the edit form are an unusual structural action —
    // the server allows it but we're explicit about not sending `type` when
    // unchanged so the backend doesn't run avoidable validation.
    if (payload.type === project.type) delete payload.type;

    const submitBtn = form.querySelector<HTMLButtonElement>("button[type=submit]")!;
    submitBtn.disabled = true;
    try {
      const resp = await fetch(`/api/projects/${project.id}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      });
      if (!resp.ok) {
        const errBody = await resp.json().catch(() => ({ error: "unknown" }));
        msg.textContent = errBody.error || t("projects.error.generic");
        msg.className = "form-msg form-msg-error";
        return;
      }
      project = await resp.json();
      closeEditModal();
      if (project) {
        await Promise.all([loadAncestors(project.id), loadTimeline(project.id)]);
        renderHeader();
        renderBreadcrumb();
        renderTimeline();
      }
    } catch (err) {
      msg.textContent = t("projects.error.generic");
      msg.className = "form-msg form-msg-error";
    } finally {
      submitBtn.disabled = false;
    }
  });
}

function initPartiesForm() {
  const addBtn = document.getElementById("party-add-btn") as HTMLButtonElement;
  const form = document.getElementById("party-form") as HTMLFormElement;
  const cancelBtn = document.getElementById("party-cancel") as HTMLButtonElement;
  const msg = document.getElementById("party-msg")!;

  // Both the toolbar button and the empty-state CTA (F-43) trigger the
  // same form-open path.
  const openForm = () => {
    form.style.display = "";
    addBtn.style.display = "none";
    (document.getElementById("party-name") as HTMLInputElement).focus();
  };
  addBtn.addEventListener("click", openForm);
  document.getElementById("parties-empty-cta")?.addEventListener("click", openForm);

  cancelBtn.addEventListener("click", () => {
    form.reset();
    form.style.display = "none";
    addBtn.style.display = "";
    msg.textContent = "";
  });

  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    if (!project) return;
    const name = (document.getElementById("party-name") as HTMLInputElement).value.trim();
    const role = (document.getElementById("party-role") as HTMLSelectElement).value;
    const rep = (document.getElementById("party-rep") as HTMLInputElement).value.trim();
    if (!name) return;

    msg.textContent = "";
    const submitBtn = form.querySelector<HTMLButtonElement>("button[type=submit]")!;
    submitBtn.disabled = true;

    const payload: Record<string, unknown> = { name, role };
    if (rep) payload.representative = rep;

    try {
      const resp = await fetch(`/api/projects/${project.id}/parties`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      });
      if (resp.ok) {
        form.reset();
        form.style.display = "none";
        addBtn.style.display = "";
        await loadParties(project.id);
        renderParties();
        await loadTimeline(project.id);
        renderTimeline();
      } else {
        const data = await resp.json().catch(() => ({}) as { error?: string });
        msg.textContent = data.error || t("projects.error.generic");
        msg.className = "form-msg form-msg-error";
      }
    } catch {
      msg.textContent = t("projects.error.generic");
      msg.className = "form-msg form-msg-error";
    } finally {
      submitBtn.disabled = false;
    }
  });
}

function initDeadlineAddLink() {
  if (!project) return;
  const link = document.getElementById("deadline-add-link") as HTMLAnchorElement | null;
  if (link) link.href = `/projects/${project.id}/deadlines/new`;
}

function initDelete() {
  const btn = document.getElementById("project-delete-btn")!;
  const modal = document.getElementById("delete-modal")!;
  const close = document.getElementById("delete-modal-close")!;
  const cancel = document.getElementById("delete-modal-cancel")!;
  const confirmBtn = document.getElementById("delete-modal-confirm") as HTMLButtonElement;

  btn.addEventListener("click", () => {
    modal.style.display = "flex";
  });
  const closeModal = () => {
    modal.style.display = "none";
  };
  close.addEventListener("click", closeModal);
  cancel.addEventListener("click", closeModal);
  modal.addEventListener("click", (e) => {
    if (e.target === e.currentTarget) closeModal();
  });
  confirmBtn.addEventListener("click", async () => {
    if (!project) return;
    confirmBtn.disabled = true;
    const resp = await fetch(`/api/projects/${project.id}`, { method: "DELETE" });
    if (resp.ok) {
      window.location.href = "/projects";
    } else {
      confirmBtn.disabled = false;
      closeModal();
    }
  });
}

async function main() {
  const id = parseProjectID();
  const loading = document.getElementById("project-detail-loading")!;
  const notfound = document.getElementById("project-detail-notfound")!;
  const body = document.getElementById("project-detail-body")!;

  if (!id) {
    loading.style.display = "none";
    notfound.style.display = "block";
    return;
  }

  // Read subtree mode from URL once at startup; subsequent toggles update
  // the URL via persistSubtreeMode (replaceState — back-button friendly).
  subtreeMode = parseSubtreeMode();

  await loadMe();
  const ok = await loadProject(id);
  if (!ok || !project) {
    loading.style.display = "none";
    notfound.style.display = "block";
    return;
  }

  // loadEvents stays in this Promise.all so the unfiltered Verlauf is
  // ready by first paint (avoids an empty-state flash before the bar's
  // customRunner finishes its first run, t-paliad-170). When the URL
  // carries filter params (?time=…, ?pe_kind=…) the bar's mount triggers
  // a second fetch that narrows to the requested rows — accepted cost.
  await Promise.all([
    loadParties(id),
    loadEvents(id),
    loadTimeline(id),
    loadDeadlines(id),
    loadAppointments(id),
    loadAncestors(id),
    loadChildren(id),
    loadTeam(id),
    loadDescendantStaffed(id),
    loadDerivedMembers(id),
    loadAttachedUnits(id),
    loadAllUnits(),
    loadUserList(),
  ]);

  loading.style.display = "none";
  body.style.display = "";
  renderHeader();
  renderBreadcrumb();
  renderParties();
  renderTimeline();
  renderDeadlines();
  renderAppointments();
  renderChildren();
  renderTeam();
  initDeadlineAddLink();
  initChildAddLink();
  initTabs();
  initEditModal();
  initPartiesForm();
  initProjectAppointmentForm();
  initTeamForm(id);
  initDelete();
  initEventsLoadMore();
  initSubtreeToggles(id);
  initSmartTimelineAuditToggle(id);
  initSmartTimelineClientToggle(id);
  initSmartTimelineAddModal(id);
  initAttachUnitForm(id);
  initNotesContainer(id);
  mountVerlaufFilterBar(id);
  wireExportButton(id);
  showTab(parseTab());
}

// mountVerlaufFilterBar mounts the universal FilterBar inside the
// Verlauf tab (t-paliad-170 → t-paliad-176). The bar owns URL params
// (?time=, ?pe_kind=, ?tl_status=, ?tl_track=) and the displayed filter
// chrome; on every state change it invokes the customRunner below, which
// drains the bar state into `verlaufFilters` and lets the bar's onResult
// callback trigger renderTimeline — narrowing happens client-side over
// `timelineRows` in `applyTimelineRowFilters`.
//
// Why customRunner instead of the substrate POST: the SmartTimeline
// endpoint isn't a substrate-managed system view, and timeline_status /
// timeline_track / project_event_kind don't all map cleanly onto the
// substrate's per-source predicates. The customRunner stays as the bar's
// integration point with the SmartTimeline read pipeline.
function mountVerlaufFilterBar(id: string): void {
  const host = document.getElementById("project-events-filter-bar");
  if (!host) return;

  // Synthetic spec — never reaches the substrate (customRunner short-
  // circuits the bar's POST), but the bar's contract requires shapes
  // that the substrate validator would accept. Sources / scope mirror
  // what a future ProjectHistorySystemView would look like.
  const baseFilter: FilterSpec = {
    version: 1,
    sources: ["project_event"],
    scope: { projects: { mode: "explicit", ids: [id] } },
    time: { horizon: "any" },
  };
  const baseRender: RenderSpec = { shape: "list" };

  verlaufBar = mountFilterBar(host, {
    baseFilter,
    baseRender,
    // t-paliad-176 — exposing timeline_status + timeline_track on the
    // Verlauf tab. They were declared in the bar's axis catalogue from
    // Slice 2 onward but never mounted on this surface; chips were
    // therefore invisible and the wire-up was a no-op.
    axes: ["time", "timeline_status", "timeline_track", "project_event_kind"],
    surfaceKey: "project-history",
    showSaveAsView: false,
    timePresets: ["past_7d", "past_30d", "past_90d", "any"],
    customRunner: async (effective, state) => {
      // project_event_kind rides through effective.filter.predicates
      // (substrate-shaped); timeline_status / timeline_track live on raw
      // BarState. The bar passes both to keep first-run hydration honest
      // (the bar handle hasn't been assigned to verlaufBar yet on first
      // run, so we can't reach getState() — the state argument fixes that).
      const kinds = effective.filter.predicates?.project_event?.event_types;
      const tlStatus = state.timeline_status;
      const tlTrack = state.timeline_track;
      verlaufFilters = {
        eventKinds: kinds && kinds.length ? new Set(kinds) : undefined,
        timelineStatuses: tlStatus && tlStatus.length ? new Set(tlStatus) : undefined,
        timelineTracks: tlTrack && tlTrack.length ? new Set(tlTrack) : undefined,
        ...horizonBounds(effective.filter.time?.horizon ?? "any"),
      };
      return { rows: [], inaccessible_project_ids: [] };
    },
    onResult: () => renderTimeline(),
  });
}

// initAttachUnitForm wires the "Partner Unit zuordnen" form on the Team
// tab (project lead / global_admin only). The select is populated from
// /api/partner-units excluding units already attached.
function initAttachUnitForm(id: string) {
  const wrap = document.getElementById("unit-attach-form-wrap");
  const form = document.getElementById("unit-attach-form") as HTMLFormElement | null;
  const showBtn = document.getElementById("unit-attach-show") as HTMLButtonElement | null;
  const cancelBtn = document.getElementById("unit-attach-cancel") as HTMLButtonElement | null;
  const select = document.getElementById("unit-attach-select") as HTMLSelectElement | null;
  if (!wrap || !form || !showBtn || !cancelBtn || !select) return;

  if (!canManagePartnerUnits()) {
    showBtn.style.display = "none";
    return;
  }

  const refreshSelect = () => {
    const attachedIDs = new Set(attachedUnits.map((u) => u.partner_unit_id));
    const placeholder = `<option value="">${esc(t("projects.team.units.choose") || "Bitte Unit wählen…")}</option>`;
    const opts = allUnits
      .filter((u) => !attachedIDs.has(u.id))
      .map((u) => `<option value="${esc(u.id)}">${esc(u.name)}</option>`)
      .join("");
    select.innerHTML = placeholder + opts;
  };
  refreshSelect();

  showBtn.addEventListener("click", () => {
    refreshSelect();
    wrap.style.display = "";
    showBtn.style.display = "none";
  });
  cancelBtn.addEventListener("click", () => {
    form.reset();
    wrap.style.display = "none";
    showBtn.style.display = "";
  });
  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    const unitID = select.value;
    if (!unitID) return;
    const rolePA = (document.getElementById("unit-attach-role-pa") as HTMLInputElement).checked;
    const roleSenior = (document.getElementById("unit-attach-role-senior_pa") as HTMLInputElement).checked;
    const roleAtty = (document.getElementById("unit-attach-role-attorney") as HTMLInputElement).checked;
    const grantsAuthority = (document.getElementById("unit-attach-authority") as HTMLInputElement).checked;
    const roles: string[] = [];
    if (rolePA) roles.push("pa");
    if (roleSenior) roles.push("senior_pa");
    if (roleAtty) roles.push("attorney");
    if (roles.length === 0) {
      // Defaults: pa + senior_pa.
      roles.push("pa", "senior_pa");
    }
    const resp = await fetch(`/api/projects/${id}/partner-units`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        partner_unit_id: unitID,
        derive_unit_roles: roles,
        derive_grants_authority: grantsAuthority,
      }),
    });
    if (resp.ok) {
      form.reset();
      wrap.style.display = "none";
      showBtn.style.display = "";
      await Promise.all([loadAttachedUnits(id), loadDerivedMembers(id)]);
      renderTeam();
    }
  });
}

// initSubtreeToggles wires the "Inkl. Unterprojekte / Nur direkt" buttons
// in the History, Deadlines, and Appointments sections. State is shared
// across the three sections (one toggle flips all) and persisted in the
// URL via ?subtree=false. Default = subtree (true).
function initSubtreeToggles(id: string) {
  const buttons = document.querySelectorAll<HTMLButtonElement>(".subtree-toggle");
  if (buttons.length === 0) return;

  const refreshLabels = () => {
    buttons.forEach((btn) => {
      btn.textContent = subtreeMode
        ? t("aggregation.toggle.subtree")
        : t("aggregation.toggle.direct_only");
      btn.setAttribute("aria-pressed", subtreeMode ? "true" : "false");
      btn.classList.toggle("subtree-toggle--active", !subtreeMode);
    });
  };

  refreshLabels();

  buttons.forEach((btn) => {
    btn.addEventListener("click", async () => {
      subtreeMode = !subtreeMode;
      persistSubtreeMode();
      refreshLabels();
      // verlaufBar.refresh() drives loadEvents through the bar's
      // customRunner (so the current filter state stays applied).
      // verlaufBar.refresh() drives loadEvents through the bar's
      // customRunner, but render is now driven entirely by loadTimeline.
      const barRefresh = verlaufBar ? verlaufBar.refresh() : Promise.resolve();
      await Promise.all([
        barRefresh,
        loadTimeline(id),
        loadDeadlines(id),
        loadAppointments(id),
      ]);
      renderTimeline();
      renderDeadlines();
      renderAppointments();
    });
  });
}

// ----- Breadcrumb + ancestor resolution -----------------------------------

function inheritedClientNumber(): string | null {
  // Walks ancestor chain (root → parent) and returns the nearest non-null
  // client_number for display when the project itself has none.
  for (let i = ancestors.length - 1; i >= 0; i--) {
    const a = ancestors[i] as ProjectMini & { client_number?: string | null };
    if (a.client_number) return a.client_number;
  }
  return null;
}

async function loadAncestors(id: string) {
  try {
    const resp = await fetch(`/api/projects/${id}/ancestors`);
    if (resp.ok) ancestors = ((await resp.json()) as ProjectMini[]) ?? [];
  } catch {
    ancestors = [];
  }
}

// Lucide-style 24x24 icons matched to the project tree's icon set so the
// visual language stays consistent across the app.
const TYPE_ICONS: Record<string, string> = {
  client:
    `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">` +
    `<rect x="2" y="7" width="20" height="14" rx="2"/>` +
    `<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>` +
    `</svg>`,
  litigation:
    `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">` +
    `<path d="M16 16l3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1z"/>` +
    `<path d="M2 16l3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1z"/>` +
    `<path d="M7 21h10"/>` +
    `<path d="M12 3v18"/>` +
    `<path d="M3 7h18"/>` +
    `</svg>`,
  patent:
    `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">` +
    `<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V7"/>` +
    `<path d="M3 7v12a2 2 0 0 0 2 2h0"/>` +
    `<path d="M21 3a2 2 0 0 0-2 2v14"/>` +
    `<line x1="9" y1="9" x2="15" y2="9"/>` +
    `<line x1="9" y1="13" x2="15" y2="13"/>` +
    `<line x1="9" y1="17" x2="13" y2="17"/>` +
    `</svg>`,
  case:
    `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">` +
    `<path d="M14 13l-7 7-3-3 7-7"/>` +
    `<path d="M11.5 7.5l5 5"/>` +
    `<path d="M16 3l5 5-3 3-5-5z"/>` +
    `<path d="M5 21h6"/>` +
    `</svg>`,
  project:
    `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">` +
    `<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>` +
    `</svg>`,
};

const BREADCRUMB_CHEVRON =
  `<svg class="projekt-breadcrumb-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">` +
  `<polyline points="9 18 15 12 9 6"/>` +
  `</svg>`;

function typeIcon(type: string): string {
  return TYPE_ICONS[type] || TYPE_ICONS.project;
}

function renderBreadcrumb() {
  if (!project) return;
  const el = document.getElementById("project-breadcrumb");
  if (!el) return;
  // Hide the breadcrumb when there's no chain — at the root of a tree the
  // single crumb just echoes the H1 directly below it (F-27).
  if (ancestors.length === 0) {
    el.innerHTML = "";
    el.style.display = "none";
    return;
  }
  el.style.display = "";
  const crumbs: string[] = ancestors.map((a) => {
    const label = tDyn(`projects.type.${a.type}`) || a.type;
    return (
      `<a href="/projects/${esc(a.id)}" class="projekt-crumb projekt-crumb-link projekt-crumb-${esc(a.type)}" title="${escAttr(label)}: ${escAttr(a.title)}">` +
      `<span class="projekt-crumb-icon">${typeIcon(a.type)}</span>` +
      `<span class="projekt-crumb-title">${esc(a.title)}</span>` +
      `</a>`
    );
  });
  const currentLabel = tDyn(`projects.type.${project.type}`) || project.type;
  crumbs.push(
    `<span class="projekt-crumb projekt-crumb-current projekt-crumb-${esc(project.type)}" title="${escAttr(currentLabel)}: ${escAttr(project.title)}">` +
      `<span class="projekt-crumb-icon">${typeIcon(project.type)}</span>` +
      `<span class="projekt-crumb-title">${esc(project.title)}</span>` +
      `</span>`,
  );
  el.innerHTML = crumbs.join(BREADCRUMB_CHEVRON);
}

// ----- Project Tree (Projektbaum) -----------------------------------------
// Renders the full visible project hierarchy with the current node highlighted.
// One round-trip to /api/projects/tree gets every project the user can see;
// the renderer walks the tree and produces a nested <ul> with the current
// node visually marked. Direct children of the current node still drive the
// "no sub-projects" empty state, since that is the actionable signal for
// the "+ Untervorhaben anlegen" CTA.

async function loadChildren(id: string) {
  // Direct children kept for the "Keine untergeordneten Projekte" empty state
  // and the create-new pre-fill (parent_id from the current node).
  try {
    const resp = await fetch(`/api/projects/${id}/children`);
    if (resp.ok) children = ((await resp.json()) as ProjectMini[]) ?? [];
  } catch {
    children = [];
  }
}

// renderChildren is the Projektbaum tab's mount point. m's 2026-05-08
// 21:28: "should just be the same as the Tree in Projects. It has
// symbols, everything." Reuse the /projects tree component
// (project-tree.ts) verbatim — type icons, pin stars, deadline badges,
// expand/collapse, search highlighting all come along for free. The
// current project is highlighted via a CSS modifier we add to its
// data-id row after the tree mounts.
function renderChildren() {
  const root = document.getElementById("project-tree")!;
  const empty = document.getElementById("children-empty")!;
  // Empty state only when the current node has zero direct children — the
  // CTA next to the empty message is "create sub-project", which would be
  // misleading if the tree itself has other branches.
  empty.style.display = children.length ? "none" : "";
  // Mount the shared tree. initProjectTree fetches /api/projects/tree on
  // first call and caches; subsequent tab-switches re-render from cache.
  // Set aria-current on the row matching this project — the shared tree
  // already styles aria-current=true with a lime highlight (global.css
  // .projekt-tree-node[aria-current="true"] > .projekt-tree-row).
  void initProjectTree(root).then(() => {
    const currentId = project?.id ?? "";
    if (!currentId) return;
    root.querySelectorAll<HTMLLIElement>(".projekt-tree-node").forEach((li) => {
      if (li.dataset.id === currentId) {
        li.setAttribute("aria-current", "true");
      } else {
        li.removeAttribute("aria-current");
      }
    });
  });
}

function initChildAddLink() {
  const link = document.getElementById("child-add-link") as HTMLAnchorElement | null;
  if (!link || !project) return;
  // Pre-fill parent_id for the create form via query param.
  link.href = `/projects/new?parent=${encodeURIComponent(project.id)}`;
}

// ----- Team tab -----------------------------------------------------------

async function loadTeam(id: string) {
  try {
    const resp = await fetch(`/api/projects/${id}/team`);
    if (resp.ok) teamMembers = ((await resp.json()) as ProjectTeamMember[]) ?? [];
  } catch {
    teamMembers = [];
  }
}

// t-paliad-139 — Team-tab subsection loaders. All three are independent so
// main() runs them in parallel.
async function loadDescendantStaffed(id: string) {
  try {
    const resp = await fetch(`/api/projects/${id}/team/from-descendants`);
    if (resp.ok) {
      descendantStaffed = ((await resp.json()) as ProjectTeamMember[]) ?? [];
    } else {
      descendantStaffed = [];
    }
  } catch {
    descendantStaffed = [];
  }
}

async function loadDerivedMembers(id: string) {
  try {
    const resp = await fetch(`/api/projects/${id}/team/derived`);
    if (resp.ok) {
      derivedMembers = ((await resp.json()) as DerivedMember[]) ?? [];
    } else {
      derivedMembers = [];
    }
  } catch {
    derivedMembers = [];
  }
}

async function loadAttachedUnits(id: string) {
  try {
    const resp = await fetch(`/api/projects/${id}/partner-units`);
    if (resp.ok) {
      attachedUnits = ((await resp.json()) as AttachedUnit[]) ?? [];
    } else {
      attachedUnits = [];
    }
  } catch {
    attachedUnits = [];
  }
}

async function loadAllUnits() {
  try {
    const resp = await fetch(`/api/partner-units`);
    if (resp.ok) {
      const all = (await resp.json()) as { id: string; name: string; office: string }[];
      allUnits = all ?? [];
    }
  } catch {
    allUnits = [];
  }
}

async function loadUserList() {
  try {
    const resp = await fetch("/api/users");
    if (resp.ok) userOptions = ((await resp.json()) as typeof userOptions) ?? [];
  } catch {
    userOptions = [];
  }
}

function renderTeam() {
  const body = document.getElementById("team-body")!;
  const empty = document.getElementById("team-empty")!;

  // Existing team-body shows the direct + ancestor-inherited members
  // returned by /api/projects/{id}/team. The derived + descendant
  // sections render into separate tbodies (added in TSX). Empty state
  // applies to the union — only show when EVERY section is empty.
  const totalRows =
    teamMembers.length + descendantStaffed.length + derivedMembers.length;
  if (totalRows === 0) {
    body.innerHTML = "";
    empty.style.display = "";
    renderDescendantStaffed();
    renderDerivedMembers();
    renderAttachedUnits();
    return;
  }
  empty.style.display = "none";

  body.innerHTML = teamMembers
    .map((m) => {
      // t-paliad-148: profession is firm-wide (read-only badge) and
      // responsibility is per-project. Both are surfaced; the legacy
      // .role field is still set by the server during the deprecation
      // window but the UI ignores it.
      const responsibility = m.responsibility || "member";
      const responsibilityLabel = tDyn(`projects.team.responsibility.${responsibility}`) || responsibility;
      const professionLabel = m.user_profession
        ? tDyn(`projects.team.profession.${m.user_profession}`) || m.user_profession
        : (t("projects.team.profession.none") || "(extern)");
      const professionTitle = m.user_profession
        ? (t("projects.team.profession.hint") || "Profession — gesetzt im Firmenprofil")
        : (t("projects.team.profession.none.hint") || "Keine Profession gesetzt — keine 4-Augen-Befugnis");
      const source = m.inherited
        ? `<span class="projekt-team-inherited" title="${escAttr(t("projects.team.inherited.hint") || "Inherited from ancestor")}">
              &uarr; ${esc(m.inherited_from_title || "")}
           </span>`
        : `<span class="projekt-team-direct">${esc(t("projects.team.direct") || "direkt")}</span>`;
      const removeBtn =
        !m.inherited && canRemoveTeamMember(m)
          ? `<button type="button" class="btn-ghost btn-small team-remove-btn" data-user-id="${esc(m.user_id)}">${esc(t("projects.detail.team.remove") || "Entfernen")}</button>`
          : "";
      const officeLabel = m.user_office ? tDyn("office." + m.user_office) || m.user_office : "";
      const profCls = m.user_profession ? "projekt-team-profession" : "projekt-team-profession projekt-team-profession--none";
      return `<tr>
        <td><strong>${esc(m.user_display_name || m.user_email)}</strong>
            <span class="form-hint">&middot; ${esc(m.user_email)}${officeLabel ? " &middot; " + esc(officeLabel) : ""}</span></td>
        <td><span class="${profCls}" title="${escAttr(professionTitle)}">${esc(professionLabel)}</span></td>
        <td><span class="projekt-team-responsibility">${esc(responsibilityLabel)}</span></td>
        <td>${source}</td>
        <td>${removeBtn}</td>
      </tr>`;
    })
    .join("");

  body.querySelectorAll<HTMLButtonElement>(".team-remove-btn").forEach((btn) => {
    btn.addEventListener("click", async () => {
      if (!project) return;
      const userID = btn.dataset.userId!;
      if (!window.confirm(t("projects.detail.team.confirm_remove") || "Mitglied entfernen?")) return;
      const resp = await fetch(
        `/api/projects/${project.id}/team/${encodeURIComponent(userID)}`,
        { method: "DELETE" },
      );
      if (resp.ok) {
        await loadTeam(project.id);
        renderTeam();
      }
    });
  });

  renderDescendantStaffed();
  renderDerivedMembers();
  renderAttachedUnits();
}

// t-paliad-139 — "Aus Unterprojekten" subsection.
function renderDescendantStaffed() {
  const section = document.getElementById("team-section-descendants");
  const body = document.getElementById("team-descendants-body");
  if (!section || !body) return;
  if (descendantStaffed.length === 0) {
    section.style.display = "none";
    body.innerHTML = "";
    return;
  }
  section.style.display = "";
  body.innerHTML = descendantStaffed
    .map((m) => {
      const responsibility = m.responsibility || "member";
      const responsibilityLabel = tDyn(`projects.team.responsibility.${responsibility}`) || responsibility;
      const officeLabel = m.user_office ? tDyn("office." + m.user_office) || m.user_office : "";
      const sourceTitle = esc(m.inherited_from_title || "");
      return `<tr>
        <td><strong>${esc(m.user_display_name || m.user_email)}</strong>
            <span class="form-hint">&middot; ${esc(m.user_email)}${officeLabel ? " &middot; " + esc(officeLabel) : ""}</span></td>
        <td><span class="projekt-team-responsibility">${esc(responsibilityLabel)}</span></td>
        <td><span class="projekt-team-inherited" title="${escAttr(t("aggregation.attribution.on") || "auf")}: ${sourceTitle}">&darr; ${sourceTitle}</span></td>
      </tr>`;
    })
    .join("");
}

// t-paliad-139 — "Abgeleitet (Partner Unit)" subsection.
function renderDerivedMembers() {
  const section = document.getElementById("team-section-derived");
  const body = document.getElementById("team-derived-body");
  if (!section || !body) return;
  if (derivedMembers.length === 0) {
    section.style.display = "none";
    body.innerHTML = "";
    return;
  }
  section.style.display = "";
  body.innerHTML = derivedMembers
    .map((m) => {
      const memberships = m.memberships || [];
      // Role column shows distinct unit_role values (usually one — only
      // diverges if the user has different roles in different units).
      const distinctRoles = Array.from(new Set(memberships.map((x) => x.unit_role)));
      const roleLabel = distinctRoles
        .map((r) => tDyn(`unit_role.${r}`) || r)
        .join(", ");
      // Herkunft column lists every (unit, role) pair so multi-unit users
      // surface all their sources, not just the closest one (t-paliad-143).
      // Multi-unit: bold each unit name and append the role in parentheses.
      // Single-unit: bold the one unit name (matches the legacy rendering).
      const sourceLabel = memberships
        .map((x) => {
          const name = `<strong>${esc(x.unit_name)}</strong>`;
          if (memberships.length === 1) return name;
          const role = esc(tDyn(`unit_role.${x.unit_role}`) || x.unit_role);
          return `${name} (${role})`;
        })
        .join(", ");
      const officeLabel = m.user_office ? tDyn("office." + m.user_office) || m.user_office : "";
      const authBadge = m.derive_grants_authority
        ? `<span class="derived-badge derived-badge--authority" title="${escAttr(t("projects.team.derived.authority.hint") || "Authority granted")}">${esc(t("projects.team.derived.authority") || "Sicht & 4-Augen")}</span>`
        : `<span class="derived-badge">${esc(t("projects.team.derived.visibility") || "Sicht")}</span>`;
      return `<tr>
        <td><strong>${esc(m.user_display_name || m.user_email)}</strong>
            <span class="form-hint">&middot; ${esc(m.user_email)}${officeLabel ? " &middot; " + esc(officeLabel) : ""}</span></td>
        <td><span class="projekt-team-role">${esc(roleLabel)}</span></td>
        <td>${esc(t("projects.team.derived.from") || "über")}: ${sourceLabel} ${authBadge}</td>
      </tr>`;
    })
    .join("");
}

// t-paliad-139 — Partner Units management section. Lists attached units
// with detach buttons; admin/lead can add new attachments.
function renderAttachedUnits() {
  const section = document.getElementById("team-section-units");
  const body = document.getElementById("team-units-body");
  if (!section || !body) return;
  const canManage = canManagePartnerUnits();
  // Always show the section to admins/leads (even if empty so they can attach).
  if (!canManage && attachedUnits.length === 0) {
    section.style.display = "none";
    return;
  }
  section.style.display = "";
  if (attachedUnits.length === 0) {
    body.innerHTML = `<tr><td colspan="4" class="form-hint">${esc(t("projects.team.units.empty") || "Keine Partner Units zugeordnet.")}</td></tr>`;
    return;
  }
  body.innerHTML = attachedUnits
    .map((u) => {
      const roles = (u.derive_unit_roles || []).map((r) => tDyn(`unit_role.${r}`) || r).join(", ");
      const auth = u.derive_grants_authority
        ? esc(t("projects.team.derived.authority") || "Sicht & 4-Augen")
        : esc(t("projects.team.derived.visibility") || "Sicht");
      const detachBtn = canManage
        ? `<button type="button" class="btn-ghost btn-small unit-detach-btn" data-unit-id="${esc(u.partner_unit_id)}">${esc(t("projects.team.units.detach") || "Entfernen")}</button>`
        : "";
      return `<tr>
        <td><strong>${esc(u.unit_name)}</strong></td>
        <td>${esc(roles)}</td>
        <td>${auth}</td>
        <td>${u.derived_member_count} ${esc(t("projects.team.units.members") || "Mitglieder")} ${detachBtn}</td>
      </tr>`;
    })
    .join("");

  body.querySelectorAll<HTMLButtonElement>(".unit-detach-btn").forEach((btn) => {
    btn.addEventListener("click", async () => {
      if (!project) return;
      const unitID = btn.dataset.unitId!;
      if (!window.confirm(t("projects.team.units.confirm_detach") || "Partner Unit entfernen?")) return;
      const resp = await fetch(
        `/api/projects/${project.id}/partner-units/${encodeURIComponent(unitID)}`,
        { method: "DELETE" },
      );
      if (resp.ok) {
        await Promise.all([loadAttachedUnits(project.id), loadDerivedMembers(project.id)]);
        renderTeam();
      }
    });
  });
}

// canManagePartnerUnits returns true for global_admin or this project's
// lead. Mirrors the migration-055 RLS write policy.
function canManagePartnerUnits(): boolean {
  if (!me) return false;
  if (me.global_role === "global_admin") return true;
  if (!project) return false;
  return teamMembers.some(
    (m) => m.user_id === me!.id && m.responsibility === "lead" && m.project_id === project!.id,
  );
}

// canExportProject mirrors the §4 server-side gate for /api/projects/{id}/export:
// global_admin OR direct team responsibility ∈ {lead, member}. Used to
// reveal the export button — server still re-enforces on the request.
function canExportProject(): boolean {
  if (!me || !project) return false;
  if (me.global_role === "global_admin") return true;
  return teamMembers.some(
    (m) =>
      m.user_id === me!.id &&
      m.project_id === project!.id &&
      (m.responsibility === "lead" || m.responsibility === "member"),
  );
}

// wireExportButton reveals + hooks up the project-export button on the
// tabs nav. Triggers a download via a transient <a download> — same
// pattern as the personal export in client/settings.ts.
function wireExportButton(projectID: string): void {
  const btn = document.getElementById("project-export-btn") as HTMLButtonElement | null;
  if (!btn) return;
  if (!canExportProject()) {
    btn.style.display = "none";
    return;
  }
  btn.style.display = "";
  btn.addEventListener("click", () => {
    const a = document.createElement("a");
    a.href = `/api/projects/${encodeURIComponent(projectID)}/export`;
    a.download = "";
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
  });
}

function canRemoveTeamMember(m: ProjectTeamMember): boolean {
  if (!me) return false;
  if (m.user_id === me.id) return true;
  return me.global_role === "global_admin";
}

function initTeamForm(id: string) {
  const addBtn = document.getElementById("team-add-btn") as HTMLButtonElement | null;
  const form = document.getElementById("team-form") as HTMLFormElement | null;
  const cancel = document.getElementById("team-cancel") as HTMLButtonElement | null;
  const input = document.getElementById("team-user-input") as HTMLInputElement | null;
  const hidden = document.getElementById("team-user-id") as HTMLInputElement | null;
  const sugs = document.getElementById("team-user-suggestions") as HTMLDivElement | null;
  const msg = document.getElementById("team-msg") as HTMLParagraphElement | null;
  const responsibility = document.getElementById("team-responsibility") as HTMLSelectElement | null;
  const professionHint = document.getElementById("team-profession-hint") as HTMLParagraphElement | null;
  const inviteHint = document.getElementById("team-user-invite-hint") as HTMLDivElement | null;
  const inviteHintText = document.getElementById("team-user-invite-hint-text") as HTMLSpanElement | null;
  const inviteBtn = document.getElementById("team-user-invite-btn") as HTMLButtonElement | null;
  if (!addBtn || !form || !cancel || !input || !hidden || !sugs || !msg || !responsibility) return;

  const hideInviteHint = () => {
    if (inviteHint) inviteHint.style.display = "none";
  };
  const showInviteHint = (q: string) => {
    if (!inviteHint || !inviteHintText) return;
    const looksLikeEmail = /@/.test(q) && /\./.test(q.split("@")[1] || "");
    inviteHintText.textContent = looksLikeEmail
      ? t("projects.detail.team.invite.hint_email") || "Niemand mit dieser E-Mail."
      : t("projects.detail.team.invite.hint") || "Benutzer nicht gefunden?";
    inviteHint.dataset.email = looksLikeEmail ? q : "";
    inviteHint.style.display = "";
  };

  addBtn.addEventListener("click", () => {
    form.style.display = "";
    addBtn.style.display = "none";
    input.focus();
  });
  cancel.addEventListener("click", () => {
    form.style.display = "none";
    addBtn.style.display = "";
    input.value = "";
    hidden.value = "";
    sugs.innerHTML = "";
    hideInviteHint();
    msg.textContent = "";
  });

  input.addEventListener("input", () => {
    const q = input.value.trim();
    const lc = q.toLowerCase();
    hidden.value = "";
    if (!q) {
      sugs.innerHTML = "";
      hideInviteHint();
      return;
    }
    const matches = userOptions
      .filter((u) => (u.display_name + " " + u.email).toLowerCase().includes(lc))
      .slice(0, 8);
    sugs.innerHTML = matches
      .map(
        (u) => `<div class="collab-suggestion" data-id="${esc(u.id)}" data-label="${escAttr(u.display_name || u.email)}">
          <strong>${esc(u.display_name || u.email)}</strong>
          <span class="form-hint">${esc(u.email)}</span>
        </div>`,
      )
      .join("");
    sugs.querySelectorAll<HTMLDivElement>(".collab-suggestion").forEach((el) => {
      el.addEventListener("click", () => {
        hidden.value = el.dataset.id!;
        input.value = el.dataset.label!;
        sugs.innerHTML = "";
        hideInviteHint();
        // t-paliad-148: surface the picked person's profession so the
        // adder sees what firm tier they're staffing on this matter,
        // and gets a warning when the user has no profession set.
        if (professionHint) {
          const picked = userOptions.find((u) => u.id === hidden.value);
          const prof = picked?.profession;
          if (!prof) {
            professionHint.textContent = t("projects.detail.team.form.profession.none") ||
              "Keine Profession gesetzt — kann keine 4-Augen-Genehmigungen erteilen.";
            professionHint.className = "form-hint form-hint--warning";
            professionHint.style.display = "";
          } else {
            const profLabel = tDyn(`projects.team.profession.${prof}`) || prof;
            professionHint.textContent = `${t("projects.detail.team.form.profession.label") || "Profession"}: ${profLabel}`;
            professionHint.className = "form-hint";
            professionHint.style.display = "";
          }
        }
      });
    });

    if (matches.length === 0) {
      showInviteHint(q);
    } else {
      hideInviteHint();
    }
  });

  inviteBtn?.addEventListener("click", () => {
    const sidebarBtn = document.getElementById("sidebar-invite-btn") as HTMLButtonElement | null;
    if (!sidebarBtn) return;
    sidebarBtn.click();
    const prefill = inviteHint?.dataset.email || "";
    if (prefill) {
      const inviteEmail = document.getElementById("invite-email") as HTMLInputElement | null;
      if (inviteEmail) {
        inviteEmail.value = prefill;
        inviteEmail.dispatchEvent(new Event("input", { bubbles: true }));
      }
    }
  });

  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    msg.textContent = "";
    if (!hidden.value) {
      msg.textContent = t("projects.detail.team.error.user_required") || "Benutzer auswählen";
      return;
    }
    const resp = await fetch(`/api/projects/${id}/team`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ user_id: hidden.value, responsibility: responsibility.value }),
    });
    if (!resp.ok) {
      const b = await resp.json().catch(() => ({ error: "unknown" }));
      msg.textContent = b.error || "failed";
      return;
    }
    input.value = "";
    hidden.value = "";
    sugs.innerHTML = "";
    hideInviteHint();
    form.style.display = "none";
    addBtn.style.display = "";
    await loadTeam(id);
    renderTeam();
  });
}

// initNotesContainer hooks the shared Notes module into the project detail's
// Notizen tab. Called once per page load — the list lazy-fetches so other
// tabs aren't slowed down by the notes query on initial render.
let notesInited = false;
function initNotesContainer(projectID: string) {
  if (notesInited) return;
  const container = document.getElementById("notes-container");
  if (!container) return;
  container.setAttribute("data-parent-id", projectID);
  void initNotes(container as HTMLElement, "project", projectID);
  notesInited = true;
}

document.addEventListener("DOMContentLoaded", () => {
  initI18n();
  initSidebar();
  onLangChange(() => {
    renderHeader();
    renderBreadcrumb();
    renderTimeline();
    renderParties();
    renderDeadlines();
    renderAppointments();
    renderChildren();
    renderTeam();
  });
  main();
});
