"use client";

/**
 * Settings → Users and roles, rebuilt on the Medora design system (Stage 7, 24 Oct 2026) to match
 * `/design-system/access` exactly: the same roles-and-permissions block, users table, user drawer
 * and activity log, wired to the real API (PRD 1.1 roles, 0.5 user rules, card 007 deactivation).
 *
 * Plan C (26 Oct 2026): first and last name, phone and location are stored (ST-12); users filter by branch, and a
 * locked sign-in can be unlocked (ST-13, ST-16); the activity log is fetched by date with each user's branch (ST-15).
 * Passcode sign-in still waits for its design input (Q2), so the form refuses it.
 */

import * as React from "react";
import { BanIcon, KeyRoundIcon, LockOpenIcon, PencilIcon, PlusIcon, RotateCcwIcon, SearchIcon, Trash2Icon } from "lucide-react";

import { deactivateBlock, PermissionGrid, roleDeleteBlock, roleNameProblem, UserForm, userProblems, UserStatus, type AppUser, type Grants, type PermPage, type Role, type UserDraft } from "@/components/medora/access";
import { ActivityLog, type ActivityEntry } from "@/components/medora/activity-log";
import { PanelPermissions, type PanelGrants } from "@/components/medora/dashboard-panels";
import { ConfirmDialog } from "@/components/medora/confirm-dialog";
import { RecordDrawer } from "@/components/medora/drawer";
import { MedoraPageHeader } from "@/components/medora/page-header";
import { RowActions } from "@/components/medora/row-actions";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
import { Spinner } from "@/components/ui/spinner";
import { toast } from "@/components/ui/toast";
import { AuditApi, BranchesApi, LocationsApi, RolesApi, UsersApi, type LocationRow } from "@/lib/api";
import { useMenuPermissions } from "@/lib/permissions";
import { Surface } from "@/components/medora/surface";
import { Loading } from "@/components/medora/loading";

/* ------------------------------------ API mapping ------------------------------------ */

type ApiUser = {
  id: number;
  name: string;
  username: string;
  email: string;
  role: string;
  status: string;
  branchId: number | null;
  firstName?: string | null;
  lastName?: string | null;
  phone?: string | null;
  locationId?: number | null;
  loginType?: string;
  lastSeenAt?: string | null;
  lockedUntil?: string | null;
};
/** A user as listed, with what the design's `AppUser` has no field for: a sign-in lock (ST-16). */
type ListedUser = AppUser & { lockedUntil?: string };
type ApiBranch = { id: number; name: string };
type ApiRole = Role & { id: number; locked: boolean };
type ApiAudit = { id: number; userName?: string; action: string; entity?: string; details?: string; createdAt: string; branchName?: string | null };

const UR = "Settings/Users and roles";

/** The stored 2547… phone as people write it, 0712 345 678. */
const localPhone = (p?: string | null) => (p && /^254\d{9}$/.test(p) ? `0${p.slice(3, 6)} ${p.slice(6, 9)} ${p.slice(9)}` : (p ?? ""));

/** Users from before plan C have only `name`: its first word is the first name, the rest the last (as the API splits it). */
function mapUser(u: ApiUser, branches: ApiBranch[], locations: LocationRow[]): ListedUser {
  const [first, ...rest] = (u.name ?? "").trim().split(/\s+/);
  const locked = u.lockedUntil && new Date(u.lockedUntil) > new Date() ? u.lockedUntil : undefined;
  return {
    id: u.id,
    username: u.username,
    first: u.firstName ?? first ?? "",
    last: u.firstName ? (u.lastName ?? "") : rest.join(" "),
    phone: localPhone(u.phone),
    email: u.email ?? "",
    role: u.role,
    branch: branches.find((b) => b.id === u.branchId)?.name ?? "",
    location: locations.find((l) => l.id === u.locationId)?.name ?? "",
    login: "Password", // passcode sign-in waits for its design input (plan C, Q2)
    status: u.status === "Active" ? "Active" : "Deactivated",
    lastSeen: u.lastSeenAt ? localIso(u.lastSeenAt).replace("T", " ") : undefined,
    lockedUntil: locked,
  };
}

const MODULES: Record<string, string> = { User: "Users and roles", Role: "Users and roles", Pharmacy: "Settings", Setting: "Settings", Sale: "Point of sale", Expense: "Accounts", Product: "Inventory", Customer: "Customers", Supplier: "Suppliers" };
const humanise = (code: string) => code.toLowerCase().replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
const pad = (n: number) => String(n).padStart(2, "0");
const localIso = (s: string) => {
  const d = new Date(s);
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
};

