"use client";

import * as React from "react";
import { ChevronDownIcon, ChevronRightIcon, LockIcon } from "lucide-react";

import { Checkbox } from "@/components/ui/checkbox";
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
import { cn } from "@/lib/utils";

/*
 * Access control (PRD 9.5–9.8, ST-08 to ST-13). The grid is what the API enforces (ST-10): hiding a menu
 * is never the control. Actions depend on Read: you can't create, change, approve or export what you can't
 * see, so ticking any of them ticks Read, and clearing Read clears them.
 */

export const ACTIONS = ["Read", "Create", "Update", "Delete", "Approve", "Export", "Show in menu", "All branches", "All records"] as const;
export type Action = (typeof ACTIONS)[number];

/** A page under one of the 10 menus; `actions` lists what applies to it (Approve exists only where approvals do). */
export type PermPage = { id: string; menu: string; name: string; actions: Action[] };

/** role → page id → actions granted */
export type Grants = Record<string, Record<string, Action[]>>;

const needsRead = (a: Action) => a !== "Read";

/** Apply the Read dependency to one page's set after a change. */
export function normalise(set: Action[], changed: Action, on: boolean): Action[] {
  let next = on ? [...new Set([...set, changed])] : set.filter((a) => a !== changed);
  if (on && needsRead(changed) && !next.includes("Read")) next = [...next, "Read"];
  if (!on && changed === "Read") next = [];
  return next;
}

/**
 * Permission grid (ST-09, ST-11) for one role: pages grouped by the 10 menus, collapsible; one column per
 * action; select-all per page (row) and per menu × action (group header); "copy from role". Actions that
 * don't apply to a page show a dash. The locked role (Admin) shows everything ticked and read-only.
 */
