/**
 * `McpToolPermissionsPanel` — the "Tools MCP IdeA" tab of the project
 * `Permissions` panel (ticket #82). Lets the user grant/revoke MCP tool
 * capabilities per agent (or project-wide default), grouped by domain rather
 * than as a flat 25-checkbox list (carnet #82 — Conception UX/F).
 *
 * Two-column layout: a target selector (`Défaut projet` + agents, each
 * showing `Hérité`/`Override`) on the left, and the grouped tool editor for
 * the selected target on the right. An agent without an override shows its
 * inherited effective state read-only until `Créer un override`; the draft
 * then starts prefilled with that exact effective allowlist. Unsaved edits
 * are tracked per mounted editor instance (remounted via `key` on target
 * change) and protected by a confirmation when switching target.
 */

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

import type { Agent, McpToolCatalogue } from "@/domain";
import { Button, Panel, cn } from "@/shared";
import { MCP_TOOL_GROUPS, mcpToolLabel } from "./mcpToolGroups";
import { useMcpToolPermissions } from "./useMcpToolPermissions";

export interface McpToolPermissionsPanelProps {
  projectId: string;
  agents: Agent[];
}

type McpTarget =
  | { type: "project" }
  | { type: "agent"; agentId: string; agentName: string };

const READ_GROUP_IDS = new Set(["readProject", "readTickets"]);

function toggleTool(list: string[], tool: string): string[] {
  return list.includes(tool) ? list.filter((t) => t !== tool) : [...list, tool];
}

function sameToolSet(a: string[], b: string[]): boolean {
  if (a.length !== b.length) return false;
  const bs = new Set(b);
  return a.every((t) => bs.has(t));
}

