import { initI18n, t } from "./i18n";
import { initSidebar } from "./sidebar";

// t-paliad-238 Slice A — client bundle for the dedicated
// Submissions/Schriftsätze editor at
//   /projects/{id}/submissions/{code}/draft
//   /projects/{id}/submissions/{code}/draft/{draft_id}
//
// Reads (project_id, submission_code, optional draft_id) from the URL,
// loads the editor payload (draft row + resolved bag + merged bag +
// HTML preview), and wires the sidebar / preview / autosave / export.
//
// Autosave is debounced 500ms after the lawyer stops typing in any
// variable input. Each PATCH returns a fresh editor payload, so the
// preview pane stays in lockstep with the variable overrides.

interface SubmissionDraftJSON {
  id: string;
  project_id: string | null;
  submission_code: string;
  user_id: string;
  name: string;
  variables: Record<string, string>;
  last_exported_at?: string | null;
  last_exported_sha?: string | null;
  created_at: string;
  updated_at: string;
}

interface SubmissionRuleSummary {
  name: string;
  name_en: string;
  submission_code: string;
  primary_party?: string;
  event_type?: string;
  legal_source?: string;
  legal_source_pretty?: string;
}

interface SubmissionDraftView {
  draft: SubmissionDraftJSON;
  rule?: SubmissionRuleSummary;
  resolved_bag: Record<string, string>;
  merged_bag: Record<string, string>;
  preview_html: string;
  lang: string;
  has_template: boolean;
  template_missing?: boolean;
}

interface SubmissionDraftListResponse {
  project_id: string;
  submission_code: string;
  drafts: SubmissionDraftJSON[];
}

interface ParsedPath {
  // Project-scoped path: /projects/{id}/submissions/{code}/draft[/{draft_id}].
  // Global path: /submissions/draft/{draft_id} — projectID + submissionCode are derived
  // from the loaded draft row after fetch.
  projectID: string | null;
  submissionCode: string | null;
  draftID?: string;
  // mode tracks the URL shape we were entered from. Affects redirect
  // semantics when we create a new draft or navigate away.
  mode: "project" | "global";
}

const PROJECT_PATH_RE = /^\/projects\/([0-9a-fA-F-]{36})\/submissions\/([^/]+)\/draft(?:\/([0-9a-fA-F-]{36}))?\/?$/;
const GLOBAL_PATH_RE = /^\/submissions\/draft\/([0-9a-fA-F-]{36})\/?$/;

function parsePath(): ParsedPath | null {
  let m = PROJECT_PATH_RE.exec(window.location.pathname);
  if (m) {
    return {
      projectID: m[1],
      submissionCode: decodeURIComponent(m[2]),
      draftID: m[3],
      mode: "project",
    };
  }
  m = GLOBAL_PATH_RE.exec(window.location.pathname);
  if (m) {
    return {
      projectID: null,
      submissionCode: null,
      draftID: m[1],
      mode: "global",
    };
  }
  return null;
}

function isEN(): boolean {
  return document.documentElement.lang === "en";
}

