/**
 * `AgentsPanel` — feature component for the agents panel (L6).
 *
 * Pure presentation: all behaviour comes from {@link useAgents}. Styled with
 * `@/shared` design system tokens; no inline styles, no classes invented outside
 * the theme.
 *
 * When the user clicks Launch, an agent terminal is mounted below the agent
 * list using a {@link TerminalView} with a custom `open` prop that delegates
 * to `agent.launchAgent`. A Stop button unmounts it.
 *
 * Feature #3: the create form includes a template selector. Choosing a template
 * calls `createAgentFromTemplate`; leaving it at "(none / from scratch)" uses
 * the existing `createAgent` path with the selected profile.
 */

import { useEffect, useState } from "react";

import { Button, Input, Panel, SmallDropdown, Spinner, cn } from "@/shared";
import { TerminalView } from "@/features/terminals/TerminalView";
import { AnnouncementsPreview } from "@/features/announcements";
import { useDrift } from "@/features/templates/useDrift";
import { useGateways } from "@/app/di";
import { useAgents } from "./useAgents";
import { correlateModelServerStatus } from "./modelServerLaunch";
import { AgentLimitBadge } from "./AgentLimitBadge";
import { ModelServerLaunchBadge } from "./ModelServerLaunchBadge";
import type { ResolvedAgentSystemPermissions } from "@/domain";

export interface AgentsPanelProps {
  /** The project whose agents to manage. */
  projectId: string;
  /**
   * The filesystem root of the project. Passed as the `cwd` to the agent
   * terminal. Supplied by `ProjectsView` which has `active.root`.
   */
  projectRoot?: string;
}

