"use client";

import { toastApiError } from "@/lib/toast-api-error";
import * as React from "react";
import { Building2, Check, Filter, Loader2, Users, X } from "lucide-react";
import { toast } from "sonner";

import type { ImpersonationCapabilities, ImpersonationOptionRow } from "@/app/customer/_lib/admin/access-types";
import {
  listImpersonationOptions,
  updateImpersonationSelection,
} from "@/app/customer/_lib/admin/impersonation-api";
import { dispatchImpersonationScopeChanged, IMPERSONATION_SCOPE_CHANGED_EVENT, OPEN_PORTFOLIO_SCOPE_EVENT } from "@/app/customer/_lib/admin/impersonation-scope";
import { useCustomerPortalSession } from "@/app/customer/_components/customer-portal-session-context";
import { DEFAULT_LOOKUP_LIMIT } from "@/config/pagination";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet";
import { cn } from "@/lib/utils";

type ImpersonationUsersSelectorProps = {
  className?: string;
  /** Session capability — render Scope immediately; options fetch only hydrates data. */
  canUsePortfolioScope?: boolean;
};

type ScopeListProps = {
  title: string;
  icon: React.ReactNode;
  search: string;
  onSearchChange: (value: string) => void;
  rows: ImpersonationOptionRow[];
  selectedIds: number[];
  max: number;
  loading: boolean;
  emptyLabel: string;
  onToggle: (id: number, checked: boolean) => void;
  onSelectVisible: () => void;
  onClearSelected: () => void;
};

function dedupeRows(rows: ImpersonationOptionRow[]): ImpersonationOptionRow[] {
  const byId = new Map<number, ImpersonationOptionRow>();
  for (const row of rows) {
    const id = Number(row.id);
    if (!Number.isFinite(id) || id <= 0 || byId.has(id)) continue;
    byId.set(id, { ...row, id });
  }
  return [...byId.values()];
}

function displayName(row: ImpersonationOptionRow): string {
  return (row.name || row.subdomain || row.email || `ID ${row.id}`).trim();
}

/** Disambiguate identical display names with id / subdomain. */
function primaryLabel(row: ImpersonationOptionRow, nameCounts: Map<string, number>): string {
  const name = displayName(row);
  const collisions = nameCounts.get(name.toLowerCase()) ?? 0;
  if (collisions > 1) {
    const suffix = row.subdomain?.trim() || String(row.id);
    return `${name} (${suffix})`;
  }
  return name;
}

function buildNameCounts(rows: ImpersonationOptionRow[]): Map<string, number> {
  const counts = new Map<string, number>();
  for (const row of rows) {
    const key = displayName(row).toLowerCase();
    counts.set(key, (counts.get(key) ?? 0) + 1);
  }
  return counts;
}