function escapeHtml(s: string): string {
  return s
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

// ─────────────────────────────────────────────────────────────────────
// Variable contract — DE/EN labels per dotted-path placeholder.
// Mirrors the same shape the email-template variables sidebar uses;
// keeps the lawyer's mental model anchored on the same vocabulary.
// ─────────────────────────────────────────────────────────────────────

interface VariableLabel {
  de: string;
  en: string;
}

interface VariableGroup {
  id: string;
  label: VariableLabel;
  keys: string[];
}

const VARIABLE_LABELS: Record<string, VariableLabel> = {
  "firm.name":                       { de: "Kanzlei",                    en: "Firm" },
  "firm.signature_block":            { de: "Signatur-Block",             en: "Signature block" },
  "today":                           { de: "Heute",                       en: "Today" },
  "today.iso":                       { de: "Heute (ISO)",                 en: "Today (ISO)" },
  "today.long_de":                   { de: "Heute (DE lang)",             en: "Today (DE long)" },
  "today.long_en":                   { de: "Heute (EN lang)",             en: "Today (EN long)" },
  "user.display_name":               { de: "Bearbeiter",                  en: "Author" },
  "user.email":                      { de: "E-Mail",                      en: "Email" },
  "user.office":                     { de: "Büro",                   en: "Office" },
  "project.title":                   { de: "Projekttitel",                en: "Project title" },
  "project.reference":               { de: "Aktenzeichen (intern)",       en: "Internal reference" },
  "project.case_number":             { de: "Aktenzeichen (Gericht)",      en: "Court case number" },
  "project.court":                   { de: "Gericht",                     en: "Court" },
  "project.patent_number":           { de: "Patentnummer",                en: "Patent number" },
  "project.patent_number_upc":       { de: "Patentnummer (UPC-Format)",   en: "Patent number (UPC format)" },
  "project.filing_date":             { de: "Anmeldedatum",                en: "Filing date" },
  "project.grant_date":              { de: "Erteilungsdatum",             en: "Grant date" },
  "project.our_side":                { de: "Unsere Seite",                en: "Our side" },
  "project.our_side_de":             { de: "Unsere Seite (DE)",           en: "Our side (DE)" },
  "project.our_side_en":             { de: "Unsere Seite (EN)",           en: "Our side (EN)" },
  "project.instance_level":          { de: "Instanz",                     en: "Instance" },
  "project.client_number":           { de: "Mandantennummer",             en: "Client number" },
  "project.matter_number":           { de: "Matter-Nummer",               en: "Matter number" },
  "project.proceeding.code":         { de: "Verfahrenstyp (Code)",        en: "Proceeding type (code)" },
  "project.proceeding.name":         { de: "Verfahrenstyp",               en: "Proceeding type" },
  "project.proceeding.name_de":      { de: "Verfahrenstyp (DE)",          en: "Proceeding type (DE)" },
  "project.proceeding.name_en":      { de: "Verfahrenstyp (EN)",          en: "Proceeding type (EN)" },
  "parties.claimant.name":           { de: "Klägerin",               en: "Claimant" },
  "parties.claimant.representative": { de: "Klägerin-Vertreter",     en: "Claimant representative" },
  "parties.defendant.name":          { de: "Beklagte",                    en: "Defendant" },
  "parties.defendant.representative":{ de: "Beklagten-Vertreter",         en: "Defendant representative" },
  "parties.other.name":              { de: "Weitere Partei",              en: "Other party" },
  "parties.other.representative":    { de: "Weitere-Partei-Vertreter",    en: "Other party representative" },
  "rule.submission_code":            { de: "Schriftsatz-Code",            en: "Submission code" },
  "rule.name":                       { de: "Schriftsatz",                 en: "Submission" },
  "rule.name_de":                    { de: "Schriftsatz (DE)",            en: "Submission (DE)" },
  "rule.name_en":                    { de: "Schriftsatz (EN)",            en: "Submission (EN)" },
  "rule.legal_source":               { de: "Rechtsgrundlage (Code)",      en: "Legal source (code)" },
  "rule.legal_source_pretty":        { de: "Rechtsgrundlage",             en: "Legal source" },
  "rule.primary_party":              { de: "Partei (typisch)",            en: "Primary party" },
  "rule.event_type":                 { de: "Schriftsatz-Typ",             en: "Event type" },
  "deadline.due_date":               { de: "Frist (ISO)",                 en: "Deadline (ISO)" },
  "deadline.due_date_long_de":       { de: "Frist (DE lang)",             en: "Deadline (DE long)" },
  "deadline.due_date_long_en":       { de: "Frist (EN lang)",             en: "Deadline (EN long)" },
  "deadline.original_due_date":      { de: "Ursprüngliche Frist",    en: "Original deadline" },
  "deadline.computed_from":          { de: "Frist berechnet aus",         en: "Deadline computed from" },
  "deadline.title":                  { de: "Frist-Titel",                 en: "Deadline title" },
  "deadline.source":                 { de: "Frist-Quelle",                en: "Deadline source" },
};

const VARIABLE_GROUPS: VariableGroup[] = [
  {
    id: "rule",
    label: { de: "Schriftsatz", en: "Submission" },
    keys: [
      "rule.name",
      "rule.legal_source_pretty",
      "rule.primary_party",
      "rule.event_type",
      "rule.submission_code",
    ],
  },
  {
    id: "parties",
    label: { de: "Mandanten & Parteien", en: "Clients & parties" },
    keys: [
      "parties.claimant.name",
      "parties.claimant.representative",
      "parties.defendant.name",
      "parties.defendant.representative",
      "parties.other.name",
      "parties.other.representative",
    ],
  },
  {
    id: "project",
    label: { de: "Verfahren", en: "Proceeding" },
    keys: [
      "project.title",
      "project.case_number",
      "project.court",
      "project.patent_number",
      "project.patent_number_upc",
      "project.filing_date",
      "project.grant_date",
      "project.our_side",
      "project.proceeding.name",
      "project.client_number",
      "project.matter_number",
      "project.reference",
      "project.instance_level",
    ],
  },
  {
    id: "deadline",
    label: { de: "Frist", en: "Deadline" },
    keys: [
      "deadline.due_date",
      "deadline.due_date_long_de",
      "deadline.due_date_long_en",
      "deadline.computed_from",
      "deadline.title",
      "deadline.original_due_date",
    ],
  },
  {
    id: "firm",
    label: { de: "Kanzlei & Datum", en: "Firm & date" },
    keys: [
      "firm.name",
      "user.display_name",
      "user.email",
      "user.office",
      "today.long_de",
      "today.long_en",
    ],
  },
];

function labelFor(key: string): string {
  const entry = VARIABLE_LABELS[key];
  if (!entry) return key;
  return isEN() ? entry.en : entry.de;
}

// ─────────────────────────────────────────────────────────────────────
// Module state
// ─────────────────────────────────────────────────────────────────────

interface State {
  parsed: ParsedPath;
  view: SubmissionDraftView | null;
  drafts: SubmissionDraftJSON[];
  saveTimer: number | null;
  pendingOverrides: Record<string, string> | null;
  inFlight: AbortController | null;
}

const state: State = {
  parsed: null as unknown as ParsedPath,
  view: null,
  drafts: [],
  saveTimer: null,
  pendingOverrides: null,
  inFlight: null,
};

// ─────────────────────────────────────────────────────────────────────
// Boot
// ─────────────────────────────────────────────────────────────────────

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

  const parsed = parsePath();
  if (!parsed) {
    showNotFound();
    return;
  }
  state.parsed = parsed;

  try {
    if (parsed.mode === "global") {
      // Global path: we have a draft_id, fetch by id alone. Drafts
      // list (the sidebar switcher) is scoped to the same project +
      // submission_code AFTER we've loaded the draft.
      if (!parsed.draftID) {
        showNotFound();
        return;
      }
      const view = await fetchGlobalView(parsed.draftID);
      state.view = view;
      // Backfill parsed.* from the loaded draft so the sidebar
      // switcher can list peers; project-less drafts get no peer list
      // beyond themselves (no useful (project, code) cross-section).
      state.parsed = {
        ...parsed,
        projectID: view.draft.project_id,
        submissionCode: view.draft.submission_code,
      };
      if (view.draft.project_id) {
        try {
          const list = await fetchDrafts(state.parsed);
          state.drafts = list.drafts;
        } catch { state.drafts = [view.draft]; }
      } else {
        state.drafts = [view.draft];
      }
      paint();
      return;
    }

    // Project-scoped path: same logic as before.
    if (!parsed.projectID || !parsed.submissionCode) {
      showNotFound();
      return;
    }
    const list = await fetchDrafts(parsed);
    state.drafts = list.drafts;
    let draft: SubmissionDraftJSON | null = null;
    if (parsed.draftID) {
      draft = state.drafts.find((d) => d.id === parsed.draftID) ?? null;
      if (!draft) {
        showNotFound();
        return;
      }
    } else if (state.drafts.length > 0) {
      draft = state.drafts[0];
      // Redirect to the canonical /draft/{id} URL so refresh + share
      // both land on the same draft.
      const url = `/projects/${parsed.projectID}/submissions/${encodeURIComponent(parsed.submissionCode)}/draft/${draft.id}`;
      window.history.replaceState({}, "", url);
      state.parsed = { ...parsed, draftID: draft.id };
    } else {
      draft = await createProjectDraft(parsed);
      state.drafts = [draft];
      const url = `/projects/${parsed.projectID}/submissions/${encodeURIComponent(parsed.submissionCode)}/draft/${draft.id}`;
      window.history.replaceState({}, "", url);
      state.parsed = { ...parsed, draftID: draft.id };
    }
    const view = await fetchView(state.parsed.projectID!, state.parsed.submissionCode!, draft.id);
    state.view = view;
    paint();
  } catch (err) {
    console.error("submission-draft boot:", err);
    showError(isEN() ? "Failed to load draft." : "Entwurf konnte nicht geladen werden.");
  }
}

