/**
 * Live web workspace — ticket #13/#86, extended by #90 (missing menus).
 *
 * After pairing: list/create projects (web variant — no native folder browse,
 * see {@link WebProjectsPanel}), open one read-only, and route the main surface
 * through the **"Panneaux" menu** (#90) across the transport-neutral panels also
 * used by the desktop `ProjectsView` (context/tickets/sprints/agents/templates/
 * skills/permissions/memory/git/projects) plus the web-specific live work-state
 * view below. Selecting a panel replaces the main surface — never a floating
 * window or dock (desktop-only chrome, out of scope for web, per Architect).
 *
 * The live work-state ("Travail") panel keeps its own bespoke, mobile-adapted
 * implementation rather than the desktop `ProjectWorkStatePanel`: that component's
 * "open" affordance attaches into a `LayoutGrid` cell, which the web client never
 * mounts (#69 pins this — no `layout-*` grid on web). This panel's own "Ouvrir"
 * instead mounts {@link WebAgentCell}, a touch-drivable terminal cell (F4) — the
 * mobile equivalent, kept transport-neutral via the same {@link useProjectWorkState}
 * hook and {@link WorkStateGateway} writes (cancel/retry). {@link useLiveReconnect}
 * re-synchronises the read-model after a WS outage.
 */

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

import type {
  AgentWorkState,
  BackgroundCompletion,
  GatewayError,
  Project,
} from "@/domain";
import { useGateways } from "@/app/di";
import { useProjectWorkState } from "@/features/workstate/useProjectWorkState";
import { ProjectContextPanel } from "@/features/projects/ProjectContextPanel";
import { AgentsPanel } from "@/features/agents";
import { TemplatesPanel } from "@/features/templates";
import { SkillsPanel } from "@/features/skills";
import { PermissionsPanel } from "@/features/permissions";
import { MemoryPanel } from "@/features/memory";
import { EmbedderSettings } from "@/features/embedder";
import { GitPanel } from "@/features/git";
import { Button, Panel, Spinner, cn } from "@/shared";
import { WebAgentCell } from "./WebAgentCell";
import { useLiveReconnect } from "./useLiveReconnect";
import { useLiveConnectionState } from "./useLiveConnectionState";
import { WebTicketsView } from "./tickets/WebTicketsView";
import { WebSprintsView } from "./tickets/WebSprintsView";
import { WebProjectsPanel } from "./WebProjectsPanel";
import { WebMenuSheet } from "./WebMenuSheet";

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

/** The panels reachable from the web "Panneaux" menu (#90). */
export type WebProjectPanelId =
  | "context"
  | "work"
  | "tickets"
  | "sprints"
  | "agents"
  | "templates"
  | "skills"
  | "permissions"
  | "memory"
  | "git"
  | "projects";

/** Menu order + French labels (ticket #78 — the human UI defaults to French). */
const WEB_PANEL_ORDER: WebProjectPanelId[] = [
  "context",
  "work",
  "tickets",
  "sprints",
  "agents",
  "templates",
  "skills",
  "permissions",
  "memory",
  "git",
  "projects",
];

const WEB_PANEL_LABEL: Record<WebProjectPanelId, string> = {
  context: "Contexte projet",
  work: "Travail",
  tickets: "Tickets",
  sprints: "Sprints",
  agents: "Agents",
  templates: "Templates",
  skills: "Skills",
  permissions: "Permissions",
  memory: "Mémoire",
  git: "Git",
  projects: "Projets",
};