export function McpToolPermissionsPanel({
  projectId,
  agents,
}: McpToolPermissionsPanelProps) {
  const vm = useMcpToolPermissions(projectId);
  const [target, setTarget] = useState<McpTarget>({ type: "project" });
  // Read imperatively (not via re-render) by `selectTarget` so an unsaved
  // draft in the currently-mounted editor blocks a target switch without
  // needing the editor's dirty state to live in this component.
  const dirtyRef = useRef(false);

  function selectTarget(next: McpTarget) {
    if (dirtyRef.current) {
      const proceed = window.confirm(
        "Des modifications non enregistrées seront perdues. Continuer ?",
      );
      if (!proceed) return;
    }
    dirtyRef.current = false;
    setTarget(next);
  }

  if (vm.loading && !vm.doc) {
    return (
      <div data-testid="mcp-tool-permissions-skeleton" className="flex flex-col gap-3 p-4">
        <div className="h-4 w-40 animate-pulse rounded bg-raised" />
        <div className="h-8 w-full animate-pulse rounded bg-raised" />
        <div className="h-8 w-full animate-pulse rounded bg-raised" />
        <div className="h-32 w-full animate-pulse rounded bg-raised" />
      </div>
    );
  }

  if (vm.error && !vm.doc) {
    return (
      <div className="flex flex-col items-start gap-2 p-4">
        <p role="alert" className="text-sm text-danger">
          {vm.error}
        </p>
        <Button size="sm" onClick={() => void vm.refresh()}>
          Réessayer
        </Button>
      </div>
    );
  }

  if (!vm.doc) return null;
  const doc = vm.doc;

  const selectedAgentOverride =
    target.type === "agent"
      ? doc.agents.find((entry) => entry.agentId === target.agentId) ?? null
      : null;

  return (
    <div className="flex min-h-0 flex-1 flex-col gap-3 p-4 md:flex-row">
      <nav aria-label="cibles des permissions MCP" className="flex w-full shrink-0 flex-col gap-2 md:w-56">
        <button
          type="button"
          aria-pressed={target.type === "project"}
          onClick={() => selectTarget({ type: "project" })}
          className={cn(
            "flex min-h-[32px] w-full items-center justify-between gap-2 rounded-md border px-3 py-2 text-left text-sm",
            "transition-colors hover:border-border-strong hover:bg-raised",
            target.type === "project" ? "border-primary bg-raised" : "border-border bg-surface",
          )}
        >
          <span className="font-medium text-content">Défaut projet</span>
          <span className="text-xs text-muted">
            {doc.projectDefault ? "Personnalisé" : "Lecture seule"}
          </span>
        </button>

        <h4 className="mt-1 text-xs font-semibold uppercase tracking-wide text-faint">Agents</h4>
        {agents.length === 0 ? (
          <p className="text-sm text-muted">Aucun agent dans ce projet.</p>
        ) : (
          <ul className="flex flex-col gap-1.5">
            {agents.map((agent) => {
              const hasOverride = doc.agents.some((entry) => entry.agentId === agent.id);
              const active = target.type === "agent" && target.agentId === agent.id;
              return (
                <li key={agent.id}>
                  <button
                    type="button"
                    aria-pressed={active}
                    onClick={() =>
                      selectTarget({ type: "agent", agentId: agent.id, agentName: agent.name })
                    }
                    className={cn(
                      "flex min-h-[32px] w-full items-center justify-between gap-2 rounded-md border px-3 py-2 text-left text-sm",
                      "transition-colors hover:border-border-strong hover:bg-raised",
                      active ? "border-primary bg-raised" : "border-border bg-surface",
                    )}
                  >
                    <span className="truncate font-medium text-content">{agent.name}</span>
                    <span
                      className={cn(
                        "shrink-0 rounded-full px-2 py-0.5 text-xs font-medium",
                        hasOverride
                          ? "bg-warning/15 text-warning"
                          : "bg-primary/10 text-muted",
                      )}
                    >
                      {hasOverride ? "Override" : "Hérité"}
                    </span>
                  </button>
                </li>
              );
            })}
          </ul>
        )}
      </nav>

      <div className="min-w-0 flex-1">
        {target.type === "project" ? (
          <McpToolEditor
            key="project"
            targetKind="project"
            title="Tools MCP IdeA — Défaut projet"
            catalogue={doc.catalogue}
            storedAllowed={doc.projectDefault?.allowedTools ?? null}
            comparisonBaseline={doc.catalogue.readOnlyTools}
            busy={vm.busy}
            onDirtyChange={(dirty) => {
              dirtyRef.current = dirty;
            }}
            onSaveProjectDefault={async (allowed) => {
              await vm.saveProjectDefault({ allowedTools: allowed });
            }}
          />
        ) : (
          <McpToolEditor
            key={target.agentId}
            targetKind="agent"
            title={`Tools MCP IdeA — ${target.agentName}`}
            catalogue={doc.catalogue}
            storedAllowed={selectedAgentOverride?.policy.allowedTools ?? null}
            comparisonBaseline={
              doc.projectDefault?.allowedTools ?? doc.catalogue.readOnlyTools
            }
            busy={vm.busy}
            onDirtyChange={(dirty) => {
              dirtyRef.current = dirty;
            }}
            onSaveAgentOverride={async (allowed) => {
              await vm.saveAgentOverride(target.agentId, { allowedTools: allowed });
            }}
            onResetOverride={async () => {
              await vm.resetAgentOverride(target.agentId);
            }}
          />
        )}
      </div>
    </div>
  );
}

interface McpToolEditorProps {
  targetKind: "project" | "agent";
  title: string;
  catalogue: McpToolCatalogue;
  /** Stored policy allowlist for this exact target, or `null` if unset/inherited. */
  storedAllowed: string[] | null;
  /**
   * For `project`: the canonical read-only fallback (used as the starting
   * draft when no default is stored yet). For `agent`: the effective
   * inherited allowlist (project default, or read-only fallback) — used both
   * as the read-only display when there is no override, and to prefill the
   * draft the moment `Créer un override` is clicked, and to compute the
   * per-row `Ajouté`/`Retiré` diff once editing.
   */
  comparisonBaseline: string[];
  busy: boolean;
  onDirtyChange: (dirty: boolean) => void;
  onSaveProjectDefault?: (allowed: string[]) => Promise<void>;
  onSaveAgentOverride?: (allowed: string[]) => Promise<void>;
  onResetOverride?: () => Promise<void>;
}