/** Audit rows as the design's activity log entries. Passwords and keys only ever say "Changed". */
function mapAudit(a: ApiAudit): ActivityEntry {
  const sensitive = /PASSWORD|SECRET|KEY/.test(a.action);
  return {
    id: String(a.id),
    at: localIso(a.createdAt),
    user: a.userName ?? "System",
    branch: a.branchName ?? "",
    module: MODULES[a.entity ?? ""] ?? a.entity ?? "",
    action: humanise(a.action),
    record: sensitive ? (a.details ?? "").split(":")[0] : "",
    changes: sensitive ? [{ field: "Value", sensitive: true }] : a.details ? [{ field: "Details", after: a.details, long: a.details.length > 60 }] : [],
  };
}

const apiError = (e: unknown, fallback: string): string => {
  const m = (e as { response?: { data?: { message?: string | string[] } } })?.response?.data?.message;
  return Array.isArray(m) ? m.join(", ") : m || fallback;
};

/** Frame matching the design system's example boxes. */
function Panel({ title, description, children }: { title: string; description?: string; children: React.ReactNode }) {
  return (
    <section className="overflow-hidden rounded-lg border border-border bg-card">
      <header className="flex items-baseline justify-between gap-4 border-b border-border bg-muted/50 px-4 py-2">
        <h2 className="text-xs font-medium text-foreground">{title}</h2>
        {description ? <p className="truncate text-xs text-muted-foreground">{description}</p> : null}
      </header>
      {children}
    </section>
  );
}

/** A group of framed components under one heading (form layout spec: section heading, heading-sm, optional caption). */
function Group({ title, description, children }: { title: string; description?: string; children: React.ReactNode }) {
  return (
    <section className="space-y-3">
      <div>
        <h2 className="text-sm font-semibold text-foreground">{title}</h2>
        {description ? <p className="text-xs text-muted-foreground">{description}</p> : null}
      </div>
      {children}
    </section>
  );
}

/** Section heading matching the design system's sections. Without a title (R7, card 037) the page header names it. */
function Section({ id, title, description, children }: { id: string; title?: string; description?: string; children: React.ReactNode }) {
  return (
    <section id={id} className={title ? "scroll-mt-20 py-8 [&+&]:border-t [&+&]:border-border" : "scroll-mt-20 pt-4 pb-8"}>
      {title ? (
        <div className="mb-4">
          <h2 className="text-base font-semibold text-foreground">{title}</h2>
          {description ? <p className="mt-1 max-w-3xl text-sm text-muted-foreground">{description}</p> : null}
        </div>
      ) : null}
      <div className="space-y-4">{children}</div>
    </section>
  );
}

/* ----------------------------------- Roles and permissions ----------------------------------- */