function ScopeList({
  title,
  icon,
  search,
  onSearchChange,
  rows,
  selectedIds,
  max,
  loading,
  emptyLabel,
  onToggle,
  onSelectVisible,
  onClearSelected,
}: ScopeListProps) {
  const nameCounts = React.useMemo(() => buildNameCounts(rows), [rows]);
  const selectedSet = React.useMemo(() => new Set(selectedIds), [selectedIds]);

  const orderedRows = React.useMemo(() => {
    const selected: ImpersonationOptionRow[] = [];
    const rest: ImpersonationOptionRow[] = [];
    for (const row of rows) {
      if (selectedSet.has(row.id)) selected.push(row);
      else rest.push(row);
    }
    return [...selected, ...rest];
  }, [rows, selectedSet]);

  return (
    <section className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-2xl border border-border/60 bg-card shadow-sm">
      <div className="bg-gradient-to-r from-primary/10 via-transparent to-transparent px-4 py-3">
        <div className="flex items-center justify-between gap-2">
          <div className="flex min-w-0 items-center gap-2.5">
            <span className="flex size-8 items-center justify-center rounded-xl bg-primary text-primary-foreground shadow-md shadow-primary/25">
              {icon}
            </span>
            <div className="min-w-0">
              <h3 className="font-semibold text-sm tracking-tight">{title}</h3>
              <p className="text-muted-foreground text-[11px] tabular-nums">
                {selectedIds.length} of {Math.max(rows.length, selectedIds.length)} selected
              </p>
            </div>
          </div>
          <div className="flex shrink-0 items-center gap-2 text-xs">
            <button
              type="button"
              className="font-medium text-muted-foreground transition-colors hover:text-foreground disabled:opacity-40"
              disabled={loading || orderedRows.length === 0}
              onClick={onSelectVisible}
            >
              All
            </button>
            <span className="text-muted-foreground/40">·</span>
            <button
              type="button"
              className="font-medium text-muted-foreground transition-colors hover:text-foreground disabled:opacity-40"
              disabled={loading || selectedIds.length === 0}
              onClick={onClearSelected}
            >
              None
            </button>
          </div>
        </div>
      </div>

      <div className="space-y-2 border-t border-border/60 px-3 py-3">
        <Input
          placeholder={`Search ${title.toLowerCase()}…`}
          value={search}
          onChange={(event) => onSearchChange(event.target.value)}
          className="h-9"
          aria-label={`Search ${title}`}
        />

        <div className="relative min-h-0 overflow-hidden rounded-xl border border-border/60 bg-muted/30">
          {loading ? (
            <div className="absolute inset-0 z-10 flex items-center justify-center bg-background/70 backdrop-blur-sm">
              <Loader2 className="size-4 animate-spin text-muted-foreground" />
            </div>
          ) : null}
          <div className="h-full max-h-[min(42vh,300px)] overflow-y-auto p-1.5">
            {orderedRows.length === 0 ? (
              <p className="px-2 py-6 text-center text-muted-foreground text-xs">{emptyLabel}</p>
            ) : (
              <ul className="space-y-0.5">
                {orderedRows.map((row) => {
                  const checked = selectedSet.has(row.id);
                  return (
                    <li key={row.id}>
                      <label
                        className={cn(
                          "flex cursor-pointer items-center gap-2.5 rounded-lg border border-transparent px-2.5 py-2 text-sm transition-colors",
                          checked
                            ? "border-border/60 bg-primary/10"
                            : "hover:border-border/60 hover:bg-muted/40",
                        )}
                      >
                        <Checkbox
                          checked={checked}
                          onCheckedChange={(value) => onToggle(row.id, value === true)}
                        />
                        <span className="min-w-0 flex-1 truncate font-medium leading-tight">
                          {primaryLabel(row, nameCounts)}
                        </span>
                        {checked ? (
                          <Check className="size-3.5 shrink-0 text-primary" />
                        ) : null}
                      </label>
                    </li>
                  );
                })}
              </ul>
            )}
          </div>
        </div>
      </div>
    </section>
  );
}

/**
 * Header control for Corporate → Users → Customers portfolio scope.
 */
