"use client";

import { useState, useCallback, useRef } from "react";
import { ThreadState, ThreadMessage, ThreadContext, OpenPanel } from "@/lib/types";
import { generateId } from "@/lib/utils";

// ─── Persistence helpers ─────────────────────────────────────────────────────
const STORAGE_KEY = "lb-threads";
const PANELS_KEY = "lb-open-panels";
const MAX_OPEN_PANELS = 6;
let _restored = false;

function serializeThreads(): string {
  const obj: Record<string, Record<string, ThreadState>> = {};
  for (const [channelId, threads] of threadStore) {
    const channelObj: Record<string, ThreadState> = {};
    for (const [threadId, state] of threads) {
      // Don't persist loading state
      channelObj[threadId] = { ...state, isLoading: false };
    }
    if (Object.keys(channelObj).length > 0) {
      obj[channelId] = channelObj;
    }
  }
  return JSON.stringify(obj);
}

function restoreThreads() {
  if (_restored) return;
  _restored = true;
  if (typeof window === "undefined") return; // SSR guard
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return;
    const obj = JSON.parse(raw) as Record<
      string,
      Record<string, ThreadState>
    >;
    for (const [channelId, threads] of Object.entries(obj)) {
      const map = new Map<string, ThreadState>();
      for (const [threadId, state] of Object.entries(threads)) {
        // Restore Date objects from JSON strings
        map.set(threadId, {
          ...state,
          messages: state.messages.map((m) => ({
            ...m,
            timestamp: new Date(m.timestamp),
          })),
        });
      }
      threadStore.set(channelId, map);
    }
  } catch {
    // corrupted — wipe it
    if (typeof window !== "undefined") localStorage.removeItem(STORAGE_KEY);
  }

  // Restore open panels
  try {
    const raw = localStorage.getItem(PANELS_KEY);
    if (raw) {
      const panels = JSON.parse(raw) as OpenPanel[];
      // Filter out panels whose thread data no longer exists
      openPanels = panels.filter((p) => {
        const channelThreads = threadStore.get(p.channelId);
        return channelThreads?.has(p.threadId) ?? false;
      });
      persistOpenPanels();
    }
  } catch {
    if (typeof window !== "undefined") localStorage.removeItem(PANELS_KEY);
  }
}

function persistThreads() {
  if (typeof window === "undefined") return; // SSR guard
  try {
    localStorage.setItem(STORAGE_KEY, serializeThreads());
  } catch {
    // storage full — silently fail
  }
}

function persistOpenPanels() {
  if (typeof window === "undefined") return;
  try {
    localStorage.setItem(PANELS_KEY, JSON.stringify(openPanels));
  } catch {
    // storage full — silently fail
  }
}

// ─── Module-level stores ─────────────────────────────────────────────────────
const threadStore = new Map<string, Map<string, ThreadState>>();
let openPanels: OpenPanel[] = [];
restoreThreads(); // load from localStorage on module init

function getChannelThreads(channelId: string): Map<string, ThreadState> {
  if (!threadStore.has(channelId)) {
    threadStore.set(channelId, new Map());
  }
  return threadStore.get(channelId)!;
}

// ─── Panel management (module-level) ─────────────────────────────────────────

function addOpenPanel(channelId: string, threadId: string): boolean {
  // Check if already open
  const exists = openPanels.some(
    (p) => p.channelId === channelId && p.threadId === threadId
  );
  if (exists) return true; // already open, that's fine

  if (openPanels.length >= MAX_OPEN_PANELS) return false; // at capacity

  openPanels = [...openPanels, { channelId, threadId }];
  persistOpenPanels();
  return true;
}

function removeOpenPanel(channelId: string, threadId: string) {
  openPanels = openPanels.filter(
    (p) => !(p.channelId === channelId && p.threadId === threadId)
  );
  persistOpenPanels();
}