export function PermissionGrid({
  pages,
  role,
  grants,
  onChange,
  locked = false,
}: {
  pages: PermPage[];
  role: string;
  grants: Record<string, Action[]>;
  onChange: (g: Record<string, Action[]>) => void;
  locked?: boolean;
}) {
  const menus = [...new Set(pages.map((p) => p.menu))];
  const [closed, setClosed] = React.useState<Set<string>>(new Set());
  const has = (p: PermPage, a: Action) => locked || (grants[p.id] ?? []).includes(a);
  const setPage = (id: string, list: Action[]) => onChange({ ...grants, [id]: list });
  const groupState = (list: PermPage[], a: Action) => {
    const applicable = list.filter((p) => p.actions.includes(a));
    const on = applicable.filter((p) => has(p, a)).length;
    return { applicable, all: applicable.length > 0 && on === applicable.length, some: on > 0 && on < applicable.length };
  };
  return (
    <div className="overflow-x-auto rounded-lg border border-[var(--color-border-panel)] bg-card">
      <table className="w-full border-collapse text-sm">
        <caption className="sr-only">Permissions for {role}</caption>
        <thead className="sticky top-0 z-[1] bg-[var(--color-bg-surface-subtle)] text-xs text-muted-foreground">
          <tr>
            <th scope="col" className="min-w-52 px-3 py-2 text-left font-medium">
              Page
            </th>
            <th scope="col" className="w-12 px-1 py-2 font-medium">
              All
            </th>
            {ACTIONS.map((a) => (
              <th key={a} scope="col" className="w-16 px-1 py-2 text-center font-medium leading-tight">
                {a}
              </th>
            ))}
          </tr>
        </thead>
        {menus.map((menu) => {
          const list = pages.filter((p) => p.menu === menu);
          const open = !closed.has(menu);
          const granted = list.reduce((s, p) => s + (locked ? p.actions.length : (grants[p.id] ?? []).length), 0);
          const possible = list.reduce((s, p) => s + p.actions.length, 0);
          return (
            <tbody key={menu} className="divide-y divide-[var(--color-border-subtle)] border-t-2 border-[var(--color-border-panel)]">
              <tr className="bg-muted/30">
                <th scope="rowgroup" className="px-2 py-1.5 text-left">
                  <button
                    type="button"
                    onClick={() =>
                      setClosed((s) => {
                        const n = new Set(s);
                        if (n.has(menu)) n.delete(menu);
                        else n.add(menu);
                        return n;
                      })
                    }
                    aria-expanded={open}
                    className="flex cursor-pointer items-center gap-1 text-xs font-semibold tracking-wide uppercase"
                  >
                    {open ? <ChevronDownIcon className="size-3.5" aria-hidden /> : <ChevronRightIcon className="size-3.5" aria-hidden />}
                    {menu}
                    <span className="ml-1 font-normal normal-case text-muted-foreground tabular-nums">
                      {granted} of {possible}
                    </span>
                  </button>
                </th>
                <td />
                {ACTIONS.map((a) => {
                  const g = groupState(list, a);
                  return (
                    <td key={a} className="px-1 py-1.5 text-center">
                      {g.applicable.length ? (
                        <Checkbox
                          aria-label={`${a}, every ${menu} page`}
                          checked={g.all}
                          indeterminate={g.some}
                          disabled={locked}
                          onCheckedChange={(v) => {
                            const next = { ...grants };
                            for (const p of g.applicable) next[p.id] = normalise(next[p.id] ?? [], a, !!v);
                            onChange(next);
                          }}
                        />
                      ) : null}
                    </td>
                  );
                })}
              </tr>
              {open
                ? list.map((p) => {
                    const set = grants[p.id] ?? [];
                    const all = locked || p.actions.every((a) => set.includes(a));
                    const some = !all && set.length > 0;
                    return (
                      <tr key={p.id}>
                        <th scope="row" className="px-3 py-1.5 pl-7 text-left font-normal">
                          {p.name}
                        </th>
                        <td className="px-1 py-1.5 text-center">
                          <Checkbox aria-label={`Everything on ${p.name}`} checked={all} indeterminate={some} disabled={locked} onCheckedChange={(v) => setPage(p.id, v ? [...p.actions] : [])} />
                        </td>
                        {ACTIONS.map((a) => (
                          <td key={a} className="px-1 py-1.5 text-center">
                            {p.actions.includes(a) ? (
                              <Checkbox aria-label={`${a}, ${p.name}`} checked={has(p, a)} disabled={locked} onCheckedChange={(v) => setPage(p.id, normalise(set, a, !!v))} />
                            ) : (
                              <span className="text-muted-foreground" aria-label={`${a} doesn't apply to ${p.name}`}>
                                –
                              </span>
                            )}
                          </td>
                        ))}
                      </tr>
                    );
                  })
                : null}
            </tbody>
          );
        })}
      </table>
      {locked ? (
        <p className="flex items-center gap-1.5 border-t border-[var(--color-border-subtle)] px-3 py-2 text-xs text-muted-foreground">
          <LockIcon className="size-3.5" aria-hidden /> {role} always has every permission, so the pharmacy can&apos;t lock itself out.
        </p>
      ) : null}
    </div>
  );
}

/* --------------------------------------- Roles --------------------------------------- */

export type Role = { name: string; users: number; builtIn?: boolean };

/** Problems with a role name, in words. */
export function roleNameProblem(name: string, roles: Role[], editing?: string) {
  const n = name.trim();
  if (!n) return "Enter a name.";
  if (n.length > 40) return "Keep the name under 40 characters.";
  if (roles.some((r) => r.name.toLowerCase() === n.toLowerCase() && r.name !== editing)) return `There's already a role called ${n}.`;
  return undefined;
}

/** Why a role can't be deleted, or undefined if it can (ST-08). */
export function roleDeleteBlock(r: Role) {
  if (r.name === "Admin") return "Admin can't be deleted.";
  if (r.users > 0) return `${r.users} ${r.users === 1 ? "user has" : "users have"} this role. Give them another role first.`;
  return undefined;
}

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

export type UserDraft = { username: string; first: string; last: string; phone: string; email: string; role: string; branch: string; location: string; login: "Password" | "Passcode"; secret: string };
export type AppUser = Omit<UserDraft, "secret"> & { id: number; status: "Active" | "Deactivated"; lastSeen?: string };

/** A passcode that's easy to guess: one digit repeated, or a straight run (1234, 9876). */
const weakPasscode = (p: string) => /^(\d)\1+$/.test(p) || "0123456789".includes(p) || "9876543210".includes(p);