// ─────────────────────────────────────────────────────────────────────
// API
// ─────────────────────────────────────────────────────────────────────

async function fetchDrafts(p: ParsedPath): Promise<SubmissionDraftListResponse> {
  if (!p.projectID || !p.submissionCode) throw new Error("no project context");
  const url = `/api/projects/${p.projectID}/submissions/${encodeURIComponent(p.submissionCode)}/drafts`;
  const resp = await fetch(url);
  if (!resp.ok) throw new Error(`drafts list ${resp.status}`);
  return resp.json();
}

async function createProjectDraft(p: ParsedPath): Promise<SubmissionDraftJSON> {
  if (!p.projectID || !p.submissionCode) throw new Error("no project context");
  const url = `/api/projects/${p.projectID}/submissions/${encodeURIComponent(p.submissionCode)}/drafts`;
  const resp = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" } });
  if (!resp.ok) throw new Error(`create draft ${resp.status}`);
  const view = (await resp.json()) as SubmissionDraftView;
  return view.draft;
}

async function fetchView(projectID: string, code: string, draftID: string): Promise<SubmissionDraftView> {
  const url = `/api/projects/${projectID}/submissions/${encodeURIComponent(code)}/drafts/${draftID}`;
  const resp = await fetch(url);
  if (!resp.ok) throw new Error(`get draft ${resp.status}`);
  return resp.json();
}

async function fetchGlobalView(draftID: string): Promise<SubmissionDraftView> {
  const resp = await fetch(`/api/submission-drafts/${draftID}`);
  if (!resp.ok) throw new Error(`get draft ${resp.status}`);
  return resp.json();
}

async function patchDraft(payload: { name?: string; variables?: Record<string, string>; project_id?: string | null }): Promise<SubmissionDraftView> {
  const p = state.parsed;
  if (!p.draftID) throw new Error("no draft id");
  if (state.inFlight) {
    state.inFlight.abort();
    state.inFlight = null;
  }
  const ctl = new AbortController();
  state.inFlight = ctl;
  // The global PATCH endpoint accepts both project-scoped and
  // project-less drafts — route everything through it so attach (set
  // project_id) works from both URL shapes.
  const url = `/api/submission-drafts/${p.draftID}`;
  try {
    const resp = await fetch(url, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
      signal: ctl.signal,
    });
    if (!resp.ok) throw new Error(`patch draft ${resp.status}`);
    return resp.json();
  } finally {
    if (state.inFlight === ctl) state.inFlight = null;
  }
}

async function deleteDraft(): Promise<void> {
  const p = state.parsed;
  if (!p.draftID) return;
  const resp = await fetch(`/api/submission-drafts/${p.draftID}`, { method: "DELETE" });
  if (!resp.ok && resp.status !== 204) throw new Error(`delete draft ${resp.status}`);
}

