import { useCallback, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useClient } from './useClient';

// What the signed-in account may reach, as the frontend sees it.
//
// THIS IS NOT ACCESS CONTROL. Every answer here is a courtesy: it stops the app offering a member a
// Terminal icon that would 403, and stops a screen mounting a panel whose every request will fail. The
// lock is server-side, in the capability gate and the websocket gate — both of which
// hold regardless of what this hook returns, including when it returns nothing because the request failed.
//
// Which is why the failure mode below is deliberately generous rather than restrictive: if this request
// errors we show the full dock rather than an empty one. A member clicking through to a 403 is a bad
// minute; an owner locked out of their own platform by a transient network error is an incident. The
// server refuses what it should refuse either way.

export type CapabilityLevel = 'read' | 'write';

export type SelfCapabilities = {
  isOwner: boolean;
  capabilities: { key: string; level: CapabilityLevel }[];
  /** Frontend route prefixes the account holds, flattened across its capabilities. */
  routes: string[];
  /**
   * Route prefixes claimed by capabilities the account does NOT hold. Both lists are needed: absence from
   * `routes` cannot tell a denied route from one no capability claims (`/`, the settings shell), and a
   * guard that cannot tell those apart either blanks the app or guards nothing.
   */
  deniedRoutes: string[];
  /** Capabilities the account holds whose sidecar is not installed, or is installed but disabled. */
  unavailable?: string[];
  /**
   * Dock tiles and routes of the sidecars actually installed on this server.
   *
   * The dock is the baseline shell plus these. A feature appears when it is installed and leaves when it
   * is removed, with nothing in the shell to edit — which is what lets a sidecar ship from its own
   * repository and still show up.
   */
  plugins?: PluginManifest[];
};

/** What an installed sidecar says about how it should appear. Mirrors UiManifest on the server. */
export type PluginManifest = {
  sidecarId: string;
  capability: string | null;
  name: string;
  icon?: string;
  image?: string;
  color: string;
  rootRoute: string;
  routes: string[];
  extraTiles?: Array<{ name: string; icon?: string; image?: string; color: string; route: string }>;
};

export const CAPABILITIES_QUERY_KEY = ['self-capabilities'];

export function useCapabilities() {
  const client = useClient();

  const { data, isLoading, isError } = useQuery<SelfCapabilities>({
    queryKey: CAPABILITIES_QUERY_KEY,
    queryFn: () => client.get<SelfCapabilities>('/user/capabilities'),
    // Grants change rarely and only by an owner action, but they change the shape of the whole app when
    // they do. A minute matches the `tasks` / `task-categories` caches the rest of the app uses.
    staleTime: 60_000,
  });

  const held = useMemo(() => new Map((data?.capabilities ?? []).map((c) => [c.key, c.level])), [data]);

  // `can` and `canVisit` are memoised on `data` alone, and that matters more than it looks. A verb rebuilt
  // every render gets a new identity every render, so anything putting one in a useCallback or useMemo
  // dependency list silently recomputes forever — which is exactly how every playback report in the
  // Jellyfin player was disabled for days by an unmount cleanup re-running mid-playback. `data` is a React
  // Query value with a stable reference between fetches, so these change only when the answer does.

  /** Whether the account holds a capability, optionally at write level. */
  const can = useCallback(
    (key: string, level: CapabilityLevel = 'read'): boolean => {
      if (!data) return true; // see the note above: fail open, the server does not
      if (data.isOwner) return true;
      const granted = held.get(key);
      if (!granted) return false;
      return level === 'read' || granted === 'write';
    },
    [data, held],
  );

  /**
   * Whether a frontend route is reachable. Only routes a capability actually claims are ever denied —
   * `/`, the settings shell and anything else unclaimed stays open, because denying by default here would
   * blank the app for everyone rather than restrict it for anyone.
   */
  const canVisit = useCallback(
    (path: string): boolean => {
      if (!data) return true;
      if (data.isOwner) return true;
      return !data.deniedRoutes.some((route) => path === route || path.startsWith(`${route}/`));
    },
    [data],
  );

  /**
   * Why a route is refused, or null if it is not.
   *
   * Two answers, because they need two different screens. `not-installed` is a fact about the SERVER and the
   * owner can fix it from the app store; `not-granted` is a fact about the ACCOUNT and only the owner can
   * change it. Presenting either as the other sends the reader looking in the wrong place.
   *
   * Same fail-open posture as `can`: no data means no denial.
   */
  const denialReason = useCallback(
    (path: string): 'not-installed' | 'not-granted' | null => {
      if (!data) return null;
      if (!data.deniedRoutes.some((route) => path === route || path.startsWith(`${route}/`))) return null;

      // Held but unavailable → the sidecar is missing. Checked against the capability that claims the route,
      // which is why `unavailable` is returned as capability keys rather than routes.
      const unavailable = new Set(data.unavailable ?? []);
      const heldAndUnavailable = data.capabilities.some(({ key }) => unavailable.has(key));
      if (data.isOwner || heldAndUnavailable) return 'not-installed';
      return 'not-granted';
    },
    [data],
  );

  return {
    isOwner: data?.isOwner ?? false,
    capabilities: held,
    routes: data?.routes ?? [],
    unavailable: data?.unavailable ?? [],
    denialReason,
    // Empty rather than undefined when the request has not landed or failed: the dock then renders its
    // baseline, which is the honest "we do not know yet" — not an empty app.
    plugins: data?.plugins ?? [],
    isLoading,
    isError,
    can,
    canVisit,
  };
}