export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
  const vm = useAgents(projectId);
  const drift = useDrift(projectId);
  const gateways = useGateways();
  const templateGw = gateways.template ?? null;
  const permissionGw = gateways.permission ?? null;

  // Create form state
  const [newName, setNewName] = useState("");
  const [newProfileId, setNewProfileId] = useState("");
  const [newTemplateId, setNewTemplateId] = useState("");

  // Templates available for the selector (loaded lazily on first render via a
  // separate effect handled inline via hook state)
  const [templates, setTemplates] = useState<import("@/domain").Template[]>([]);
  const [templatesLoaded, setTemplatesLoaded] = useState(false);

  // Load templates once on mount (best-effort — if it fails the selector just stays empty)
  if (!templatesLoaded && templateGw) {
    setTemplatesLoaded(true);
    void templateGw.listTemplates().then(setTemplates).catch(() => {});
  }

  // Skills available for assignment (global + project), loaded lazily once.
  const skillGw = gateways.skill ?? null;
  const [skills, setSkills] = useState<import("@/domain").Skill[]>([]);
  const [skillsLoaded, setSkillsLoaded] = useState(false);
  const [skillToAssign, setSkillToAssign] = useState("");

  async function refreshSkills() {
    if (!skillGw) return;
    try {
      const [globals, projects] = await Promise.all([
        skillGw.listSkills(projectId, "global"),
        skillGw.listSkills(projectId, "project"),
      ]);
      setSkills([...projects, ...globals]);
    } catch {
      // Skills are optional — leave the list empty on failure.
    }
  }

  if (!skillsLoaded && skillGw) {
    setSkillsLoaded(true);
    void refreshSkills();
  }

  async function handleAssignSkill() {
    if (!skillGw || !vm.selectedAgentId || !skillToAssign) return;
    const target = skills.find((s) => s.id === skillToAssign);
    if (!target) return;
    await skillGw.assignSkill(
      projectId,
      vm.selectedAgentId,
      target.id,
      target.scope,
    );
    setSkillToAssign("");
    await vm.refresh();
  }

  async function handleUnassignSkill(skillId: string) {
    if (!skillGw || !vm.selectedAgentId) return;
    await skillGw.unassignSkill(projectId, vm.selectedAgentId, skillId);
    await vm.refresh();
  }

  // Context editor state — local copy before Save
  const [editedContext, setEditedContext] = useState(vm.context);

  // When the hook loads the context for a newly selected agent, mirror it.
  // We use a derived check: if the hook's context changed externally we reset.
  const [lastLoadedContext, setLastLoadedContext] = useState(vm.context);
  if (vm.context !== lastLoadedContext) {
    setLastLoadedContext(vm.context);
    setEditedContext(vm.context);
  }

  /**
   * Id of the agent currently being launched / running in the terminal pane.
   * Kept in local state so the terminal container can appear immediately when
   * the user clicks Launch (before the gateway resolves). vm.runningAgentId is
   * set by the hook once launchAgent resolves.
   */
  const [activeAgentId, setActiveAgentId] = useState<string | null>(null);
  const [panelNodeIds, setPanelNodeIds] = useState<Record<string, string>>({});

  /**
   * Live PTY session id of the running agent terminal, keyed by agent id. Lets
   * the terminal re-attach (rather than re-launch) when the panel re-mounts the
   * view, so navigating away never kills the agent.
   */
  const [agentSessions, setAgentSessions] = useState<Record<string, string>>(
    {},
  );
  const [networkByAgent, setNetworkByAgent] = useState<
    Record<string, ResolvedAgentSystemPermissions>
  >({});

  useEffect(() => {
    if (!permissionGw || vm.agents.length === 0) {
      setNetworkByAgent({});
      return;
    }
    let cancelled = false;
    void Promise.all(
      vm.agents.map(async (candidate) => {
        try {
          return [
            candidate.id,
            await permissionGw.resolveAgentSystemPermissions(
              projectId,
              candidate.id,
            ),
          ] as const;
        } catch {
          return [candidate.id, null] as const;
        }
      }),
    ).then((pairs) => {
      if (cancelled) return;
      setNetworkByAgent(
        Object.fromEntries(
          pairs.filter(
            (pair): pair is readonly [string, ResolvedAgentSystemPermissions] =>
              pair[1] !== null,
          ),
        ),
      );
    });
    return () => {
      cancelled = true;
    };
  }, [permissionGw, projectId, vm.agents]);

  /**
   * Pending profile change awaiting confirmation: the target agent + the chosen
   * profile id. `null` when no dialog is open. Switching the engine abandons the
   * conversation history, so it is gated behind an explicit confirmation.
   */
  const [pendingProfileChange, setPendingProfileChange] = useState<{
    agentId: string;
    profileId: string;
  } | null>(null);

  async function confirmProfileChange() {
    if (!pendingProfileChange) return;
    const { agentId, profileId } = pendingProfileChange;
    setPendingProfileChange(null);
    // Use the running agent terminal's size when available; fall back to a sane
    // default (the backend only needs these for a possible hot relaunch).
    const relaunched = await vm.changeAgentProfile(agentId, profileId, 24, 80);
    // On a hot relaunch the backend mints a fresh session; rebind the panel's
    // terminal to it and drop any persisted conversation id for this cell (the
    // history is gone — the front reflects the swap for the current session).
    if (relaunched && agentId === activeAgentId) {
      setAgentSessions((prev) => ({ ...prev, [agentId]: relaunched.sessionId }));
    }
  }

  const canCreate = newName.trim().length > 0 && !vm.busy;

  async function handleCreate(e: React.FormEvent) {
    e.preventDefault();
    if (!canCreate) return;
    if (newTemplateId && templateGw) {
      // Create from template — the template imposes its default profile.
      await templateGw.createAgentFromTemplate(projectId, newTemplateId, {
        name: newName.trim(),
        synchronized: true,
      });
      // Refresh the agents list since we bypassed the hook's createAgent path.
      await vm.refresh();
    } else {
      const profileId = newProfileId.trim() || "";
      await vm.createAgent(newName.trim(), profileId);
    }
    setNewName("");
    setNewProfileId("");
    setNewTemplateId("");
  }

  function handleLaunch(agentId: string) {
    setPanelNodeIds((prev) => ({
      ...prev,
      [agentId]: prev[agentId] ?? crypto.randomUUID(),
    }));
    setActiveAgentId(agentId);
  }

  function handleStop(agentId?: string) {
    void vm.stopAgent(agentId);
    setActiveAgentId(null);
  }

  const selectedAgent = vm.agents.find((a) => a.id === vm.selectedAgentId) ?? null;

  // Determine if a template is chosen → profile selector is hidden (template imposes it).
  const hasTemplate = newTemplateId !== "";

  const profileLabel = (profile: import("@/domain").AgentProfile): string => {
    const model =
      profile.model ??
      profile.opencode?.model ??
      profile.opencodeProvider?.model ??
      profile.chatHttp?.model;
    return model ? `${profile.name} · ${model}` : profile.name;
  };

  return (
    <Panel title="Agents" className="flex flex-col gap-0">
      {vm.error && (
        <p
          role="alert"
          className="mx-4 mt-3 rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger"
        >
          {vm.error}
        </p>
      )}

      {/* ── Create form ── */}
      <form
        onSubmit={(e) => void handleCreate(e)}
        className="flex flex-col gap-3 border-b border-border p-4"
      >
        <div className="flex min-w-0 flex-col gap-1">
          <label
            htmlFor="agent-name-input"
            className="text-xs font-medium text-muted"
          >
            Name
          </label>
          <Input
            id="agent-name-input"
            aria-label="agent name"
            placeholder="My agent"
            value={newName}
            onChange={(e) => setNewName(e.target.value)}
            className="w-full"
          />
        </div>

        {/* Template selector */}
        <div className="flex min-w-0 flex-col gap-1">
          <label
            htmlFor="agent-template-select"
            className="text-xs font-medium text-muted"
          >
            Template
          </label>
          <SmallDropdown
            id="agent-template-select"
            aria-label="agent template"
            value={newTemplateId}
            onChange={setNewTemplateId}
            className="w-full"
            disabled={vm.busy}
            size="md"
            options={[
              { value: "", label: "(none / from scratch)" },
              ...templates.map((t) => ({ value: t.id, label: t.name })),
            ]}
          />
        </div>

        {/* Profile selector — hidden when a template is chosen */}
        {!hasTemplate && (
          <div className="flex min-w-0 flex-col gap-1">
            <label
              htmlFor="agent-profile-select"
              className="text-xs font-medium text-muted"
            >
              Profile
              {vm.profiles.length === 0 && (
                <span className="ml-1 text-faint">(no profiles configured — you can still enter an id)</span>
              )}
            </label>
            {vm.profiles.length > 0 ? (
              <SmallDropdown
                id="agent-profile-select"
                aria-label="agent profile"
                value={newProfileId}
                onChange={setNewProfileId}
                className="w-full"
                size="md"
                options={[
                  { value: "", label: "— select profile —" },
                  ...vm.profiles.map((p) => ({
                    value: p.id,
                    label: profileLabel(p),
                  })),
                ]}
              />
            ) : (
              <Input
                id="agent-profile-select"
                aria-label="agent profile"
                placeholder="profile-id (optional)"
                value={newProfileId}
                onChange={(e) => setNewProfileId(e.target.value)}
                className="min-w-32"
              />
            )}
          </div>
        )}

        <Button
          type="submit"
          variant="primary"
          aria-label="create agent"
          disabled={!canCreate}
          loading={vm.busy}
          className="self-end"
        >
          Create
        </Button>
      </form>

      {/* ── Agent list ── */}
      <div className="p-4">
        {vm.agents.length === 0 ? (
          <p className="text-sm text-muted">No agents yet.</p>
        ) : (
          <ul className="flex flex-col divide-y divide-border">
            {vm.agents.map((a) => {
              const isSelected = a.id === vm.selectedAgentId;
              const isRunning = a.id === activeAgentId;
              const live = vm.liveAgents.find((candidate) => candidate.agentId === a.id);
              const profileName =
                (() => {
                  const p = vm.profiles.find((p) => p.id === a.profileId);
                  return p ? profileLabel(p) : null;
                })() ??
                a.profileId;
              const agentDrift = drift.driftByAgentId.get(a.id);
              // Source of this agent's last orchestration delegation (mcp vs
              // file), if any has been observed. Absent ⇒ no badge.
              const delegationSource = vm.delegationSourceByRequester[a.id];
              // Session-limit state (ARCHITECTURE §21), if the agent is limited.
              const limitState = vm.limitByAgent[a.id];
              // F35 — local model server launch state, correlated from the
              // agent's profile binding (`opencode.localModelServerId`), plus the
              // agent's last launch failure (if any).
              const modelServerStatus = correlateModelServerStatus(
                a,
                vm.profiles,
                vm.modelServerStatusByServer,
              );
              const launchFailure = vm.launchFailureByAgent[a.id];
              const network = networkByAgent[a.id] ?? null;
              return (
                <li
                  key={a.id}
                  className={cn(
                    "flex flex-col gap-2 py-3 first:pt-0 last:pb-0",
                    isSelected && "rounded-md bg-raised px-2",
                  )}
                >
                  <button
                    type="button"
                    onClick={() => void vm.selectAgent(a.id)}
                    className="flex min-w-0 flex-col items-start gap-0.5 text-left"
                    aria-pressed={isSelected}
                  >
                    <span className="flex min-w-0 max-w-full flex-wrap items-center gap-2">
                      <span className="font-medium text-content">{a.name}</span>
                      {agentDrift && (
                        <span
                          aria-label="update available"
                          className="rounded-full bg-primary/20 px-2 py-0.5 text-xs font-medium text-primary"
                        >
                          update available
                        </span>
                      )}
                      {delegationSource && (
                        <span
                          aria-label={`delegation source ${delegationSource}`}
                          title={
                            delegationSource === "mcp"
                              ? "Last delegation arrived via the MCP server"
                              : "Last delegation arrived via .ideai/requests"
                          }
                          className="rounded-full bg-raised px-2 py-0.5 text-xs font-medium uppercase tracking-wide text-muted"
                        >
                          {delegationSource}
                        </span>
                      )}
                      {network && <NetworkPermissionBadge state={network} />}
                    </span>
                    <span className="text-xs text-muted">{profileName}</span>
                    {live && (
                      <span className="block w-full min-w-0 truncate text-xs text-primary">
                        running in IdeA · {live.sessionId ?? live.nodeId}
                      </span>
                    )}
                  </button>

                  {limitState && (
                    <AgentLimitBadge
                      state={limitState}
                      busy={vm.busy}
                      onCancelResume={() => void vm.cancelResume(a.id)}
                      onSetResumeAt={(resetsAtMs) =>
                        void vm.setResumeAt(a.id, resetsAtMs)
                      }
                    />
                  )}

                  {/* F35 — local model server launch state / actionable failure. */}
                  <ModelServerLaunchBadge
                    status={modelServerStatus}
                    failure={launchFailure}
                  />

                  {/* F2 (ticket #4): announcements this agent is waiting on while
                      it talks to another agent (requester == this row's id). Sits
                      just above the agent drop-list. */}
                  <AnnouncementsPreview requester={a.id} />

                  <div className="flex flex-wrap items-center gap-1.5">
                    {/* Profile hot-swap selector (Chantier A). Changing the
                        engine abandons the conversation history → confirmation. */}
                    {vm.profiles.length > 0 && (
                      <SmallDropdown
                        aria-label={`profile for ${a.name}`}
                        value={a.profileId}
                        disabled={vm.busy}
                        onChange={(profileId) => {
                          if (profileId && profileId !== a.profileId) {
                            setPendingProfileChange({ agentId: a.id, profileId });
                          }
                        }}
                        options={[
                          ...(vm.profiles.every((p) => p.id !== a.profileId)
                            ? [{ value: a.profileId, label: a.profileId }]
                            : []),
                          ...vm.profiles.map((p) => ({
                            value: p.id,
                            label: profileLabel(p),
                          })),
                        ]}
                      />
                    )}
                    {agentDrift && (
                      <Button
                        size="sm"
                        variant="primary"
                        aria-label={`sync ${a.name}`}
                        disabled={vm.busy || drift.driftBusy}
                        onClick={() =>
                          void drift.syncAgent(a.id, vm.refresh)
                        }
                      >
                        Sync
                      </Button>
                    )}
                    {isRunning || live ? (
                      <Button
                        size="sm"
                        variant="ghost"
                        aria-label={`stop ${a.name}`}
                        onClick={() => handleStop(a.id)}
                        className="text-danger hover:text-danger"
                      >
                        Stop
                      </Button>
                    ) : (
                      <Button
                        size="sm"
                        variant="primary"
                        aria-label={`launch ${a.name}`}
                        disabled={vm.busy || activeAgentId !== null}
                        onClick={() => handleLaunch(a.id)}
                      >
                        Launch
                      </Button>
                    )}
                    <Button
                      size="sm"
                      variant="ghost"
                      aria-label={`delete ${a.name}`}
                      disabled={vm.busy}
                      onClick={() => void vm.deleteAgent(a.id)}
                      className="text-danger hover:text-danger"
                    >
                      Delete
                    </Button>
                  </div>
                </li>
              );
            })}
          </ul>
        )}
      </div>

      {/* ── Agent terminal ── */}
      {activeAgentId !== null && (
        <div className="border-t border-border p-4">
          <div className="h-96 overflow-hidden rounded-lg border border-border bg-surface">
            <TerminalView
              key={activeAgentId}
              cwd={projectRoot}
              open={(opts, onData) =>
                vm.launchAgent(
                  activeAgentId,
                  { ...opts, nodeId: panelNodeIds[activeAgentId] },
                  onData,
                )
              }
              reattach={(sessionId, onData) =>
                gateways.agent.reattach(sessionId, onData)
              }
              sessionId={agentSessions[activeAgentId] ?? null}
              onSessionId={(sid) =>
                setAgentSessions((prev) => ({ ...prev, [activeAgentId]: sid }))
              }
              systemPermissions={networkByAgent[activeAgentId] ?? null}
            />
          </div>
        </div>
      )}

      {/* ── Skill assignment ── */}
      {selectedAgent && skillGw && (
        <div className="flex flex-col gap-2 border-t border-border p-4">
          <h4 className="text-sm font-semibold text-content">
            Skills — {selectedAgent.name}
          </h4>

          {/* Assigned skills */}
          {selectedAgent.skills.length === 0 ? (
            <p className="text-xs text-muted">No skills assigned.</p>
          ) : (
            <ul className="flex flex-wrap gap-1.5">
              {selectedAgent.skills.map((ref) => {
                const name =
                  skills.find((s) => s.id === ref.skillId)?.name ?? ref.skillId;
                return (
                  <li
                    key={ref.skillId}
                    className="flex items-center gap-1 rounded-full bg-raised px-2 py-0.5 text-xs text-content"
                  >
                    <span>{name}</span>
                    <button
                      type="button"
                      aria-label={`unassign ${name}`}
                      disabled={vm.busy}
                      onClick={() => void handleUnassignSkill(ref.skillId)}
                      className="text-faint hover:text-danger"
                    >
                      ✕
                    </button>
                  </li>
                );
              })}
            </ul>
          )}

          {/* Assign selector */}
          <div className="flex items-end gap-2">
            <SmallDropdown
              aria-label="skill to assign"
              value={skillToAssign}
              onChange={setSkillToAssign}
              disabled={vm.busy}
              className="min-w-0 flex-1"
              size="md"
              options={[
                { value: "", label: "— assign a skill —" },
                ...skills
                  .filter(
                    (s) =>
                      !selectedAgent.skills.some((r) => r.skillId === s.id),
                  )
                  .map((s) => ({
                    value: s.id,
                    label: `${s.name} (${s.scope})`,
                  })),
              ]}
            />
            <Button
              variant="primary"
              aria-label="assign skill"
              disabled={vm.busy || !skillToAssign}
              onClick={() => void handleAssignSkill()}
            >
              Assign
            </Button>
          </div>
        </div>
      )}

      {/* ── Context editor ── */}
      {selectedAgent && (
        <div className="flex flex-col gap-2 border-t border-border p-4">
          <div className="flex items-center justify-between gap-2">
            <h4 className="text-sm font-semibold text-content">
              Context — {selectedAgent.name}
            </h4>
            <code className="text-xs text-muted">{selectedAgent.contextPath}</code>
          </div>
          <textarea
            aria-label="agent context"
            value={editedContext}
            onChange={(e) => setEditedContext(e.target.value)}
            rows={10}
            className={cn(
              "w-full rounded-md bg-raised px-3 py-2 text-sm text-content",
              "border border-border outline-none transition-colors",
              "focus:border-primary placeholder:text-faint",
              "disabled:cursor-not-allowed disabled:opacity-50 resize-y font-mono",
            )}
            disabled={vm.busy}
          />
          <div className="flex items-center justify-end gap-2">
            {vm.busy && <Spinner size={14} />}
            <Button
              variant="primary"
              disabled={vm.busy}
              onClick={() => void vm.saveContext(editedContext)}
            >
              Save
            </Button>
          </div>
        </div>
      )}

      {/* ── Profile-change confirmation dialog ── */}
      {pendingProfileChange && (
        <div
          role="dialog"
          aria-modal="true"
          aria-label="Confirm profile change"
          className="fixed inset-0 z-50 flex items-center justify-center bg-black/55 p-4"
        >
          <div className="w-full max-w-sm rounded-lg border border-border bg-surface p-5 shadow-xl">
            <h4 className="text-sm font-semibold text-content">
              Changer le moteur IA
            </h4>
            <p className="mt-2 text-sm text-muted">
              Changer le moteur abandonne l'historique de conversation (le
              contexte et la mémoire sont conservés). Continuer&nbsp;?
            </p>
            <div className="mt-4 flex items-center justify-end gap-2">
              <Button
                variant="ghost"
                aria-label="cancel profile change"
                disabled={vm.busy}
                onClick={() => setPendingProfileChange(null)}
              >
                Annuler
              </Button>
              <Button
                variant="primary"
                aria-label="confirm profile change"
                loading={vm.busy}
                disabled={vm.busy}
                onClick={() => void confirmProfileChange()}
              >
                Continuer
              </Button>
            </div>
          </div>
        </div>
      )}
    </Panel>
  );
}

function NetworkPermissionBadge({
  state,
}: {
  state: ResolvedAgentSystemPermissions;
}) {
  const locked = state.runtimeLock.state === "locked";
  const label = locked
    ? "Verrouillé"
    : state.effective === "allow"
      ? "Autorisé"
      : state.effective === "deny"
        ? "Interdit"
        : "Demande";
  return (
    <span
      aria-label={`network ${label}`}
      title={
        locked
          ? (state.runtimeLock.reason ?? "Réseau verrouillé par le runtime")
          : `État effectif : ${label}`
      }
      className={cn(
        "rounded-full px-2 py-0.5 text-xs font-medium",
        locked
          ? "bg-warning/15 text-warning"
          : state.effective === "allow"
            ? "bg-success/15 text-success"
            : state.effective === "deny"
              ? "bg-danger/15 text-danger"
              : "bg-primary/15 text-primary",
      )}
    >
      Réseau: {label}
    </span>
  );
}
