import { initI18n, t } from "./i18n";
import { initSidebar } from "./sidebar";
import { mountFilterBar, type BarHandle } from "./filter-bar";
import type { AxisKey } from "./filter-bar";
import type { FilterSpec, RenderSpec, SystemView, ViewRunResult } from "./views/types";
import { renderListShape } from "./views/shape-list";
import { openApprovalEditModal } from "./components/approval-edit-modal";

// /inbox client — t-paliad-249 unified inbox feed.
//
// The bar exposes:
//   - inbox_focus:        coarse Alles / Genehmigungen / +Termine / +Fristen
//   - unread_only:        Nur ungelesen / Alle (default: ungelesen)
//   - time:               last 30 days default; chip cluster + custom range
//   - project:            single-select autocomplete from visible projects
//   - approval_viewer_role: Zur Genehmigung / Eigene / Alle sichtbaren
//   - approval_status / approval_entity_type / project_event_kind: power-user overrides
//   - sort / density:     newest first default
//
// Row rendering: shape-list.ts with row_action="inbox" dispatches per
// row.kind. Approval rows keep approve/reject/revoke; project_event
// rows render compact with an Öffnen link.

const INBOX_AXES: AxisKey[] = [
  "inbox_focus",
  "unread_only",
  "time",
  "project",
  "approval_viewer_role",
  "approval_status",
  "approval_entity_type",
  "project_event_kind",
  "sort",
  "density",
];

// Last paint's newest row timestamp — used to pin mark-all-seen so a
// second tab can't race the cursor past items the user hasn't seen.
let newestVisibleAt: string | null = null;

let bar: BarHandle | null = null;

document.addEventListener("DOMContentLoaded", () => {
  initI18n();
  initSidebar();
  applyLegacyTabRedirect();
  wireMarkAllSeen();
  void hydrate();
});

// ?tab=pending-mine | mine -> ?a_role=approver_eligible | self_requested.
// Done client-side because /inbox serves a static dist file (no Go
// router involvement). Bookmarks from the sidebar bell + outbound
// emails keep landing on the right sub-view through the bar.
function applyLegacyTabRedirect(): void {
  const url = new URL(window.location.href);
  const tab = url.searchParams.get("tab");
  if (!tab) return;
  url.searchParams.delete("tab");
  if (tab === "mine") {
    url.searchParams.set("a_role", "self_requested");
  } else if (tab === "pending-mine") {
    url.searchParams.set("a_role", "approver_eligible");
  }
  history.replaceState(null, "", url.toString());
}

async function hydrate(): Promise<void> {
  const host = document.getElementById("inbox-filter-bar");
  const loading = document.getElementById("inbox-loading");
  const results = document.getElementById("inbox-results");
  const empty = document.getElementById("inbox-empty");
  if (!host || !loading || !results || !empty) return;

  const sys = await fetchInboxSystemView();
  if (!sys) {
    loading.style.display = "none";
    empty.style.display = "";
    empty.textContent = t("approvals.error.internal");
    return;
  }

  bar = mountFilterBar(host, {
    baseFilter: sys.Filter,
    baseRender: sys.Render,
    axes: INBOX_AXES,
    surfaceKey: "inbox",
    systemViewSlug: sys.Slug,
    onResult: (result, effective) => paint(result, effective.render, results, empty, loading),
  });
}

async function fetchInboxSystemView(): Promise<SystemView | null> {
  try {
    const r = await fetch("/api/views/system", { credentials: "include" });
    if (!r.ok) return null;
    const list = (await r.json()) as SystemView[];
    return list.find((v) => v.Slug === "inbox") ?? null;
  } catch (_e) {
    return null;
  }
}

