/**
 * `DevicesScreen` — the paired-device surface (ticket #77, lot F2).
 *
 * Mounted **identically** in the web UI and in the desktop app
 * (`Paramètres → Appareils`): same component, same vocabulary, same actions. It
 * is mobile-first by necessity, not by taste — a headless install has only the
 * web UI to generate a code from, and that is often reached from a phone.
 *
 * It shows a name, a badge, an activity phrase and a pairing date. It never
 * shows an IP or a User-Agent: this is an access-management surface for a
 * single-user instance, not an audit log.
 *
 * Transport-neutral (gateways via DI). The session consequence of revoking the
 * current device is *not* decided here: {@link onSessionEnded} hands it to the
 * mounting surface, which is the only layer that knows what "logged out" means.
 */

import { useEffect, useState } from "react";

import type { PairedDevice } from "@/domain";
import { Button, Field, Input, Panel, Spinner, cn } from "@/shared";
import { useDevices } from "./useDevices";
import { formatLastSeen, formatPairedAt } from "./formatActivity";
import { PairingCodePanel } from "./PairingCodePanel";
import { ConfirmDialog } from "./ConfirmDialog";
import { DEVICE_NAME_MAX } from "@/features/web/deviceName";

interface DevicesScreenProps {
  /**
   * Called when this device's own session has just been revoked (directly or by
   * "revoke all"). Web routes back to pairing; desktop leaves it unset — the
   * desktop app hosts the server and is never itself a paired device.
   */
  onSessionEnded?: () => void;
}

export function DevicesScreen({ onSessionEnded }: DevicesScreenProps) {
  const vm = useDevices();
  const [confirmRevoke, setConfirmRevoke] = useState<PairedDevice | null>(null);
  const [confirmRevokeAll, setConfirmRevokeAll] = useState(false);
  const [renaming, setRenaming] = useState<string | null>(null);

  useEffect(() => {
    if (vm.sessionEnded) onSessionEnded?.();
  }, [vm.sessionEnded, onSessionEnded]);

  const devices = vm.devices;

  return (
    <section data-testid="devices-screen" className="flex flex-col gap-4">
      <div className="flex flex-wrap items-center justify-between gap-2">
        <div className="flex flex-col gap-0.5">
          <h2 className="text-base font-semibold tracking-tight">Appareils</h2>
          <p className="text-sm text-muted">
            Les appareils autorisés à accéder à cette instance IdeA.
          </p>
        </div>
        <Button onClick={() => void vm.generateCode()} loading={vm.generating}>
          Appairer
        </Button>
      </div>

      {vm.code && (
        <PairingCodePanel
          code={vm.code}
          onRegenerate={() => void vm.generateCode()}
          onDismiss={vm.dismissCode}
        />
      )}

      {vm.error && (
        <Panel className="border-danger/40">
          <p role="alert" className="text-sm text-danger">
            {vm.error}
          </p>
        </Panel>
      )}

      {devices === null ? (
        <span className="inline-flex items-center gap-1.5 text-sm text-muted">
          <Spinner size={12} /> Chargement des appareils…
        </span>
      ) : devices.length === 0 ? (
        <p className="text-sm text-muted">Aucun appareil appairé.</p>
      ) : (
        <ul className="flex flex-col divide-y divide-border" data-testid="device-list">
          {devices.map((device) => (
            <DeviceRow
              key={device.deviceId}
              device={device}
              fresh={vm.freshDeviceIds.includes(device.deviceId)}
              renaming={renaming === device.deviceId}
              onStartRename={() => setRenaming(device.deviceId)}
              onCancelRename={() => setRenaming(null)}
              onRename={async (name) => {
                await vm.rename(device.deviceId, name);
                setRenaming(null);
              }}
              onRevoke={() => setConfirmRevoke(device)}
            />
          ))}
        </ul>
      )}

      {devices !== null && devices.length > 0 && (
        <div className="pt-1">
          <Button variant="ghost" size="sm" onClick={() => setConfirmRevokeAll(true)}>
            Révoquer tous les appareils
          </Button>
        </div>
      )}

      {confirmRevoke && (
        <ConfirmDialog
          title="Révoquer cet appareil ?"
          body={
            confirmRevoke.isCurrentDevice
              ? "Il devra être appairé à nouveau pour accéder à IdeA. Vous serez déconnecté immédiatement."
              : "Il devra être appairé à nouveau pour accéder à IdeA."
          }
          confirmLabel="Révoquer"
          busy={vm.busy}
          onConfirm={async () => {
            const target = confirmRevoke;
            setConfirmRevoke(null);
            await vm.revoke(target);
          }}
          onCancel={() => setConfirmRevoke(null)}
        />
      )}

      {confirmRevokeAll && (
        <ConfirmDialog
          title="Révoquer tous les appareils ?"
          // Stated without needing the list: the point of this action is the lost
          // phone you cannot enumerate calmly.
          body="Tous les appareils, y compris celui-ci, devront être appairés à nouveau. Vous serez déconnecté immédiatement."
          confirmLabel="Tout révoquer"
          danger
          busy={vm.busy}
          onConfirm={async () => {
            setConfirmRevokeAll(false);
            await vm.revokeAll();
          }}
          onCancel={() => setConfirmRevokeAll(false)}
        />
      )}
    </section>
  );
}

