// t-paliad-216 Slice B (initial) + t-paliad-217 Slice C (rewrite) —
// modal for the "Suggest changes" approval action.
//
// The approver authors a counter-proposal: edits any field on the
// underlying deadline / appointment AND/OR leaves a free-text note. On
// submit the caller POSTs to /api/approval-requests/{id}/suggest-changes,
// which closes the OLD row as `changes_requested` and spawns a NEW pending
// row authored by the approver carrying counter_payload as its payload.
//
// Scope (t-paliad-217 m's Q1 Reading A — 2026-05-20):
//   - Every editable field on the entity is in the form, not just the
//     date allowlist that triggers approval (t-paliad-138 §Q4). The
//     backend's counter-allowlist (buildCounterSetClauses in
//     approval_service.go) accepts the wider set:
//       deadline:    title, due_date, original_due_date, warning_date,
//                    description, notes, rule_code, event_type_ids
//       appointment: title, start_at, end_at, description, location,
//                    appointment_type
//   - Lifecycle restriction: update-only. shape-list.ts hides the
//     suggest_changes button for create / complete / delete; this modal
//     refuses to open on them as defence-in-depth.
//
// Built on the unified openModal() primitive (t-paliad-217 Slice A) —
// the primitive owns ESC, focus, backdrop, close button, browser
// back-button, mobile takeover. This module only constructs the body.
//
// API:
//   const result = await openApprovalEditModal({
//     entityType: "deadline",
//     lifecycleEvent: "update",
//     payload: {...},  // requester's proposed values (= current entity row)
//     preImage: {...}, // pre-mutation values (for "vorher" diff hints)
//   });
//   if (result) {
//     // result.counterPayload + result.note ready to POST
//   } else {
//     // user cancelled
//   }

import { t } from "../i18n";
import {
  attachEventTypePicker,
  fetchEventTypes,
  type PickerHandle,
} from "../event-types";
import { openModal } from "./modal";

export interface ApprovalEditModalArgs {
  entityType: "deadline" | "appointment";
  lifecycleEvent: string;
  payload: Record<string, unknown> | null;
  preImage: Record<string, unknown> | null;
  // Optional context for the read-only context section. The caller can
  // hydrate these from the row's API response (project_title,
  // requester_name, requested_at) when available; the modal degrades
  // gracefully when they're missing.
  projectTitle?: string;
  requesterName?: string;
  requestedAt?: string;
}

export interface ApprovalEditModalResult {
  counterPayload: Record<string, unknown>;
  note: string;
}

// FieldSpec — one editable input row. The type determines the <input>
// (or <textarea>) shape; getValue / setValue normalise the form-element
// value to the server-friendly counter_payload shape.
interface FieldSpec {
  key: string;
  labelKey: string;       // i18n key
  inputType: "text" | "date" | "datetime-local" | "textarea";
  // Required = title (NOT NULL on the column). Other fields are nullable;
  // empty string clears (server's addText helper handles this).
  required?: boolean;
}

// Deadline-only fields rendered in the editable section. `rule_code` and
// `event_type_ids` are intentionally NOT here — they're bundled into the
// dedicated "Verfahrenshandlung" section below the base fields so the
// event-type (parent concept) reads before the rule (m/paliad#56).
const DEADLINE_FIELDS: ReadonlyArray<FieldSpec> = [
  { key: "title",             labelKey: "deadlines.field.title",        inputType: "text",     required: true },
  { key: "due_date",          labelKey: "deadlines.field.due",          inputType: "date" },
  { key: "original_due_date", labelKey: "approvals.suggest.field.original_due_date", inputType: "date" },
  { key: "warning_date",      labelKey: "approvals.suggest.field.warning_date",      inputType: "date" },
  { key: "description",       labelKey: "approvals.suggest.field.description",       inputType: "textarea" },
  { key: "notes",             labelKey: "deadlines.field.notes",        inputType: "textarea" },
];

