/**
 * Custom agent CLI for structured/headless profiles (#147).
 *
 * This is an alternative human view for an agent cell, not a replacement for
 * the native TUI. It uses the structured chat commands when available and
 * deliberately does not try to parse PTY bytes.
 */

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

import type { AgentProfile, GatewayError, ReplyChunk } from "@/domain";
import { useGateways } from "@/app/di";
import { Button, Spinner, cn } from "@/shared";

export interface CustomAgentChatViewProps {
  projectId: string;
  agentId: string;
  agentName: string;
  profile: AgentProfile;
  cwd: string;
  nodeId: string;
  sessionId: string | null;
  conversationId: string | null;
  onSessionId: (sessionId: string | null) => void;
  onConversationId: (conversationId: string | null) => void;
}

type ChatTurn =
  | { role: "user"; text: string; attachment?: string }
  | { role: "agent"; text: string; pending?: boolean }
  | { role: "tool"; label: string }
  | { role: "final"; text: string }
  | { role: "error"; text: string }
  | { role: "unknown"; text: string };

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

function unknownChunkLabel(chunk: unknown): string {
  try {
    return JSON.stringify(chunk);
  } catch {
    return String(chunk);
  }
}

function isReplyRecord(chunk: unknown): chunk is Record<string, unknown> {
  return Boolean(chunk && typeof chunk === "object" && "kind" in chunk);
}

function appendAgentDelta(turns: ChatTurn[], text: string): ChatTurn[] {
  const next = [...turns];
  const last = next[next.length - 1];
  if (last?.role === "agent") {
    next[next.length - 1] = {
      role: "agent",
      text: last.text + text,
      pending: true,
    };
    return next;
  }
  next.push({ role: "agent", text, pending: true });
  return next;
}

function foldChunk(turns: ChatTurn[], raw: unknown): ChatTurn[] {
  if (!isReplyRecord(raw)) {
    return [...turns, { role: "unknown", text: unknownChunkLabel(raw) }];
  }
  switch (raw.kind) {
    case "textDelta":
      return appendAgentDelta(turns, String(raw.text ?? ""));
    case "toolActivity":
      return [...turns, { role: "tool", label: String(raw.label ?? "Activité") }];
    case "final": {
      const content = String(raw.content ?? "");
      const next = [...turns];
      const last = next[next.length - 1];
      if (last?.role === "agent") next[next.length - 1] = { ...last, pending: false };
      next.push({ role: "final", text: content });
      return next;
    }
    case "error": {
      const next = [...turns];
      const last = next[next.length - 1];
      if (last?.role === "agent") next[next.length - 1] = { ...last, pending: false };
      next.push({ role: "error", text: String(raw.message ?? "Erreur agent") });
      return next;
    }
    case "userPrompt":
    case "UserPrompt":
      return [...turns, { role: "user", text: String(raw.text ?? raw.prompt ?? "") }];
    default:
      return [...turns, { role: "unknown", text: unknownChunkLabel(raw) }];
  }
}