function getOpenPanelThreads(): Array<{ channelId: string; thread: ThreadState }> {
  const result: Array<{ channelId: string; thread: ThreadState }> = [];
  for (const panel of openPanels) {
    const channelThreads = threadStore.get(panel.channelId);
    if (channelThreads) {
      const thread = channelThreads.get(panel.threadId);
      if (thread) {
        result.push({ channelId: panel.channelId, thread });
      }
    }
  }
  return result;
}

// ─── Hook ────────────────────────────────────────────────────────────────────

export interface UseThreadManagerReturn {
  threads: Map<string, ThreadState>;
  openPanels: OpenPanel[];
  openPanelThreads: Array<{ channelId: string; thread: ThreadState }>;
  canOpenMore: boolean;
  createThread: (
    appId: string,
    category: string,
    parentMessageId: string
  ) => string;
  openThread: (threadId: string) => void;
  closePanel: (channelId: string, threadId: string) => void;
  skipThread: (threadId: string) => void;
  sendMessage: (
    targetChannelId: string,
    threadId: string,
    content: string
  ) => Promise<void>;
  destroyThread: (threadId: string) => void;
  getThread: (threadId: string) => ThreadState | null;
  findThreadByAppId: (appId: string) => ThreadState | null;
  getThreadFromAnyChannel: (
    channelId: string,
    threadId: string
  ) => ThreadState | null;
}