function RolesAndPermissions({ roles, pages, reload, canCreate, canUpdate, canDelete }: { roles: ApiRole[]; pages: PermPage[]; reload: () => Promise<void>; canCreate: boolean; canUpdate: boolean; canDelete: boolean }) {
  const [role, setRole] = React.useState("");
  const [saved, setSaved] = React.useState<Grants>({});
  const [draft, setDraft] = React.useState<Grants>({});
  const [saving, setSaving] = React.useState(false);
  const [naming, setNaming] = React.useState<{ editing?: string; value: string; tried: boolean; busy?: boolean } | null>(null);
  const [deleting, setDeleting] = React.useState<ApiRole | null>(null);
  const byName = (n: string) => roles.find((r) => r.name === n);

  const load = React.useCallback(
    async (name: string) => {
      const r = roles.find((x) => x.name === name);
      if (!r) return {};
      const { grants } = (await RolesApi.get(r.id)) as { grants: Grants[string] };
      setSaved((s) => ({ ...s, [name]: grants }));
      setDraft((d) => (name in d ? d : { ...d, [name]: grants }));
      return grants;
    },
    [roles],
  );

  React.useEffect(() => {
    if (!role && roles.length) setRole(roles.find((r) => r.name === "Cashier")?.name ?? roles[0].name);
  }, [roles, role]);
  React.useEffect(() => {
    if (role && !(role in saved)) load(role).catch((e) => toast.add({ type: "error", title: "Couldn't load permissions", description: apiError(e, "Try again.") }));
  }, [role, saved, load]);

  const changed = roles.filter((r) => r.name in draft && JSON.stringify(draft[r.name] ?? {}) !== JSON.stringify(saved[r.name] ?? {})).map((r) => r.name);
  const problem = naming ? roleNameProblem(naming.value, roles, naming.editing) : undefined;
  const editable = (r: ApiRole) => !r.builtIn; // PRD ST-08: the default roles always exist

  return (
    <Group title="Roles and permissions" description="Tick what the role may do on each page. Create, Approve and Export also tick Read.">
      <div className="grid gap-4 xl:grid-cols-[16rem_minmax(0,1fr)]">
        <div className="space-y-2">
          <div className="flex items-center justify-between">
            <h4 className="text-sm font-semibold">Roles</h4>
            {canCreate ? (
              <Button size="xs" variant="outline" onClick={() => setNaming({ value: "", tried: false })}>
                <PlusIcon data-icon="inline-start" />
                New role
              </Button>
            ) : null}
          </div>
          <ul className="divide-y divide-[var(--color-border-subtle)] rounded-lg border border-[var(--color-border-panel)] text-sm bg-card">
            {roles.map((r) => (
              <li key={r.id} className={r.name === role ? "bg-[var(--color-bg-selected)]" : undefined}>
                <div className="flex items-center gap-1 px-2 py-1.5">
                  <button type="button" className="min-w-0 flex-1 cursor-pointer text-left" aria-pressed={r.name === role} onClick={() => setRole(r.name)}>
                    <span className="font-medium">{r.name}</span>
                    {changed.includes(r.name) ? <span className="ml-1.5 text-xs text-status-warning-text">Unsaved</span> : null}
                    <span className="block text-xs text-muted-foreground tabular-nums">
                      {r.users} {r.users === 1 ? "user" : "users"}
                      {r.builtIn ? " · default" : ""}
                    </span>
                  </button>
                  {editable(r) && (canUpdate || canDelete) ? (
                    <RowActions
                      label={`role ${r.name}`}
                      actions={[
                        ...(canUpdate ? [{ label: "Rename…", icon: PencilIcon, onSelect: () => setNaming({ editing: r.name, value: r.name, tried: false }) }] : []),
                        ...(canDelete ? [{ label: "Delete…", icon: Trash2Icon, destructive: true, onSelect: () => (roleDeleteBlock(r) ? toast.add({ type: "error", title: `Can't delete ${r.name}`, description: roleDeleteBlock(r) }) : setDeleting(r)) }] : []),
                      ]}
                    />
                  ) : null}
                </div>
              </li>
            ))}
          </ul>
          {naming ? (
            <div className="space-y-2 rounded-lg border border-[var(--color-border-panel)] p-2 bg-card">
              <Input aria-label={naming.editing ? `New name for ${naming.editing}` : "Name of the new role"} value={naming.value} onChange={(e) => setNaming({ ...naming, value: e.target.value })} aria-invalid={naming.tried && !!problem} autoFocus />
              {naming.tried && problem ? <p className="text-xs text-status-error-text">{problem}</p> : null}
              <div className="flex justify-end gap-1">
                <Button size="xs" variant="ghost" disabled={naming.busy} onClick={() => setNaming(null)}>
                  Cancel
                </Button>
                <Button
                  size="xs"
                  disabled={naming.busy}
                  onClick={async () => {
                    if (problem) return setNaming({ ...naming, tried: true });
                    const name = naming.value.trim();
                    setNaming({ ...naming, busy: true });
                    try {
                      if (naming.editing) {
                        await RolesApi.rename(byName(naming.editing)!.id, name);
                        const move = (g: Grants) => Object.fromEntries(Object.entries(g).map(([k, v]) => [k === naming.editing ? name : k, v]));
                        setDraft(move);
                        setSaved(move);
                        if (role === naming.editing) setRole(name);
                      } else {
                        await RolesApi.create(name);
                        setRole(name);
                      }
                      await reload();
                      setNaming(null);
                    } catch (e) {
                      setNaming({ ...naming, busy: false });
                      toast.add({ type: "error", title: naming.editing ? "Couldn't rename the role" : "Couldn't create the role", description: apiError(e, "Try again.") });
                    }
                  }}
                >
                  {naming.editing ? "Rename" : "Create"}
                </Button>
              </div>
            </div>
          ) : null}
        </div>

        <div className="min-w-0 space-y-3">
          <Surface className="flex flex-wrap items-center gap-2">
            <h4 className="text-sm font-semibold">{role}</h4>
            {role && role !== "Admin" && canUpdate ? (
              <NativeSelect
                size="sm"
                aria-label={`Copy permissions to ${role} from another role`}
                value=""
                className="ml-auto"
                onChange={async (e) => {
                  const from = e.target.value;
                  if (!from) return;
                  try {
                    const source = from in saved ? saved[from] : await load(from);
                    setDraft((d) => ({ ...d, [role]: JSON.parse(JSON.stringify(source ?? {})) }));
                    toast.add({ title: `Copied ${from}'s permissions to ${role}`, description: "Adjust, then save." });
                  } catch (err) {
                    toast.add({ type: "error", title: "Couldn't copy permissions", description: apiError(err, "Try again.") });
                  }
                }}
              >
                <NativeSelectOption value="">Copy from role…</NativeSelectOption>
                {roles
                  .filter((r) => r.name !== role)
                  .map((r) => (
                    <NativeSelectOption key={r.id}>{r.name}</NativeSelectOption>
                  ))}
              </NativeSelect>
            ) : null}
          </Surface>
          <div className="max-h-[520px] overflow-y-auto">
            {role in draft ? (
              <PermissionGrid pages={pages} role={role} locked={role === "Admin" || !canUpdate} grants={draft[role] ?? {}} onChange={(g) => setDraft((d) => ({ ...d, [role]: g }))} />
            ) : (
              <Loading what="permissions" columns={9} rows={8} />
            )}
          </div>
          {changed.length ? (
            <div role="region" aria-label="Unsaved changes" className="flex flex-wrap items-center gap-3 border-t border-[var(--color-border-subtle)] pt-3">
              <p className="mr-auto text-sm">
                <span className="font-medium">Unsaved changes</span> <span className="text-muted-foreground">for {changed.join(", ")}. Users get them at their next request; the API enforces them.</span>
              </p>
              <Button variant="ghost" disabled={saving} onClick={() => setDraft(saved)}>
                Discard
              </Button>
              <Button
                disabled={saving}
                onClick={async () => {
                  setSaving(true);
                  const done: string[] = [];
                  try {
                    for (const name of changed) {
                      const { grants } = (await RolesApi.setGrants(byName(name)!.id, draft[name] as Record<string, string[]>)) as { grants: Grants[string] };
                      setSaved((s) => ({ ...s, [name]: grants }));
                      setDraft((d) => ({ ...d, [name]: grants }));
                      done.push(name);
                    }
                    toast.add({ title: "Permissions saved", description: done.join(", ") });
                  } catch (e) {
                    toast.add({ type: "error", title: done.length ? `Saved ${done.join(", ")}; the rest weren't` : "Permissions not saved", description: apiError(e, "Try again.") });
                  } finally {
                    setSaving(false);
                  }
                }}
              >
                {saving ? <Spinner /> : null}
                {saving ? "Saving…" : "Save permissions"}
              </Button>
            </div>
          ) : null}
        </div>
      </div>
      {deleting ? (
        <ConfirmDialog
          open
          onOpenChange={(o) => !o && setDeleting(null)}
          level="A"
          title={`Delete the ${deleting.name} role?`}
          consequences={["No users have it.", "Its permissions are removed. The activity log keeps the record of who had it."]}
          actionLabel="Delete role"
          onConfirm={async () => {
            try {
              await RolesApi.remove(deleting.id);
            } catch (e) {
              throw new Error(apiError(e, "The role wasn't deleted."));
            }
            if (role === deleting.name) setRole("Cashier");
            setDeleting(null);
            await reload();
          }}
        />
      ) : null}
    </Group>
  );
}

