import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
  ActionIcon,
  Alert,
  Badge,
  Button,
  Container,
  FileButton,
  Grid,
  Group,
  Loader,
  Modal,
  NumberInput,
  Paper,
  SegmentedControl,
  Skeleton,
  Stack,
  Tabs,
  Text,
  Textarea,
  ThemeIcon,
  Timeline,
  Title,
  Tooltip,
} from "@mantine/core";
import {
  IconAlertTriangle,
  IconCheck,
  IconEye,
  IconFileDownload,
  IconLayoutSidebarRightCollapse,
  IconLayoutSidebarRightExpand,
  IconPaperclip,
  IconPencil,
  IconQuestionMark,
  IconX,
} from "@tabler/icons-react";
import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
import {
  STATUS_COLORS,
  STATUS_LABELS,
  applicantOrCompanyName,
  extractErrorMessage,
  useLocalized,
  useApproveDocumentsMutation,
  useAssignApplicationMutation,
  useAssignReviewerMutation,
  useAssignInspectorMutation,
  useReportReviewMutation,
  useReportInspectionMutation,
  useClaimApplicationMutation,
  useCompleteReviewMutation,
  useConfirmPaymentMutation,
  useScheduleIssuanceMutation,
  useIssueCertificateMutation,
  useEscalateApplicationMutation,
  useFinalApproveMutation,
  useGetApplicationForReviewQuery,
  useGetAttachmentsQuery,
  useGetDocumentReviewsQuery,
  useGetInspectionsQuery,
  useGetAssignableOfficersQuery,
  useGetLicenseTypeRequirementsQuery,
  useHoldApplicationMutation,
  useRecordInspectionResultMutation,
  useRejectApplicationMutation,
  useRequestAdjustmentMutation,
  useResumeApplicationMutation,
  useScheduleInspectionMutation,
  useRescheduleInspectionMutation,
  useGetCertificateUrlForOfficerMutation,
  uploadDocument,
  type RemarkTargetType,
  type StaffEvidenceRequirement,
} from "@ema-platform/api";
import {
  AdvancedTable,
  AmharicDatePicker,
  ErrorState,
  ModalFooter,
  PdfPreviewModal,
  useServerTable,
} from "@ema-platform/ui";
import { useDateDisplayer } from "@ema-platform/shared";
import { usePermissions } from "@ema-platform/auth";
import { useAppSelector } from "../../../../store/hooks";
import { DecisionBar } from "../../components/DecisionBar";
import {
  DecisionConfirmModal,
  type DecisionSubmission,
} from "../../components/DecisionConfirmModal";
import { ActivityRail } from "../../components/ActivityRail";
import { DocumentsTab } from "../../components/DocumentsTab";
import { FormDetailsTab } from "../../components/FormDetailsTab";
import { ApplicantCard } from "../../components/ApplicantCard";
import { ExamStatePanel } from "../../components/ExamStatePanel";
import { useGetLocationsQuery } from "../../../location/api/location-api";
import { computeSla } from "../../sla";
import { reviewStaffColumns } from "./columns";
import {
  evaluateEligibility,
  presentationFor,
} from "../../config/license-types";
import {
  resolveActions,
  type ActionId,
  type ResolvedAction,
} from "../../config/actions";

type FlagMap = Record<string, { targetType: RemarkTargetType; remark: string }>;

/**
 * Standard site-visit checklist (US-LOG-016 / US-VES-007). The inspection
 * entity has carried a checklist column since day one; this is the first UI
 * that fills it in.
 */
const INSPECTION_CHECKLIST_ITEMS = [
  {
    key: "office_premises",
    labelKey: "review.checklist.officePremises",
    fallback: "Office premises",
  },
  {
    key: "storage_facilities",
    labelKey: "review.checklist.storageFacilities",
    fallback: "Warehouse / storage facilities",
  },
  {
    key: "vehicles_equipment",
    labelKey: "review.checklist.vehiclesEquipment",
    fallback: "Vehicles / equipment",
  },
  {
    key: "safety_compliance",
    labelKey: "review.checklist.safetyCompliance",
    fallback: "Safety & regulatory compliance",
  },
] as const;

function buildChecklist(
  outcomes: Record<string, "PASS" | "FAIL" | "NEEDS_CORRECTION">,
) {
  return INSPECTION_CHECKLIST_ITEMS.map((item) => ({
    key: item.key,
    // Persisted as-is (stable English), not the officer's display language —
    // this is an audit record, not UI text.
    label: item.fallback,
    // Untouched rows default to PASS — the segmented control shows exactly
    // that, so what the officer saw is what gets recorded.
    outcome: outcomes[item.key] ?? "PASS",
  }));
}

/**
 * The officer's review workspace.
 *
 * Three zones: a sticky left rail carrying the summary and eligibility, a
 * centre column of tabs driven by the licence type's sections, and a
 * collapsible activity trail on the right. Every decision goes through the
 * Decision Bar pinned to the bottom, so an officer can act from any scroll
 * position instead of scrolling back to a column of buttons.
 */
/** The examination leg — the only statuses the exam panel has anything to say about. */
const EXAM_LEG_STATUSES: string[] = [
  "EXAM_PAYMENT_PENDING",
  "EXAM_PAID",
  "EXAM_SCHEDULED",
  "EXAM_PASSED",
  "EXAM_FAILED",
];