/** One device: identity, activity, and the `⋯` actions. */
function DeviceRow({
  device,
  fresh,
  renaming,
  onStartRename,
  onCancelRename,
  onRename,
  onRevoke,
}: {
  device: PairedDevice;
  fresh: boolean;
  renaming: boolean;
  onStartRename: () => void;
  onCancelRename: () => void;
  onRename: (name: string) => Promise<void>;
  onRevoke: () => void;
}) {
  return (
    <li
      data-testid="device-row"
      data-fresh={fresh || undefined}
      className={cn(
        "py-3 transition-colors first:pt-0 last:pb-0",
        fresh && "bg-success/10",
      )}
    >
      {renaming ? (
        <RenameForm device={device} onSubmit={onRename} onCancel={onCancelRename} />
      ) : (
        <div className="flex items-start justify-between gap-2">
          <div className="flex min-w-0 flex-col gap-0.5">
            <span className="flex flex-wrap items-center gap-2">
              <span className="truncate text-sm font-medium text-content">{device.name}</span>
              {device.isCurrentDevice && (
                <span className="shrink-0 rounded-full bg-primary/15 px-2 py-0.5 text-[0.65rem] font-medium text-primary">
                  Cet appareil
                </span>
              )}
            </span>
            <span className="text-xs text-muted">{formatLastSeen(device.lastSeenAtMs)}</span>
            <span className="text-[0.7rem] text-faint">{formatPairedAt(device.pairedAtMs)}</span>
          </div>
          <RowMenu deviceName={device.name} onRename={onStartRename} onRevoke={onRevoke} />
        </div>
      )}
    </li>
  );
}

/** Inline rename (1-40 characters), submitted with Enter or the button. */
function RenameForm({
  device,
  onSubmit,
  onCancel,
}: {
  device: PairedDevice;
  onSubmit: (name: string) => Promise<void>;
  onCancel: () => void;
}) {
  const [name, setName] = useState(device.name);
  const trimmed = name.trim();

  return (
    <form
      className="flex flex-col gap-2"
      onSubmit={(e) => {
        e.preventDefault();
        if (trimmed.length > 0) void onSubmit(trimmed);
      }}
    >
      <Field label="Nom de l'appareil" hideLabel>
        {({ id }) => (
          <Input
            id={id}
            value={name}
            autoFocus
            maxLength={DEVICE_NAME_MAX}
            onChange={(e) => setName(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === "Escape") onCancel();
            }}
          />
        )}
      </Field>
      <div className="flex gap-2">
        <Button type="submit" size="sm" disabled={trimmed.length === 0}>
          Renommer
        </Button>
        <Button type="button" size="sm" variant="ghost" onClick={onCancel}>
          Annuler
        </Button>
      </div>
    </form>
  );
}

/**
 * The per-row `⋯` menu. Local to this feature rather than a shared primitive:
 * the kit has no dropdown yet, and inventing one is a design-system decision
 * (UX/Architect), not a side effect of this ticket.
 */
function RowMenu({
  deviceName,
  onRename,
  onRevoke,
}: {
  deviceName: string;
  onRename: () => void;
  onRevoke: () => void;
}) {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    if (!open) return;
    const close = () => setOpen(false);
    // Any click outside dismisses; capture so a click on another row's trigger
    // still opens that one (its handler runs after this closes us).
    window.addEventListener("click", close);
    return () => window.removeEventListener("click", close);
  }, [open]);

  return (
    <div className="relative shrink-0" onClick={(e) => e.stopPropagation()}>
      <button
        type="button"
        aria-haspopup="menu"
        aria-expanded={open}
        aria-label={`Actions pour ${deviceName}`}
        onClick={() => setOpen((v) => !v)}
        className="rounded-md px-2 py-1 text-sm text-muted transition-colors hover:bg-raised hover:text-content"
      >
        ⋯
      </button>
      {open && (
        <div
          role="menu"
          className="absolute right-0 top-full z-10 mt-1 flex min-w-36 flex-col rounded-md border border-border bg-raised py-1 shadow-lg"
        >
          <button
            type="button"
            role="menuitem"
            onClick={() => {
              setOpen(false);
              onRename();
            }}
            className="px-3 py-1.5 text-left text-sm text-content transition-colors hover:bg-canvas"
          >
            Renommer
          </button>
          <button
            type="button"
            role="menuitem"
            onClick={() => {
              setOpen(false);
              onRevoke();
            }}
            className="px-3 py-1.5 text-left text-sm text-danger transition-colors hover:bg-canvas"
          >
            Révoquer
          </button>
        </div>
      )}
    </div>
  );
}