function McpToolEditor({
  targetKind,
  title,
  catalogue,
  storedAllowed,
  comparisonBaseline,
  busy,
  onDirtyChange,
  onSaveProjectDefault,
  onSaveAgentOverride,
  onResetOverride,
}: McpToolEditorProps) {
  const initialAllowed = storedAllowed ?? comparisonBaseline;
  const [overrideActive, setOverrideActive] = useState(
    targetKind === "project" || storedAllowed != null,
  );
  const [hasPersistedOverride, setHasPersistedOverride] = useState(storedAllowed != null);
  const [draft, setDraft] = useState<string[]>(initialAllowed);
  const [baseline, setBaseline] = useState<string[]>(initialAllowed);
  const [pendingConfirm, setPendingConfirm] = useState(false);
  const [savedFlash, setSavedFlash] = useState(false);
  const [saveError, setSaveError] = useState<string | null>(null);
  const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>(() =>
    Object.fromEntries(
      MCP_TOOL_GROUPS.map((group) => [
        group.id,
        READ_GROUP_IDS.has(group.id) ? targetKind === "project" : true,
      ]),
    ),
  );

  const editable = targetKind === "project" || overrideActive;
  const displaySet = editable ? draft : comparisonBaseline;
  const dirty = editable && !sameToolSet(draft, baseline);

  useEffect(() => {
    onDirtyChange(dirty);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dirty]);

  useEffect(() => {
    if (!savedFlash) return;
    const id = window.setTimeout(() => setSavedFlash(false), 2000);
    return () => window.clearTimeout(id);
  }, [savedFlash]);

  const readAllowedCount = catalogue.readOnlyTools.filter((t) => displaySet.includes(t)).length;
  const writeAllowedCount = catalogue.writeActionTools.filter((t) =>
    displaySet.includes(t),
  ).length;

  const knownTools = useMemo(
    () => new Set([...catalogue.readOnlyTools, ...catalogue.writeActionTools]),
    [catalogue],
  );
  const unknownTools = useMemo(
    () => Array.from(new Set([...displaySet, ...comparisonBaseline])).filter((t) => !knownTools.has(t)),
    [displaySet, comparisonBaseline, knownTools],
  );

  function toggle(tool: string) {
    setSaveError(null);
    setDraft((prev) => toggleTool(prev, tool));
  }

  function toggleGroup(id: string) {
    setExpandedGroups((prev) => ({ ...prev, [id]: !prev[id] }));
  }

  function createOverride() {
    setOverrideActive(true);
    setDraft(comparisonBaseline);
    setBaseline(comparisonBaseline);
  }

  function cancelDraft() {
    setDraft(baseline);
    setPendingConfirm(false);
    setSaveError(null);
  }

  async function handleSave() {
    setSaveError(null);
    if (targetKind === "project") {
      const introducesNewWrite = draft.some(
        (t) => catalogue.writeActionTools.includes(t) && !baseline.includes(t),
      );
      if (introducesNewWrite && !pendingConfirm) {
        setPendingConfirm(true);
        return;
      }
      setPendingConfirm(false);
      try {
        await onSaveProjectDefault?.(draft);
        setBaseline(draft);
        setSavedFlash(true);
      } catch (e) {
        setSaveError(describeError(e));
      }
      return;
    }
    try {
      await onSaveAgentOverride?.(draft);
      setBaseline(draft);
      setHasPersistedOverride(true);
      setSavedFlash(true);
    } catch (e) {
      setSaveError(describeError(e));
    }
  }

  async function handleReset() {
    setSaveError(null);
    try {
      await onResetOverride?.();
      setOverrideActive(false);
      setHasPersistedOverride(false);
      setDraft(comparisonBaseline);
      setBaseline(comparisonBaseline);
      setSavedFlash(true);
    } catch (e) {
      setSaveError(describeError(e));
    }
  }

  return (
    <Panel title={title} className="flex h-full flex-col" flush>
      <div className="flex flex-col gap-4 p-4">
        {targetKind === "agent" && (
          <p className="text-xs text-muted">
            {overrideActive ? "Override personnalisé" : "Hérite du défaut projet"}
          </p>
        )}

        {targetKind === "agent" && !overrideActive && (
          <Button size="sm" onClick={createOverride} disabled={busy}>
            Créer un override
          </Button>
        )}

        <div>
          <h5 className="text-xs font-semibold uppercase tracking-wide text-faint">
            Résumé effectif
          </h5>
          <p className="text-sm text-content">
            {readAllowedCount} lecture autorisés · {writeAllowedCount} écriture autorisé
            {writeAllowedCount > 1 ? "s" : ""}
          </p>
        </div>

        <div className="flex flex-col gap-2">
          <h5 className="text-xs font-semibold uppercase tracking-wide text-faint">
            Détail des tools
          </h5>
          {MCP_TOOL_GROUPS.map((group) => (
            <ToolGroupSection
              key={group.id}
              group={group}
              expanded={expandedGroups[group.id] ?? true}
              onToggle={() => toggleGroup(group.id)}
              catalogue={catalogue}
              editable={editable}
              targetKind={targetKind}
              draftSet={draft}
              displaySet={displaySet}
              comparisonBaseline={comparisonBaseline}
              onToggleTool={toggle}
            />
          ))}

          {unknownTools.length > 0 && (
            <ToolGroupSection
              group={{ id: "unknown", label: "Tools inconnus", tools: unknownTools }}
              expanded={expandedGroups.unknown ?? true}
              onToggle={() => toggleGroup("unknown")}
              catalogue={catalogue}
              editable={false}
              targetKind={targetKind}
              draftSet={draft}
              displaySet={displaySet}
              comparisonBaseline={comparisonBaseline}
              onToggleTool={toggle}
              unknown
            />
          )}
        </div>

        {pendingConfirm && (
          <div
            role="alert"
            className="flex flex-col gap-2 rounded-md border border-warning/40 bg-warning/10 px-3 py-2 text-sm text-warning"
          >
            <p>
              Ce tool sera autorisé pour tous les agents sans override. Confirmer cette
              modification ?
            </p>
            <div className="flex justify-end gap-2">
              <Button size="sm" variant="ghost" onClick={() => setPendingConfirm(false)}>
                Annuler
              </Button>
              <Button size="sm" variant="primary" onClick={() => void handleSave()}>
                Confirmer
              </Button>
            </div>
          </div>
        )}

        {saveError && (
          <p role="alert" className="text-sm text-danger">
            {saveError}
          </p>
        )}

        {savedFlash && (
          <p role="status" className="text-sm text-success">
            Permissions enregistrées
          </p>
        )}

        <div className="flex flex-wrap items-center justify-between gap-2 pt-1">
          <div>
            {dirty && (
              <span className="text-xs text-warning">Modifications non enregistrées</span>
            )}
          </div>
          <div className="flex gap-2">
            {targetKind === "agent" && hasPersistedOverride && (
              <Button size="sm" variant="ghost" disabled={busy} onClick={() => void handleReset()}>
                Réinitialiser l'override
              </Button>
            )}
            <Button size="sm" variant="ghost" disabled={busy || !dirty} onClick={cancelDraft}>
              Annuler
            </Button>
            <Button
              size="sm"
              variant="primary"
              disabled={busy || !editable || !dirty}
              loading={busy && dirty}
              onClick={() => void handleSave()}
            >
              Enregistrer
            </Button>
          </div>
        </div>
      </div>
    </Panel>
  );
}