export function WebWorkspace() {
  const { project } = useGateways();
  const [projects, setProjects] = useState<Project[] | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [openId, setOpenId] = useState<string | null>(null);
  const [panel, setPanel] = useState<WebProjectPanelId>("projects");
  // Set by the Sprints panel's "Voir tickets"; consumed once by the Tickets panel.
  const [focusSprintId, setFocusSprintId] = useState<string | null>(null);

  const openRoot = projects?.find((p) => p.id === openId)?.root ?? null;

  const refresh = useCallback(async () => {
    setError(null);
    try {
      setProjects(await project.listProjects());
    } catch (e) {
      setError(describe(e));
    }
  }, [project]);

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

  const openReadOnly = useCallback(
    async (projectId: string) => {
      setError(null);
      setOpenId(null);
      try {
        // Read-only: resolve the project server-side; the live panel then streams
        // its work-state. No layout/PTY is mounted here.
        await project.openProject(projectId);
        setOpenId(projectId);
        setPanel("work");
      } catch (e) {
        setError(describe(e));
      }
    },
    [project],
  );

  const createProject = useCallback(
    async (name: string, root: string): Promise<Project> => {
      setError(null);
      try {
        const created = await project.createProject(name, root);
        await refresh();
        return created;
      } catch (e) {
        setError(describe(e));
        throw e;
      }
    },
    [project, refresh],
  );

  return (
    <div
      data-testid="web-workspace"
      className="flex h-full flex-col gap-4 overflow-y-auto p-4 pb-[max(1rem,env(safe-area-inset-bottom))] pl-[max(1rem,env(safe-area-inset-left))] pr-[max(1rem,env(safe-area-inset-right))] sm:p-6"
    >
      <ReconnectBanner />

      <WebPanelMenuButton activePanel={panel} hasProject={Boolean(openId)} onSelect={setPanel} />

      {error && (
        <Panel className="border-danger/40">
          <p className="text-sm text-danger">{error}</p>
        </Panel>
      )}

      {panel === "projects" ? (
        <WebProjectsPanel
          projects={projects}
          openId={openId}
          onOpen={(id) => void openReadOnly(id)}
          onCreate={createProject}
          onRefresh={() => void refresh()}
        />
      ) : !openId ? (
        <p className="text-sm text-muted">Ouvrir un projet pour utiliser ce panneau.</p>
      ) : (
        <WebPanelBody
          panel={panel}
          projectId={openId}
          root={openRoot}
          focusSprintId={focusSprintId}
          onViewSprintTickets={(sprintId) => {
            setFocusSprintId(sprintId);
            setPanel("tickets");
          }}
        />
      )}
    </div>
  );
}

/** Compact trigger + full-screen sheet for the "Panneaux" menu (#90). */
function WebPanelMenuButton({
  activePanel,
  hasProject,
  onSelect,
}: {
  activePanel: WebProjectPanelId;
  hasProject: boolean;
  onSelect: (panel: WebProjectPanelId) => void;
}) {
  const [open, setOpen] = useState(false);
  return (
    <div className="flex items-center justify-between">
      <Button variant="ghost" size="sm" onClick={() => setOpen(true)}>
        Panneaux : {WEB_PANEL_LABEL[activePanel]}
      </Button>
      {open && (
        <WebMenuSheet
          title="Panneaux"
          note={
            !hasProject ? "Ouvrir un projet pour utiliser ce panneau." : undefined
          }
          onClose={() => setOpen(false)}
          entries={WEB_PANEL_ORDER.map((id) => {
            const disabled = id !== "projects" && !hasProject;
            return {
              id,
              label: WEB_PANEL_LABEL[id],
              active: id === activePanel,
              disabled,
              hint: disabled ? "Ouvrir un projet pour utiliser ce panneau." : undefined,
              onSelect: () => {
                onSelect(id);
                setOpen(false);
              },
            };
          })}
        />
      )}
    </div>
  );
}