export function useThreadManager(
  channelId: string
): UseThreadManagerReturn {
  const [, setVersion] = useState(0);
  const [, setPanelVersion] = useState(0);
  const bump = useCallback(() => setVersion((v) => v + 1), []);
  const bumpPanels = useCallback(() => setPanelVersion((v) => v + 1), []);
  const abortRefs = useRef<Map<string, AbortController>>(new Map());

  const threads = getChannelThreads(channelId);

  // ─── Find thread by appId ──────────────────────────────────────────────

  const findThreadByAppId = useCallback(
    (appId: string): ThreadState | null => {
      const channelThreads = getChannelThreads(channelId);
      for (const thread of channelThreads.values()) {
        if (thread.appId === appId) return thread;
      }
      return null;
    },
    [channelId]
  );

  // ─── Get thread from any channel ──────────────────────────────────────

  const getThreadFromAnyChannel = useCallback(
    (targetChannelId: string, threadId: string): ThreadState | null => {
      const channelThreads = getChannelThreads(targetChannelId);
      return channelThreads.get(threadId) ?? null;
    },
    []
  );

  // ─── Create a new thread ──────────────────────────────────────────────

  const createThread = useCallback(
    (appId: string, category: string, parentMessageId: string): string => {
      const threadId = generateId();
      const channelThreads = getChannelThreads(channelId);
      channelThreads.set(threadId, {
        threadId,
        appId,
        parentMessageId,
        category,
        messages: [],
        appData: null,
        appRefreshKey: 0,
        isLoading: false,
        skipped: false,
      });
      // Also open the panel
      addOpenPanel(channelId, threadId);
      bump();
      bumpPanels();
      persistThreads();
      return threadId;
    },
    [channelId, bump, bumpPanels]
  );

  // ─── Open existing thread (add to panels) ────────────────────────────

  const openThread = useCallback(
    (threadId: string) => {
      const channelThreads = getChannelThreads(channelId);
      const thread = channelThreads.get(threadId);
      if (thread?.appData) {
        if (thread.appId === "n8n-workflow-builder") {
          fetch("/api/workflow-ops", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(thread.appData),
          }).catch(() => {});
        } else {
          fetch("/api/app-data", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ appId: thread.appId, data: thread.appData }),
          }).catch(() => {});
        }
      }
      addOpenPanel(channelId, threadId);
      bumpPanels();
    },
    [channelId, bumpPanels]
  );

  // ─── Close panel (remove from open panels, keep thread data) ──────────

  const closePanel = useCallback(
    (panelChannelId: string, threadId: string) => {
      removeOpenPanel(panelChannelId, threadId);
      bumpPanels();
    },
    [bumpPanels]
  );

  // ─── Skip thread (load app with default data) ────────────────────────

  const skipThread = useCallback(
    (threadId: string) => {
      const channelThreads = getChannelThreads(channelId);
      const thread = channelThreads.get(threadId);
      if (!thread) return;
      channelThreads.set(threadId, {
        ...thread,
        skipped: true,
        appRefreshKey: thread.appRefreshKey + 1,
      });
      // Ensure panel is open
      addOpenPanel(channelId, threadId);
      bump();
      bumpPanels();
      persistThreads();
    },
    [channelId, bump, bumpPanels]
  );

  // ─── Get a specific thread ────────────────────────────────────────────

  const getThread = useCallback(
    (threadId: string): ThreadState | null => {
      return getChannelThreads(channelId).get(threadId) ?? null;
    },
    [channelId]
  );

  // ─── Send message in a thread (streaming, cross-channel aware) ────────

  const sendMessage = useCallback(
    async (targetChannelId: string, threadId: string, content: string) => {
      const channelThreads = getChannelThreads(targetChannelId);
      const thread = channelThreads.get(threadId);
      if (!thread || thread.isLoading || !content.trim()) return;

      // Abort any pending request for this thread
      const existingAbort = abortRefs.current.get(threadId);
      if (existingAbort) existingAbort.abort();

      const userMsg: ThreadMessage = {
        id: generateId(),
        role: "user",
        content: content.trim(),
        timestamp: new Date(),
      };

      const assistantMsg: ThreadMessage = {
        id: generateId(),
        role: "assistant",
        content: "",
        timestamp: new Date(),
      };

      // Update thread with new messages + loading
      channelThreads.set(threadId, {
        ...thread,
        messages: [...thread.messages, userMsg, assistantMsg],
        isLoading: true,
      });
      bump();

      const abortController = new AbortController();
      abortRefs.current.set(threadId, abortController);

      try {
        // Build API payload (all thread messages except the empty placeholder)
        const apiMessages = [...thread.messages, userMsg].map((m) => ({
          role: m.role,
          content: m.content,
        }));

        const threadContext: ThreadContext = {
          appId: thread.appId,
          category: thread.category,
          previousAppData: thread.appData,
        };

        const response = await fetch("/api/chat", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            messages: apiMessages,
            channelId: targetChannelId,
            threadContext,
          }),
          signal: abortController.signal,
        });

        if (!response.ok) {
          throw new Error(
            `HTTP ${response.status}: ${response.statusText}`
          );
        }

        const reader = response.body?.getReader();
        if (!reader) throw new Error("No response body");

        const decoder = new TextDecoder();
        let textContent = "";
        let parsedAppData: Record<string, unknown> | null = null;

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

          textContent += decoder.decode(value, { stream: true });

          let displayContent = textContent;
          let newAppData: Record<string, unknown> | null = null;

          // Parse hidden data blocks — strip from display but DON'T update app yet
          const appDataMatch = textContent.match(
            /<!--APP_DATA:([\s\S]*?):END_APP_DATA-->/
          );
          if (appDataMatch) {
            try {
              newAppData = JSON.parse(appDataMatch[1]) as Record<string, unknown>;
            } catch { /* ignore */ }
            displayContent = textContent
              .replace(/<!--APP_DATA:[\s\S]*?:END_APP_DATA-->/, "")
              .trim();
          } else {
            const appStartIdx = textContent.indexOf("<!--APP_DATA:");
            if (appStartIdx !== -1) {
              displayContent = textContent.substring(0, appStartIdx).trim();
            }
          }

          // Strip <!--WORKFLOW_JSON:{...}:END_WORKFLOW--> from display
          if (!newAppData) {
            const wfMatch = textContent.match(
              /<!--WORKFLOW_JSON:([\s\S]*?):END_WORKFLOW-->/
            );
            if (wfMatch) {
              try {
                newAppData = JSON.parse(wfMatch[1]) as Record<string, unknown>;
              } catch { /* ignore */ }
              displayContent = textContent
                .replace(/<!--WORKFLOW_JSON:[\s\S]*?:END_WORKFLOW-->/, "")
                .trim();
            } else {
              const wfStartIdx = textContent.indexOf("<!--WORKFLOW_JSON:");
              if (wfStartIdx !== -1 && !appDataMatch) {
                displayContent = textContent.substring(0, wfStartIdx).trim();
              }
            }
          }

          // Update thread messages (text only — no app data yet)
          const currentThread = channelThreads.get(threadId);
          if (currentThread) {
            const msgs = [...currentThread.messages];
            const lastMsg = msgs[msgs.length - 1];
            if (lastMsg && lastMsg.role === "assistant") {
              msgs[msgs.length - 1] = { ...lastMsg, content: displayContent };
            }
            channelThreads.set(threadId, { ...currentThread, messages: msgs });
            bump();
          }

          // Stash parsed data for end-of-stream update
          if (newAppData) {
            parsedAppData = newAppData;
          }
        }

        // ─── Stream complete: now update app data ONCE ─────────────
        if (parsedAppData) {
          const finalThread = channelThreads.get(threadId);
          if (finalThread) {
            channelThreads.set(threadId, {
              ...finalThread,
              appData: parsedAppData,
            });
            bump();
            persistThreads();

            // For workflow builder: POST to /api/workflow-ops
            if (finalThread.appId === "n8n-workflow-builder") {
              fetch("/api/workflow-ops", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify(parsedAppData),
              }).catch(() => {});

              const iframes = document.querySelectorAll("iframe");
              iframes.forEach((iframe) => {
                try {
                  iframe.contentWindow?.postMessage(
                    { type: "workflow_ops", payload: parsedAppData },
                    "*"
                  );
                } catch { /* cross-origin */ }
              });
            } else {
              fetch("/api/app-data", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                  appId: finalThread.appId,
                  data: parsedAppData,
                }),
              }).catch(() => {});

              const iframes = document.querySelectorAll("iframe");
              iframes.forEach((iframe) => {
                try {
                  iframe.contentWindow?.postMessage(
                    { type: "mcp_app_data", data: parsedAppData },
                    "*"
                  );
                } catch { /* cross-origin */ }
              });
            }
          }
        }
      } catch (error: unknown) {
        if (error instanceof Error && error.name === "AbortError") return;

        const errorMsg =
          error instanceof Error ? error.message : "An error occurred";
        const currentThread = channelThreads.get(threadId);
        if (currentThread) {
          const msgs = [...currentThread.messages];
          const lastMsg = msgs[msgs.length - 1];
          if (lastMsg && lastMsg.role === "assistant") {
            msgs[msgs.length - 1] = {
              ...lastMsg,
              content: `⚠️ Error: ${errorMsg}\n\nPlease try again.`,
            };
          }
          channelThreads.set(threadId, {
            ...currentThread,
            messages: msgs,
          });
          bump();
        }
      } finally {
        const currentThread = channelThreads.get(threadId);
        if (currentThread) {
          channelThreads.set(threadId, {
            ...currentThread,
            isLoading: false,
          });
          bump();
          persistThreads();
        }
        abortRefs.current.delete(threadId);
      }
    },
    [bump]
  );

  // ─── Destroy a thread (full cleanup) ─────────────────────────────────

  const destroyThread = useCallback(
    (threadId: string) => {
      const channelThreads = getChannelThreads(channelId);
      const thread = channelThreads.get(threadId);
      if (thread) {
        fetch(`/api/app-data?app=${thread.appId}`, {
          method: "DELETE",
        }).catch(() => {});
      }
      channelThreads.delete(threadId);
      // Also remove from open panels
      removeOpenPanel(channelId, threadId);
      bump();
      bumpPanels();
      persistThreads();
    },
    [channelId, bump, bumpPanels]
  );

  return {
    threads,
    openPanels,
    openPanelThreads: getOpenPanelThreads(),
    canOpenMore: openPanels.length < MAX_OPEN_PANELS,
    createThread,
    openThread,
    closePanel,
    skipThread,
    sendMessage,
    destroyThread,
    getThread,
    findThreadByAppId,
    getThreadFromAnyChannel,
  };
}