interface ToolGroupSectionProps {
  group: { id: string; label: string; tools: string[] };
  expanded: boolean;
  onToggle: () => void;
  catalogue: McpToolCatalogue;
  editable: boolean;
  targetKind: "project" | "agent";
  draftSet: string[];
  displaySet: string[];
  comparisonBaseline: string[];
  onToggleTool: (tool: string) => void;
  unknown?: boolean;
}

function ToolGroupSection({
  group,
  expanded,
  onToggle,
  catalogue,
  editable,
  targetKind,
  displaySet,
  comparisonBaseline,
  onToggleTool,
  unknown,
}: ToolGroupSectionProps) {
  const allowedCount = group.tools.filter((t) => displaySet.includes(t)).length;
  const total = group.tools.length;

  return (
    <div className="rounded-md border border-border">
      <button
        type="button"
        aria-expanded={expanded}
        aria-label={`${group.label}, ${allowedCount} sur ${total} autorisé${total > 1 ? "s" : ""}`}
        onClick={onToggle}
        className="flex min-h-[32px] w-full items-center justify-between gap-2 px-3 py-2 text-left text-sm hover:bg-raised"
      >
        <span className="font-medium text-content">{group.label}</span>
        <span className="text-xs text-muted">
          {allowedCount}/{total} autorisés
        </span>
      </button>
      {expanded && (
        <ul className="flex flex-col divide-y divide-border border-t border-border">
          {group.tools.map((tool) => (
            <ToolRow
              key={tool}
              tool={tool}
              isRead={catalogue.readOnlyTools.includes(tool)}
              unknown={unknown}
              editable={editable}
              checked={displaySet.includes(tool)}
              status={rowStatus(tool, editable, targetKind, displaySet, comparisonBaseline, unknown)}
              onToggle={() => onToggleTool(tool)}
            />
          ))}
        </ul>
      )}
    </div>
  );
}