const APPOINTMENT_FIELDS: ReadonlyArray<FieldSpec> = [
  { key: "title",            labelKey: "appointments.field.title",       inputType: "text",     required: true },
  { key: "start_at",         labelKey: "appointments.field.start",       inputType: "datetime-local" },
  { key: "end_at",           labelKey: "appointments.field.end",         inputType: "datetime-local" },
  { key: "location",         labelKey: "appointments.field.location",    inputType: "text" },
  { key: "appointment_type", labelKey: "appointments.field.type",        inputType: "text" },
  { key: "description",      labelKey: "appointments.field.description", inputType: "textarea" },
];

export async function openApprovalEditModal(
  args: ApprovalEditModalArgs,
): Promise<ApprovalEditModalResult | null> {
  if (args.lifecycleEvent !== "update") {
    window.alert(t("approvals.suggest.unsupported_lifecycle"));
    return null;
  }

  const fields = args.entityType === "deadline" ? DEADLINE_FIELDS : APPOINTMENT_FIELDS;
  const original = (args.payload ?? {}) as Record<string, unknown>;
  const preImage = (args.preImage ?? {}) as Record<string, unknown>;

  // Build the body element imperatively so we can wire input handlers
  // before openModal mounts the dialog.
  const body = document.createElement("div");
  body.className = "approval-suggest-body";

  body.appendChild(renderIntro());
  body.appendChild(renderFieldsSection(fields, original, preImage));

  // event_type_ids picker (deadline-only) — async because the picker
  // needs to fetch the firm's event-type catalogue. We attach a host
  // element synchronously and populate it once the fetch returns.
  let eventTypePicker: PickerHandle | null = null;
  let eventTypePickerLoaded = false;
  if (args.entityType === "deadline") {
    const pickerSection = renderEventTypePickerSection(original, preImage);
    body.appendChild(pickerSection.section);
    void (async () => {
      try {
        await fetchEventTypes();
        eventTypePicker = attachEventTypePicker(pickerSection.host, {
          initialIDs: (original.event_type_ids as string[] | undefined) ?? [],
        });
        eventTypePickerLoaded = true;
      } catch (_e) {
        // Fail-soft: leave the section empty; counter still works
        // without event_type_ids in the payload.
        pickerSection.host.textContent = t("approvals.suggest.event_type_picker_unavailable");
      }
    })();
  }

  body.appendChild(renderContextSection(args, original));
  const noteEl = renderNoteSection();
  body.appendChild(noteEl.section);

  // Read inputs back at submit time. The same list is what we listen to
  // for the dirty-state gate.
  const fieldInputs = Array.from(
    body.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>("[data-suggest-field]"),
  );

  return openModal<ApprovalEditModalResult>({
    title: `${t("approvals.suggest.modal_title")} — ${t(("approvals.entity." + args.entityType) as never)}`,
    body,
    size: "lg",
    primary: {
      label: t("approvals.suggest.submit"),
      handler: (close) => {
        const result = buildResult(fieldInputs, noteEl.textarea, original, eventTypePicker, eventTypePickerLoaded);
        if (!result.dirty && !result.note) {
          // Server enforces too. Client-side guard avoids the 400 round-trip.
          window.alert(t("approvals.suggest.submit_disabled_hint"));
          return;
        }
        close({
          counterPayload: result.counterPayload,
          note: result.note,
        });
      },
    },
    secondary: { label: t("approvals.suggest.cancel") },
  });
}

function renderIntro(): HTMLElement {
  const p = document.createElement("p");
  p.className = "approval-suggest-intro muted";
  p.textContent = t("approvals.suggest.intro");
  return p;
}

function renderFieldsSection(
  fields: ReadonlyArray<FieldSpec>,
  original: Record<string, unknown>,
  preImage: Record<string, unknown>,
): HTMLElement {
  const section = document.createElement("section");
  section.className = "approval-suggest-section approval-suggest-section--editable";
  const h = document.createElement("h3");
  h.className = "approval-suggest-section-title";
  h.textContent = t("approvals.suggest.section.editable");
  section.appendChild(h);

  for (const f of fields) {
    section.appendChild(renderSingleField(f, original, preImage));
  }
  return section;
}