/** Renders the requested panel's body for the opened project. */
function WebPanelBody({
  panel,
  projectId,
  root,
  focusSprintId,
  onViewSprintTickets,
}: {
  panel: Exclude<WebProjectPanelId, "projects">;
  projectId: string;
  root: string | null;
  focusSprintId: string | null;
  onViewSprintTickets: (sprintId: string) => void;
}) {
  switch (panel) {
    case "context":
      return <ProjectContextPanel projectId={projectId} />;
    case "work":
      return <LiveProjectPanel projectId={projectId} root={root} />;
    case "tickets":
      return <WebTicketsView projectId={projectId} focusSprintId={focusSprintId} />;
    case "sprints":
      return (
        <WebSprintsView projectId={projectId} onViewSprintTickets={onViewSprintTickets} />
      );
    case "agents":
      return <AgentsPanel projectId={projectId} projectRoot={root ?? ""} />;
    case "templates":
      return <TemplatesPanel projectId={projectId} />;
    case "skills":
      return <SkillsPanel projectId={projectId} />;
    case "permissions":
      return <PermissionsPanel projectId={projectId} />;
    case "memory":
      return (
        <div className="flex flex-col gap-4">
          <MemoryPanel projectId={projectId} />
          <EmbedderSettings />
        </div>
      );
    case "git":
      return <GitPanel projectId={projectId} />;
  }
}

/**
 * Global reconnection banner (F6). The F3 terminal path writes a "déconnecté"
 * notice into xterm, but live-only surfaces (no terminal open) had no visible
 * signal. This surfaces the shared live socket's `reconnecting` state so the user
 * always knows the view may be momentarily stale; `useLiveReconnect` re-syncs the
 * read-model on recovery. Inert on desktop (hook returns `null`).
 */
function ReconnectBanner() {
  const connection = useLiveConnectionState();
  if (connection !== "reconnecting") return null;
  return (
    <div
      role="status"
      data-testid="web-reconnect-banner"
      className="flex items-center gap-2 rounded-md border border-warning/40 bg-warning/10 px-3 py-2 text-xs text-warning"
    >
      <Spinner size={12} />
      Connexion perdue — reconnexion en cours…
    </div>
  );
}

/** Live work-state + background + inbox for the opened project (F5). */
function LiveProjectPanel({ projectId, root }: { projectId: string; root: string | null }) {
  const vm = useProjectWorkState(projectId);
  // Re-sync the read-model when the WS reconnects (events missed while offline).
  useLiveReconnect(vm.refresh);
  const [openAgentId, setOpenAgentId] = useState<string | null>(null);
  const agents = vm.state?.agents ?? [];

  // Per-agent background-tasks disclosure (collapsed by default). A live refresh
  // must never flip this — only drop an entry once its agent has no tasks left,
  // so state doesn't leak for agents that no longer have a background section.
  const [expandedBgAgents, setExpandedBgAgents] = useState<Set<string>>(new Set());
  useEffect(() => {
    const withTasks = new Set(
      agents.filter((a) => (a.backgroundTasks ?? []).length > 0).map((a) => a.agentId),
    );
    setExpandedBgAgents((cur) => {
      const next = new Set([...cur].filter((id) => withTasks.has(id)));
      return next.size === cur.size ? cur : next;
    });
  }, [agents]);
  const toggleBgExpanded = useCallback((agentId: string) => {
    setExpandedBgAgents((cur) => {
      const next = new Set(cur);
      if (next.has(agentId)) next.delete(agentId);
      else next.add(agentId);
      return next;
    });
  }, []);

  return (
    <Panel className="mt-2">
      <div className="mb-2 flex items-center justify-between">
        <h3 className="text-sm font-semibold">
          État live
          <span className="ml-2 rounded bg-canvas px-1.5 py-0.5 text-[0.6rem] uppercase text-muted">
            read-only
          </span>
        </h3>
        <Button variant="ghost" size="sm" loading={vm.busy} onClick={() => void vm.refresh()}>
          Rafraîchir
        </Button>
      </div>

      {vm.error && <p role="alert" className="mb-2 text-xs text-danger">{vm.error}</p>}

      {vm.busy && vm.state === null ? (
        <p className="text-xs text-muted">Chargement de l'état…</p>
      ) : agents.length === 0 ? (
        <p className="text-xs text-muted">Aucun agent actif.</p>
      ) : (
        <ul className="flex flex-col divide-y divide-border" data-testid="web-workstate">
          {agents.map((a) => (
            <AgentLiveRow
              key={a.agentId}
              agent={a}
              open={openAgentId === a.agentId}
              onToggleOpen={() =>
                setOpenAgentId((cur) => (cur === a.agentId ? null : a.agentId))
              }
              onRefresh={vm.refresh}
              bgExpanded={expandedBgAgents.has(a.agentId)}
              onToggleBgExpanded={() => toggleBgExpanded(a.agentId)}
            />
          ))}
        </ul>
      )}

      {root && openAgentId && (
        <div className="mt-3">
          <div className="mb-1 flex items-center justify-between">
            <span className="text-xs font-semibold text-muted">Agent</span>
            <Button variant="ghost" size="sm" onClick={() => setOpenAgentId(null)}>
              Fermer
            </Button>
          </div>
          <WebAgentCell key={openAgentId} projectId={projectId} agentId={openAgentId} cwd={root} />
        </div>
      )}
    </Panel>
  );
}