export function CustomAgentChatView({
  projectId,
  agentId,
  agentName,
  profile,
  cwd,
  nodeId,
  sessionId,
  conversationId,
  onSessionId,
  onConversationId,
}: CustomAgentChatViewProps) {
  const { agent, system } = useGateways();
  const [turns, setTurns] = useState<ChatTurn[]>([]);
  const [currentSession, setCurrentSession] = useState(sessionId);
  const [draft, setDraft] = useState("");
  const [attachment, setAttachment] = useState<string | null>(null);
  const [opening, setOpening] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const scrollRef = useRef<HTMLDivElement | null>(null);
  const sessionRef = useRef<string | null>(sessionId);
  sessionRef.current = currentSession;
  const onSessionIdRef = useRef(onSessionId);
  onSessionIdRef.current = onSessionId;
  const onConversationIdRef = useRef(onConversationId);
  onConversationIdRef.current = onConversationId;

  const supported = Boolean(
    profile.structuredAdapter &&
      agent.launchAgentChat &&
      agent.reattachAgentChat &&
      agent.sendAgentChat &&
      agent.cancelAgentChat &&
      agent.closeAgentChat,
  );

  useEffect(() => {
    const el = scrollRef.current;
    if (el) el.scrollTop = el.scrollHeight;
  }, [turns]);

  useEffect(() => {
    if (!supported) return;
    let cancelled = false;
    const receive = (chunk: ReplyChunk) => {
      setTurns((prev) => foldChunk(prev, chunk));
      if (chunk.kind === "final" || chunk.kind === "error") setBusy(false);
    };

    async function openOrAttach() {
      setOpening(true);
      setError(null);
      try {
        if (sessionId) {
          const reattached = await agent.reattachAgentChat!(sessionId, receive);
          if (cancelled) return;
          setCurrentSession(reattached.sessionId);
          setTurns(reattached.scrollback.reduce(foldChunk, [] as ChatTurn[]));
          return;
        }
        const launched = await agent.launchAgentChat!(projectId, agentId, {
          cwd,
          rows: 24,
          cols: 80,
          conversationId: conversationId ?? undefined,
          nodeId,
        });
        if (cancelled) return;
        setCurrentSession(launched.sessionId);
        onSessionIdRef.current(launched.sessionId);
        if (launched.assignedConversationId) {
          onConversationIdRef.current(launched.assignedConversationId);
        }
        // Attach the view so any in-flight chunks can be replayed after launch.
        await agent.reattachAgentChat!(launched.sessionId, receive).catch(() => {});
      } catch (e) {
        if (!cancelled) setError(describe(e));
      } finally {
        if (!cancelled) setOpening(false);
      }
    }

    void openOrAttach();
    return () => {
      cancelled = true;
    };
  }, [
    supported,
    agent,
    projectId,
    agentId,
    cwd,
    nodeId,
    sessionId,
    conversationId,
  ]);

  const canSend = useMemo(
    () =>
      supported &&
      Boolean(currentSession) &&
      Boolean(draft.trim()) &&
      !busy &&
      !opening,
    [supported, currentSession, draft, busy, opening],
  );

  async function pickAttachment() {
    const path = await system.pickFile();
    if (path) setAttachment(path);
  }

  async function send() {
    const text = draft.trim();
    if (!canSend || !currentSession || !agent.sendAgentChat) return;
    const prompt = attachment ? `${text}\n\n[Fichier joint: ${attachment}]` : text;
    setDraft("");
    setAttachment(null);
    setBusy(true);
    setError(null);
    setTurns((prev) => [...prev, { role: "user", text, attachment: attachment ?? undefined }]);
    try {
      await agent.sendAgentChat(currentSession, prompt, (chunk) => {
        setTurns((prev) => foldChunk(prev, chunk));
        if (chunk.kind === "final" || chunk.kind === "error") setBusy(false);
      });
    } catch (e) {
      setBusy(false);
      setError(describe(e));
      setTurns((prev) => [...prev, { role: "error", text: describe(e) }]);
    }
  }

  async function cancel() {
    const sid = sessionRef.current;
    if (!sid || !agent.cancelAgentChat) return;
    setBusy(false);
    setOpening(false);
    setError(null);
    try {
      await agent.cancelAgentChat(sid);
      setTurns((prev) => [...prev, { role: "tool", label: "Tour interrompu." }]);
    } catch (e) {
      setError(describe(e));
    }
  }

  return (
    <div
      data-testid="custom-agent-chat-view"
      className="flex h-full min-h-0 flex-col bg-surface text-content"
    >
      <div className="flex shrink-0 items-center justify-between gap-2 border-b border-border px-3 py-2">
        <div className="min-w-0">
          <div className="truncate text-sm font-medium">{agentName}</div>
          <div className="truncate text-xs text-muted">
            CLI custom · {profile.name}
          </div>
        </div>
        {(opening || busy) && (
          <Button size="sm" variant="danger" onClick={() => void cancel()}>
            Cancel
          </Button>
        )}
      </div>

      {!supported && (
        <p role="alert" className="m-3 rounded-md border border-warning/40 bg-warning/10 p-2 text-xs text-warning">
          CLI custom indisponible pour ce profil ou ce transport. Utilisez la TUI native.
        </p>
      )}
      {error && (
        <p role="alert" className="m-3 rounded-md border border-danger/40 bg-danger/10 p-2 text-xs text-danger">
          {error}
        </p>
      )}

      <div ref={scrollRef} className="flex min-h-0 flex-1 flex-col gap-2 overflow-auto px-3 py-3">
        {opening && turns.length === 0 ? (
          <div className="flex items-center gap-2 text-sm text-muted">
            <Spinner size={14} />
            <span>Ouverture de la session structurée…</span>
          </div>
        ) : turns.length === 0 ? (
          <p className="m-auto text-xs text-muted">
            Envoyez un message pour démarrer la conversation structurée.
          </p>
        ) : (
          turns.map((turn, index) => <ChatBubble key={index} turn={turn} />)
        )}
      </div>

      <div className="flex shrink-0 flex-col gap-2 border-t border-border bg-raised/40 p-2">
        {attachment && (
          <div className="flex items-center justify-between gap-2 rounded-md border border-border bg-surface px-2 py-1 text-xs text-muted">
            <span className="truncate">Fichier joint: {attachment}</span>
            <button type="button" className="text-content" onClick={() => setAttachment(null)}>
              Retirer
            </button>
          </div>
        )}
        <div className="flex items-end gap-2">
          <textarea
            aria-label={`message CLI custom ${nodeId}`}
            className={cn(
              "min-h-10 flex-1 resize-none rounded-md border border-border bg-surface p-2 text-sm text-content outline-none",
              "focus:border-primary disabled:cursor-not-allowed disabled:opacity-50",
            )}
            rows={2}
            value={draft}
            disabled={!supported || opening || busy}
            placeholder="Message à l'agent…"
            onChange={(e) => setDraft(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === "Enter" && !e.shiftKey) {
                e.preventDefault();
                void send();
              }
            }}
          />
          <Button size="sm" variant="ghost" disabled={!supported || opening || busy} onClick={() => void pickAttachment()}>
            Joindre
          </Button>
          <Button size="sm" disabled={!canSend} loading={busy} onClick={() => void send()}>
            Envoyer
          </Button>
        </div>
      </div>
    </div>
  );
}