export function userProblems(d: UserDraft, users: AppUser[], editingId?: number): Partial<Record<keyof UserDraft, string>> {
  const p: Partial<Record<keyof UserDraft, string>> = {};
  if (!/^[a-z][a-z0-9._]{2,23}$/.test(d.username)) p.username = "3–24 lowercase letters, numbers, dots or underscores, starting with a letter.";
  else if (users.some((u) => u.username === d.username && u.id !== editingId)) p.username = `${d.username} is taken.`;
  if (!d.first.trim()) p.first = "Enter the first name.";
  if (!d.last.trim()) p.last = "Enter the last name.";
  if (d.phone && !/^(\+?254|0)[17]\d{8}$/.test(d.phone.replace(/\s/g, ""))) p.phone = "Use a Kenyan mobile number, e.g. 0712 345 678.";
  if (d.email && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(d.email)) p.email = "This isn't an email address.";
  if (!d.role) p.role = "Choose a role.";
  if (!d.branch) p.branch = "Choose a branch.";
  if (d.role === "Admin" && d.login === "Passcode") p.login = "Admins sign in with a password: a passcode is too short to protect every branch's data.";
  if (editingId === undefined || d.secret) {
    if (d.login === "Password") {
      if (d.secret.length < 8) p.secret = "At least 8 characters.";
      else if (d.secret.toLowerCase().includes(d.username)) p.secret = "The password can't contain the username.";
    } else if (!/^\d{4,6}$/.test(d.secret)) p.secret = "4 to 6 digits.";
    else if (weakPasscode(d.secret)) p.secret = "Too easy to guess: avoid repeated digits and runs like 1234.";
  }
  return p;
}

/** Why a user can't be deactivated, or undefined (ST-13: never yourself, never the last active Admin). */
export function deactivateBlock(u: AppUser, users: AppUser[], me: number) {
  if (u.id === me) return "You can't deactivate yourself.";
  if (u.role === "Admin" && users.filter((x) => x.role === "Admin" && x.status === "Active").length === 1) return "This is the only active Admin. Make someone else an Admin first.";
  return undefined;
}

/**
 * User form (ST-12): who they are, what they can do (role, branch, location) and how they sign in. A
 * passcode is for quick sign-in at a shared till; Admins always use a password. On edit, the password field
 * is optional: leave it empty to keep the current one.
 */