// ─────────────────────────────────────────────────────────────────────
// Render
// ─────────────────────────────────────────────────────────────────────

function paint(): void {
  if (!state.view) return;
  hide("submission-draft-loading");
  hide("submission-draft-notfound");
  hide("submission-draft-error");
  show("submission-draft-body");

  paintHeader();
  paintBackLink();
  paintNoProjectBanner();
  paintSwitcher();
  paintNameRow();
  paintVariables();
  paintPreview();
}

function paintHeader(): void {
  const view = state.view!;
  const title = document.getElementById("submission-draft-title");
  if (title) {
    const ruleName = view.rule?.name ?? view.draft.submission_code;
    title.textContent = ruleName;
  }
  const subtitle = document.getElementById("submission-draft-subtitle");
  if (subtitle) {
    const code = view.draft.submission_code;
    const source = view.rule?.legal_source_pretty ?? view.rule?.legal_source ?? "";
    const parts: string[] = [code];
    if (source) parts.push(source);
    subtitle.textContent = parts.join(" · ");
  }
}

function paintBackLink(): void {
  const back = document.getElementById("submission-draft-back-link") as HTMLAnchorElement | null;
  if (!back || !state.view) return;
  if (state.view.draft.project_id) {
    back.href = `/projects/${state.view.draft.project_id}/submissions`;
    back.textContent = isEN() ? "← Back to project" : "← Zurück zum Projekt";
  } else {
    back.href = "/submissions";
    back.textContent = isEN() ? "← Back to drafts" : "← Zurück zur Übersicht";
  }
}

// paintNoProjectBanner adds (or removes) the "Kein Projekt zugeordnet"
// banner above the editor body. The banner offers a "Projekt zuweisen"
// button that opens an inline project picker — same modal pattern the
// /submissions/new page uses. Removed once the draft has a project_id.
function paintNoProjectBanner(): void {
  const body = document.getElementById("submission-draft-body");
  if (!body || !state.view) return;
  let banner = document.getElementById("submission-draft-noproject-banner");

  if (state.view.draft.project_id) {
    if (banner) banner.remove();
    return;
  }

  const msg = isEN()
    ? "No project assigned — all variables are filled manually."
    : "Kein Projekt zugeordnet — alle Variablen werden manuell befüllt.";
  const cta = isEN() ? "Assign project…" : "Projekt zuweisen…";
  const html = `<p class="submission-draft-noproject-banner-msg">${escapeHtml(msg)}</p>
                <button type="button" id="submission-draft-noproject-assign"
                        class="btn-secondary btn-small">${escapeHtml(cta)}</button>`;

  if (banner) {
    banner.innerHTML = html;
  } else {
    banner = document.createElement("aside");
    banner.id = "submission-draft-noproject-banner";
    banner.className = "submission-draft-noproject-banner";
    banner.innerHTML = html;
    // Insert before the header.
    const header = body.querySelector(".submission-draft-header");
    if (header && header.parentElement) {
      header.parentElement.insertBefore(banner, header);
    } else {
      body.prepend(banner);
    }
  }

  const btn = document.getElementById("submission-draft-noproject-assign") as HTMLButtonElement | null;
  if (btn) btn.onclick = () => openProjectAssignPicker();
}

function paintSwitcher(): void {
  const sel = document.getElementById("submission-draft-pick") as HTMLSelectElement | null;
  if (!sel || !state.view) return;
  sel.innerHTML = state.drafts
    .map((d) => `<option value="${escapeHtml(d.id)}"${d.id === state.view!.draft.id ? " selected" : ""}>${escapeHtml(d.name)}</option>`)
    .join("");
  sel.onchange = () => {
    const id = sel.value;
    if (!id || !state.view) return;
    if (id === state.view.draft.id) return;
    const p = state.parsed;
    const url = `/projects/${p.projectID}/submissions/${encodeURIComponent(p.submissionCode)}/draft/${id}`;
    window.location.href = url;
  };
}

function paintNameRow(): void {
  const input = document.getElementById("submission-draft-name") as HTMLInputElement | null;
  const del = document.getElementById("submission-draft-delete-btn") as HTMLButtonElement | null;
  if (input && state.view) {
    input.value = state.view.draft.name;
    input.onchange = () => {
      const newName = input.value.trim();
      if (!newName || newName === state.view!.draft.name) return;
      void renameDraft(newName);
    };
  }
  if (del) del.onclick = () => onDelete();

  const newBtn = document.getElementById("submission-draft-new-btn") as HTMLButtonElement | null;
  if (newBtn) newBtn.onclick = () => onCreateNew();

  const exportBtn = document.getElementById("submission-draft-export-btn") as HTMLButtonElement | null;
  if (exportBtn) exportBtn.onclick = () => onExport(exportBtn);
}