/** One agent row: live/idle/busy + inbox + background tasks + open affordance. */
function AgentLiveRow({
  agent,
  open,
  onToggleOpen,
  onRefresh,
  bgExpanded,
  onToggleBgExpanded,
}: {
  agent: AgentWorkState;
  open: boolean;
  onToggleOpen: () => void;
  onRefresh: () => Promise<void>;
  bgExpanded: boolean;
  onToggleBgExpanded: () => void;
}) {
  const live = agent.live !== undefined;
  const busy = agent.busy?.state === "busy";
  const inbox = [...(agent.inbox ?? [])].sort((a, b) => a.createdAtMs - b.createdAtMs);
  const backgroundTasks = [...(agent.backgroundTasks ?? [])].sort(
    (a, b) => b.updatedAtMs - a.updatedAtMs || a.taskId.localeCompare(b.taskId),
  );

  return (
    <li className="py-2 first:pt-0 last:pb-0">
      {/* #69 — at 360px the name + two badges + the button no longer fit on one
          line, so the name takes its own row on phones and the status cluster
          wraps under it. From `sm` up this collapses back to the original
          single-line desktop layout. */}
      <div className="flex flex-col gap-1.5 sm:flex-row sm:items-center sm:justify-between sm:gap-2">
        <span className="min-w-0 truncate text-sm text-content">{agent.name}</span>
        <span className="flex shrink-0 items-center gap-1.5 text-xs">
          <span className={cn("rounded-full px-2 py-0.5 font-medium", live ? "bg-success/15 text-success" : "bg-raised text-muted")}>
            {live ? "Live" : "Offline"}
          </span>
          <span className={cn("rounded-full px-2 py-0.5 font-medium", busy ? "bg-warning/15 text-warning" : "bg-raised text-muted")}>
            {busy ? "Busy" : "Idle"}
          </span>
          <Button variant="ghost" size="sm" aria-pressed={open} onClick={onToggleOpen}>
            {open ? "Fermer" : "Ouvrir"}
          </Button>
        </span>
      </div>

      {inbox.length > 0 && (
        <div className="mt-2">
          <p className="text-[11px] font-semibold uppercase tracking-wide text-faint">Inbox</p>
          <ul aria-label={`${agent.name} inbox`} className="mt-1 flex flex-col gap-1">
            {inbox.map((item) => (
              <li key={item.id} className="flex min-w-0 items-start gap-2 text-xs text-muted">
                <span className="mt-0.5 shrink-0 rounded-full bg-raised px-1.5 py-0.5 font-medium">{item.kind}</span>
                <span className="min-w-0 flex-1 break-words">{item.body}</span>
              </li>
            ))}
          </ul>
        </div>
      )}

      {backgroundTasks.length > 0 && (
        <div className="mt-2">
          <button
            type="button"
            aria-expanded={bgExpanded}
            aria-controls={`web-bg-tasks-${agent.agentId}`}
            aria-label={
              bgExpanded
                ? `Masquer les background tasks de ${agent.name}`
                : `Afficher les background tasks de ${agent.name}`
            }
            onClick={onToggleBgExpanded}
            className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-faint hover:text-muted"
          >
            <span aria-hidden="true">{bgExpanded ? "▾" : "▸"}</span>
            Background tasks
            <span className="rounded-full bg-raised px-1.5 py-0.5 font-medium normal-case tracking-normal text-muted">
              {backgroundTasks.length}
            </span>
          </button>
          {bgExpanded && (
            <ul
              id={`web-bg-tasks-${agent.agentId}`}
              aria-label={`${agent.name} background tasks`}
              className="mt-1 flex flex-col gap-1"
            >
              {backgroundTasks.map((task) => (
                <WebBackgroundTaskRow key={task.taskId} task={task} onRefresh={onRefresh} />
              ))}
            </ul>
          )}
        </div>
      )}
    </li>
  );
}