function paint(
  result: ViewRunResult,
  render: RenderSpec,
  results: HTMLElement,
  empty: HTMLElement,
  loading: HTMLElement,
): void {
  loading.style.display = "none";

  if (!result.rows || result.rows.length === 0) {
    results.innerHTML = "";
    empty.style.display = "";
    empty.textContent = t("inbox.empty.feed");
    newestVisibleAt = null;
    void maybeShowAdminNudge();
    return;
  }
  hideAdminNudge();
  empty.style.display = "none";

  // Remember the newest timestamp so mark-all-seen can pin the cursor
  // to it (race-safety: a second tab adding a row between this paint
  // and the click won't get wiped out).
  newestVisibleAt = result.rows.reduce<string | null>((acc, r) => {
    if (!acc) return r.event_date;
    return r.event_date > acc ? r.event_date : acc;
  }, null);

  // shape-list.ts honours render.list.row_action — InboxSystemView's
  // RenderSpec sets row_action="inbox" so we get the unified dispatch
  // (approval rows + project_event rows).
  renderListShape(results, result.rows, render);

  // Wire action handlers on the freshly stamped DOM. The action
  // POSTs land on the same endpoints the legacy /inbox used; on
  // success we trigger a bar refresh so the new state propagates.
  wireApprovalActions(results);
}

// wireMarkAllSeen wires the page-header "Alles als gelesen markieren"
// button. POSTs the newest visible row's timestamp as `up_to` so a
// stale second tab can't rewind anyone else's cursor; on success the
// bar refreshes (rows newer than now disappear under unread_only) and
// the sidebar badge re-counts.
function wireMarkAllSeen(): void {
  const btn = document.getElementById("inbox-mark-all-seen") as HTMLButtonElement | null;
  if (!btn) return;
  btn.addEventListener("click", async () => {
    btn.disabled = true;
    try {
      const body = newestVisibleAt ? JSON.stringify({ up_to: newestVisibleAt }) : "{}";
      const r = await fetch("/api/inbox/mark-all-seen", {
        method: "POST",
        credentials: "include",
        headers: { "Content-Type": "application/json" },
        body,
      });
      if (!r.ok) {
        alert(t("approvals.error.internal"));
        return;
      }
      await bar?.refresh();
      await refreshInboxBadge();
    } catch (_e) {
      alert("Network error");
    } finally {
      btn.disabled = false;
    }
  });
}

function wireApprovalActions(host: HTMLElement): void {
  host.querySelectorAll<HTMLButtonElement>(".views-approval-action").forEach((btn) => {
    const action = btn.dataset.action as
      | "approve"
      | "reject"
      | "revoke"
      | "suggest_changes"
      | undefined;
    const li = btn.closest<HTMLLIElement>(".views-approval-row");
    const id = li?.dataset.requestId;
    if (!action || !id) return;
    btn.addEventListener("click", async () => {
      if (action === "suggest_changes") {
        await handleSuggestChanges(btn, id, li!);
        return;
      }
      let note = "";
      if (action === "reject") {
        note = window.prompt(t("approvals.note.placeholder")) || "";
      }
      btn.disabled = true;
      try {
        const r = await fetch(`/api/approval-requests/${id}/${action}`, {
          method: "POST",
          credentials: "include",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ note }),
        });
        if (!r.ok) {
          const body = await r.json().catch(() => ({} as { error?: string; code?: string }));
          alert(mapApprovalError(body.code || body.error || "internal"));
          btn.disabled = false;
          return;
        }
        await bar?.refresh();
        await refreshInboxBadge();
      } catch (_e) {
        alert("Network error");
        btn.disabled = false;
      }
    });
  });
}

