import { useEffect, useMemo, useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { Loader2 } from 'lucide-react';
import { useClient } from 'hooks/useClient';
import { CAPABILITIES_QUERY_KEY } from 'hooks/useCapabilities';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';

// What each ROLE may reach. Not each user — see the table comment in schema/capabilities.ts for why.
//
// The screen is one role at a time on purpose. A grid of every role against every capability is the
// obvious design and it is the wrong one: it invites reading across rows, which is not a question anyone
// has, and it makes the destructive action ("uncheck Gitea for Members") a single click among fifty. One
// role, an explicit Save, and a visible dirty state instead.

type CapabilityInfo = {
  key: string;
  label: string;
  description: string;
  routes: string[];
  hasPersonalWrites: boolean;
};

type Grant = { role: string; capability: string; level: 'read' | 'write' };

type CapabilitiesResponse = {
  /** Grantable AND installed. What this server can currently do. */
  capabilities: CapabilityInfo[];

  roles: string[];
  grants: Grant[];
};

type Level = 'none' | 'read' | 'write';

const PERMISSIONS_KEY = ['ROLE_CAPABILITIES'];

export const PermissionsSection = () => {
  const client = useClient();
  const queryClient = useQueryClient();
  const [role, setRole] = useState<string | null>(null);
  const [draft, setDraft] = useState<Record<string, Level>>({});
  const [saving, setSaving] = useState(false);

  const { data, isLoading, isError } = useQuery<CapabilitiesResponse>({
    queryKey: PERMISSIONS_KEY,
    queryFn: () => client.get<CapabilitiesResponse>('/users/capabilities'),
  });

  const activeRole = role ?? data?.roles[0] ?? null;

  // What the server currently says, for this role. The comparison baseline for the dirty state below.
  const saved = useMemo(() => {
    const levels: Record<string, Level> = {};
    for (const capability of data?.capabilities ?? []) levels[capability.key] = 'none';
    for (const grant of data?.grants ?? []) {
      if (grant.role === activeRole) levels[grant.capability] = grant.level;
    }
    return levels;
  }, [data, activeRole]);

  // Reset the draft whenever the role changes or the server answer arrives, so switching roles never
  // carries an unsaved edit across to a role it was not meant for.
  useEffect(() => setDraft(saved), [saved]);

  const dirty = useMemo(() => Object.keys(saved).some((key) => (draft[key] ?? 'none') !== saved[key]), [draft, saved]);

  const save = async () => {
    if (!activeRole) return;
    setSaving(true);
    try {
      const grants = Object.entries(draft)
        .filter(([, level]) => level !== 'none')
        .map(([capability, level]) => ({ capability, level }));
      await client.put(`/users/capabilities/${encodeURIComponent(activeRole)}`, { grants });
      await queryClient.invalidateQueries({ queryKey: PERMISSIONS_KEY });
      // The owner may be editing their own view's inputs — and anyone already signed in needs the dock to
      // catch up without a reload.
      await queryClient.invalidateQueries({ queryKey: CAPABILITIES_QUERY_KEY });
      toast.success(`Saved what ${activeRole}s can reach`);
    } catch (ex) {
      toast.error(ex instanceof Error ? ex.message : 'Could not save');
    } finally {
      setSaving(false);
    }
  };

  if (isLoading) {
    return (
      <div className="flex items-center gap-2 p-6 text-sm text-muted-foreground">
        <Loader2 className="h-4 w-4 animate-spin" /> Loading capabilities…
      </div>
    );
  }
  if (isError || !data) {
    return <div className="p-6 text-sm text-destructive">Could not load capabilities.</div>;
  }

  return (
    <div className="flex flex-col gap-5 p-1">
      {/* Tabs rather than a dropdown. There are three roles and they are the axis you move along — a select
          hides two of them behind a click and gives no sense of "which one am I editing" at a glance. Real
          buttons, because switching role mutates a draft rather than navigating. */}
      <div className="flex flex-wrap items-center justify-between gap-3">
        <div className="flex items-center gap-1 rounded-lg border p-1" role="tablist" aria-label="Role">
          {data.roles.map((r) => (
            <button
              key={r}
              type="button"
              role="tab"
              aria-selected={activeRole === r}
              onClick={() => setRole(r)}
              className={`rounded-md px-3 py-1.5 text-sm transition-colors ${
                activeRole === r
                  ? 'bg-accent font-medium text-accent-foreground'
                  : 'text-muted-foreground hover:bg-accent/50'
              }`}
            >
              {r}
            </button>
          ))}
        </div>
        <Button onClick={save} disabled={!dirty || saving}>
          {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
          {dirty ? 'Save changes' : 'Saved'}
        </Button>
      </div>

      <p className="text-xs text-muted-foreground">
        Everything is denied unless granted here. <strong>Read</strong> allows viewing, plus changes to things that are
        only ever the person&rsquo;s own — their favourites, their playlists, their devices.
        <strong> Full</strong> allows everything within the app.
      </p>

      <div className="divide-y rounded-lg border">
        {data.capabilities.map((capability) => {
          const level = draft[capability.key] ?? 'none';
          return (
            <div key={capability.key} className="flex items-center justify-between gap-4 p-3">
              {/* Label only. The descriptions went because with three rows called Terminal, Chat and Files
                  they explained nothing anyone needed — and the "needs a Linux account" line went with them:
                  every account gets one at creation, so warning about it on every row was noise about a state
                  that no longer occurs on its own. */}
              <div className="min-w-0 text-sm font-medium">{capability.label}</div>
              <Select
                value={level}
                onValueChange={(value) => setDraft((prev) => ({ ...prev, [capability.key]: value as Level }))}
              >
                <SelectTrigger className="w-32 shrink-0">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="none">No access</SelectItem>
                  <SelectItem value="read">Read</SelectItem>
                  <SelectItem value="write">Full</SelectItem>
                </SelectContent>
              </Select>
            </div>
          );
        })}
      </div>

      {/* Two explanatory blocks used to sit here: one naming every capability whose sidecar is not installed,
          and one naming everything that can never be granted. Both are gone, and for the same reason — a
          server should not enumerate what it does not have. The first was a catalogue of uninstallable
          features presented as a permissions decision; the second described chat, tasks, the desktop and the
          wallet to an owner who may have none of them installed. What is on this screen is what this server
          can actually do. */}
    </div>
  );
};