// Verfahrenshandlung section — bundles the event-type picker and the
// rule_code input so the editor reads "what procedural step? which rule
// cites it?" instead of two disconnected fields with rule above type
// (m/paliad#56). The hint underneath spells out the parent/child
// relationship so first-time editors don't read them as peers.
function renderEventTypePickerSection(
  original: Record<string, unknown>,
  preImage: Record<string, unknown>,
): { section: HTMLElement; host: HTMLElement } {
  const section = document.createElement("section");
  section.className = "approval-suggest-section approval-suggest-section--editable";

  const h = document.createElement("h3");
  h.className = "approval-suggest-section-title";
  h.textContent = t("approvals.suggest.section.event_type_rule");
  section.appendChild(h);

  const host = document.createElement("div");
  host.className = "approval-suggest-event-type-picker";
  section.appendChild(host);

  // Rule citation — rendered as a sub-field directly beneath the picker so
  // the visual hierarchy matches the conceptual one (rule is meta on the
  // event type, not a peer).
  const ruleField: FieldSpec = {
    key: "rule_code",
    labelKey: "approvals.suggest.field.rule_code",
    inputType: "text",
  };
  section.appendChild(renderSingleField(ruleField, original, preImage));

  return { section, host };
}

// renderSingleField builds one labelled input in the same shape as the
// fields-section loop. Extracted so the Verfahrenshandlung section can
// host the rule_code input next to the picker without duplicating the
// wiring (dirty-tracking, pre_image hint, label/for binding).
function renderSingleField(
  f: FieldSpec,
  original: Record<string, unknown>,
  preImage: Record<string, unknown>,
): HTMLElement {
  const wrap = document.createElement("div");
  wrap.className = "form-field approval-suggest-field";

  const label = document.createElement("label");
  label.textContent = t(f.labelKey as never);
  wrap.appendChild(label);

  const value = formatFieldForInput(original[f.key], f.inputType);

  let input: HTMLInputElement | HTMLTextAreaElement;
  if (f.inputType === "textarea") {
    input = document.createElement("textarea");
    input.rows = 3;
    (input as HTMLTextAreaElement).value = value;
  } else {
    input = document.createElement("input");
    (input as HTMLInputElement).type = f.inputType;
    (input as HTMLInputElement).value = value;
  }
  input.dataset.suggestField = f.key;
  input.dataset.suggestOriginal = value;
  input.dataset.suggestInputType = f.inputType;
  if (f.required) input.required = true;

  const inputID = `suggest-field-${f.key}`;
  input.id = inputID;
  label.setAttribute("for", inputID);

  wrap.appendChild(input);

  const preVal = formatFieldForInput(preImage[f.key], f.inputType);
  if (preVal && preVal !== value) {
    const hint = document.createElement("span");
    hint.className = "approval-suggest-prehint";
    hint.textContent = `${t("approvals.diff.before")}: ${preVal}`;
    wrap.appendChild(hint);
  }
  return wrap;
}

function renderContextSection(
  args: ApprovalEditModalArgs,
  original: Record<string, unknown>,
): HTMLElement {
  const section = document.createElement("section");
  section.className = "approval-suggest-section approval-suggest-section--context";

  const h = document.createElement("h3");
  h.className = "approval-suggest-section-title";
  h.textContent = t("approvals.suggest.section.context");
  section.appendChild(h);

  const rows: Array<[string, string]> = [];
  if (args.projectTitle) {
    rows.push([t("approvals.suggest.context.project"), args.projectTitle]);
  }
  if (args.requesterName) {
    rows.push([t("approvals.suggest.context.requester"), args.requesterName]);
  }
  if (args.requestedAt) {
    rows.push([t("approvals.suggest.context.requested_at"), formatDateForDisplay(args.requestedAt)]);
  }
  // Approval status — entity row's current approval_status (typically
  // "pending" while the modal is open, but display the requester's
  // perspective for completeness).
  const approvalStatus = original.approval_status as string | undefined;
  if (approvalStatus) {
    rows.push([
      t("approvals.suggest.context.approval_status"),
      t(("approvals.status." + approvalStatus) as never) || approvalStatus,
    ]);
  }

  if (rows.length === 0) {
    section.style.display = "none";
    return section;
  }

  const dl = document.createElement("dl");
  dl.className = "approval-suggest-context-grid";
  for (const [label, value] of rows) {
    const dt = document.createElement("dt");
    dt.textContent = label;
    const dd = document.createElement("dd");
    dd.textContent = value;
    dl.appendChild(dt);
    dl.appendChild(dd);
  }
  section.appendChild(dl);
  return section;
}