function paintVariables(): void {
  const host = document.getElementById("submission-draft-variables");
  if (!host || !state.view) return;
  const overrides = state.view.draft.variables ?? {};
  const resolved = state.view.resolved_bag ?? {};
  const merged = state.view.merged_bag ?? {};

  let html = "";
  for (const group of VARIABLE_GROUPS) {
    const groupLabel = isEN() ? group.label.en : group.label.de;
    html += `<section class="submission-draft-var-group" data-group="${group.id}">`;
    html += `<h3 class="submission-draft-var-group-title">${escapeHtml(groupLabel)}</h3>`;
    for (const key of group.keys) {
      const label = labelFor(key);
      const override = overrides[key];
      const resolvedVal = resolved[key] ?? "";
      const mergedVal = merged[key] ?? "";
      const overridden = Object.prototype.hasOwnProperty.call(overrides, key);
      html += `<label class="submission-draft-var-row" data-key="${escapeHtml(key)}">`;
      html += `<span class="submission-draft-var-label">${escapeHtml(label)}</span>`;
      html += `<input type="text" class="submission-draft-var-input entity-form-input"`;
      html += ` data-var="${escapeHtml(key)}"`;
      html += ` value="${escapeHtml(overridden ? override : resolvedVal)}"`;
      html += ` data-resolved="${escapeHtml(resolvedVal)}" />`;
      const hintParts: string[] = [];
      hintParts.push(`<code>{{${escapeHtml(key)}}}</code>`);
      if (overridden) {
        if (override === "") {
          hintParts.push(`<span class="submission-draft-var-marker">${escapeHtml(isEN() ? "→ [NO VALUE: " + key + "]" : "→ [KEIN WERT: " + key + "]")}</span>`);
        } else if (override !== resolvedVal) {
          const original = resolvedVal === "" ? (isEN() ? "(empty)" : "(leer)") : resolvedVal;
          hintParts.push(`<span class="submission-draft-var-was">${escapeHtml((isEN() ? "Project: " : "Projekt: ") + original)}</span>`);
        }
      }
      html += `<span class="submission-draft-var-hint">${hintParts.join(" · ")}</span>`;
      if (overridden && override !== resolvedVal) {
        html += `<button type="button" class="submission-draft-var-reset btn-small btn-link" data-reset-key="${escapeHtml(key)}">${escapeHtml(isEN() ? "Reset" : "Zurücksetzen")}</button>`;
      }
      html += `</label>`;
      // Visual hint: marker text appears in preview when override is "".
      void mergedVal;
    }
    html += `</section>`;
  }
  host.innerHTML = html;

  host.querySelectorAll<HTMLInputElement>(".submission-draft-var-input").forEach((inp) => {
    inp.addEventListener("input", () => onVarChange(inp));
  });
  host.querySelectorAll<HTMLButtonElement>(".submission-draft-var-reset").forEach((btn) => {
    btn.addEventListener("click", () => onVarReset(btn.dataset.resetKey ?? ""));
  });
}

function paintPreview(): void {
  const host = document.getElementById("submission-draft-preview");
  if (!host || !state.view) return;
  host.innerHTML = state.view.preview_html ?? "";
  wireDraftVars(host);
}

// t-paliad-261 (B) — click a substituted variable in the preview to
// jump to the matching sidebar input. Re-wires on every paintPreview
// since the preview HTML is replaced wholesale. The server side wraps
// each substituted placeholder (resolved OR missing marker) in
// <span class="draft-var" data-var="<key>">…</span>; clicks here scroll
// the corresponding input into view, focus + select, and flash the row.
// If the key has no matching sidebar input (derived variables not
// exposed in VARIABLE_GROUPS), the click is a silent no-op — the span
// is still rendered so the user gets the visible hint that this is a
// resolved variable.
function wireDraftVars(previewHost: HTMLElement): void {
  previewHost.querySelectorAll<HTMLElement>(".draft-var").forEach((el) => {
    const key = el.dataset.var;
    if (!key) return;
    if (findVarInput(key)) {
      el.classList.add("draft-var--has-input");
      el.setAttribute("role", "button");
      el.setAttribute("tabindex", "0");
      el.setAttribute(
        "aria-label",
        (isEN() ? "Edit variable " : "Variable bearbeiten: ") + labelFor(key),
      );
    }
    el.addEventListener("click", (ev) => onDraftVarClick(key, ev));
    el.addEventListener("keydown", (ev) => {
      if (ev.key === "Enter" || ev.key === " ") {
        ev.preventDefault();
        onDraftVarClick(key, ev);
      }
    });
  });
}

function findVarInput(key: string): HTMLInputElement | null {
  const host = document.getElementById("submission-draft-variables");
  if (!host) return null;
  return host.querySelector<HTMLInputElement>(
    `.submission-draft-var-input[data-var="${cssEscape(key)}"]`,
  );
}

function cssEscape(s: string): string {
  // CSS.escape covers our placeholder keys ([A-Za-z][A-Za-z0-9_.]*) but
  // older browsers may lack it; defensive fallback escapes characters
  // CSS treats as special. Placeholder keys never carry whitespace or
  // quotes so escaping is straightforward.
  if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
    return CSS.escape(s);
  }
  return s.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, "\\$1");
}

function onDraftVarClick(key: string, ev: Event): void {
  const input = findVarInput(key);
  if (!input) return;
  ev.preventDefault();
  ev.stopPropagation();
  // Smooth-scroll the input into view, then focus on the next tick so
  // the scroll animation has started and the focus call doesn't trigger
  // a second jarring jump.
  input.scrollIntoView({ behavior: "smooth", block: "center" });
  window.setTimeout(() => {
    input.focus();
    try {
      input.select();
    } catch {
      /* select() throws on number/email inputs; safe to ignore */
    }
  }, 50);
  flashVarRow(input);
}