type RowStatus = "allowed" | "denied" | "inherited" | "added" | "removed" | "unknown";

function rowStatus(
  tool: string,
  editable: boolean,
  targetKind: "project" | "agent",
  displaySet: string[],
  comparisonBaseline: string[],
  unknown?: boolean,
): RowStatus {
  if (unknown) return "unknown";
  if (targetKind === "agent" && !editable) return "inherited";
  if (targetKind === "agent" && editable) {
    const inDraft = displaySet.includes(tool);
    const inBaseline = comparisonBaseline.includes(tool);
    if (inDraft && !inBaseline) return "added";
    if (!inDraft && inBaseline) return "removed";
    return inDraft ? "allowed" : "denied";
  }
  return displaySet.includes(tool) ? "allowed" : "denied";
}

const STATUS_LABEL: Record<RowStatus, string> = {
  allowed: "Autorisé",
  denied: "Refusé",
  inherited: "Hérité",
  added: "Ajouté",
  removed: "Retiré",
  unknown: "Inconnu",
};

interface ToolRowProps {
  tool: string;
  isRead: boolean;
  unknown?: boolean;
  editable: boolean;
  checked: boolean;
  status: RowStatus;
  onToggle: () => void;
}

function ToolRow({ tool, isRead, unknown, editable, checked, status, onToggle }: ToolRowProps) {
  const label = mcpToolLabel(tool);
  const inputId = `mcp-tool-${tool}`;
  return (
    <li className="flex min-h-[32px] items-center gap-2 px-3 py-1.5">
      <input
        id={inputId}
        type="checkbox"
        checked={checked}
        disabled={!editable || unknown}
        onChange={onToggle}
        aria-label={`${label}, ${tool}`}
      />
      <label htmlFor={inputId} className="flex min-w-0 flex-1 flex-col gap-0.5">
        <span className="flex items-center gap-2">
          <code className="font-mono text-xs text-content">{tool}</code>
          {!unknown && (
            <span
              className={cn(
                "rounded-full px-1.5 py-0.5 text-[10px] font-medium",
                isRead ? "bg-primary/10 text-primary" : "bg-warning/15 text-warning",
              )}
            >
              {isRead ? "Lecture" : "Écriture"}
            </span>
          )}
        </span>
        <span className="text-xs text-muted">{label}</span>
      </label>
      <span
        className={cn(
          "shrink-0 rounded-full px-2 py-0.5 text-xs font-medium",
          status === "allowed" || status === "added"
            ? "bg-success/15 text-success"
            : status === "denied" || status === "removed"
              ? "bg-danger/10 text-muted"
              : status === "unknown"
                ? "bg-danger/15 text-danger"
                : "bg-primary/10 text-muted",
        )}
      >
        {STATUS_LABEL[status]}
      </span>
    </li>
  );
}

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