export function LicenseReviewPage() {
  const navigate = useNavigate();
  const { t, i18n } = useTranslation();
  const showDate = useDateDisplayer();
  const localized = useLocalized();
  const { id = "" } = useParams();
  const { can } = usePermissions();
  const currentUserId = useAppSelector((state) => state.auth.user?.id) ?? "";

  const { data, isLoading, isError, error, refetch } =
    useGetApplicationForReviewQuery(id, {
      skip: !id,
    });
  const { data: inspections = [], refetch: refetchInspections } =
    useGetInspectionsQuery(id, {
      skip: !id,
    });
  const { data: requirements } = useGetLicenseTypeRequirementsQuery(
    {
      idOrKey: data?.application.licenseTypeId ?? "",
      kind: data?.application.kind ?? "NEW",
    },
    { skip: !data?.application.licenseTypeId },
  );
  // Staff role names are already bilingual on the config — the wire data only
  // carries the role key (e.g. 'CAPTAIN'), so this is what turns it back into
  // the label an officer reads.
  const roleNameByKey = useMemo(
    () =>
      new Map(
        requirements?.staffRoleRequirements.map((r) => [r.roleKey, r.name]) ??
          [],
      ),
    [requirements],
  );
  // What each role is *required* to produce (CV, work agreement, ERB
  // certificate). Without this the tab can only list what was uploaded, so a
  // missing CV looks identical to a role that never needed one.
  const evidenceByRole = useMemo(
    () =>
      new Map(
        requirements?.staffRoleRequirements.map((r) => [
          r.roleKey,
          r.requiredEvidence ?? [],
        ]) ?? [],
      ),
    [requirements],
  );

  const [claimApplication] = useClaimApplicationMutation();
  const [completeReview] = useCompleteReviewMutation();
  const [requestAdjustment] = useRequestAdjustmentMutation();
  const [approveDocuments] = useApproveDocumentsMutation();
  const [finalApprove] = useFinalApproveMutation();
  const [rejectApplication] = useRejectApplicationMutation();
  const [scheduleInspection] = useScheduleInspectionMutation();
  const [rescheduleInspection] = useRescheduleInspectionMutation();
  const [recordResult] = useRecordInspectionResultMutation();
  const [confirmPayment] = useConfirmPaymentMutation();
  const [scheduleIssuance] = useScheduleIssuanceMutation();
  const [issueCertificate] = useIssueCertificateMutation();
  const [getCertificateUrlForOfficer] = useGetCertificateUrlForOfficerMutation();
  const [certificateBusy, setCertificateBusy] = useState<"view" | "download" | null>(
    null,
  );
  const [certificatePreview, setCertificatePreview] = useState<string | null>(null);
  const [holdApplication] = useHoldApplicationMutation();
  const [resumeApplication] = useResumeApplicationMutation();
  const [escalateApplication] = useEscalateApplicationMutation();
  const [assignApplication] = useAssignApplicationMutation();
  const [assignReviewer, { isLoading: assigningReviewer }] =
    useAssignReviewerMutation();
  const [assignInspector, { isLoading: assigningInspector }] =
    useAssignInspectorMutation();
  const [reportReview] = useReportReviewMutation();
  const [reportInspection] = useReportInspectionMutation();

  // Real officer list, so Assign and Escalate name a person instead of
  // silently reassigning to whoever already held the application.
  const { data: officers = [] } = useGetAssignableOfficersQuery();
  // Same cached query the Documents tab reads, so the decision bar reacts the
  // moment a verdict is saved.
  const { data: documentReviews = [] } = useGetDocumentReviewsQuery(id, { skip: !id });

  const staffTable = useServerTable();
  const [flags, setFlags] = useState<FlagMap>({});
  const [capital, setCapital] = useState<number | undefined>();
  const [pendingAction, setPendingAction] = useState<ResolvedAction | null>(
    null,
  );
  const [busyAction, setBusyAction] = useState<ActionId | null>(null);
  const [railOpen, setRailOpen] = useState(true);
  const [inspectionOpen, setInspectionOpen] = useState(false);
  const [inspectionDate, setInspectionDate] = useState("");
  const [inspectionTimeSlot, setInspectionTimeSlot] = useState<"MORNING" | "AFTERNOON">(
    "MORNING",
  );
  /** The booking modal moves an existing visit rather than creating one. */
  const [rescheduling, setRescheduling] = useState(false);
  const [rescheduleReason, setRescheduleReason] = useState("");
  const [issuanceOpen, setIssuanceOpen] = useState(false);
  const [issuanceDate, setIssuanceDate] = useState("");
  const [issuancePeriod, setIssuancePeriod] = useState<"MORNING" | "AFTERNOON">(
    "MORNING",
  );
  const [resultOpen, setResultOpen] = useState(false);
  const [findings, setFindings] = useState("");
  const [findingsUploadBusy, setFindingsUploadBusy] = useState(false);
  const [findingsPreview, setFindingsPreview] = useState<
    { url: string; title: string } | null
  >(null);
  const [checklist, setChecklist] = useState<
    Record<string, "PASS" | "FAIL" | "NEEDS_CORRECTION">
  >({});

  // Prefill from whatever is recorded, else the declared figure, so the officer
  // confirms a number rather than retyping it. Runs before the early return
  // below because hooks must be called unconditionally.
  const seeded = useRef(false);
  const loadedApp = data?.application;
  useEffect(() => {
    if (seeded.current || !loadedApp) return;
    const existing =
      loadedApp.capitalAmountVerified ?? loadedApp.capitalAmountDeclared;
    if (existing != null) setCapital(Number(existing));
    seeded.current = true;
  }, [loadedApp]);

  const toggleFlag = useCallback(
    (targetType: RemarkTargetType, key: string) => {
      setFlags((prev) => {
        const next = { ...prev };
        if (next[key]) delete next[key];
        else next[key] = { targetType, remark: "" };
        return next;
      });
    },
    [],
  );

  const documentFlags = useMemo(() => {
    const map: Record<string, string> = {};
    for (const [key, flag] of Object.entries(flags)) {
      if (flag.targetType === "DOCUMENT") map[key] = flag.remark;
    }
    return map;
  }, [flags]);

  const pendingInspection = inspections.find((i) => i.status === 'SCHEDULED');
  // A visit cannot have an outcome before it happens — mirror of the server's
  // inspection_not_yet_due guard. The column holds a calendar day, so the
  // comparison is between day strings in the authority's timezone: parsing
  // "2026-08-28" as a Date would read it as UTC midnight, i.e. 03:00 in Addis.
  const inspectionNotYetDue = Boolean(
    pendingInspection?.scheduledDate &&
      new Intl.DateTimeFormat("en-CA", { timeZone: "Africa/Addis_Ababa" }).format(
        new Date(),
      ) < pendingInspection.scheduledDate.slice(0, 10),
  );

  const { data: findingsEvidence = [], refetch: refetchFindingsEvidence } =
    useGetAttachmentsQuery(
      { ownerType: 'INSPECTION', ownerId: pendingInspection?.id ?? '' },
      { skip: !pendingInspection },
    );

  // Approving means every uploaded document was accepted — one unjudged or
  // rejected file is enough to keep the decision buttons dead. The counts feed
  // the hover explanation, so the officer sees how much is left rather than
  // just that the button is dead.
  const documentProgress = useMemo(() => {
    const attachments = data?.attachments ?? [];
    const accepted = new Set(
      documentReviews
        .filter((review) => review.decision === 'ACCEPTED')
        .map((review) => review.documentKey),
    );
    const acceptedCount = attachments.filter((a) => accepted.has(a.documentKey)).length;
    return { acceptedCount, total: attachments.length };
  }, [data?.attachments, documentReviews]);
  const allDocumentsAccepted =
    documentProgress.total > 0 && documentProgress.acceptedCount === documentProgress.total;
  const flagged = Object.entries(flags);

  /**
   * The flagged items as the officer should see them in the confirmation.
   *
   * Only a document key reads acceptably on its own. A form section is
   * camelCase, and a staff flag is keyed by the ApplicationStaff id — showing
   * a uuid in the checklist would make the list impossible to tick with
   * confidence.
   */
  const flaggedItems = useMemo(
    () =>
      flagged.map(([key, flag]) => {
        if (flag.targetType === "STAFF") {
          const member = data?.staff.find((s) => s.id === key);
          return {
            key,
            label: member
              ? `${localized(roleNameByKey.get(member.roleKey)) || member.roleKey} — ${member.fullName}`
              : t("review.staffMember", "Staff member"),
          };
        }
        if (flag.targetType === "FORM_SECTION") {
          return { key, label: key.replace(/([A-Z])/g, " $1").trim() };
        }
        return { key, label: key };
      }),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [flags, data?.staff, t, localized, roleNameByKey],
  );

  const actions = useMemo(() => {
    if (!data) return [];
    return resolveActions({
      detail: data,
      currentUserId,
      can,
      flaggedCount: flagged.length,
      hasPendingInspection: Boolean(pendingInspection),
      inspectionNotYetDue,
      allDocumentsAccepted,
      reasons: {
        wrongStatus: t('review.disabled.wrongStatus', 'Not available at this stage'),
        notAssigned: t('review.disabled.notAssigned', 'Assigned to another officer'),
        noPermission: t('review.disabled.noPermission', 'You do not have permission'),
        needsFlags: t('review.disabled.needsFlags', 'Flag at least one item to request a correction'),
        needsCapital: t('review.disabled.needsCapital', 'Record the verified capital first'),
        needsInspection: t('review.disabled.needsInspection', 'Requires an inspection result'),
        inspectionNotYetDue: t('review.disabled.inspectionNotYetDue', {
          date: pendingInspection?.scheduledDate
            ? showDate(pendingInspection.scheduledDate)
            : '',
          defaultValue:
            'Inspection scheduled for {{date}}. Results can be recorded after the scheduled time.',
        }),
        needsDocumentReviews:
          documentProgress.total === 0
            ? t(
                'review.disabled.needsDocumentsUploaded',
                'No documents uploaded to review yet',
              )
            : t('review.disabled.needsDocumentReviews', {
                accepted: documentProgress.acceptedCount,
                total: documentProgress.total,
                defaultValue:
                  'Accept all documents first — {{accepted}} of {{total}} accepted. Open the Documents tab and accept the rest.',
              }),
      },
    });
  }, [data, currentUserId, can, flagged.length, pendingInspection, inspectionNotYetDue, showDate, t]);

  // Location answers are tree ids. The picker the applicant used resolves them
  // client-side from the same list, so the reviewer reads the place rather than
  // the uuid. Fetched only when the form actually has a location field.
  //
  // Above the early returns because it is a hook: React requires the same hook
  // order on every render, and the loading and error branches return before the
  // application is known.
  const needsLocations = (
    requirements?.licenseType.formSchema.sections ?? []
  ).some((section) =>
    (section.fields ?? []).some(
      (f) =>
        f.key === "locationId" ||
        (f.label?.en ?? "").trim().toLowerCase() === "location",
    ),
  );
  const { data: locationsRes } = useGetLocationsQuery(
    { take: 10000 },
    { skip: !needsLocations },
  );
  const resolveLocation = useMemo(() => {
    const all = locationsRes?.items ?? [];
    if (all.length === 0) return undefined;
    const byId = new Map(all.map((loc) => [loc.id, loc]));
    return (locationId: string) => {
      // Walks to the root so the answer reads as a place, not a leaf name:
      // "Woreda 03" alone does not say which sub-city it belongs to. Bounded by
      // the map size so a cyclic tree cannot spin the render.
      const path: string[] = [];
      let current = byId.get(locationId);
      let hops = 0;
      while (current && hops++ <= byId.size) {
        path.unshift(localized(current.names) || current.code);
        current = current.parentId ? byId.get(current.parentId) : undefined;
      }
      return path.length ? path.join(" → ") : undefined;
    };
  }, [locationsRes, localized]);

  if (isLoading) {
    // Skeleton mirrors the real three-zone layout so nothing jumps on load.
    return (
      <Container size="xl" py="md">
        <Skeleton height={36} width={320} mb="lg" />
        <Grid>
          <Grid.Col span={{ base: 12, md: 3 }}>
            <Skeleton height={280} radius="md" />
          </Grid.Col>
          <Grid.Col span={{ base: 12, md: 6 }}>
            <Skeleton height={420} radius="md" />
          </Grid.Col>
          <Grid.Col span={{ base: 12, md: 3 }}>
            <Skeleton height={280} radius="md" />
          </Grid.Col>
        </Grid>
      </Container>
    );
  }

  if (isError || !data) {
    return (
      <Container size="xl" py="md">
        <ErrorState
          title={t("review.errorTitle", "Could not load this application")}
          description={extractErrorMessage(error)}
          onRetry={() => refetch()}
        />
      </Container>
    );
  }

  const app = data.application;
  const status = app.status;
  const staffPaged = staffTable.paginate(data.staff);
  const presentation = presentationFor(app.licenseType?.key);
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- i18next's overloaded TFunction type doesn't structurally match a plain callback signature
  const sla = computeSla(
    app,
    undefined,
    i18n.language,
    (key, options) => t(key, options as any) as string,
  );
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- i18next's overloaded TFunction type doesn't structurally match a plain callback signature
  const eligibility = evaluateEligibility(
    app,
    app.licenseType,
    i18n.language,
    (key, options) => t(key, options as any) as string,
  );

  const rawThreshold = app.licenseType?.capitalThreshold;
  const threshold =
    rawThreshold === null || rawThreshold === undefined
      ? undefined
      : Number(rawThreshold);
  /** Stages where the officer can still record the verified capital. */
  const needsCapital =
    Boolean(threshold) &&
    [
      "UNDER_REVIEW",
      "UNDER_EVALUATION",
      "INSPECTION_PENDING",
      "INSPECTION_COMPLETED",
    ].includes(status);

  async function run(action: () => Promise<unknown>, success: string) {
    try {
      await action();
      notifications.show({ color: "teal", title: success, message: "" });
      refetch();
      refetchInspections();
    } catch (err) {
      notifications.show({
        color: "red",
        title: t("review.actionFailed", "Action failed"),
        message: extractErrorMessage(err),
      });
    }
  }

  /**
   * Fetches a fresh presigned link and either opens the in-app PDF preview
   * or a new tab, depending which button was pressed — the link itself is
   * short-lived, so each click gets its own rather than caching one.
   */
  async function openCertificate(mode: "view" | "download") {
    if (!app.issuedLicenseId) return;
    setCertificateBusy(mode);
    try {
      const { url } = await getCertificateUrlForOfficer(
        app.issuedLicenseId,
      ).unwrap();
      if (mode === "view") {
        setCertificatePreview(url);
      } else {
        window.open(url, "_blank", "noopener");
      }
    } catch (err) {
      notifications.show({
        color: "red",
        title: t("review.certificateError", "Could not open the certificate"),
        message: extractErrorMessage(err),
      });
    } finally {
      setCertificateBusy(null);
    }
  }

  /** Opens the booking modal seeded with the visit already on the books. */
  function openReschedule() {
    if (!pendingInspection) return;
    setRescheduling(true);
    setInspectionDate(pendingInspection.scheduledDate ?? "");
    setInspectionTimeSlot(pendingInspection.timeSlot ?? "MORNING");
    setRescheduleReason("");
    setInspectionOpen(true);
  }

  /** Actions with their own dedicated form open that; the rest confirm. */
  function handleAction(action: ResolvedAction) {
    switch (action.id) {
      case "schedule-inspection":
        setRescheduling(false);
        setInspectionDate("");
        setInspectionTimeSlot("MORNING");
        setRescheduleReason("");
        setInspectionOpen(true);
        return;
      case "reschedule-inspection":
        openReschedule();
        return;
      case "schedule-issuance":
        setIssuanceOpen(true);
        return;
      case "record-inspection":
        setResultOpen(true);
        return;
      case "copy-link":
        navigator.clipboard.writeText(window.location.href);
        notifications.show({
          color: "teal",
          title: t("review.linkCopied", "Link copied"),
          message: "",
        });
        return;
      case "print":
        window.print();
        return;
      case "audit-trail":
        setRailOpen(true);
        return;
      case "download-documents":
        for (const attachment of data?.attachments ?? []) {
          const url = attachment.files?.[0]?.url;
          if (url) window.open(url, "_blank", "noopener");
        }
        return;
      default:
        setPendingAction(action);
    }
  }

  async function submitDecision(submission: DecisionSubmission) {
    const action = pendingAction;
    if (!action) return;
    setBusyAction(action.id);
    try {
      switch (action.id) {
        case "claim":
          // Usually fired from the queue, but an officer who opened an
          // unclaimed application directly claims it from here.
          await run(
            () => claimApplication(id).unwrap(),
            t("review.done.claim", "Claimed — the application is now yours"),
          );
          break;
        // Handing work out. Reuses the decision modal's officer picker rather
        // than a dialog of its own — it already resolves the real officer list.
        case "assign-reviewer":
          if (!submission.officerId) return;
          await run(
            () =>
              assignReviewer({
                id,
                officerId: submission.officerId as string,
                remark: submission.reason,
              }).unwrap(),
            t("review.done.assignReviewer", "Review assigned"),
          );
          break;
        case "assign-inspector":
          if (!submission.officerId) return;
          await run(
            () =>
              assignInspector({
                id,
                inspectorId: submission.officerId as string,
                remark: submission.reason,
              }).unwrap(),
            t("review.done.assignInspector", "Inspection assigned"),
          );
          break;
        // Handing work back. Parks the file with the team leader — an employee
        // finishing their task decides nothing.
        case "report-review":
          await run(
            () => reportReview({ id, remark: submission.reason }).unwrap(),
            t("review.done.reportReview", "Sent to your team leader"),
          );
          break;
        case "report-inspection":
          await run(
            () => reportInspection({ id, remark: submission.reason }).unwrap(),
            t("review.done.reportInspection", "Inspection result sent"),
          );
          break;
        case "complete-review":
          await run(
            () =>
              completeReview({ id, capitalAmountVerified: capital }).unwrap(),
            t("review.done.completeReview", "Review completed"),
          );
          break;
        case "approve-documents":
          await run(
            () => approveDocuments({ id }).unwrap(),
            t("review.done.approveDocuments", "Documents approved"),
          );
          break;
        case "final-approve":
          await run(
            () => finalApprove({ id, capitalAmountVerified: capital }).unwrap(),
            t("review.done.finalApprove", "Approved"),
          );
          break;
        case "request-adjustment": {
          const items = flagged
            // Only the ticked deficiencies are sent, so the applicant can
            // edit exactly the list they were shown.
            .filter(([key]) =>
              submission.deficiencies.length
                ? submission.deficiencies.includes(key)
                : true,
            )
            .map(([key, flag]) => ({
              targetType: flag.targetType,
              targetKey: key,
              remark: flag.remark.trim(),
            }));

          // Each item is an instruction the applicant has to act on, so the
          // API requires wording for every one. Catching it here names the
          // items that need it; letting it through produced a bare 400 saying
          // "items.0.remark should not be empty", which tells an officer
          // nothing about which box to go and fill in.
          const unexplained = items.filter((item) => !item.remark);
          if (items.length === 0 || unexplained.length > 0) {
            notifications.show({
              color: "orange",
              title:
                items.length === 0
                  ? t(
                      "review.adjustment.nothingFlagged",
                      "Nothing has been flagged",
                    )
                  : t(
                      "review.adjustment.reasonsMissing",
                      "Every flagged item needs a reason",
                    ),
              message:
                items.length === 0
                  ? t(
                      "review.adjustment.nothingFlaggedHint",
                      "Tick what the applicant must correct before requesting an adjustment.",
                    )
                  : `${t(
                      "review.adjustment.reasonsMissingHint",
                      "Say what must be corrected for:",
                    )} ${unexplained.map((item) => item.targetKey).join(", ")}`,
            });
            // `finally` closes the modal and clears the busy flag.
            return;
          }

          await run(
            async () => {
              await requestAdjustment({
                id,
                generalRemark: submission.reason,
                notificationBody: submission.notificationBody,
                items,
              }).unwrap();
              setFlags({});
            },
            t("review.done.requestAdjustment", "Adjustment requested"),
          );
          break;
        }
        case "reject":
          await run(
            () =>
              rejectApplication({
                id,
                reason: submission.reason,
                // The preview the officer edited is what actually gets sent.
                notificationBody: submission.notificationBody,
              }).unwrap(),
            t("review.done.reject", "Application rejected"),
          );
          break;
        case "confirm-payment":
          await run(
            () => confirmPayment(id).unwrap(),
            t("review.done.confirmPayment", "Payment confirmed"),
          );
          break;
        case "issue-certificate":
          await run(
            () => issueCertificate(id).unwrap(),
            t("review.done.issueCertificate", "Certificate issued"),
          );
          break;
        case "hold":
          await run(
            () => holdApplication({ id, reason: submission.reason }).unwrap(),
            t("review.done.hold", "Application placed on hold"),
          );
          break;
        case "resume":
          await run(
            () => resumeApplication({ id, remark: submission.reason }).unwrap(),
            t("review.done.resume", "Application resumed"),
          );
          break;
        case "escalate":
          if (!submission.officerId) return;
          await run(
            () =>
              escalateApplication({
                id,
                supervisorId: submission.officerId as string,
                reason: submission.reason,
              }).unwrap(),
            t("review.done.escalate", "Escalated"),
          );
          break;
        case "assign":
          if (!submission.officerId) return;
          await run(
            () =>
              assignApplication({
                id,
                officerId: submission.officerId as string,
                remark: submission.reason,
              }).unwrap(),
            t("review.done.assign", "Reassigned"),
          );
          break;
        default:
          break;
      }
    } finally {
      setBusyAction(null);
      setPendingAction(null);
    }
  }

  const sections = presentation.detailSections;
  const hasFormAnswers = Object.keys(app.formData ?? {}).length > 0;
  // The bilingual section/field labels the applicant's wizard renders — this
  // page already fetches them (`requirements` above) but used to fall back to
  // the raw formData keys, so an officer saw `vesselId` instead of a label in
  // either language.
  const configSections = requirements?.licenseType.formSchema.sections ?? [];

  // A person-centric service has no company, so the company-shaped facts are
  // not merely empty — they are the wrong question. TIN is hidden rather than
  // shown blank, and the applicant's own identity card takes its place.
  const isPersonal = !app.companyName;
  const applicant = data.applicant;
  const applicantFullName = [
    applicant?.firstName,
    applicant?.middleName,
    applicant?.lastName,
  ]
    .filter(Boolean)
    .join(" ");
  // The profile is the reliable name for a personal service — `formData.account`
  // holds one only for applications filed after that field was added, and the
  // application number identifies the paperwork rather than the person.
  const headerName =
    app.companyName ||
    applicantFullName ||
    applicantOrCompanyName(app) ||
    app.applicationNumber;
  const linkedBookServices = (data.relatedApplications ?? []).filter(
    (related) =>
      ["SEAMAN_BOOK", "BTC_BASIC_TRAINING"].includes(
        related.licenseType?.key ?? "",
      ),
  );

  return (
    <Container size="xl" py="md">
      <Group justify="space-between" mb="md">
        <div>
          <Title order={2}>{headerName}</Title>
          <Group gap="xs">
            <Text size="sm" c="dimmed">
              {app.applicationNumber}
            </Text>
            <Badge color={STATUS_COLORS[status]} variant="light">
              {t(`queue.statusValues.${status}`, STATUS_LABELS[status])}
            </Badge>
            {data.issuedLicenseStatus === "SUPERSEDED" && (
              <Badge color="gray" variant="light">
                {t("review.certificateSuperseded", "Certificate superseded")}
              </Badge>
            )}
            {app.adjustmentRound > 0 && (
              <Badge color="orange" variant="light" size="sm">
                {t("review.round", {
                  count: app.adjustmentRound,
                  defaultValue: "round {{count}}",
                })}
              </Badge>
            )}
          </Group>
          {linkedBookServices.length > 1 && (
            <Group gap="xs" mt="xs">
              {linkedBookServices.map((related) => (
                <Badge
                  key={related.id}
                  variant={related.id === app.id ? "filled" : "outline"}
                  color={STATUS_COLORS[related.status]}
                  style={{ cursor: "pointer" }}
                  onClick={() => navigate(`/licence-review/${related.id}`)}
                >
                  {related.licenseType?.key === "SEAMAN_BOOK"
                    ? "Seaman Book"
                    : "BTC"}{" "}
                  · {related.applicationNumber} ·{" "}
                  {STATUS_LABELS[related.status]}
                </Badge>
              ))}
            </Group>
          )}
        </div>
        <Group gap="xs">
          <Tooltip
            label={
              railOpen
                ? t("review.hideActivity", "Hide activity")
                : t("review.showActivity", "Show activity")
            }
          >
            <ActionIcon
              variant="default"
              size="lg"
              onClick={() => setRailOpen((o) => !o)}
            >
              {railOpen ? (
                <IconLayoutSidebarRightCollapse size={18} />
              ) : (
                <IconLayoutSidebarRightExpand size={18} />
              )}
            </ActionIcon>
          </Tooltip>
        </Group>
      </Group>

      <Grid>
        {/* Zone 1 — sticky summary rail. */}
        <Grid.Col span={{ base: 12, md: 3 }}>
          <Stack style={{ position: "sticky", top: 16 }}>
            {/* Who, before what: a person-centric review is about the applicant,
                and the licence facts below are the context. */}
            {isPersonal && applicant && <ApplicantCard applicant={applicant} />}

            <Paper withBorder p="md">
              <Text fw={600} size="sm" mb="sm">
                {t("review.summary", "Summary")}
              </Text>
              <Stack gap={6}>
                <SummaryRow
                  label={t("review.type", "Type")}
                  value={localized(app.licenseType?.name)}
                />
                {/* A person has no TIN; showing the row blank invited the
                    reviewer to wonder what was missing. */}
                {!isPersonal && (
                  <SummaryRow
                    label={t("review.tin", "TIN")}
                    value={app.tinNumber}
                  />
                )}
                <SummaryRow
                  label={t("review.kind", "Kind")}
                  value={t(`review.kindValues.${app.kind}`, app.kind)}
                />
                <SummaryRow
                  label={t("review.submitted", "Submitted")}
                  value={showDate(app.submittedAt)}
                />
                <SummaryRow
                  label={t("review.slaLabel", "SLA")}
                  value={sla.label}
                />
              </Stack>
            </Paper>

            {/* Only once the certificate actually exists — before that there
                is nothing to view or download yet. */}
            {app.issuedLicenseId && (
              <Paper withBorder p="md">
                <Text fw={600} size="sm" mb="sm">
                  {t("review.certificate", "Certificate")}
                </Text>
                <Group gap="xs">
                  <Button
                    size="xs"
                    variant="light"
                    leftSection={<IconEye size={14} />}
                    loading={certificateBusy === "view"}
                    disabled={Boolean(certificateBusy)}
                    onClick={() => openCertificate("view")}
                  >
                    {t("review.view", "View")}
                  </Button>
                  <Button
                    size="xs"
                    variant="light"
                    leftSection={<IconFileDownload size={14} />}
                    loading={certificateBusy === "download"}
                    disabled={Boolean(certificateBusy)}
                    onClick={() => openCertificate("download")}
                  >
                    {t("review.download", "Download")}
                  </Button>
                </Group>
              </Paper>
            )}

            {/* Eligibility, checked and shown — not applied invisibly. */}
            {eligibility.length > 0 && (
              <Paper withBorder p="md">
                <Text fw={600} size="sm" mb="sm">
                  {t("review.eligibility", "Eligibility")}
                </Text>
                <Stack gap="xs">
                  {eligibility.map((rule) => (
                    <Group
                      key={rule.id}
                      gap="xs"
                      wrap="nowrap"
                      align="flex-start"
                    >
                      <ThemeIcon
                        size={18}
                        radius="xl"
                        variant="light"
                        color={
                          rule.status === "pass"
                            ? "teal"
                            : rule.status === "fail"
                              ? "red"
                              : "gray"
                        }
                      >
                        {rule.status === "pass" ? (
                          <IconCheck size={11} />
                        ) : rule.status === "fail" ? (
                          <IconX size={11} />
                        ) : (
                          <IconQuestionMark size={11} />
                        )}
                      </ThemeIcon>
                      <div style={{ minWidth: 0 }}>
                        <Text size="xs" fw={500}>
                          {rule.label}
                        </Text>
                        <Text size="xs" c="dimmed">
                          {rule.actual}
                        </Text>
                      </div>
                    </Group>
                  ))}
                </Stack>
              </Paper>
            )}

            {/* Examined certificates only — null for everything else. */}
            {EXAM_LEG_STATUSES.includes(app.status) && (
              <ExamStatePanel applicationId={id} />
            )}

            <Paper withBorder p="md">
              <Text fw={600} size="sm" mb="sm">
                {t("review.statusTimeline", "Progress")}
              </Text>
              <Timeline
                bulletSize={12}
                lineWidth={2}
                active={data.history.length}
              >
                {data.history.slice(-5).map((entry) => (
                  <Timeline.Item
                    key={entry.id}
                    title={
                      <Text size="xs" fw={600}>
                        {t(
                          `queue.statusValues.${entry.toStatus}`,
                          STATUS_LABELS[entry.toStatus] ?? entry.toStatus,
                        )}
                      </Text>
                    }
                  >
                    <Text size="xs" c="dimmed">
                      {showDate(entry.createdAt)}
                    </Text>
                  </Timeline.Item>
                ))}
              </Timeline>
            </Paper>
          </Stack>
        </Grid.Col>

        {/* Zone 2 — the application itself. */}
        <Grid.Col span={{ base: 12, md: railOpen ? 6 : 9 }}>
          <Tabs defaultValue={sections[0]}>
            <Tabs.List mb="md">
              {/* Tabs with nothing behind them are not rendered at all. */}
              {sections.includes("overview") && hasFormAnswers && (
                <Tabs.Tab value="overview">
                  {t("review.tabs.overview", "Overview")}
                </Tabs.Tab>
              )}
              {sections.includes("financials") && (
                <Tabs.Tab value="financials">
                  {t("review.tabs.financials", "Financials")}
                </Tabs.Tab>
              )}
              {sections.includes("documents") && (
                <Tabs.Tab value="documents">
                  {t("review.tabs.documents", "Documents")} (
                  {data.attachments.length})
                </Tabs.Tab>
              )}
              {sections.includes("staff") && data.staff.length > 0 && (
                <Tabs.Tab value="staff">
                  {t("review.tabs.staff", "Staff")} ({data.staff.length})
                </Tabs.Tab>
              )}
              {sections.includes("inspection") &&
                app.licenseType?.inspectionRequired && (
                  <Tabs.Tab value="inspection">
                    {t("review.tabs.inspection", "Inspection")}
                  </Tabs.Tab>
                )}
            </Tabs.List>

            <Tabs.Panel value="overview">
              <FormDetailsTab
                formData={app.formData ?? {}}
                configSections={configSections}
                currency={app.feeCurrency ?? undefined}
                flags={flags}
                onToggleFlag={(sectionKey) =>
                  toggleFlag("FORM_SECTION", sectionKey)
                }
                onFlagRemark={(sectionKey, remark) =>
                  setFlags((p) => ({
                    ...p,
                    [sectionKey]: { ...p[sectionKey], remark },
                  }))
                }
                resolveLocation={resolveLocation}
              />
            </Tabs.Panel>

            <Tabs.Panel value="financials">
              <Paper withBorder p="md">
                {needsCapital ? (
                  <>
                    <NumberInput
                      label={t(
                        "review.verifiedCapital",
                        "Verified capital (ETB)",
                      )}
                      description={
                        threshold
                          ? t("review.capitalHint", {
                              min: threshold.toLocaleString(i18n.language),
                              defaultValue:
                                "Minimum {{min}} — check against the bank letter",
                            })
                          : t(
                              "review.capitalHintNoMin",
                              "Checked against the bank letter",
                            )
                      }
                      value={capital ?? ""}
                      onChange={(v) => setCapital(Number(v) || undefined)}
                      thousandSeparator=","
                      error={
                        capital !== undefined &&
                        threshold &&
                        capital < threshold
                          ? t("review.belowMinimum", {
                              min: threshold.toLocaleString(i18n.language),
                              defaultValue: "Below the {{min}} minimum",
                            })
                          : undefined
                      }
                    />
                    <Text size="xs" c="dimmed" mt="xs">
                      {t("review.declared", "Applicant declared")}{" "}
                      {app.capitalAmountDeclared
                        ? Number(app.capitalAmountDeclared).toLocaleString(
                            i18n.language,
                          )
                        : "—"}
                    </Text>
                  </>
                ) : (
                  <Text size="sm" c="dimmed">
                    {t(
                      "review.capitalLocked",
                      "Capital can no longer be edited at this stage.",
                    )}
                  </Text>
                )}
              </Paper>
            </Tabs.Panel>

            <Tabs.Panel value="documents">
              <DocumentsTab
                applicationId={id}
                attachments={data.attachments}
                requirements={requirements?.documentRequirements ?? []}
                formData={app.formData ?? {}}
                flags={documentFlags}
                onToggleFlag={(key) => toggleFlag("DOCUMENT", key)}
                onFlagRemark={(key, remark) =>
                  setFlags((p) => ({ ...p, [key]: { ...p[key], remark } }))
                }
              />
            </Tabs.Panel>

            <Tabs.Panel value="staff">
              <AdvancedTable
                tableName={t("review.tabs.staff", "Staff")}
                columns={reviewStaffColumns(t, {
                  flags,
                  roleName: (member) =>
                    localized(roleNameByKey.get(member.roleKey)) ||
                    member.roleKey,
                  renderEvidence: (member) => (
                    <StaffEvidenceCell
                      staffId={member.id}
                      required={evidenceByRole.get(member.roleKey) ?? []}
                      fallback={member.documents}
                    />
                  ),
                  onToggleFlag: (member) => toggleFlag("STAFF", member.id),
                  onRemarkChange: (member, remark) =>
                    setFlags((p) => ({
                      ...p,
                      [member.id]: { ...p[member.id], remark },
                    })),
                })}
                data={staffPaged.rows}
                itemCount={staffPaged.itemCount}
                pageIndex={staffPaged.pageIndex}
                onPageChange={staffTable.setPageIndex}
                pageSize={staffTable.pageSize}
                refresh={refetch}
              />
            </Tabs.Panel>

            <Tabs.Panel value="inspection">
              <Paper withBorder p="md">
                {inspections.length === 0 ? (
                  <Text size="sm" c="dimmed">
                    {t(
                      "review.noInspections",
                      "No inspection has been scheduled yet.",
                    )}
                  </Text>
                ) : (
                  <Stack gap="xs">
                    {inspections.map((inspection) => (
                      <Group key={inspection.id} justify="space-between">
                        <div>
                          <Text size="sm">
                            {inspection.scheduledDate
                              ? `${showDate(inspection.scheduledDate)}${
                                  inspection.timeSlot
                                    ? ` — ${
                                        inspection.timeSlot === "MORNING"
                                          ? t("review.morning", "Morning")
                                          : t("review.afternoon", "Afternoon")
                                      }`
                                    : ""
                                }`
                              : t("review.unscheduled", "Not scheduled")}
                          </Text>
                          {inspection.findings && (
                            <Text size="xs" c="dimmed">
                              {inspection.findings}
                            </Text>
                          )}
                        </div>
                        <Group gap="xs">
                          {inspection.status === "SCHEDULED" &&
                            inspection.id === pendingInspection?.id &&
                            can([
                              "can:create:inspection",
                              "can:update:inspection",
                            ]) && (
                              <Tooltip
                                label={t(
                                  "review.actions.rescheduleInspection",
                                  "Reschedule inspection",
                                )}
                              >
                                <ActionIcon
                                  variant="subtle"
                                  size="sm"
                                  aria-label={t(
                                    "review.actions.rescheduleInspection",
                                    "Reschedule inspection",
                                  )}
                                  onClick={openReschedule}
                                >
                                  <IconPencil size={16} />
                                </ActionIcon>
                              </Tooltip>
                            )}
                          <Badge
                            variant="light"
                            color={
                              inspection.result === "FAILED" ? "red" : "teal"
                            }
                          >
                            {inspection.result === "PASSED"
                              ? t("review.passed", "Passed")
                              : inspection.result === "FAILED"
                                ? t("review.failed", "Failed")
                                : t(
                                    `review.inspectionStatus.${inspection.status}`,
                                    inspection.status,
                                  )}
                          </Badge>
                        </Group>
                      </Group>
                    ))}
                  </Stack>
                )}
              </Paper>
            </Tabs.Panel>
          </Tabs>

          {/* Page-level, not inside the inspection tab: a license type
              configured without an inspection detail section must still show
              why approval is blocked if it ever lands here. */}
          {status === "INSPECTION_FAILED" && (
            <Alert mt="md" color="red" icon={<IconAlertTriangle size={16} />}>
              {t(
                "review.inspectionFailedBlocked",
                "Approval is unavailable because the inspection failed. Schedule a re-inspection, request corrections, or reject the application.",
              )}
            </Alert>
          )}

          {status === "PAYMENT_PENDING" && (
            <Alert
              mt="md"
              color="yellow"
              icon={<IconAlertTriangle size={16} />}
            >
              {t("review.awaitingPayment", {
                amount: app.feeAmount,
                currency: app.feeCurrency,
                defaultValue:
                  "Waiting for the applicant to pay {{amount}} {{currency}}.",
              })}
            </Alert>
          )}
        </Grid.Col>

        {/* Zone 3 — activity and audit trail. */}
        {railOpen && (
          <Grid.Col span={{ base: 12, md: 3 }}>
            <ActivityRail detail={data} />
          </Grid.Col>
        )}
      </Grid>

      <DecisionBar
        status={status}
        assigneeName={
          app.assignedOfficerId ? t("review.assigned", "Assigned") : null
        }
        sla={sla}
        actions={actions}
        busyAction={busyAction}
        onAction={handleAction}
      />

      <DecisionConfirmModal
        action={pendingAction}
        applicantName={
          app.companyName ||
          applicantFullName ||
          t("review.theApplicant", "the applicant")
        }
        applicationNumber={app.applicationNumber}
        flaggedItems={flaggedItems}
        officers={officers}
        submitting={Boolean(busyAction)}
        onClose={() => setPendingAction(null)}
        onConfirm={submitDecision}
      />

      {/* No exam scheduling or outcome modal. Candidates register themselves
          for a published sitting, and the outcome is whatever the exam engine
          computed and a supervisor approved — the queue reads results, it does
          not write them. */}

      <Modal
        opened={inspectionOpen}
        onClose={() => setInspectionOpen(false)}
        title={
          rescheduling
            ? t("review.actions.rescheduleInspection", "Reschedule inspection")
            : t("review.actions.scheduleInspection", "Schedule inspection")
        }
      >
        <Stack>
          <AmharicDatePicker
            label={t("review.date", "Date")}
            value={inspectionDate}
            onChange={setInspectionDate}
          />
          <SegmentedControl
            value={inspectionTimeSlot}
            onChange={(value) => setInspectionTimeSlot(value as "MORNING" | "AFTERNOON")}
            data={[
              { value: "MORNING", label: t("review.morning", "Morning") },
              { value: "AFTERNOON", label: t("review.afternoon", "Afternoon") },
            ]}
          />
          {rescheduling && (
            <Textarea
              label={t("review.rescheduleReason", "Why is it moving?")}
              description={t(
                "review.rescheduleReasonHint",
                "Kept in the audit trail and sent to the applicant.",
              )}
              value={rescheduleReason}
              onChange={(e) => setRescheduleReason(e.currentTarget.value)}
              autosize
              minRows={2}
            />
          )}
          <ModalFooter>
            {/* Mantine strips pointer events from a disabled control, so the
                tooltip wraps a span — same trick as DecisionBar's ActionButton;
                a disabled button must still say why. */}
            <Tooltip
              label={t("review.pickDate", "Pick a date first")}
              disabled={Boolean(inspectionDate)}
            >
              <span style={{ display: "inline-flex" }}>
            <ActionIcon
              variant="filled"
              size="lg"
              disabled={!inspectionDate}
              aria-label={
                rescheduling
                  ? t("review.reschedule", "Reschedule")
                  : t("review.schedule", "Schedule")
              }
              onClick={() =>
                run(
                  async () => {
                    if (rescheduling) {
                      // Guarded by the action's own gate, which only offers
                      // rescheduling while a booking exists.
                      if (!pendingInspection) return;
                      await rescheduleInspection({
                        inspectionId: pendingInspection.id,
                        applicationId: id,
                        scheduledDate: inspectionDate,
                        timeSlot: inspectionTimeSlot,
                        ...(rescheduleReason.trim()
                          ? { reason: rescheduleReason.trim() }
                          : {}),
                      }).unwrap();
                    } else {
                      await scheduleInspection({
                        applicationId: id,
                        scheduledDate: inspectionDate,
                        timeSlot: inspectionTimeSlot,
                      }).unwrap();
                    }
                    setInspectionOpen(false);
                  },
                  rescheduling
                    ? t("review.done.rescheduled", "Inspection rescheduled")
                    : t("review.done.scheduled", "Inspection scheduled"),
                )
              }
            >
              <IconCheck size={18} />
            </ActionIcon>
              </span>
            </Tooltip>
          </ModalFooter>
        </Stack>
      </Modal>

      <Modal
        opened={issuanceOpen}
        onClose={() => setIssuanceOpen(false)}
        title={t("review.actions.scheduleIssuance", "Schedule pickup")}
      >
        <Stack>
          <AmharicDatePicker
            label={t("review.pickupDate", "Pickup date")}
            value={issuanceDate}
            onChange={setIssuanceDate}
          />
          <SegmentedControl
            value={issuancePeriod}
            onChange={(value) => setIssuancePeriod(value as "MORNING" | "AFTERNOON")}
            data={[
              { value: "MORNING", label: t("review.morning", "Morning") },
              { value: "AFTERNOON", label: t("review.afternoon", "Afternoon") },
            ]}
          />
          <ModalFooter>
            {/* Span-wrapped like DecisionBar's ActionButton — Mantine strips
                pointer events from a disabled control, and a disabled button
                must still say why. */}
            <Tooltip
              label={t("review.pickDate", "Pick a date first")}
              disabled={Boolean(issuanceDate)}
            >
              <span style={{ display: "inline-flex" }}>
                <ActionIcon
                  variant="filled"
                  size="lg"
                  disabled={!issuanceDate}
                  aria-label={t("review.schedule", "Schedule")}
                  onClick={() =>
                    run(
                      async () => {
                        await scheduleIssuance({
                          id,
                          scheduledDate: issuanceDate,
                          scheduledPeriod: issuancePeriod,
                        }).unwrap();
                        setIssuanceOpen(false);
                      },
                      t("review.done.scheduleIssuance", "Pickup scheduled"),
                    )
                  }
                >
                  <IconCheck size={18} />
                </ActionIcon>
              </span>
            </Tooltip>
          </ModalFooter>
        </Stack>
      </Modal>

      <Modal
        opened={resultOpen}
        onClose={() => setResultOpen(false)}
        title={t("review.inspectionResult", "Inspection result")}
      >
        <Stack>
          {/* Structured per-area outcomes; persisted to the inspection's
              checklist column, which was previously never populated. */}
          <Stack gap={6}>
            {INSPECTION_CHECKLIST_ITEMS.map((item) => (
              <Group key={item.key} justify="space-between" wrap="nowrap">
                <Text size="sm">{t(item.labelKey, item.fallback)}</Text>
                <SegmentedControl
                  size="xs"
                  value={checklist[item.key] ?? "PASS"}
                  onChange={(value) =>
                    setChecklist((prev) => ({
                      ...prev,
                      [item.key]: value as "PASS" | "FAIL" | "NEEDS_CORRECTION",
                    }))
                  }
                  data={[
                    { value: "PASS", label: t("review.checkPass", "Pass") },
                    {
                      value: "NEEDS_CORRECTION",
                      label: t("review.checkFix", "Fix"),
                    },
                    { value: "FAIL", label: t("review.checkFail", "Fail") },
                  ]}
                />
              </Group>
            ))}
          </Stack>
          <Textarea
            label={t("review.findings", "Findings")}
            withAsterisk
            value={findings}
            onChange={(e) => setFindings(e.currentTarget.value)}
            autosize
            minRows={3}
          />
          <Stack gap={4}>
            <Text size="sm" fw={500}>
              {t("review.findingsEvidence", "Supporting documents")}
            </Text>
            <Group gap="xs">
              {findingsEvidence.flatMap((attachment) =>
                (attachment.files ?? []).map((file) => (
                  <ActionIcon
                    key={file.id}
                    variant="light"
                    size="lg"
                    aria-label={file.originalName}
                    onClick={() =>
                      file.url &&
                      setFindingsPreview({ url: file.url, title: file.originalName })
                    }
                  >
                    <IconEye size={16} />
                  </ActionIcon>
                )),
              )}
              <FileButton
                accept="application/pdf,image/jpeg,image/png"
                onChange={async (file) => {
                  if (!file || !pendingInspection) return;
                  setFindingsUploadBusy(true);
                  await uploadDocument({
                    ownerType: "INSPECTION",
                    ownerId: pendingInspection.id,
                    documentKey: `evidence-${Date.now()}`,
                    file,
                  });
                  setFindingsUploadBusy(false);
                  refetchFindingsEvidence();
                }}
              >
                {(props) => (
                  <ActionIcon
                    {...props}
                    variant="outline"
                    size="lg"
                    disabled={!pendingInspection || findingsUploadBusy}
                    aria-label={t("review.uploadEvidence", "Upload document")}
                  >
                    {findingsUploadBusy ? (
                      <Loader size={14} type="oval" />
                    ) : (
                      <IconPaperclip size={16} />
                    )}
                  </ActionIcon>
                )}
              </FileButton>
            </Group>
          </Stack>
          {inspectionNotYetDue && (
            <Alert color="yellow" icon={<IconAlertTriangle size={16} />}>
              {t("review.disabled.inspectionNotYetDue", {
                date: pendingInspection?.scheduledDate
                  ? showDate(pendingInspection.scheduledDate)
                  : "",
                defaultValue:
                  "Inspection scheduled for {{date}}. Results can be recorded after the scheduled time.",
              })}
            </Alert>
          )}
          <ModalFooter grow>
            <ActionIcon
              variant="light"
              color="teal"
              size="lg"
              disabled={!findings.trim() || !pendingInspection || inspectionNotYetDue}
              aria-label={t("review.passed", "Passed")}
              onClick={() =>
                run(
                  async () => {
                    // Guarded by the disabled state above; narrowing it here
                    // keeps that guarantee in the type system too.
                    if (!pendingInspection) return;
                    await recordResult({
                      inspectionId: pendingInspection.id,
                      applicationId: id,
                      result: "PASSED",
                      findings,
                      checklist: buildChecklist(checklist),
                    }).unwrap();
                    setResultOpen(false);
                  },
                  t("review.done.inspectionPassed", "Inspection passed"),
                )
              }
            >
              <IconCheck size={18} />
            </ActionIcon>
            <ActionIcon
              variant="light"
              color="red"
              size="lg"
              disabled={!findings.trim() || !pendingInspection || inspectionNotYetDue}
              aria-label={t("review.failed", "Failed")}
              onClick={() =>
                run(
                  async () => {
                    // Guarded by the disabled state above; narrowing it here
                    // keeps that guarantee in the type system too.
                    if (!pendingInspection) return;
                    await recordResult({
                      inspectionId: pendingInspection.id,
                      applicationId: id,
                      result: "FAILED",
                      findings,
                      checklist: buildChecklist(checklist),
                    }).unwrap();
                    setResultOpen(false);
                  },
                  t("review.done.inspectionFailed", "Inspection failed"),
                )
              }
            >
              <IconX size={18} />
            </ActionIcon>
          </ModalFooter>
        </Stack>
      </Modal>

      <PdfPreviewModal
        opened={Boolean(findingsPreview)}
        onClose={() => setFindingsPreview(null)}
        url={findingsPreview?.url ?? ""}
        title={findingsPreview?.title}
      />

      <PdfPreviewModal
        opened={Boolean(certificatePreview)}
        onClose={() => setCertificatePreview(null)}
        url={certificatePreview ?? ""}
        title={t("review.certificate", "Certificate")}
      />
    </Container>
  );
}