function flashVarRow(input: HTMLElement): void {
  const row = input.closest<HTMLElement>(".submission-draft-var-row");
  if (!row) return;
  row.classList.remove("submission-draft-var-row--flash");
  // Force reflow so removing+re-adding the class restarts the animation
  // even on rapid successive clicks.
  void row.offsetWidth;
  row.classList.add("submission-draft-var-row--flash");
  window.setTimeout(() => row.classList.remove("submission-draft-var-row--flash"), 1200);
}

// ─────────────────────────────────────────────────────────────────────
// Event handlers
// ─────────────────────────────────────────────────────────────────────

function onVarChange(input: HTMLInputElement): void {
  const key = input.dataset.var;
  if (!key || !state.view) return;
  // Stage the override on the draft view so paintPreview reflects.
  const overrides = { ...state.view.draft.variables };
  overrides[key] = input.value;
  state.view.draft.variables = overrides;
  state.pendingOverrides = overrides;
  setSaveStatus(isEN() ? "Saving…" : "Speichert…");
  if (state.saveTimer) window.clearTimeout(state.saveTimer);
  state.saveTimer = window.setTimeout(() => {
    void flushAutosave();
  }, 500);
}

function onVarReset(key: string): void {
  if (!state.view) return;
  const overrides = { ...state.view.draft.variables };
  delete overrides[key];
  state.view.draft.variables = overrides;
  state.pendingOverrides = overrides;
  setSaveStatus(isEN() ? "Saving…" : "Speichert…");
  if (state.saveTimer) window.clearTimeout(state.saveTimer);
  state.saveTimer = window.setTimeout(() => {
    void flushAutosave();
  }, 500);
}

async function flushAutosave(): Promise<void> {
  if (!state.pendingOverrides) return;
  const payload = { variables: state.pendingOverrides };
  state.pendingOverrides = null;
  // t-paliad-261 (A) — paintVariables() below replaces every input in
  // the sidebar via innerHTML, which blows away the active-element
  // reference. Capture the focused input's key + selection range before
  // the repaint and restore on the new element after, so the user can
  // keep typing without clicking back into the field.
  const focusSnap = captureVarFocus();
  try {
    const view = await patchDraft(payload);
    state.view = view;
    paintVariables();
    paintPreview();
    restoreVarFocus(focusSnap);
    setSaveStatus(isEN() ? "Saved" : "Gespeichert");
  } catch (err) {
    if ((err as Error).name === "AbortError") return;
    console.error("submission-draft autosave:", err);
    setSaveStatus(isEN() ? "Save failed" : "Speichern fehlgeschlagen", true);
  }
}

// captureVarFocus / restoreVarFocus — focus-preservation across the
// paintVariables() innerHTML-replace cycle (t-paliad-261 part A).
// Tracks selection start/end/direction so the cursor lands exactly
// where it was before the repaint, including any active selection
// range. Handles both <input> and <textarea> via the shared
// HTMLInputElement|HTMLTextAreaElement contract for selectionStart /
// selectionEnd / selectionDirection / setSelectionRange.

interface VarFocusSnapshot {
  key: string;
  start: number | null;
  end: number | null;
  dir: "forward" | "backward" | "none";
}

type SelectableEl = HTMLInputElement | HTMLTextAreaElement;

function isVarField(el: Element | null): el is SelectableEl {
  if (!el) return false;
  if (!(el instanceof HTMLInputElement) && !(el instanceof HTMLTextAreaElement)) {
    return false;
  }
  return el.classList.contains("submission-draft-var-input");
}

function captureVarFocus(): VarFocusSnapshot | null {
  const active = document.activeElement;
  if (!isVarField(active)) return null;
  const key = active.dataset.var;
  if (!key) return null;
  return {
    key,
    start: active.selectionStart,
    end: active.selectionEnd,
    dir: (active.selectionDirection as "forward" | "backward" | "none" | null) ?? "forward",
  };
}

function restoreVarFocus(snap: VarFocusSnapshot | null): void {
  if (!snap) return;
  const host = document.getElementById("submission-draft-variables");
  if (!host) return;
  const next = host.querySelector<SelectableEl>(
    `.submission-draft-var-input[data-var="${cssEscape(snap.key)}"]`,
  );
  if (!next) return;
  next.focus();
  if (snap.start !== null && snap.end !== null) {
    try {
      next.setSelectionRange(snap.start, snap.end, snap.dir);
    } catch {
      /* setSelectionRange throws on inputs whose type doesn't support
         selection ranges (number, email, etc.); safe to ignore — the
         focus() call above is enough for those. */
    }
  }
}

async function renameDraft(newName: string): Promise<void> {
  setSaveStatus(isEN() ? "Saving…" : "Speichert…");
  try {
    const view = await patchDraft({ name: newName });
    state.view = view;
    // Refresh the draft list cache.
    const idx = state.drafts.findIndex((d) => d.id === view.draft.id);
    if (idx >= 0) state.drafts[idx].name = view.draft.name;
    paintSwitcher();
    setSaveStatus(isEN() ? "Renamed" : "Umbenannt");
  } catch (err) {
    if ((err as Error).name === "AbortError") return;
    console.error("submission-draft rename:", err);
    const msg = (err as Error).message?.includes("409")
      ? (isEN() ? "Name already in use" : "Name bereits vergeben")
      : (isEN() ? "Rename failed" : "Umbenennen fehlgeschlagen");
    setSaveStatus(msg, true);
  }
}

