/**
 * Live web workspace — ticket #13, lots F2 (read-only) + F5 (live surfaces).
 *
 * After pairing: list projects, open one read-only, and show its **live**
 * work-state — agents (live/idle/busy), background tasks (with cancel/retry) and
 * per-agent inbox — updated in real time. The live mechanism is the desktop's own
 * transport-neutral {@link useProjectWorkState} hook (refreshes the read-model on
 * relevant `event.domain` events pushed over the WS, B7); {@link useLiveReconnect}
 * re-synchronises after a WS outage. Background cancel/retry go through the
 * {@link WorkStateGateway} — writes via DI, no direct transport. A CLI agent can
 * be opened into a live cell (F4). No component touches `@tauri-apps/api`.
 *
 * Read-only allowlist used (B4/B7): `list_projects`, `open_project`,
 * `get_project_work_state`, plus the background `cancel`/`retry` commands and the
 * `event.domain` live stream.
 */

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 { Button, Panel, Spinner, cn } from "@/shared";
import { WebAgentCell } from "./WebAgentCell";
import { useLiveReconnect } from "./useLiveReconnect";
import { useLiveConnectionState } from "./useLiveConnectionState";

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

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 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);
      } catch (e) {
        setError(describe(e));
      }
    },
    [project],
  );

  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 />
      <div className="flex items-center justify-between">
        <h2 className="text-base font-semibold tracking-tight">Projets</h2>
        <Button variant="ghost" size="sm" onClick={() => void refresh()}>
          Rafraîchir
        </Button>
      </div>

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

      {projects === null ? (
        <span className="inline-flex items-center gap-1.5 text-sm text-muted">
          <Spinner size={12} /> Chargement des projets…
        </span>
      ) : projects.length === 0 ? (
        <p className="text-sm text-muted">Aucun projet.</p>
      ) : (
        <ul className="flex flex-col gap-2" data-testid="web-project-list">
          {projects.map((p) => (
            <li key={p.id}>
              <button
                type="button"
                onClick={() => void openReadOnly(p.id)}
                aria-pressed={openId === p.id}
                className="flex w-full flex-col items-start rounded-md border border-border bg-raised px-3 py-2 text-left transition-colors hover:border-primary aria-pressed:border-primary"
              >
                <span className="text-sm font-medium text-content">{p.name}</span>
                <span className="text-xs text-faint">{p.root}</span>
              </button>
            </li>
          ))}
        </ul>
      )}

      {openId && (
        <LiveProjectPanel projectId={openId} root={openRoot} />
      )}
    </div>
  );
}

/**
 * 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 ?? [];

  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}
            />
          ))}
        </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,
}: {
  agent: AgentWorkState;
  open: boolean;
  onToggleOpen: () => void;
  onRefresh: () => Promise<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">
          <p className="text-[11px] font-semibold uppercase tracking-wide text-faint">Background tasks</p>
          <ul 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>
  );
}