/**
 * Evidence badges for one staff member.
 *
 * `application-for-review` nests a `documents` array per staff member, but it
 * doesn't always carry the uploaded file (the portal's own upload widget
 * hits the attachments endpoint directly for the same reason). Query
 * attachments by owner here too, so the officer gets a working link instead
 * of a badge with nowhere to go.
 */
function StaffEvidenceCell({
  staffId,
  required,
  fallback,
}: {
  staffId: string;
  /** What this person's role must produce, from the licence-type config. */
  required: StaffEvidenceRequirement[];
  fallback?: { id: string; documentKey: string; files: { url?: string }[] }[];
}) {
  const localized = useLocalized();
  const { t } = useTranslation();
  const [preview, setPreview] = useState<{ url: string; title: string } | null>(
    null,
  );
  const { data: attachments } = useGetAttachmentsQuery({
    ownerType: "APPLICATION_STAFF",
    ownerId: staffId,
  });
  const docs = attachments?.length ? attachments : (fallback ?? []);
  const uploadedBy = new Map(docs.map((d) => [d.documentKey, d]));

  // Drive the list off the requirements, not off what happens to have been
  // uploaded: a mandatory CV that is absent has to be visible as absent, which
  // is the whole point of the officer looking at this column. Anything
  // uploaded outside the list still gets shown rather than silently dropped.
  const extras = docs.filter(
    (d) => !required.some((r) => r.docKey === d.documentKey),
  );

  if (!required.length && !extras.length) {
    return (
      <Text size="xs" c="dimmed">
        —
      </Text>
    );
  }

  const badge = (
    key: string,
    label: string,
    url: string | undefined,
    mandatory: boolean,
  ) => {
    const missing = !url;
    return (
      <Tooltip
        key={key}
        label={
          missing
            ? mandatory
              ? t("review.evidenceMissingRequired", "Required — not uploaded")
              : t("review.evidenceMissing", "Not uploaded")
            : t("licensing.documents.view", "View")
        }
        withArrow
      >
        <Badge
          size="xs"
          variant={missing ? "outline" : "light"}
          color={missing ? (mandatory ? "red" : "gray") : "teal"}
          style={missing ? undefined : { cursor: "pointer" }}
          onClick={
            missing ? undefined : () => setPreview({ url: url, title: label })
          }
        >
          {label}
          {missing && mandatory ? " *" : ""}
        </Badge>
      </Tooltip>
    );
  };

  return (
    <Group gap={4}>
      {required.map((item) =>
        badge(
          item.docKey,
          localized(item.label) || item.docKey,
          uploadedBy.get(item.docKey)?.files?.[0]?.url,
          item.mandatory,
        ),
      )}
      {extras.map((doc) =>
        badge(doc.id, doc.documentKey, doc.files?.[0]?.url, false),
      )}
      <PdfPreviewModal
        opened={Boolean(preview)}
        onClose={() => setPreview(null)}
        url={preview?.url ?? ""}
        title={preview?.title}
      />
    </Group>
  );
}

function SummaryRow({
  label,
  value,
}: {
  label: string;
  value?: string | null;
}) {
  return (
    <Group justify="space-between" gap="xs" wrap="nowrap">
      <Text size="xs" c="dimmed">
        {label}
      </Text>
      <Text size="xs" fw={500} ta="right" truncate>
        {value || "—"}
      </Text>
    </Group>
  );
}

export default LicenseReviewPage;