/* --------------------------------------- Users --------------------------------------- */

/** The form's copy of a listed user: only the design's fields, so "unsaved changes" compares like with like. */
const draftOf = (u: AppUser): UserDraft => ({ username: u.username, first: u.first, last: u.last, phone: u.phone, email: u.email, role: u.role, branch: u.branch, location: u.location, login: u.login, secret: "" });
const blankUser: UserDraft = { username: "", first: "", last: "", phone: "", email: "", role: "", branch: "", location: "", login: "Password", secret: "" };

/** A random temporary password: 12 characters, no look-alikes (0/O, 1/l). */
function temporaryPassword() {
  const chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
  const bytes = crypto.getRandomValues(new Uint8Array(12));
  return Array.from(bytes, (b) => chars[b % chars.length]).join("");
}

function Users({ users, branches, locations, roleNames, me, reload, canCreate, canUpdate }: { users: ListedUser[]; branches: ApiBranch[]; locations: LocationRow[]; roleNames: string[]; me: number; reload: () => Promise<void>; canCreate: boolean; canUpdate: boolean }) {
  const [q, setQ] = React.useState("");
  const [fRole, setFRole] = React.useState("");
  const [fBranch, setFBranch] = React.useState("");
  const [fStatus, setFStatus] = React.useState("Active");
  const [form, setForm] = React.useState<{ draft: UserDraft; id?: number; tried: boolean } | null>(null);
  const [saving, setSaving] = React.useState(false);
  const [resetting, setResetting] = React.useState<AppUser | null>(null);
  const [issued, setIssued] = React.useState<{ user: AppUser; password: string } | null>(null);
  const shown = users.filter((u) => (!fRole || u.role === fRole) && (!fBranch || (fBranch === "-" ? !u.branch : u.branch === fBranch)) && (!fStatus || u.status === fStatus) && (!q.trim() || `${u.first} ${u.last} ${u.username} ${u.phone}`.toLowerCase().includes(q.trim().toLowerCase())));
  // ST-04: a user's location is one of their branch's locations that sells at the till
  const branchLocations = (branch: string) => locations.filter((l) => l.branchId === branches.find((b) => b.name === branch)?.id && l.showInPos && !l.system);
  const original = form?.id ? users.find((u) => u.id === form.id) : undefined;

  const problems = React.useMemo(() => {
    if (!form) return {};
    const p = userProblems(form.draft, users, form.id);
    // Existing usernames predate the design's format and can't be changed (the field is locked on edit)
    if (form.id && form.draft.username === original?.username) delete p.username;
    if (!form.draft.email.trim()) p.email = "Enter an email: the API needs one for each user, and it's used for email-code sign-in.";
    // Plan C, Q2: refused rather than silently dropped
    if (form.draft.login === "Passcode") p.login = "Passcode sign-in isn't available yet: its rules are still an open design input.";
    if (form.draft.location && !branchLocations(form.draft.branch).some((l) => l.name === form.draft.location)) p.location = `Choose one of ${form.draft.branch || "the branch"}'s locations.`;
    return p;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [form, users, original, locations]);

  const branchId = (name: string) => branches.find((b) => b.name === name)?.id ?? null;
  const patch = async (u: AppUser, body: Record<string, unknown>, done: string) => {
    try {
      await UsersApi.update(u.id, body);
      await reload();
      toast.add({ title: done });
    } catch (e) {
      toast.add({ type: "error", title: `Couldn't update ${u.first} ${u.last}`, description: apiError(e, "Try again.") });
    }
  };

  return (
    <Panel title="Users" description="Users are never deleted, only deactivated, so their sales keep their name.">
      <div className="flex flex-wrap items-center gap-2 border-b border-border px-4 py-3">
        <div className="relative w-full sm:w-56">
          <SearchIcon className="pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground" aria-hidden />
          <Input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Find a user" aria-label="Find a user" className="pl-8" />
        </div>
        <NativeSelect aria-label="Role" value={fRole} onChange={(e) => setFRole(e.target.value)}>
          <NativeSelectOption value="">All roles</NativeSelectOption>
          {roleNames.map((r) => (
            <NativeSelectOption key={r}>{r}</NativeSelectOption>
          ))}
        </NativeSelect>
        <NativeSelect aria-label="Branch" value={fBranch} onChange={(e) => setFBranch(e.target.value)}>
          <NativeSelectOption value="">All branches</NativeSelectOption>
          {branches.map((b) => (
            <NativeSelectOption key={b.id}>{b.name}</NativeSelectOption>
          ))}
          <NativeSelectOption value="-">No branch</NativeSelectOption>
        </NativeSelect>
        <NativeSelect aria-label="Status" value={fStatus} onChange={(e) => setFStatus(e.target.value)}>
          <NativeSelectOption value="">Any status</NativeSelectOption>
          <NativeSelectOption>Active</NativeSelectOption>
          <NativeSelectOption>Deactivated</NativeSelectOption>
        </NativeSelect>
        {canCreate ? (
          <Button size="sm" className="ml-auto" onClick={() => setForm({ draft: blankUser, tried: false })}>
            <PlusIcon data-icon="inline-start" />
            New user
          </Button>
        ) : null}
      </div>
      <div className="overflow-x-auto">
        <table className="w-full border-collapse text-sm">
          <thead className="bg-[var(--color-bg-surface-subtle)] text-left text-xs text-muted-foreground">
            <tr>
              {["Name", "Role and branch", "Signs in with", "Status", ""].map((h, i) => (
                <th key={i} scope="col" className="px-3 py-2 font-medium">
                  {h || <span className="sr-only">Actions</span>}
                </th>
              ))}
            </tr>
          </thead>
          <tbody className="divide-y divide-[var(--color-border-subtle)]">
            {shown.map((u) => {
              const block = deactivateBlock(u, users, me);
              return (
                <tr key={u.id}>
                  <th scope="row" className="px-3 py-2 text-left font-medium">
                    {u.first} {u.last}
                    {u.id === me ? <span className="ml-1.5 text-xs font-normal text-muted-foreground">You</span> : null}
                    <span className="block font-mono text-xs font-normal text-muted-foreground">{u.username}</span>
                  </th>
                  <td className="px-3 py-2">
                    {u.role}
                    <span className="block text-xs text-muted-foreground">
                      {u.branch || "All branches"}
                      {u.location ? `, ${u.location}` : ""}
                    </span>
                  </td>
                  <td className="px-3 py-2">{u.login}</td>
                  <td className="px-3 py-2">
                    <UserStatus u={u} />
                    {u.lockedUntil ? <span className="mt-1 block text-xs font-medium text-status-error-text">Sign-in locked until {localIso(u.lockedUntil).slice(11)}</span> : null}
                    {u.lastSeen ? <span className="block text-xs text-muted-foreground">Last seen {u.lastSeen}</span> : null}
                  </td>
                  <td className="px-2 py-2 text-right">
                    {canUpdate || u.id === me ? (
                      <RowActions
                        label={`${u.first} ${u.last}`}
                        actions={[
                          { label: "Edit", icon: PencilIcon, onSelect: () => setForm({ draft: draftOf(u), id: u.id, tried: false }) },
                          ...(canUpdate && u.status === "Active" && u.id !== me ? [{ label: `Reset ${u.login.toLowerCase()}…`, icon: KeyRoundIcon, onSelect: () => setResetting(u) }] : []),
                          ...(canUpdate && u.lockedUntil && u.id !== me ? [{ label: "Unlock sign-in", icon: LockOpenIcon, onSelect: () => void patch(u, { unlock: true }, `${u.first} ${u.last} can sign in again`) }] : []),
                          ...(canUpdate
                            ? [
                                u.status === "Active"
                                  ? { label: "Deactivate", icon: BanIcon, destructive: true, onSelect: () => (block ? toast.add({ type: "error", title: `Can't deactivate ${u.first}`, description: block }) : void patch(u, { status: "Inactive" }, `${u.first} ${u.last} deactivated: they're signed out at their next action.`)) }
                                  : { label: "Reactivate", icon: RotateCcwIcon, onSelect: () => void patch(u, { status: "Active" }, `${u.first} ${u.last} reactivated`) },
                              ]
                            : []),
                        ]}
                      />
                    ) : null}
                  </td>
                </tr>
              );
            })}
            {!shown.length ? (
              <tr>
                <td colSpan={5} className="px-3 py-8 text-center text-sm text-muted-foreground">
                  No users match these filters.
                </td>
              </tr>
            ) : null}
          </tbody>
        </table>
      </div>
      {form ? (
        <RecordDrawer
          open
          onOpenChange={(o) => !o && setForm(null)}
          variant="edit"
          width="lg"
          saving={saving}
          dirty={JSON.stringify(form.draft) !== JSON.stringify(form.id ? draftOf(original!) : blankUser)}
          title={form.id ? `Edit ${form.draft.first} ${form.draft.last}` : "New user"}
          footer={
            <>
              <Button variant="outline" disabled={saving} onClick={() => setForm(null)}>
                Cancel
              </Button>
              <Button
                disabled={saving}
                onClick={async () => {
                  if (Object.keys(problems).length) return setForm({ ...form, tried: true });
                  const d = form.draft;
                  const body: Record<string, unknown> = {
                    name: `${d.first.trim()} ${d.last.trim()}`.trim(),
                    firstName: d.first.trim(),
                    lastName: d.last.trim(),
                    phone: d.phone.trim(),
                    email: d.email.trim(),
                    role: d.role,
                    branchId: branchId(d.branch),
                    locationId: branchLocations(d.branch).find((l) => l.name === d.location)?.id ?? null,
                  };
                  if (d.secret) body.password = d.secret;
                  setSaving(true);
                  try {
                    if (form.id) await UsersApi.update(form.id, body);
                    else await UsersApi.create({ ...body, username: d.username, password: d.secret });
                    await reload();
                    toast.add({ title: form.id ? "User saved" : `${d.first} ${d.last} added`, description: form.id ? undefined : `Signs in as ${d.username}.` });
                    setForm(null);
                  } catch (e) {
                    toast.add({ type: "error", title: "User not saved", description: apiError(e, "Try again.") });
                  } finally {
                    setSaving(false);
                  }
                }}
              >
                {saving ? <Spinner /> : null}
                {form.id ? "Save user" : "Create user"}
              </Button>
            </>
          }
        >
          <UserForm draft={form.draft} onChange={(d) => setForm({ ...form, draft: d })} roles={roleNames} branches={branches.map((b) => b.name)} locations={branchLocations(form.draft.branch).map((l) => l.name)} problems={form.tried ? problems : {}} editing={!!form.id} />
        </RecordDrawer>
      ) : null}
      {resetting ? (
        <ConfirmDialog
          open
          onOpenChange={(o) => !o && setResetting(null)}
          level="A"
          title={`Reset ${resetting.first}'s password?`}
          consequences={["A temporary password is shown once, for you to give them in person.", "Their current password stops working straight away."]}
          actionLabel="Reset"
          onConfirm={async () => {
            const password = temporaryPassword();
            try {
              await UsersApi.update(resetting.id, { password });
            } catch (e) {
              throw new Error(apiError(e, "The password wasn't reset."));
            }
            setIssued({ user: resetting, password });
            setResetting(null);
          }}
        />
      ) : null}
      <Dialog open={!!issued} onOpenChange={(o) => !o && setIssued(null)}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>
              Temporary password for {issued?.user.first} {issued?.user.last}
            </DialogTitle>
            <DialogDescription>Give it to them in person. It isn&apos;t shown again.</DialogDescription>
          </DialogHeader>
          <p className="rounded-md border border-border bg-muted px-3 py-2 text-center font-mono text-lg tracking-wider select-all">{issued?.password}</p>
          <DialogFooter>
            <Button onClick={() => setIssued(null)}>Done</Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </Panel>
  );
}

/* --------------------------------------- Page --------------------------------------- */

/** DB-14: the design's panel permission grid, saved per role (`PUT /roles/:id/dashboard-panels`). */
function DashboardPanelsGrid({ canUpdate }: { canUpdate: boolean }) {
  const [data, setData] = React.useState<Awaited<ReturnType<typeof RolesApi.panelGrid>> | null>(null);
  const [draft, setDraft] = React.useState<PanelGrants>({});
  const [saving, setSaving] = React.useState(false);
  const load = React.useCallback(async () => {
    try {
      const d = await RolesApi.panelGrid();
      setData(d);
      setDraft(Object.fromEntries(d.roles.map((r) => [r.name, r.panels])));
    } catch (e) {
      toast.add({ type: "error", title: "Couldn't load dashboard panels", description: (e as { response?: { data?: { message?: string } } })?.response?.data?.message ?? "Try again." });
    }
  }, []);
  React.useEffect(() => {
    void load();
  }, [load]);
  if (!data) return <Loading what="dashboard panels" columns={5} rows={8} />;
  const saved = Object.fromEntries(data.roles.map((r) => [r.name, r.panels]));
  const changed = data.roles.filter((r) => !r.locked && JSON.stringify([...(draft[r.name] ?? [])].sort()) !== JSON.stringify([...r.panels].sort()));
  return (
    <Group title="Panels by role" description={canUpdate ? "Tick what each role sees, then save." : "Your role can view this, not change it."}>
      <div className="space-y-3 p-4">
        <PanelPermissions panels={data.panels} roles={data.roles.map((r) => r.name)} grants={draft} onChange={(g) => canUpdate && setDraft(g)} />
        {canUpdate ? (
          <div className="flex items-center justify-end gap-2">
            {changed.length ? <span className="mr-auto text-xs text-muted-foreground">Changed: {changed.map((r) => r.name).join(", ")}</span> : null}
            <Button variant="outline" disabled={!changed.length || saving} onClick={() => setDraft(saved)}>
              Reset
            </Button>
            <Button
              disabled={!changed.length || saving}
              onClick={async () => {
                setSaving(true);
                try {
                  for (const r of changed) await RolesApi.setPanels(r.id, draft[r.name] ?? []);
                  toast.add({ title: "Dashboard panels saved" });
                  await load();
                } catch (e) {
                  toast.add({ type: "error", title: "Not saved", description: (e as { response?: { data?: { message?: string } } })?.response?.data?.message ?? "Try again." });
                } finally {
                  setSaving(false);
                }
              }}
            >
              {saving ? <Spinner /> : null}
              Save panels
            </Button>
          </div>
        ) : null}
      </div>
    </Group>
  );
}

export default function UsersAndRolesPage() {
  const { can, loaded } = useMenuPermissions();
  const [me, setMe] = React.useState(0);
  const [users, setUsers] = React.useState<ListedUser[]>([]);
  const [branches, setBranches] = React.useState<ApiBranch[]>([]);
  const [locations, setLocations] = React.useState<LocationRow[]>([]);
  const [roles, setRoles] = React.useState<ApiRole[]>([]);
  const [pages, setPages] = React.useState<PermPage[]>([]);
  const [activity, setActivity] = React.useState<ActivityEntry[] | null>(null);
  const [failed, setFailed] = React.useState("");

  React.useEffect(() => {
    try {
      setMe(Number(JSON.parse(localStorage.getItem("user") || "{}").id) || 0);
    } catch {
      /* not signed in: the layout redirects */
    }
  }, []);

  const loadUsers = React.useCallback(async () => {
    const [u, b] = await Promise.all([UsersApi.getAll() as Promise<ApiUser[]>, BranchesApi.getAll() as Promise<ApiBranch[]>]);
    // ST-04: each branch's locations, for the form and the list (none when the role can't read them)
    const locs = (await Promise.all(b.map((x) => LocationsApi.list(x.id).catch(() => [] as LocationRow[])))).flat();
    setBranches(b);
    setLocations(locs);
    setUsers(u.filter((x) => x.role !== "SuperAdmin").map((x) => mapUser(x, b, locs)));
  }, []);
  const loadRoles = React.useCallback(async () => {
    const [r, c] = await Promise.all([RolesApi.list() as Promise<ApiRole[]>, RolesApi.catalog() as Promise<{ pages: PermPage[] }>]);
    setRoles(r);
    setPages(c.pages);
  }, []);
  const reloadAll = React.useCallback(async () => {
    await Promise.all([loadUsers(), loadRoles()]);
  }, [loadUsers, loadRoles]);

  React.useEffect(() => {
    reloadAll().catch((e) => setFailed(apiError(e, "Couldn't load users and roles.")));
  }, [reloadAll]);

  /** ST-15: the latest 1,000 entries, or the chosen dates from the server; the design's filters narrow what is loaded. */
  const loadActivity = React.useCallback((from = "", to = "") => {
    AuditApi.getAll({ limit: 1000, from: from || undefined, to: to || undefined })
      .then((d: { logs: ApiAudit[] }) => setActivity((d.logs ?? []).map(mapAudit)))
      .catch(() => setActivity([]));
  }, []);

  React.useEffect(() => {
    loadActivity();
  }, [loadActivity]);

  const may =(action: string) => loaded && can(UR, action);
  const roleNames = roles.map((r) => r.name);

  return (
    // The theme scope, tooltips and toasts come from the app shell (PRD 1.6)
    <div className="medora">
          <div className="mx-auto w-full max-w-6xl">
            <MedoraPageHeader title="Roles and users" description="Roles and permissions, user accounts and the activity they leave." />
            {failed ? <p className="mt-4 rounded-lg border border-status-error-border bg-status-error-bg px-4 py-3 text-sm text-status-error-text">{failed}</p> : null}
            <Section id="access">
              {roles.length && pages.length ? (
                <RolesAndPermissions roles={roles} pages={pages} reload={reloadAll} canCreate={may("Create")} canUpdate={may("Update")} canDelete={may("Delete")} />
              ) : (
                <Loading what="roles and permissions" columns={9} rows={8} />
              )}
              <Users users={users} branches={branches} locations={locations} roleNames={roleNames} me={me} reload={reloadAll} canCreate={may("Create")} canUpdate={may("Update")} />
            </Section>
            <Section id="dashboard-panels" title="Dashboard panels" description="What each role sees on its dashboard. Sensitive panels show profit, money owed or costs.">
              <DashboardPanelsGrid canUpdate={may("Update")} />
            </Section>
            <Section id="activity-log" title="Activity log" description="Who did what and when, most recent first. Passwords and keys only ever say Changed.">
              {activity === null ? <Loading what="the activity log" columns={7} rows={6} /> : <ActivityLog entries={activity} onRange={loadActivity} canExport={loaded && can("Settings/Activity log", "Export")} />}
            </Section>
          </div>
    </div>
  );
}