export function UserForm({ draft, onChange, roles, branches, locations, problems, editing }: { draft: UserDraft; onChange: (d: UserDraft) => void; roles: string[]; branches: string[]; locations: string[]; problems: Partial<Record<keyof UserDraft, string>>; editing?: boolean }) {
  const set = <K extends keyof UserDraft>(k: K, v: UserDraft[K]) => onChange({ ...draft, [k]: v });
  const err = (k: keyof UserDraft) => (problems[k] ? <FieldError>{problems[k]}</FieldError> : null);
  return (
    <div className="grid gap-4">
      <div className="grid gap-4 sm:grid-cols-2">
        <Field data-invalid={!!problems.first}>
          <FieldLabel htmlFor="u-first">First name</FieldLabel>
          <Input id="u-first" value={draft.first} onChange={(e) => set("first", e.target.value)} aria-invalid={!!problems.first} />
          {err("first")}
        </Field>
        <Field data-invalid={!!problems.last}>
          <FieldLabel htmlFor="u-last">Last name</FieldLabel>
          <Input id="u-last" value={draft.last} onChange={(e) => set("last", e.target.value)} aria-invalid={!!problems.last} />
          {err("last")}
        </Field>
      </div>
      <Field data-invalid={!!problems.username}>
        <FieldLabel htmlFor="u-username">Username</FieldLabel>
        <Input id="u-username" className="font-mono" value={draft.username} disabled={editing} onChange={(e) => set("username", e.target.value.toLowerCase().replace(/\s/g, ""))} aria-invalid={!!problems.username} />
        {err("username") ?? (editing ? <FieldDescription>Usernames don&apos;t change: they appear on sales and the activity log.</FieldDescription> : null)}
      </Field>
      <Field data-invalid={!!problems.phone}>
        <FieldLabel htmlFor="u-phone">Mobile number (optional)</FieldLabel>
        <Input id="u-phone" inputMode="tel" value={draft.phone} onChange={(e) => set("phone", e.target.value)} aria-invalid={!!problems.phone} />
        {err("phone")}
      </Field>
      <Field data-invalid={!!problems.email} className="sm:col-span-2">
        <FieldLabel htmlFor="u-email">Email (optional)</FieldLabel>
        <Input id="u-email" type="email" value={draft.email} onChange={(e) => set("email", e.target.value)} aria-invalid={!!problems.email} />
        {err("email") ?? <FieldDescription>Used for password resets and payslips.</FieldDescription>}
      </Field>
      <Field data-invalid={!!problems.role}>
        <FieldLabel htmlFor="u-role">Role</FieldLabel>
        <NativeSelect id="u-role" value={draft.role} onChange={(e) => set("role", e.target.value)} aria-invalid={!!problems.role}>
          <NativeSelectOption value="">Choose a role</NativeSelectOption>
          {roles.map((r) => (
            <NativeSelectOption key={r}>{r}</NativeSelectOption>
          ))}
        </NativeSelect>
        {err("role")}
      </Field>
      <Field data-invalid={!!problems.branch}>
        <FieldLabel htmlFor="u-branch">Branch</FieldLabel>
        <NativeSelect id="u-branch" value={draft.branch} onChange={(e) => set("branch", e.target.value)} aria-invalid={!!problems.branch}>
          <NativeSelectOption value="">Choose a branch</NativeSelectOption>
          {branches.map((b) => (
            <NativeSelectOption key={b}>{b}</NativeSelectOption>
          ))}
        </NativeSelect>
        {err("branch")}
      </Field>
      <Field>
        <FieldLabel htmlFor="u-location">Location (optional)</FieldLabel>
        <NativeSelect id="u-location" value={draft.location} onChange={(e) => set("location", e.target.value)}>
          <NativeSelectOption value="">Any location in the branch</NativeSelectOption>
          {locations.map((l) => (
            <NativeSelectOption key={l}>{l}</NativeSelectOption>
          ))}
        </NativeSelect>
      </Field>
      <Field data-invalid={!!problems.login}>
        <FieldLabel htmlFor="u-login">Signs in with</FieldLabel>
        <NativeSelect id="u-login" value={draft.login} onChange={(e) => onChange({ ...draft, login: e.target.value as UserDraft["login"], secret: "" })} aria-invalid={!!problems.login}>
          <NativeSelectOption>Password</NativeSelectOption>
          <NativeSelectOption>Passcode</NativeSelectOption>
        </NativeSelect>
        {err("login") ?? <FieldDescription>{draft.login === "Passcode" ? "4–6 digits, for quick sign-in at a shared till." : "At least 8 characters."}</FieldDescription>}
      </Field>
      <Field data-invalid={!!problems.secret} className="sm:col-span-2">
        <FieldLabel htmlFor="u-secret">
          {draft.login}
          {editing ? " (leave empty to keep the current one)" : ""}
        </FieldLabel>
        <Input id="u-secret" type="password" inputMode={draft.login === "Passcode" ? "numeric" : undefined} autoComplete="new-password" value={draft.secret} onChange={(e) => set("secret", draft.login === "Passcode" ? e.target.value.replace(/\D/g, "").slice(0, 6) : e.target.value)} aria-invalid={!!problems.secret} className={cn(draft.login === "Passcode" && "max-w-40 tracking-[0.3em]")} />
        {err("secret") ?? <FieldDescription>They&apos;re asked to change it the first time they sign in.</FieldDescription>}
      </Field>
    </div>
  );
}

export function UserStatus({ u }: { u: AppUser }) {
  return <span className={cn("inline-flex rounded-full border px-2 py-0.5 text-xs font-medium", u.status === "Active" ? "border-status-success-border bg-status-success-bg text-status-success-text" : "border-border bg-muted text-muted-foreground")}>{u.status}</span>;
}

