// frontend/src/lib/models/AgentChat.StreamProcessor.svelte.ts
// #region AgentChat.StreamProcessor [C:4] [TYPE Class] [SEMANTICS agent-chat,stream,metadata,events]
// @ingroup AgentChat
// @BRIEF Processes Gradio submit() stream events — parses event data into AgentMessage, dispatches metadata types (stream_token, tool_start/end/error, confirm_required/resolved, error), and provides SSE close watcher.
// @RELATION CALLED_BY -> [AgentChat.Model]
// @RATIONALE Extracted from AgentChat.Model per decomposition gate. Pure mutation logic that writes to a host interface (the main model's reactive atoms). No external dependencies beyond types and log — keeps the main model focused on orchestration.
import { type Client as GradioClient } from "@gradio/client";
import { log } from "$lib/cot-logger";
import type {
  StreamingState,
  StreamMetadata,
  ToolCall,
  AgentMessage,
} from "./AgentChatTypes.js";

// ── Host interface ──────────────────────────────────────────────

export interface StreamProcessorHost {
  streamingState: StreamingState;
  partialText: string;
  partialTokens: string[];
  activeToolCalls: ToolCall[];
  error: string | null;
  pendingThreadId: string | null;
  currentConversationId: string | null;
  captureConversationId(meta: StreamMetadata | undefined, convIdFromEvent: string): void;
}

// ═══ StreamProcessor ════════════════════════════════════════════

export class StreamProcessor {
  constructor(private host: StreamProcessorHost) {}

  /** Iterate Gradio submit() events and update host state. Returns true when stream completes. */
  async processStream(
    submission: ReturnType<GradioClient["submit"]>,
    convId: string | null,
  ): Promise<true> {
    for await (const event of submission) {
      if (event.type === "heartbeat") continue;

      const parsed = this._parseEvent(event, convId);
      this.host.captureConversationId(parsed.metadata, parsed.conversation_id);
      this._onStreamData(parsed);
    }
    return true;
  }

  /** Watch Client.stream_status.open — when SSE stream closes via close_stream(),
   *  return false so Promise.race terminates the iterator via .return().
   *
   *  stream_status starts as { open: false }. We wait for open=true (SSE opened),
   *  then open=false (SSE closed). Without this step the watcher fires immediately
   *  on the initial false. */
  async streamCloseWatcher(client: GradioClient | null, maxWaitMs: number): Promise<false> {
    const deadline = Date.now() + maxWaitMs;
    let wasOpen = false;
    while (Date.now() < deadline) {
      const ss = (client as any)?.stream_status;
      if (ss) {
        if (!wasOpen && ss.open === true) wasOpen = true;
        if (wasOpen && ss.open === false) return false;
      }
      await new Promise((r) => setTimeout(r, 100));
    }
    return false;
  }

  // ── Private — event parsing ─────────────────────────────────────

  private _parseEvent(event: any, convId: string | null): AgentMessage {
    const raw = event.data;

    if (Array.isArray(raw)) {
      const jsonStr = raw[0];
      if (typeof jsonStr === "string") {
        try {
          const obj = JSON.parse(jsonStr);
          return {
            id: "", conversation_id: convId ?? "", role: "assistant",
            text: obj.content ?? jsonStr, metadata: obj.metadata,
            toolCalls: [], created_at: "",
          };
        } catch {
          return {
            id: "", conversation_id: "", role: "assistant",
            text: jsonStr, toolCalls: [], created_at: "",
          };
        }
      } else {
        return {
          id: "", conversation_id: "", role: "assistant",
          text: jsonStr?.text ?? "", metadata: jsonStr?.metadata,
          toolCalls: [], created_at: "",
        };
      }
    } else if (typeof raw === "string") {
      try {
        const obj = JSON.parse(raw);
        return {
          id: "", conversation_id: convId ?? "", role: "assistant",
          text: obj.content ?? raw, metadata: obj.metadata,
          toolCalls: [], created_at: "",
        };
      } catch {
        return {
          id: "", conversation_id: "", role: "assistant",
          text: raw, toolCalls: [], created_at: "",
        };
      }
    } else {
      const obj = raw as Record<string, unknown>;
      return {
        id: "", conversation_id: convId ?? "", role: "assistant",
        text: (obj.content as string) ?? obj.text as string ?? "",
        metadata: obj.metadata as StreamMetadata,
        toolCalls: [], created_at: "",
      };
    }
  }

  /** Dispatch metadata-driven state mutations on the host. */
  private _onStreamData(msg: AgentMessage): void {
    const meta = msg.metadata;

    if (!meta || !meta.type) {
      // Plain text token — append to last assistant message
      this.host.partialText += msg.text;
      return;
    }

    switch (meta.type) {
      case "stream_token": {
        const token = meta.token ?? "";
        // Dedup: skip if token is already at the end of accumulated text
        if (token && this.host.partialText.endsWith(token)) break;
        this.host.partialText += token;
        this.host.partialTokens = [...this.host.partialTokens, token];
        break;
      }

      case "tool_start":
        this.host.activeToolCalls = [
          ...this.host.activeToolCalls,
          {
            tool: meta.tool ?? "unknown",
            input: meta.input ?? {},
            status: "executing",
          },
        ];
        break;

      case "tool_end":
        this.host.activeToolCalls = this.host.activeToolCalls.map((tc) =>
          tc.tool === meta.tool
            ? { ...tc, output: meta.output, status: "completed" as const }
            : tc,
        );
        break;

      case "tool_error":
        this.host.activeToolCalls = this.host.activeToolCalls.map((tc) =>
          tc.tool === meta.tool
            ? { ...tc, error: meta.error, status: "failed" as const }
            : tc,
        );
        break;

      case "confirm_required":
        this.host.streamingState = "awaiting_confirmation";
        this.host.pendingThreadId = meta.thread_id ?? null;
        break;

      case "confirm_resolved":
        this.host.streamingState = meta.result === "confirmed" ? "streaming" : "idle";
        this.host.pendingThreadId = null;
        break;

      case "error":
        this.host.streamingState = "error";
        this.host.error = meta.detail ?? meta.code ?? "Unknown error";
        break;

      default:
        log("AgentChat.StreamProcessor", "EXPLORE", "Unknown metadata type", { type: meta.type }, "Unhandled metadata type " + meta.type);
    }
  }
}
// #endregion AgentChat.StreamProcessor