// handleSuggestChanges — t-paliad-216. Open the edit modal with the
// requester's original payload + pre_image pre-populated. If the user
// submits non-empty changes / note, POST to
// /api/approval-requests/{id}/suggest-changes; refresh the bar on success
// so the OLD row flips to changes_requested and the NEW pending row
// appears.
async function handleSuggestChanges(
  btn: HTMLButtonElement,
  requestID: string,
  li: HTMLLIElement,
): Promise<void> {
  // Read the row's detail blob off the data-attrs the shape-list stamped.
  // shape-list serialises payload/pre_image inline; we fetch fresh via
  // the per-row API to avoid relying on stale list data.
  let payload: Record<string, unknown> | null = null;
  let preImage: Record<string, unknown> | null = null;
  let entityType: "deadline" | "appointment" = "deadline";
  let lifecycleEvent = "update";
  let projectTitle: string | undefined;
  let requesterName: string | undefined;
  let requestedAt: string | undefined;
  try {
    const r = await fetch(`/api/approval-requests/${requestID}`, { credentials: "include" });
    if (r.ok) {
      const body = (await r.json()) as {
        entity_type?: "deadline" | "appointment";
        lifecycle_event?: string;
        payload?: Record<string, unknown> | null;
        pre_image?: Record<string, unknown> | null;
        project_title?: string;
        requester_name?: string;
        requested_at?: string;
      };
      payload = body.payload ?? null;
      preImage = body.pre_image ?? null;
      if (body.entity_type === "appointment") entityType = "appointment";
      if (body.lifecycle_event) lifecycleEvent = body.lifecycle_event;
      projectTitle = body.project_title;
      requesterName = body.requester_name;
      requestedAt = body.requested_at;
    }
  } catch (_e) {
    // Modal still opens with empty defaults if the fetch fails; the
    // server-side schema validation catches a misshapen counter.
  }

  const result = await openApprovalEditModal({
    entityType,
    lifecycleEvent,
    payload,
    preImage,
    projectTitle,
    requesterName,
    requestedAt,
  });
  if (!result) return; // cancel

  btn.disabled = true;
  try {
    const r = await fetch(`/api/approval-requests/${requestID}/suggest-changes`, {
      method: "POST",
      credentials: "include",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        counter_payload: result.counterPayload,
        note: result.note,
      }),
    });
    const body = (await r.json().catch(() => ({}))) as {
      error?: string;
      code?: string;
      new_request_id?: string;
    };
    if (!r.ok) {
      alert(mapApprovalError(body.code || body.error || "internal"));
      btn.disabled = false;
      return;
    }
    await bar?.refresh();
    await refreshInboxBadge();
    btn.disabled = false;

    // Surface the new row's id on the OLD row's <li> so callers (e.g.
    // tests, future inspection) can find it without re-querying.
    if (body.new_request_id) {
      li.dataset.spawnedRequestId = body.new_request_id;
    }
  } catch (_e) {
    alert("Network error");
    btn.disabled = false;
  }
}

function mapApprovalError(key: string): string {
  switch (key) {
    case "self_approval_blocked":         return t("approvals.error.self_approval");
    case "no_qualified_approver":         return t("approvals.error.no_qualified_approver");
    case "concurrent_pending":            return t("approvals.error.concurrent_pending");
    case "not_authorized":                return t("approvals.error.not_authorized");
    case "request_not_pending":           return t("approvals.error.request_not_pending");
    case "suggestion_requires_change":    return t("approvals.error.suggestion_requires_change");
    case "suggestion_lifecycle_invalid":  return t("approvals.error.suggestion_lifecycle_invalid");
    default:                              return key;
  }
}

// t-paliad-154 — show the admin-only "configure policies" nudge when:
//   - current user is global_admin
//   - inbox empty
//   - no approval_policies row exists firm-wide
async function maybeShowAdminNudge(): Promise<void> {
  const nudge = document.getElementById("inbox-admin-nudge");
  if (!nudge) return;
  try {
    const meR = await fetch("/api/me", { credentials: "include" });
    if (!meR.ok) return;
    const me = (await meR.json()) as { global_role?: string };
    if (me.global_role !== "global_admin") return;

    const seedR = await fetch("/api/admin/approval-policies/seeded", { credentials: "include" });
    if (!seedR.ok) return;
    const data = (await seedR.json()) as { any: boolean };
    if (data.any) return;

    nudge.style.display = "";
  } catch (_e) { /* keep hidden */ }
}

function hideAdminNudge(): void {
  const nudge = document.getElementById("inbox-admin-nudge");
  if (nudge) nudge.style.display = "none";
}

async function refreshInboxBadge(): Promise<void> {
  const badge = document.getElementById("sidebar-inbox-badge");
  if (!badge) return;
  try {
    const r = await fetch("/api/inbox/count", { credentials: "include" });
    if (!r.ok) return;
    const data = (await r.json()) as { count: number };
    if (data.count > 0) {
      badge.textContent = String(data.count);
      badge.style.display = "";
    } else {
      badge.style.display = "none";
    }
  } catch (_e) { /* noop */ }
}
