import { writable } from "svelte/store";
import { CORTEX_SERVER_URL } from "$lib/constants.js";

export type AgentStreamEvent =
  | { _tag: "TextDelta"; text: string }
  | { _tag: "StreamCompleted"; output: string; metadata: Record<string, unknown>; toolSummary?: Array<{ name: string; calls: number; avgMs: number }> }
  | { _tag: "StreamError"; cause: string }
  | { _tag: "IterationProgress"; iteration: number; maxIterations: number; status: string }
  | { _tag: "ThoughtEmitted"; content: string; iteration: number }
  | { _tag: "StreamCancelled"; reason: string; iterationsCompleted: number }
  | Record<string, unknown>; // for other event types

export type ReasoningStep = {
  iteration: number;
  maxIterations: number;
  toolsCalledThisStep?: string[];
  thought?: string;
};

export type ChatTurn = {
  id: number;
  role: "user" | "assistant";
  content: string;
  tokensUsed: number;
  ts: number;
  toolsUsed?: string[];
  steps?: number;
  streaming?: boolean;
  streamProgress?: { iteration: number; maxIterations: number };
  reasoningSteps?: ReasoningStep[];
};

export type ChatSession = {
  sessionId: string;
  name: string;
  agentConfig: Record<string, unknown>;
  createdAt: number;
  lastUsedAt: number;
  turns?: ChatTurn[];
};

type ChatState = {
  sessions: ChatSession[];
  activeSessionId: string | null;
  activeTurns: ChatTurn[];
  sending: boolean;
  loadingSession: boolean;
  error: string | null;
};

type ChatSessionConfigInput = {
  provider?: string;
  model?: string;
  systemPrompt?: string;
  temperature?: number;
  maxTokens?: number;
  tools?: string[];
  runId?: string;
  enableTools?: boolean;
  streamReasoningSteps?: boolean;
  maxIterations?: number;
  strategy?: string;
  strategySwitching?: boolean;
  runtimeVerification?: boolean;
  verificationStep?: "none" | "reflect";
  contextSynthesis?: "auto" | "template" | "llm" | "none";
  guardrails?: {
    enabled?: boolean;
    injectionThreshold?: number;
    piiThreshold?: number;
    toxicityThreshold?: number;
  };
  persona?: {
    enabled?: boolean;
    role?: string;
    tone?: string;
    traits?: string;
    responseStyle?: string;
  };
  /** Merged onto default shell allowlist when `shell-execute` is allowed. */
  terminalShellAdditionalCommands?: string;
  /** Replaces default shell allowlist when non-empty (advanced). */
  terminalShellAllowedCommands?: string;
};

function mergeThoughtText(previous: string | undefined, incoming: string): string {
  const prev = (previous ?? "").trim();
  const next = incoming.trim();

  if (!prev) return next;
  if (!next) return prev;
  if (next.startsWith(prev) || prev === next) return next;
  if (prev.endsWith(next)) return prev;

  return `${prev}\n${next}`;
}

function appendThoughtDelta(previous: string | undefined, delta: string): string {
  return `${previous ?? ""}${delta}`;
}

