/**
 * View-model hook for the ticket AI assistant chat (ticket #8, F2).
 *
 * Owns a ticket-scoped assistant session: it lists the AI profiles to drive the
 * assistant, opens/closes the session via the {@link TicketGateway}, sends
 * messages and folds the streamed {@link ReplyChunk}s into a user↔assistant
 * transcript. The session is disposed on unmount (leaving the ticket) so no
 * assistant outlives its editor.
 *
 * The displayed ticket refreshes on assistant edits automatically: those edits
 * land as `Issue*` domain events which {@link useTicketDetail} already listens
 * to — this hook does not need to wire that.
 */

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

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

/** One rendered turn of the assistant transcript. */
export interface AssistantTurn {
  role: "user" | "assistant";
  text: string;
  /** True while an assistant turn is still streaming (no `final` yet). */
  pending?: boolean;
}

export interface TicketAssistantViewModel {
  /** AI profiles available to drive the assistant. */
  profiles: AgentProfile[];
  /** Live session id once opened, else `null`. */
  sessionId: string | null;
  /** The user↔assistant transcript, oldest first. */
  turns: AssistantTurn[];
  /** True while opening the session. */
  opening: boolean;
  /** True while an assistant turn is streaming. */
  busy: boolean;
  error: string | null;
  /** Opens an assistant session bound to this ticket, driven by `profileId`. */
  open: (profileId: string) => Promise<void>;
  /** Closes/disposes the session and clears the transcript. */
  close: () => Promise<void>;
  /** Sends a message and streams the assistant reply into the transcript. */
  send: (message: string) => Promise<void>;
}

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

export function useTicketAssistant(
  projectId: string,
  issueRef: string,
): TicketAssistantViewModel {
  const { ticket, profile } = useGateways();
  const [profiles, setProfiles] = useState<AgentProfile[]>([]);
  const [sessionId, setSessionId] = useState<string | null>(null);
  const [turns, setTurns] = useState<AssistantTurn[]>([]);
  const [opening, setOpening] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Mirror of `sessionId` for the unmount cleanup (which must not re-run when the
  // id changes — only dispose whatever is live at teardown).
  const sessionRef = useRef<string | null>(null);
  sessionRef.current = sessionId;

  useEffect(() => {
    let cancelled = false;
    void (async () => {
      try {
        const list = (await profile?.listProfiles()) ?? [];
        if (!cancelled) setProfiles(list);
      } catch (e) {
        if (!cancelled) setError(describe(e));
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [profile]);

  const open = useCallback(
    async (profileId: string) => {
      if (!profileId) return;
      setOpening(true);
      setError(null);
      try {
        const chat = await ticket.openTicketChat(projectId, issueRef, profileId);
        setSessionId(chat.sessionId);
        setTurns([]);
      } catch (e) {
        setError(describe(e));
      } finally {
        setOpening(false);
      }
    },
    [ticket, projectId, issueRef],
  );

  const close = useCallback(async () => {
    if (!sessionRef.current) return;
    setSessionId(null);
    setTurns([]);
    try {
      await ticket.closeTicketChat(projectId, issueRef);
    } catch (e) {
      setError(describe(e));
    }
  }, [ticket, projectId, issueRef]);

  const send = useCallback(
    async (message: string) => {
      const text = message.trim();
      if (!sessionId || !text || busy) return;
      setError(null);
      setBusy(true);
      // Append the user turn and an empty, pending assistant turn to stream into.
      setTurns((prev) => [
        ...prev,
        { role: "user", text },
        { role: "assistant", text: "", pending: true },
      ]);
      const applyChunk = (chunk: ReplyChunk) => {
        setTurns((prev) => {
          const next = [...prev];
          const i = next.length - 1;
          if (i < 0 || next[i].role !== "assistant") return prev;
          if (chunk.kind === "textDelta") {
            next[i] = { ...next[i], text: next[i].text + chunk.text };
          } else if (chunk.kind === "toolActivity") {
            // Best-effort inline activity badge.
            next[i] = { ...next[i], text: `${next[i].text}\n· ${chunk.label}` };
          } else if (chunk.kind === "error") {
            // Terminal fallback (ticket #60): the model returned no usable answer.
            // Freeze the turn with the readable explanation so the bubble is never
            // left empty and pending — the assistant no longer hangs silently.
            next[i] = {
              role: "assistant",
              text: `⚠️ ${chunk.message}`,
              pending: false,
            };
          } else {
            // `final`: freeze the turn with the aggregated content.
            next[i] = { role: "assistant", text: chunk.content, pending: false };
          }
          return next;
        });
        // `busy` tracks the REAL end-of-turn: a `final` or an `error` terminal —
        // never merely `agent_send` returning (the stream is still flowing then,
        // ticket #60). The input stays disabled until one of them arrives.
        if (chunk.kind === "final" || chunk.kind === "error") setBusy(false);
      };
      try {
        await ticket.sendTicketChat(sessionId, text, applyChunk);
      } catch (e) {
        // Transport failure: no terminal chunk will arrive, so close the pending
        // turn and drop `busy` here to guarantee the assistant never hangs.
        setError(describe(e));
        setTurns((prev) => {
          const next = [...prev];
          const i = next.length - 1;
          if (i >= 0 && next[i].role === "assistant" && next[i].pending) {
            next[i] = { ...next[i], pending: false };
          }
          return next;
        });
        setBusy(false);
      }
    },
    [ticket, sessionId, busy],
  );

  // Dispose the session when leaving the ticket (unmount). Fire-and-forget.
  useEffect(() => {
    return () => {
      if (sessionRef.current) {
        void ticket.closeTicketChat(projectId, issueRef).catch(() => {});
      }
    };
  }, [ticket, projectId, issueRef]);

  return {
    profiles,
    sessionId,
    turns,
    opening,
    busy,
    error,
    open,
    close,
    send,
  };
}