async function onCreateNew(): Promise<void> {
  const p = state.parsed;
  // From a project-less draft, "Neuer Entwurf" can't auto-pick a
  // (project, code) cross-section — kick the user out to the global
  // picker instead.
  if (!p.projectID || !p.submissionCode) {
    window.location.href = "/submissions/new";
    return;
  }
  try {
    const fresh = await createProjectDraft(p);
    const url = `/projects/${p.projectID}/submissions/${encodeURIComponent(p.submissionCode)}/draft/${fresh.id}`;
    window.location.href = url;
  } catch (err) {
    console.error("submission-draft new:", err);
    setSaveStatus(isEN() ? "Create failed" : "Anlegen fehlgeschlagen", true);
  }
}

async function onDelete(): Promise<void> {
  if (!state.view) return;
  const msg = isEN()
    ? `Delete draft "${state.view.draft.name}"? This cannot be undone.`
    : `Entwurf "${state.view.draft.name}" löschen? Das kann nicht rückgängig gemacht werden.`;
  if (!window.confirm(msg)) return;
  try {
    await deleteDraft();
    const p = state.parsed;
    const url = p.projectID && p.submissionCode
      ? `/projects/${p.projectID}/submissions/${encodeURIComponent(p.submissionCode)}/draft`
      : "/submissions";
    window.location.href = url;
  } catch (err) {
    console.error("submission-draft delete:", err);
    setSaveStatus(isEN() ? "Delete failed" : "Löschen fehlgeschlagen", true);
  }
}

async function onExport(btn: HTMLButtonElement): Promise<void> {
  if (!state.view) return;
  const p = state.parsed;
  if (!p.draftID) return;
  const originalLabel = btn.textContent ?? "";
  btn.disabled = true;
  btn.textContent = isEN() ? "Exporting…" : "Exportiert…";
  try {
    // Use the global export endpoint for both project-scoped and
    // project-less drafts; the handler routes audit + project_events
    // writes based on the draft row's project_id.
    const url = `/api/submission-drafts/${p.draftID}/export`;
    const resp = await fetch(url, { method: "POST" });
    if (!resp.ok) {
      let detail = "";
      try {
        const data = (await resp.json()) as { error?: string };
        detail = data.error ?? "";
      } catch { /* fallthrough */ }
      alert((isEN() ? "Export failed." : "Export fehlgeschlagen.") + (detail ? `\n\n${detail}` : ""));
      return;
    }
    const blob = await resp.blob();
    const filename = parseFilename(resp.headers.get("Content-Disposition") ?? "") ?? `${state.view.draft.submission_code}.docx`;
    triggerDownload(blob, filename);
  } finally {
    btn.disabled = false;
    btn.textContent = originalLabel;
  }
}

// ─────────────────────────────────────────────────────────────────────
// Project assign picker (project-less → project-scoped)
// ─────────────────────────────────────────────────────────────────────

interface PickerProjectRow {
  id: string;
  title: string;
  reference?: string | null;
}

let assignPickerProjects: PickerProjectRow[] = [];
let assignPickerLoaded = false;

function openProjectAssignPicker(): void {
  ensureAssignPickerDOM();
  const modal = document.getElementById("submission-draft-assign-modal");
  if (modal) modal.style.display = "";
  if (!assignPickerLoaded) {
    void loadAssignPickerProjects();
  } else {
    renderAssignPickerList();
  }
  const searchInput = document.getElementById("submission-draft-assign-search") as HTMLInputElement | null;
  if (searchInput) {
    searchInput.value = "";
    setTimeout(() => searchInput.focus(), 50);
  }
}

function closeProjectAssignPicker(): void {
  const modal = document.getElementById("submission-draft-assign-modal");
  if (modal) modal.style.display = "none";
}

function ensureAssignPickerDOM(): void {
  if (document.getElementById("submission-draft-assign-modal")) return;
  const titleTxt = isEN() ? "Assign project" : "Projekt zuweisen";
  const placeholder = isEN()
    ? "Search project (title or reference)…"
    : "Projekt suchen (Titel oder Aktenzeichen)…";
  const loadingTxt = isEN() ? "Loading projects…" : "Lädt Projekte…";
  const emptyTxt = isEN() ? "No visible projects." : "Keine sichtbaren Projekte.";

  const modal = document.createElement("div");
  modal.id = "submission-draft-assign-modal";
  modal.className = "modal-overlay";
  modal.setAttribute("role", "dialog");
  modal.setAttribute("aria-modal", "true");
  modal.style.display = "none";
  modal.innerHTML = `
    <div class="modal-card">
      <header class="modal-header">
        <h2>${escapeHtml(titleTxt)}</h2>
        <button type="button" id="submission-draft-assign-close" class="modal-close" aria-label="Close">×</button>
      </header>
      <div class="modal-body">
        <input type="search" id="submission-draft-assign-search" class="entity-form-input" placeholder="${escapeHtml(placeholder)}" />
        <ul id="submission-draft-assign-list" class="submissions-new-project-list"></ul>
        <p id="submission-draft-assign-loading" class="entity-events-empty" style="display:none">${escapeHtml(loadingTxt)}</p>
        <p id="submission-draft-assign-empty" class="entity-empty" style="display:none">${escapeHtml(emptyTxt)}</p>
      </div>
    </div>`;
  document.body.appendChild(modal);

  modal.addEventListener("click", (e) => {
    if (e.target === modal) closeProjectAssignPicker();
  });
  const closeBtn = document.getElementById("submission-draft-assign-close");
  if (closeBtn) closeBtn.addEventListener("click", () => closeProjectAssignPicker());
  const searchInput = document.getElementById("submission-draft-assign-search") as HTMLInputElement | null;
  if (searchInput) searchInput.addEventListener("input", () => renderAssignPickerList());
  document.addEventListener("keydown", (e) => {
    if (e.key === "Escape" && modal.style.display !== "none") closeProjectAssignPicker();
  });
}