function createChatStore() {
  const { subscribe, update } = writable<ChatState>({
    sessions: [],
    activeSessionId: null,
    activeTurns: [],
    sending: false,
    loadingSession: false,
    error: null,
  });

  async function loadSessions() {
    const res = await fetch(`${CORTEX_SERVER_URL}/api/chat/sessions`);
    const sessions = (await res.json()) as ChatSession[];
    update((s) => ({ ...s, sessions }));
    return sessions;
  }

  async function selectSession(sessionId: string) {
    update((s) => ({ ...s, activeSessionId: sessionId, loadingSession: true, error: null }));
    const res = await fetch(`${CORTEX_SERVER_URL}/api/chat/sessions/${encodeURIComponent(sessionId)}`);
    if (!res.ok) {
      update((s) => ({ ...s, loadingSession: false, error: "Session not found" }));
      return;
    }
    const session = (await res.json()) as ChatSession & { turns: ChatTurn[] };
    update((s) => ({ ...s, activeTurns: session.turns, loadingSession: false }));
  }

  async function createSession(opts: { name?: string } & ChatSessionConfigInput): Promise<string> {
    const res = await fetch(`${CORTEX_SERVER_URL}/api/chat/sessions`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(opts),
    });
    const { sessionId } = (await res.json()) as { sessionId: string };
    await loadSessions();
    await selectSession(sessionId);
    return sessionId;
  }

  async function updateSessionConfig(sessionId: string, config: ChatSessionConfigInput): Promise<void> {
    const res = await fetch(
      `${CORTEX_SERVER_URL}/api/chat/sessions/${encodeURIComponent(sessionId)}/config`,
      {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(config),
      },
    );
    if (!res.ok) {
      const body = (await res.json().catch(() => ({ error: "Failed to update session config" }))) as {
        error?: string;
      };
      throw new Error(body.error ?? "Failed to update session config");
    }
    await loadSessions();
    await selectSession(sessionId);
  }

  async function deleteSession(sessionId: string) {
    await fetch(`${CORTEX_SERVER_URL}/api/chat/sessions/${encodeURIComponent(sessionId)}`, {
      method: "DELETE",
    });
    await loadSessions();
    update((s) => ({
      ...s,
      activeSessionId: s.activeSessionId === sessionId ? null : s.activeSessionId,
      activeTurns: s.activeSessionId === sessionId ? [] : s.activeTurns,
    }));
  }

  async function renameSession(sessionId: string, newName: string): Promise<void> {
    const res = await fetch(`${CORTEX_SERVER_URL}/api/chat/sessions/${encodeURIComponent(sessionId)}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name: newName }),
    });
    if (!res.ok) {
      throw new Error("Failed to rename session");
    }
    await loadSessions();
  }

  async function sendMessage(message: string): Promise<void> {
    let sessionId: string | null = null;
    update((s) => {
      sessionId = s.activeSessionId;
      return s;
    });
    if (!sessionId) return;

    const optimisticTurn: ChatTurn = {
      id: Date.now(),
      role: "user",
      content: message,
      tokensUsed: 0,
      ts: Date.now(),
    };
    update((s) => ({
      ...s,
      activeTurns: [...s.activeTurns, optimisticTurn],
      sending: true,
      error: null,
    }));

    const res = await fetch(
      `${CORTEX_SERVER_URL}/api/chat/sessions/${encodeURIComponent(sessionId!)}/chat`,
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ message }),
      },
    );

    if (!res.ok) {
      const body = (await res.json()) as { error: string };
      update((s) => ({
        ...s,
        sending: false,
        error: body.error ?? "Request failed",
        activeTurns: s.activeTurns.filter((t) => t.id !== optimisticTurn.id),
      }));
      return;
    }

    const payload = (await res.json()) as {
      reply: string;
      tokensUsed: number;
      toolsUsed?: string[];
      steps?: number;
      cost?: number;
    };
    const assistantTurn: ChatTurn = {
      id: Date.now() + 1,
      role: "assistant",
      content: payload.reply,
      tokensUsed: payload.tokensUsed,
      ts: Date.now(),
      ...(payload.toolsUsed && payload.toolsUsed.length > 0 ? { toolsUsed: payload.toolsUsed } : {}),
      ...(payload.steps != null ? { steps: payload.steps } : {}),
    };
    update((s) => ({ ...s, activeTurns: [...s.activeTurns, assistantTurn], sending: false }));
  }

  async function sendMessageStream(message: string): Promise<void> {
    let sessionId: string | null = null;
    update((s) => {
      sessionId = s.activeSessionId;
      return s;
    });
    if (!sessionId) return;

    const optimisticUserTurn: ChatTurn = {
      id: Date.now(),
      role: "user",
      content: message,
      tokensUsed: 0,
      ts: Date.now(),
    };

    const assistantTurnId = Date.now() + 1;
    const assistantTurn: ChatTurn = {
      id: assistantTurnId,
      role: "assistant",
      content: "",
      tokensUsed: 0,
      ts: Date.now(),
      streaming: true,
    };

    update((s) => ({
      ...s,
      activeTurns: [...s.activeTurns, optimisticUserTurn, assistantTurn],
      sending: true,
      error: null,
    }));

    try {
      const url = `${CORTEX_SERVER_URL}/api/chat/sessions/${encodeURIComponent(sessionId!)}/chat/stream`;

      const res = await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ message }),
      });

      if (!res.ok) {
        let errorMsg = "Request failed";
        try {
          const body = (await res.json()) as { error: string };
          errorMsg = body.error ?? errorMsg;
        } catch {
          errorMsg = `HTTP ${res.status}`;
        }
        update((s) => ({
          ...s,
          sending: false,
          error: errorMsg,
          activeTurns: s.activeTurns.filter((t) => t.id !== optimisticUserTurn.id && t.id !== assistantTurnId),
        }));
        return;
      }

      if (!res.body) {
        update((s) => ({
          ...s,
          sending: false,
          error: "No response body",
          activeTurns: s.activeTurns.filter((t) => t.id !== optimisticUserTurn.id && t.id !== assistantTurnId),
        }));
        return;
      }

      const reader = res.body?.getReader();
      if (!reader) {
        update((s) => ({
          ...s,
          sending: false,
          error: "No response body",
          activeTurns: s.activeTurns.filter((t) => t.id !== optimisticUserTurn.id && t.id !== assistantTurnId),
        }));
        return;
      }

      const decoder = new TextDecoder();
      let toolsUsed: string[] = [];
      let tokensUsed = 0;
      let steps = 0;
      let buffer = "";
      let eventCount = 0;

      while (true) {
        const { done, value } = await reader.read();
        if (done) {
          buffer += decoder.decode();
          break;
        }

        const chunk = decoder.decode(value, { stream: true });
        buffer += chunk;

        // Process complete SSE messages (separated by \n\n)
        const parts = buffer.split("\n\n");
        // Keep the last part in buffer in case it's incomplete
        buffer = parts[parts.length - 1];

        for (let i = 0; i < parts.length - 1; i++) {
          const message = parts[i].trim();
          if (!message) continue;

          const lines = message.split("\n");
          for (const line of lines) {
            if (line.startsWith("data: ")) {
              const jsonStr = line.slice(6).trim();
              if (!jsonStr) continue;

              try {
                const event = JSON.parse(jsonStr) as AgentStreamEvent;
                eventCount++;

                if (event._tag === "TextDelta") {
                  update((s) => ({
                    ...s,
                    activeTurns: s.activeTurns.map((t) =>
                      t.id === assistantTurnId
                        ? (() => {
                            const existing = t.reasoningSteps ?? [];
                            if (existing.length === 0) {
                              return { ...t, content: t.content + event.text };
                            }

                            const currentIteration =
                              t.streamProgress?.iteration ??
                              existing[existing.length - 1]?.iteration ??
                              1;
                            const idx = existing.findIndex((r) => r.iteration === currentIteration);

                            if (idx >= 0) {
                              const prev = existing[idx]!;
                              const next: ReasoningStep = {
                                ...prev,
                                thought: appendThoughtDelta(prev.thought, event.text),
                              };
                              return {
                                ...t,
                                reasoningSteps: existing.map((r, i) => (i === idx ? next : r)),
                              };
                            }

                            return {
                              ...t,
                              reasoningSteps: [
                                ...existing,
                                {
                                  iteration: currentIteration,
                                  maxIterations: t.streamProgress?.maxIterations ?? 0,
                                  thought: appendThoughtDelta(undefined, event.text),
                                },
                              ],
                            };
                          })()
                        : t,
                    ),
                  }));
                } else if (event._tag === "IterationProgress") {
                  const iter = (event as any).iteration as number;
                  const max = (event as any).maxIterations as number;
                  const tools = (event as any).toolsCalledThisStep as string[] | undefined;
                  const step: ReasoningStep = {
                    iteration: iter,
                    maxIterations: max,
                    ...(tools && tools.length > 0 ? { toolsCalledThisStep: tools } : {}),
                  };
                  update((s) => ({
                    ...s,
                    activeTurns: s.activeTurns.map((t) => {
                      if (t.id !== assistantTurnId) return t;
                      const existing = t.reasoningSteps ?? [];
                      const idx = existing.findIndex((r) => r.iteration === iter);
                      const steps = idx >= 0
                        ? existing.map((r, i) =>
                            i === idx
                              ? {
                                  ...step,
                                  ...(r.thought && r.thought.trim().length > 0 ? { thought: r.thought } : {}),
                                }
                              : r,
                          )
                        : [...existing, step];
                      return { ...t, streamProgress: { iteration: iter, maxIterations: max }, reasoningSteps: steps };
                    }),
                  }));
                } else if (event._tag === "ThoughtEmitted") {
                  const iter = (event as any).iteration as number;
                  const thought = (event as any).content as string;
                  update((s) => ({
                    ...s,
                    activeTurns: s.activeTurns.map((t) => {
                      if (t.id !== assistantTurnId) return t;
                      const existing = t.reasoningSteps ?? [];
                      const idx = existing.findIndex((r) => r.iteration === iter);
                      if (idx >= 0) {
                        const prev = existing[idx]!;
                        const next: ReasoningStep = {
                          ...prev,
                          thought: mergeThoughtText(prev.thought, thought),
                        };
                        return {
                          ...t,
                          reasoningSteps: existing.map((r, i) => (i === idx ? next : r)),
                        };
                      }
                      return {
                        ...t,
                        reasoningSteps: [...existing, { iteration: iter, maxIterations: 0, thought }],
                      };
                    }),
                  }));
                } else if (event._tag === "StreamCompleted") {
                  tokensUsed = (event.metadata?.tokensUsed as number) ?? 0;
                  steps =
                    (event.metadata?.iterations as number | undefined) ??
                    (event.metadata?.stepsCount as number | undefined) ??
                    0;
                  if (event.toolSummary && event.toolSummary.length > 0) {
                    toolsUsed = event.toolSummary.map((t) => t.name);
                  }
                  // StreamCompleted.output is authoritative final output after verification/retries.
                  const output = (event as any).output as string | undefined;
                  if (output?.trim()) {
                    update((s) => {
                      return {
                        ...s,
                        activeTurns: s.activeTurns.map((t) =>
                          t.id === assistantTurnId ? { ...t, content: output } : t,
                        ),
                      };
                    });
                  }
                } else if (event._tag === "StreamError") {
                  // Handle stream errors
                  console.error("[Chat Stream] Stream error:", (event as any).cause);
                  update((s) => ({
                    ...s,
                    error: (event as any).cause ?? "Stream error",
                  }));
                }
              } catch (e) {
                console.error("[Chat Stream] Parse error:", e, "jsonStr:", jsonStr.slice(0, 100));
              }
            }
          }
        }
      }

      update((s) => ({
        ...s,
        sending: false,
        activeTurns: s.activeTurns.map((t) =>
          t.id === assistantTurnId
            ? {
                ...t,
                streaming: false,
                streamProgress: undefined,
                // reasoningSteps intentionally kept for post-stream inspection
                tokensUsed,
                ...(steps > 0 ? { steps } : {}),
                ...(toolsUsed.length > 0 ? { toolsUsed } : {}),
              }
            : t,
        ),
      }));
    } catch (e) {
      const errorMsg = String(e);
      console.error("[Chat Stream] Exception:", errorMsg, e);
      update((s) => ({
        ...s,
        sending: false,
        error: errorMsg,
        activeTurns: s.activeTurns.filter((t) => t.id !== optimisticUserTurn.id && t.id !== assistantTurnId),
      }));
    }
  }

  return {
    subscribe,
    loadSessions,
    selectSession,
    createSession,
    updateSessionConfig,
    deleteSession,
    renameSession,
    sendMessage,
    sendMessageStream,
  };
}

export const chatStore = createChatStore();