const TASK_STATUS_LABEL: Record<BackgroundCompletion["status"], string> = {
  running: "Running",
  completed: "Completed",
  failed: "Failed",
  cancelled: "Cancelled",
  pending: "Pending",
  delivered: "Delivered",
};

function taskStatusClass(status: BackgroundCompletion["status"]): string {
  if (status === "running" || status === "pending") return "bg-warning/15 text-warning";
  if (status === "completed" || status === "delivered") return "bg-success/10 text-success";
  if (status === "failed" || status === "cancelled") return "bg-danger/10 text-danger";
  return "bg-raised text-muted";
}

/** One background task with live status + cancel/retry via the DI gateway. */
function WebBackgroundTaskRow({
  task,
  onRefresh,
}: {
  task: BackgroundCompletion;
  onRefresh: () => Promise<void>;
}) {
  const { workState } = useGateways();
  const [actionBusy, setActionBusy] = useState(false);
  const [message, setMessage] = useState<string | null>(null);
  const canCancel = task.status === "running" || task.status === "pending";
  const canRetry = task.status === "failed" || task.status === "cancelled";

  async function runAction(action: (taskId: string) => Promise<void>): Promise<void> {
    setActionBusy(true);
    setMessage(null);
    try {
      await action(task.taskId);
      await onRefresh();
    } catch (e) {
      setMessage(describe(e));
    } finally {
      setActionBusy(false);
    }
  }

  return (
    <li className="min-w-0 text-xs text-muted">
      {/* #69 — status + kind + Cancel + Retry overflow a phone row. On narrow
          screens the status/kind pair keeps the first line and the two actions
          wrap onto a second, right-aligned one; `sm` restores the single row. */}
      <div className="flex min-w-0 flex-col gap-1 sm:flex-row sm:items-center sm:gap-2">
        <div className="flex min-w-0 items-center gap-2">
          <span className={cn("shrink-0 rounded-full px-1.5 py-0.5 font-medium", taskStatusClass(task.status))}>
            {TASK_STATUS_LABEL[task.status]}
          </span>
          <span className="min-w-0 flex-1 truncate text-content">{task.kind}</span>
        </div>
        <div className="flex shrink-0 items-center justify-end gap-1">
          <Button
            size="sm"
            variant="ghost"
            disabled={!canCancel || actionBusy}
            loading={actionBusy}
            onClick={() => void runAction((id) => workState.cancelBackgroundTask(id))}
          >
            Cancel
          </Button>
          <Button
            size="sm"
            variant="ghost"
            disabled={!canRetry || actionBusy}
            loading={actionBusy}
            onClick={() => void runAction((id) => workState.retryBackgroundTask(id))}
          >
            Retry
          </Button>
        </div>
      </div>
      {message && <p role="alert" className="mt-1 text-danger">{message}</p>}
    </li>
  );
}