async function loadAssignPickerProjects(): Promise<void> {
  const loadingEl = document.getElementById("submission-draft-assign-loading");
  if (loadingEl) loadingEl.style.display = "";
  try {
    const resp = await fetch("/api/projects?status=active");
    if (!resp.ok) throw new Error(`projects list ${resp.status}`);
    const rows = (await resp.json()) as PickerProjectRow[];
    assignPickerProjects = rows ?? [];
    assignPickerLoaded = true;
  } catch (err) {
    console.error("submission-draft assignPicker:", err);
    assignPickerProjects = [];
  } finally {
    if (loadingEl) loadingEl.style.display = "none";
  }
  renderAssignPickerList();
}

function renderAssignPickerList(): void {
  const list = document.getElementById("submission-draft-assign-list");
  const empty = document.getElementById("submission-draft-assign-empty");
  if (!list || !empty) return;

  const searchInput = document.getElementById("submission-draft-assign-search") as HTMLInputElement | null;
  const term = (searchInput?.value ?? "").trim().toLowerCase();

  const matches = assignPickerProjects.filter((p) => {
    if (term === "") return true;
    const hay = [p.title, p.reference ?? ""].join(" ").toLowerCase();
    return hay.includes(term);
  }).slice(0, 50);

  if (matches.length === 0) {
    list.innerHTML = "";
    empty.style.display = "";
    return;
  }
  empty.style.display = "none";

  list.innerHTML = matches.map((p) => {
    const ref = p.reference ? `<span class="entity-ref">${escapeHtml(p.reference)}</span> ` : "";
    return `<li class="submissions-new-project-item" data-id="${escapeHtml(p.id)}">${ref}<span class="submissions-new-project-title">${escapeHtml(p.title)}</span></li>`;
  }).join("");

  list.querySelectorAll<HTMLLIElement>(".submissions-new-project-item").forEach((li) => {
    li.addEventListener("click", () => {
      const pid = li.dataset.id;
      if (pid) void onAssignProject(pid);
    });
  });
}

async function onAssignProject(projectID: string): Promise<void> {
  closeProjectAssignPicker();
  setSaveStatus(isEN() ? "Assigning…" : "Wird zugewiesen…");
  try {
    const view = await patchDraft({ project_id: projectID });
    state.view = view;
    setSaveStatus(isEN() ? "Project assigned" : "Projekt zugewiesen");
    // Redirect to the project-scoped URL so the editor's URL matches the
    // attached project and the project-scoped draft list (sidebar
    // switcher) loads on refresh.
    const code = view.draft.submission_code;
    window.location.href = `/projects/${projectID}/submissions/${encodeURIComponent(code)}/draft/${view.draft.id}`;
  } catch (err) {
    console.error("submission-draft assign:", err);
    setSaveStatus(isEN() ? "Assign failed" : "Zuweisung fehlgeschlagen", true);
  }
}

// ─────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────

function show(id: string): void {
  const el = document.getElementById(id);
  if (el) el.style.display = "";
}

function hide(id: string): void {
  const el = document.getElementById(id);
  if (el) el.style.display = "none";
}

function showNotFound(): void {
  hide("submission-draft-loading");
  hide("submission-draft-body");
  show("submission-draft-notfound");
}

function showError(msg: string): void {
  hide("submission-draft-loading");
  hide("submission-draft-body");
  const el = document.getElementById("submission-draft-error");
  if (el) {
    el.textContent = msg;
    el.style.display = "";
  }
}

function setSaveStatus(msg: string, errorState: boolean = false): void {
  const el = document.getElementById("submission-draft-savestatus");
  if (!el) return;
  el.textContent = msg;
  el.classList.toggle("submission-draft-savestatus--error", errorState);
}

function parseFilename(header: string): string | null {
  const m = /filename\s*=\s*"?([^";]+)"?/i.exec(header);
  return m ? m[1] : null;
}

function triggerDownload(blob: Blob, filename: string): void {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  setTimeout(() => URL.revokeObjectURL(url), 0);
}

// Keep t() referenced so the bundler doesn't tree-shake it; future
// affordances will use the per-page i18n keys.
void t;

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", () => { void boot(); });
} else {
  void boot();
}
