/**
 * Device-management state (ticket #77, lot F2).
 *
 * Owns the list, the ephemeral pairing code and the revoke/rename actions, so
 * {@link DevicesScreen} stays a rendering concern. Transport-neutral: every call
 * goes through the injected {@link DeviceGateway}, which is the Tauri adapter on
 * desktop and the HTTP one on web.
 *
 * Revoking the **current** device ends this session, so the screen must not try
 * to refresh afterwards — the hook reports it through `sessionEnded` and the
 * mounting surface decides what that means (web: back to pairing; desktop: never
 * happens, no device is ever `isCurrentDevice`).
 */

import { useCallback, useEffect, useRef, useState } from "react";

import type { GatewayError, PairedDevice, PairingCode } from "@/domain";
import { useGateways } from "@/app/di";

/** How often the list is re-read while a code is live, to catch a new pairing. */
const PAIRING_POLL_MS = 3000;
/** How long a freshly-paired row stays highlighted. */
const HIGHLIGHT_MS = 2500;

function describe(e: unknown): string {
  if (e && typeof e === "object" && "message" in e) {
    return String((e as GatewayError).message);
  }
  return String(e);
}

export interface UseDevices {
  devices: PairedDevice[] | null;
  error: string | null;
  /** An action is in flight; drives the confirm dialogs' pending state. */
  busy: boolean;
  /** Narrower than `busy`: only a code generation, so `Appairer` alone spins. */
  generating: boolean;
  /** The live code, or `null` when no code has been generated (or it was dismissed). */
  code: PairingCode | null;
  /** Device ids to highlight — the ones that appeared while a code was live. */
  freshDeviceIds: string[];
  /** Set once the current device's own session has been revoked. */
  sessionEnded: boolean;
  refresh(): Promise<void>;
  generateCode(): Promise<void>;
  dismissCode(): void;
  rename(deviceId: string, name: string): Promise<void>;
  revoke(device: PairedDevice): Promise<void>;
  revokeAll(): Promise<void>;
}

export function useDevices(): UseDevices {
  const { device: gateway } = useGateways();
  const [devices, setDevices] = useState<PairedDevice[] | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);
  const [generating, setGenerating] = useState(false);
  const [code, setCode] = useState<PairingCode | null>(null);
  const [freshDeviceIds, setFreshDeviceIds] = useState<string[]>([]);
  const [sessionEnded, setSessionEnded] = useState(false);
  // Read inside the poll callback without making it a dependency (which would
  // restart the interval on every list change).
  const knownIds = useRef<Set<string> | null>(null);

  const load = useCallback(async (): Promise<PairedDevice[] | null> => {
    try {
      const list = await gateway.listDevices();
      setDevices(list);
      setError(null);
      return list;
    } catch (e) {
      setError(describe(e));
      return null;
    }
  }, [gateway]);

  const refresh = useCallback(async () => {
    const list = await load();
    if (list) knownIds.current = new Set(list.map((d) => d.deviceId));
  }, [load]);

  useEffect(() => {
    void refresh();
  }, [refresh]);

  // While a code is live, a new device may pair at any moment and the backend
  // pushes no event for it (B3 scope), so poll. The interval exists only for the
  // few minutes the code is valid, never in steady state.
  useEffect(() => {
    if (!code) return;
    const timer = setInterval(() => {
      void (async () => {
        const list = await load();
        if (!list) return;
        const known = knownIds.current;
        knownIds.current = new Set(list.map((d) => d.deviceId));
        if (!known) return;
        const fresh = list.filter((d) => !known.has(d.deviceId)).map((d) => d.deviceId);
        if (fresh.length > 0) setFreshDeviceIds(fresh);
      })();
    }, PAIRING_POLL_MS);
    return () => clearInterval(timer);
  }, [code, load]);

  // The highlight is an acknowledgement, not a state: let it fade on its own.
  useEffect(() => {
    if (freshDeviceIds.length === 0) return;
    const timer = setTimeout(() => setFreshDeviceIds([]), HIGHLIGHT_MS);
    return () => clearTimeout(timer);
  }, [freshDeviceIds]);

  const run = useCallback(
    async (action: () => Promise<void>): Promise<boolean> => {
      setBusy(true);
      setError(null);
      try {
        await action();
        return true;
      } catch (e) {
        setError(describe(e));
        return false;
      } finally {
        setBusy(false);
      }
    },
    [],
  );

  const generateCode = useCallback(async () => {
    setGenerating(true);
    try {
      await run(async () => {
        // Generating invalidates the previous code server-side; mirror that by
        // replacing it here rather than stacking panels.
        setCode(await gateway.createPairingCode());
      });
    } finally {
      setGenerating(false);
    }
  }, [gateway, run]);

  const dismissCode = useCallback(() => setCode(null), []);

  const rename = useCallback(
    async (deviceId: string, name: string) => {
      if (await run(() => gateway.renameDevice(deviceId, name))) await refresh();
    },
    [gateway, run, refresh],
  );

  const revoke = useCallback(
    async (target: PairedDevice) => {
      // Read `isCurrentDevice` *before* the call: afterwards the session may be
      // gone and the list unreadable.
      const wasCurrent = target.isCurrentDevice;
      if (!(await run(() => gateway.revokeDevice(target.deviceId)))) return;
      if (wasCurrent) setSessionEnded(true);
      else await refresh();
    },
    [gateway, run, refresh],
  );

  const revokeAll = useCallback(async () => {
    const includedCurrent = devices?.some((d) => d.isCurrentDevice) ?? false;
    if (!(await run(() => gateway.revokeAllDevices()))) return;
    if (includedCurrent) setSessionEnded(true);
    else await refresh();
  }, [devices, gateway, run, refresh]);

  return {
    devices,
    error,
    busy,
    generating,
    code,
    freshDeviceIds,
    sessionEnded,
    refresh,
    generateCode,
    dismissCode,
    rename,
    revoke,
    revokeAll,
  };
}