function renderNoteSection(): { section: HTMLElement; textarea: HTMLTextAreaElement } {
  const section = document.createElement("section");
  section.className = "approval-suggest-section approval-suggest-section--note";
  const wrap = document.createElement("div");
  wrap.className = "form-field approval-suggest-note";

  const label = document.createElement("label");
  label.textContent = t("approvals.suggest.note_label");
  label.setAttribute("for", "suggest-note");
  wrap.appendChild(label);

  const textarea = document.createElement("textarea");
  textarea.id = "suggest-note";
  textarea.rows = 3;
  textarea.placeholder = t("approvals.suggest.note_placeholder");
  textarea.dataset.suggestNote = "true";
  wrap.appendChild(textarea);

  section.appendChild(wrap);
  return { section, textarea };
}

interface BuildResult {
  counterPayload: Record<string, unknown>;
  note: string;
  dirty: boolean;
}

function buildResult(
  fieldInputs: ReadonlyArray<HTMLInputElement | HTMLTextAreaElement>,
  noteEl: HTMLTextAreaElement,
  original: Record<string, unknown>,
  eventTypePicker: PickerHandle | null,
  eventTypePickerLoaded: boolean,
): BuildResult {
  const counterPayload: Record<string, unknown> = {};
  let dirty = false;

  for (const el of fieldInputs) {
    const key = el.dataset.suggestField || "";
    const orig = el.dataset.suggestOriginal || "";
    const inputType = el.dataset.suggestInputType || "text";
    if (el.value === orig) continue;
    counterPayload[key] = formatFieldForServer(el.value, inputType);
    dirty = true;
  }

  if (eventTypePicker && eventTypePickerLoaded) {
    const currentIDs = eventTypePicker.getIDs().slice().sort();
    const originalIDs = ((original.event_type_ids as string[] | undefined) ?? []).slice().sort();
    if (currentIDs.length !== originalIDs.length
        || currentIDs.some((id, i) => id !== originalIDs[i])) {
      counterPayload.event_type_ids = currentIDs;
      dirty = true;
    }
  }

  return {
    counterPayload,
    note: noteEl.value.trim(),
    dirty,
  };
}

// formatFieldForInput — convert a server-side payload value to the format
// the <input> wants. Dates round-trip as YYYY-MM-DD; datetime-local wants
// YYYY-MM-DDTHH:MM. Server returns ISO 8601 / RFC 3339 timestamps; we
// trim to the local-input shape. Text passes through verbatim.
function formatFieldForInput(v: unknown, inputType: string): string {
  if (v == null) return "";
  const s = String(v);
  if (inputType === "date") {
    if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
    const m = s.match(/^(\d{4}-\d{2}-\d{2})/);
    return m ? m[1] : s;
  }
  if (inputType === "datetime-local") {
    const m = s.match(/^(\d{4}-\d{2}-\d{2})[T\s](\d{2}:\d{2})/);
    return m ? `${m[1]}T${m[2]}` : s;
  }
  return s;
}

// formatFieldForServer — convert input value back to server-friendly
// shape. Empty string means "clear this nullable field"; the server's
// addText helper writes NULL for "". Required fields (title) reach the
// server's non-empty CHECK on the column, which surfaces as a 400.
function formatFieldForServer(value: string, inputType: string): unknown {
  if (inputType === "date" || inputType === "datetime-local") {
    return value || null;
  }
  return value;
}

function formatDateForDisplay(iso: string): string {
  const d = Date.parse(iso);
  if (isNaN(d)) return iso;
  return new Date(d).toLocaleString();
}