export function ImpersonationUsersSelector({
  className,
  canUsePortfolioScope = false,
}: ImpersonationUsersSelectorProps) {
  const { portfolioScope } = useCustomerPortalSession();
  const scopeAllowed = canUsePortfolioScope || portfolioScope;
  const [open, setOpen] = React.useState(false);
  const [loading, setLoading] = React.useState(false);
  const [saving, setSaving] = React.useState(false);
  const [userSearch, setUserSearch] = React.useState("");
  const [customerSearch, setCustomerSearch] = React.useState("");
  const [users, setUsers] = React.useState<ImpersonationOptionRow[]>([]);
  const [customers, setCustomers] = React.useState<ImpersonationOptionRow[]>([]);
  const [capabilities, setCapabilities] = React.useState<ImpersonationCapabilities | null>(null);
  const [selectedUserIds, setSelectedUserIds] = React.useState<number[]>([]);
  const [selectedCustomerIds, setSelectedCustomerIds] = React.useState<number[]>([]);
  const [draftUserIds, setDraftUserIds] = React.useState<number[]>([]);
  const [draftCustomerIds, setDraftCustomerIds] = React.useState<number[]>([]);
  // Prefer session capability so the button is not blank while options is slow/timing out.
  const [visible, setVisible] = React.useState(scopeAllowed);

  React.useEffect(() => {
    if (scopeAllowed) setVisible(true);
  }, [scopeAllowed]);

  const applyOptions = React.useCallback((data: Awaited<ReturnType<typeof listImpersonationOptions>>) => {
    setCapabilities(data.capabilities);
    setUsers(dedupeRows(data.users ?? data.items.filter((item) => item.type !== "customer")));
    setCustomers(dedupeRows(data.customers ?? data.items.filter((item) => item.type === "customer")));
  }, []);

  React.useEffect(() => {
    if (!scopeAllowed) {
      setVisible(false);
      return;
    }

    let cancelled = false;
    let retryTimeoutId: number | undefined;

    async function bootstrap(attempt = 0) {
      try {
        const data = await listImpersonationOptions({ limit: DEFAULT_LOOKUP_LIMIT });
        if (cancelled) return;
        applyOptions(data);
        setVisible(Boolean(data.capabilities.can_impersonate_multiple) || scopeAllowed);
        setSelectedUserIds(data.effective_user_ids ?? []);
        setSelectedCustomerIds(data.effective_customer_ids ?? []);
      } catch {
        if (cancelled) return;
        // Under dashboard load options often times out (15s). Keep session-gated
        // visibility and retry once — never blank the Scope button on a transient 502.
        if (attempt < 1) {
          retryTimeoutId = window.setTimeout(() => {
            if (!cancelled) void bootstrap(attempt + 1);
          }, 1500);
          return;
        }
        setVisible(scopeAllowed);
      }
    }

    void bootstrap();
    return () => {
      cancelled = true;
      if (retryTimeoutId !== undefined) window.clearTimeout(retryTimeoutId);
    };
  }, [applyOptions, scopeAllowed]);

  React.useEffect(() => {
    if (open) {
      setDraftUserIds(selectedUserIds);
      setDraftCustomerIds(selectedCustomerIds);
      setUserSearch("");
      setCustomerSearch("");
    }
  }, [open, selectedUserIds, selectedCustomerIds]);

  React.useEffect(() => {
    const onOpen = () => setOpen(true);
    window.addEventListener(OPEN_PORTFOLIO_SCOPE_EVENT, onOpen);
    return () => window.removeEventListener(OPEN_PORTFOLIO_SCOPE_EVENT, onOpen);
  }, []);

  React.useEffect(() => {
    const onScopeChanged = (event: Event) => {
      const detail = (event as CustomEvent<{
        impersonatedUserIds?: number[];
        impersonatedCustomerIds?: number[];
      }>).detail;
      const nextUsers = detail?.impersonatedUserIds ?? [];
      const nextCustomers = detail?.impersonatedCustomerIds ?? [];
      setSelectedUserIds(nextUsers);
      setSelectedCustomerIds(nextCustomers);
      setDraftUserIds(nextUsers);
      setDraftCustomerIds(nextCustomers);
    };
    window.addEventListener(IMPERSONATION_SCOPE_CHANGED_EVENT, onScopeChanged);
    return () => window.removeEventListener(IMPERSONATION_SCOPE_CHANGED_EVENT, onScopeChanged);
  }, []);

  React.useEffect(() => {
    if (!open || !visible) return;
    let cancelled = false;
    const timeoutId = window.setTimeout(() => {
      void (async () => {
        setLoading(true);
        try {
          const data = await listImpersonationOptions({
            userQ: userSearch,
            customerQ: customerSearch,
            limit: DEFAULT_LOOKUP_LIMIT,
            userIds: draftUserIds,
          });
          if (cancelled) return;
          applyOptions(data);
        } catch (error) {
          if (!cancelled) {
            toastApiError(error, "Could not load options.");
          }
        } finally {
          if (!cancelled) setLoading(false);
        }
      })();
    }, 280);

    return () => {
      cancelled = true;
      window.clearTimeout(timeoutId);
    };
  }, [open, userSearch, customerSearch, visible, draftUserIds, applyOptions]);

  const userLabelById = React.useMemo(() => {
    const map = new Map<number, string>();
    for (const row of users) map.set(row.id, displayName(row));
    return map;
  }, [users]);

  const customerLabelById = React.useMemo(() => {
    const map = new Map<number, string>();
    for (const row of customers) map.set(row.id, displayName(row));
    return map;
  }, [customers]);

  if (!visible) return null;

  // Backend max_* is already min(safetyCeiling, allowed_*_count).
  const maxUsers = capabilities?.max_impersonation_users ?? users.length;
  const maxCustomers = capabilities?.max_impersonation_customers ?? customers.length;
  const totalSelected = selectedUserIds.length + selectedCustomerIds.length;
  const draftTotal = draftUserIds.length + draftCustomerIds.length;
  const isDirty =
    !sameIdSet(draftUserIds, selectedUserIds) || !sameIdSet(draftCustomerIds, selectedCustomerIds);

  const toggleId = (
    id: number,
    checked: boolean,
    max: number,
    setter: React.Dispatch<React.SetStateAction<number[]>>,
  ) => {
    setter((prev) => {
      if (checked) {
        if (prev.includes(id)) return prev;
        if (prev.length >= max) {
          toast.error(`Select at most ${max}.`);
          return prev;
        }
        return [...prev, id];
      }
      return prev.filter((value) => value !== id);
    });
  };

  const persistSelection = async (nextUserIds: number[], nextCustomerIds: number[]) => {
    setSaving(true);
    try {
      const result = await updateImpersonationSelection({
        userIds: nextUserIds,
        customerIds: nextCustomerIds,
      });
      const savedUsers = result.effective_user_ids ?? [];
      const savedCustomers = result.effective_customer_ids ?? [];
      setSelectedUserIds(savedUsers);
      setSelectedCustomerIds(savedCustomers);
      setDraftUserIds(savedUsers);
      setDraftCustomerIds(savedCustomers);
      dispatchImpersonationScopeChanged({
        userIds: savedUsers,
        customerIds: savedCustomers,
        // Cashflow → structure/accumulator deep-links pin clientIds/isin in the URL;
        // drop them whenever scope is saved so Apply / banner Clear can take effect.
        clearPageSearchParams: true,
      });
      setOpen(false);
      const count = savedUsers.length + savedCustomers.length;
      toast.success(
        count > 0
          ? `Scope applied · ${savedUsers.length} users · ${savedCustomers.length} customers`
          : "Portfolio scope cleared",
      );
    } catch (error) {
      toastApiError(error, "Could not update selection.");
    } finally {
      setSaving(false);
    }
  };

  const handleApply = async () => {
    if (draftUserIds.length > maxUsers) {
      toast.error(`Select at most ${maxUsers} users.`);
      return;
    }
    if (draftCustomerIds.length > maxCustomers) {
      toast.error(`Select at most ${maxCustomers} customers.`);
      return;
    }
    await persistSelection(draftUserIds, draftCustomerIds);
  };

  return (
    <Sheet open={open} onOpenChange={setOpen}>
      <SheetTrigger asChild>
        <Button
          variant="outline"
          size="sm"
          className={cn(
            "relative h-8 gap-1.5 px-2.5",
            totalSelected > 0 &&
              "border-primary/40 bg-primary/10 text-foreground",
            className,
          )}
          aria-label="Portfolio scope"
        >
          <Filter className="size-3.5" />
          <span>Scope</span>
          {totalSelected > 0 ? (
            <Badge className="h-5 border-0 bg-primary/15 px-1.5 font-semibold tabular-nums text-[10px] text-foreground">
              {totalSelected}
            </Badge>
          ) : null}
        </Button>
      </SheetTrigger>

      <SheetContent
        side="right"
        className="flex h-dvh w-full flex-col gap-0 overflow-hidden bg-muted/30 p-0 data-[side=right]:sm:max-w-xl"
      >
        <div className="shrink-0 space-y-3 px-6 pt-6 pr-14">
          <p className="text-muted-foreground text-[11px] font-semibold tracking-widest uppercase">
            Portfolio filter
          </p>
          <SheetTitle className="flex items-center gap-3 text-left text-xl font-semibold tracking-tight">
            <span className="flex size-10 items-center justify-center rounded-2xl bg-primary text-primary-foreground shadow-md shadow-primary/25">
              <Filter className="size-4" />
            </span>
            Portfolio scope
          </SheetTitle>
          <SheetDescription className="sr-only">
            Select users and customers to filter portfolio reports.
          </SheetDescription>
          {draftTotal > 0 ? (
            <div className="flex flex-wrap gap-1.5 pb-1">
              {draftUserIds.slice(0, 4).map((id) => (
                <span
                  key={`u-${id}`}
                  className="inline-flex max-w-[150px] items-center gap-1.5 rounded-full bg-muted px-2.5 py-1 text-xs font-medium"
                >
                  <Users className="size-3 shrink-0 text-muted-foreground" />
                  <span className="truncate">{userLabelById.get(id) ?? id}</span>
                  <button
                    type="button"
                    className="rounded-sm opacity-60 hover:opacity-100"
                    aria-label="Remove user"
                    onClick={() => setDraftUserIds((prev) => prev.filter((value) => value !== id))}
                  >
                    <X className="size-3" />
                  </button>
                </span>
              ))}
              {draftUserIds.length > 4 ? (
                <span className="inline-flex items-center rounded-full bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground">
                  +{draftUserIds.length - 4} users
                </span>
              ) : null}
              {draftCustomerIds.slice(0, 4).map((id) => (
                <span
                  key={`c-${id}`}
                  className="inline-flex max-w-[150px] items-center gap-1.5 rounded-full bg-muted px-2.5 py-1 text-xs font-medium"
                >
                  <Building2 className="size-3 shrink-0 text-muted-foreground" />
                  <span className="truncate">{customerLabelById.get(id) ?? id}</span>
                  <button
                    type="button"
                    className="rounded-sm opacity-60 hover:opacity-100"
                    aria-label="Remove customer"
                    onClick={() =>
                      setDraftCustomerIds((prev) => prev.filter((value) => value !== id))
                    }
                  >
                    <X className="size-3" />
                  </button>
                </span>
              ))}
              {draftCustomerIds.length > 4 ? (
                <span className="inline-flex items-center rounded-full bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground">
                  +{draftCustomerIds.length - 4} customers
                </span>
              ) : null}
            </div>
          ) : null}
        </div>

        <div className="flex min-h-0 flex-1 flex-col gap-4 overflow-hidden px-6 py-4">
          <ScopeList
            title="Users"
            icon={<Users className="size-3.5" />}
            search={userSearch}
            onSearchChange={setUserSearch}
            rows={users}
            selectedIds={draftUserIds}
            max={maxUsers}
            loading={loading}
            emptyLabel={userSearch.trim() ? "No users match this search." : "No users available."}
            onToggle={(id, checked) => toggleId(id, checked, maxUsers, setDraftUserIds)}
            onSelectVisible={() =>
              setDraftUserIds((prev) => {
                const next = new Set(prev);
                for (const row of users) {
                  if (next.size >= maxUsers) break;
                  next.add(row.id);
                }
                return [...next];
              })
            }
            onClearSelected={() => {
              setDraftUserIds([]);
              setDraftCustomerIds([]);
            }}
          />

          <ScopeList
            title="Customers"
            icon={<Building2 className="size-3.5" />}
            search={customerSearch}
            onSearchChange={setCustomerSearch}
            rows={customers}
            selectedIds={draftCustomerIds}
            max={maxCustomers}
            loading={loading}
            emptyLabel={
              customerSearch.trim()
                ? "No customers match this search."
                : draftUserIds.length > 0
                  ? "No customers for selected users."
                  : "No customers available."
            }
            onToggle={(id, checked) => toggleId(id, checked, maxCustomers, setDraftCustomerIds)}
            onSelectVisible={() =>
              setDraftCustomerIds((prev) => {
                const next = new Set(prev);
                for (const row of customers) {
                  if (next.size >= maxCustomers) break;
                  next.add(row.id);
                }
                return [...next];
              })
            }
            onClearSelected={() => setDraftCustomerIds([])}
          />
        </div>

        <div className="shrink-0 border-t border-border/60 bg-card/80 px-6 py-4 backdrop-blur-md">
          <Button
            type="button"
            className="h-11 w-full gap-2 rounded-xl shadow-sm"
            disabled={saving || !isDirty}
            onClick={() => void handleApply()}
          >
            {saving ? (
              <>
                <Loader2 className="size-4 animate-spin" />
                Saving…
              </>
            ) : (
              "Apply scope"
            )}
          </Button>
        </div>
      </SheetContent>
    </Sheet>
  );
}

function sameIdSet(a: number[], b: number[]): boolean {
  if (a.length !== b.length) return false;
  const left = [...a].sort((x, y) => x - y);
  const right = [...b].sort((x, y) => x - y);
  return left.every((value, index) => value === right[index]);
}