function ChatBubble({ turn }: { turn: ChatTurn }) {
  if (turn.role === "tool") {
    return (
      <div className="flex justify-center">
        <span className="max-w-[80%] rounded-md border border-border bg-raised px-2 py-1 text-xs text-muted">
          {turn.label}
        </span>
      </div>
    );
  }
  if (turn.role === "final") {
    return (
      <div className="flex justify-end">
        <div className="max-w-[86%] rounded-md border border-success/50 bg-success/10 px-3 py-2 text-sm text-content">
          <div className="mb-1 text-xs font-semibold text-success">Task Complete</div>
          <p className="whitespace-pre-wrap break-words">{turn.text}</p>
        </div>
      </div>
    );
  }
  if (turn.role === "error" || turn.role === "unknown") {
    return (
      <div className="flex justify-center">
        <div className="max-w-[86%] rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-xs text-danger">
          {turn.role === "unknown" ? "Chunk inconnu reçu: " : ""}
          <span className="whitespace-pre-wrap break-words">{turn.text}</span>
        </div>
      </div>
    );
  }

  const user = turn.role === "user";
  return (
    <div className={cn("flex", user ? "justify-start" : "justify-end")}>
      <div
        className={cn(
          "max-w-[82%] rounded-lg border px-3 py-2 text-sm text-content",
          user ? "border-border bg-raised" : "border-primary/25 bg-primary/10",
        )}
      >
        <p className="whitespace-pre-wrap break-words">{turn.text}</p>
        {user && turn.attachment && (
          <p className="mt-1 truncate text-xs text-muted">Fichier: {turn.attachment}</p>
        )}
        {!user && turn.pending && (
          <span className="mt-1 inline-flex items-center gap-1 text-xs text-muted">
            <Spinner size={12} />
            Réponse en cours
          </span>
        )}
      </div>
    </div>
  );
}
