import { useCallback, useEffect, useRef, useState } from 'react';
import type { WsClient } from '../lib/ws-client';
import type { ChatMessage, ToolActivity, Attachment, StoredAttachment } from './useChat';
import { authFetch } from '../lib/auth';

/**
 * Chat hook for the standalone Bloby chat app.
 * Loads/persists messages via the DB (worker API).
 * Supports cross-device sync via chat:sync WS events.
 */
export function useBlobyChat(ws: WsClient | null, triggerReload?: number, enabled = true) {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [conversationId, setConversationId] = useState<string | null>(null);
  const [streaming, setStreaming] = useState(false);
  const [streamBuffer, setStreamBuffer] = useState('');
  const [tools, setTools] = useState<ToolActivity[]>([]);
  const [hasMore, setHasMore] = useState(false);
  const loaded = useRef(false);
  const conversationIdRef = useRef<string | null>(null);
  const streamingRef = useRef(false);
  const loadingOlder = useRef(false);
  /** Ref to current streamBuffer (avoids stale closures in sendMessage) */
  const streamBufferRef = useRef('');
  /** Length of text already committed as a partial message — strip from next bot:response */
  const committedTextLength = useRef(0);
  /** ID of the pending audio message waiting for transcription */
  const pendingAudioId = useRef<string | null>(null);
  /** Page context from Chrome extension (URL, title, etc.) */
  const extensionPageContext = useRef<any>(null);
  /** Whether we're running inside the Chrome extension */
  const isExtension = useRef(new URLSearchParams(location.search).has('ext'));

  // Keep refs in sync with state
  useEffect(() => { conversationIdRef.current = conversationId; }, [conversationId]);
  useEffect(() => { streamingRef.current = streaming; }, [streaming]);

  // Listen for page context from Chrome extension panel
  useEffect(() => {
    if (!isExtension.current) return;

    function handleExtMessage(event: MessageEvent) {
      if (event.data?.type === 'bloby:page-context') {
        extensionPageContext.current = event.data.context;
      }
    }

    window.addEventListener('message', handleExtMessage);
    // Request initial context
    window.parent?.postMessage({ type: 'bloby:request-context' }, '*');

    return () => window.removeEventListener('message', handleExtMessage);
  }, []);

  // Parse a raw DB message into a ChatMessage
  const parseMessage = useCallback((m: any): ChatMessage => {
    let audioData: string | undefined;
    if (m.audio_data) {
      if (m.audio_data.startsWith('data:')) {
        audioData = m.audio_data;
      } else if (m.audio_data.includes('/')) {
        audioData = `/api/files/${m.audio_data}`;
      } else {
        audioData = `data:audio/webm;base64,${m.audio_data}`;
      }
    }

    let attachments: StoredAttachment[] | undefined;
    if (m.attachments) {
      try { attachments = JSON.parse(m.attachments); } catch { /* ignore */ }
    }

    return {
      id: m.id,
      role: m.role,
      content: m.content,
      timestamp: m.created_at,
      audioData,
      hasAttachments: !!(attachments && attachments.length > 0),
      attachments,
    };
  }, []);

  // Load current conversation from DB (last 200 messages — pagination kicks in beyond that)
  const loadFromDb = useCallback(async () => {
    try {
      const ctx = await authFetch('/api/context/current').then((r) => r.json());
      if (!ctx.conversationId) return;
      setConversationId(ctx.conversationId);

      const limit = 200;
      const res = await authFetch(`/api/conversations/${ctx.conversationId}/messages?limit=${limit}`);
      if (!res.ok) return;
      const data = await res.json();
      if (!data?.length) return;

      const filtered = data.filter((m: any) => m.role === 'user' || m.role === 'assistant');
      setMessages(filtered.map(parseMessage));
      setHasMore(data.length >= limit);
    } catch { /* worker not ready yet */ }
  }, [parseMessage]);

  // Load older messages (cursor-based pagination — cursor is the rowid-equivalent on the server)
  const loadOlder = useCallback(async () => {
    if (loadingOlder.current || !conversationIdRef.current) return;
    loadingOlder.current = true;
    try {
      const oldestId = messages[0]?.id;
      if (!oldestId) return;
      const limit = 100;
      const res = await authFetch(`/api/conversations/${conversationIdRef.current}/messages?before=${oldestId}&limit=${limit}`);
      if (!res.ok) return;
      const data = await res.json();
      if (!data?.length) { setHasMore(false); return; }

      const filtered = data
        .filter((m: any) => m.role === 'user' || m.role === 'assistant')
        .map(parseMessage);
      setMessages((prev) => [...filtered, ...prev]);
      if (data.length < limit) setHasMore(false);
    } catch { /* ignore */ } finally {
      loadingOlder.current = false;
    }
  }, [messages, parseMessage]);

  // Load on mount (only when enabled/authenticated)
  useEffect(() => {
    if (!enabled || loaded.current) return;
    loaded.current = true;
    loadFromDb();
  }, [enabled, loadFromDb]);

  // Reload on reconnect (triggerReload changes)
  useEffect(() => {
    if (enabled && triggerReload && triggerReload > 0) {
      loadFromDb();
    }
  }, [enabled, triggerReload, loadFromDb]);

  // Periodic DB sync after reconnect while streaming — catches the final bot:response
  useEffect(() => {
    if (!enabled || !triggerReload || triggerReload === 0 || !streaming) return;
    const interval = setInterval(() => {
      if (!streamingRef.current) {
        clearInterval(interval);
        return;
      }
      loadFromDb();
    }, 3000);
    return () => clearInterval(interval);
  }, [enabled, triggerReload, streaming, loadFromDb]);

  useEffect(() => {
    if (!ws) return;


    const unsubs = [
      ws.on('bot:typing', () => {
        setStreaming(true);
        setTools([]);
      }),
      ws.on('bot:token', (data: { token: string }) => {
        // Auto-detect new turn — tokens arriving means agent is working
        setStreaming(true);
        setStreamBuffer((buf) => {
          const next = buf + data.token;
          streamBufferRef.current = next;
          return next;
        });
      }),
      ws.on('bot:tool', (data: { name: string; status?: string }) => {
        // Commit current stream as a bubble when agent pauses for tool use
        // This creates separate message bubbles and shows dots during work
        const content = streamBufferRef.current;
        if (content) {
          committedTextLength.current += content.length;
          setMessages((msgs) => [
            ...msgs,
            {
              id: 'tool-' + Date.now(),
              role: 'assistant' as const,
              content,
              timestamp: new Date().toISOString(),
            },
          ]);
          setStreamBuffer('');
          streamBufferRef.current = '';
          // streaming stays true → empty buffer = typing dots
        }
        setTools((prev) => {
          const existing = prev.find((t) => t.name === data.name && t.status === 'running');
          if (existing) return prev;
          return [...prev, { name: data.name, status: 'running' }];
        });
      }),
      ws.on('bot:response', (data: { conversationId: string; messageId?: string; content: string }) => {
        if (conversationIdRef.current && data.conversationId !== conversationIdRef.current) return;
        setConversationId(data.conversationId);

        // Strip text that was already committed as a partial message
        let content = data.content || '';
        if (committedTextLength.current > 0) {
          content = content.slice(committedTextLength.current).replace(/^\n+/, '');
          committedTextLength.current = 0;
        }

        // Only add a bubble if there's new content
        if (content.trim()) {
          setMessages((msgs) => [
            ...msgs,
            {
              id: data.messageId || Date.now().toString(),
              role: 'assistant',
              content,
              timestamp: new Date().toISOString(),
            },
          ]);
        }

        setStreamBuffer('');
        streamBufferRef.current = '';
        setTools([]);
        // Don't clear streaming — wait for bot:idle from server
      }),
      // Sub-agent spawned — commit current text as bubble, show dots while it works
      ws.on('bot:task-created', () => {
        const content = streamBufferRef.current;
        if (content) {
          committedTextLength.current += content.length;
          setMessages((msgs) => [
            ...msgs,
            {
              id: 'pre-task-' + Date.now(),
              role: 'assistant',
              content,
              timestamp: new Date().toISOString(),
            },
          ]);
          setStreamBuffer('');
          streamBufferRef.current = '';
        }
        // streaming stays true → empty buffer = typing dots
      }),
      ws.on('bot:idle', () => {
        // Server confirmed agent is idle — safe to stop streaming
        setStreaming(false);
      }),
      ws.on('bot:error', (data: { error: string }) => {
        setStreamBuffer('');
        streamBufferRef.current = '';
        setStreaming(false);
        setTools([]);
        setMessages((msgs) => [
          ...msgs,
          {
            id: Date.now().toString(),
            role: 'assistant',
            content: `Error: ${data.error}`,
            timestamp: new Date().toISOString(),
          },
        ]);
      }),
      // Cross-device sync: append message from another client
      ws.on('chat:sync', (data: { conversationId: string; message: { role: string; content: string; timestamp: string; attachments?: StoredAttachment[] } }) => {
        if (conversationIdRef.current && data.conversationId !== conversationIdRef.current) return;
        setMessages((msgs) => [
          ...msgs,
          {
            id: Date.now().toString(),
            role: data.message.role as 'user' | 'assistant',
            content: data.message.content,
            timestamp: data.message.timestamp,
            hasAttachments: !!(data.message.attachments?.length),
            attachments: data.message.attachments,
          },
        ]);
      }),
      // Server created a new conversation
      ws.on('chat:conversation-created', (data: { conversationId: string }) => {
        setConversationId(data.conversationId);
      }),
      // Server sends current streaming state on (re)connect
      ws.on('chat:state', (data: { streaming: boolean; conversationId?: string; buffer?: string }) => {
        if (data.streaming) {
          setStreaming(true);
          if (data.conversationId) setConversationId(data.conversationId);
          if (data.buffer) setStreamBuffer(data.buffer);
        }
      }),
      // Context cleared (from any client)
      ws.on('chat:cleared', () => {
        setMessages([]);
        setConversationId(null);
        setStreamBuffer('');
        setStreaming(false);
        setTools([]);
        loaded.current = false;
      }),
    ];

    return () => unsubs.forEach((u) => u());
  }, [ws]);

  /** Add a pending audio bubble immediately (before transcription finishes) */
  const addPendingAudio = useCallback((audioData: string) => {
    const id = 'pending-audio-' + Date.now();
    pendingAudioId.current = id;
    setMessages((msgs) => [
      ...msgs,
      {
        id,
        role: 'user',
        content: '',
        timestamp: new Date().toISOString(),
        audioData: audioData.startsWith('data:') ? audioData : `data:audio/webm;base64,${audioData}`,
        transcribing: true,
      },
    ]);
  }, []);

  const sendMessage = useCallback(
    (content: string, attachments?: Attachment[], audioData?: string) => {
      if (!ws || (!content.trim() && (!attachments || attachments.length === 0) && !audioData)) return;

      // If bot is streaming, commit partial response first
      const partialContent = streamBufferRef.current;
      if (partialContent) {
        committedTextLength.current += partialContent.length;
        setMessages((msgs) => [
          ...msgs,
          {
            id: 'partial-' + Date.now(),
            role: 'assistant' as const,
            content: partialContent,
            timestamp: new Date().toISOString(),
          },
        ]);
        setStreamBuffer('');
        streamBufferRef.current = '';
      }

      // If updating a pending audio message, update in-place instead of adding new
      const pendingId = pendingAudioId.current;
      if (audioData && pendingId) {
        pendingAudioId.current = null;
        setMessages((msgs) =>
          msgs.map((m) =>
            m.id === pendingId
              ? { ...m, content, transcribing: false, hasAttachments: !!(attachments && attachments.length > 0) }
              : m,
          ),
        );
      } else {
        // Normal: add new user message
        const userMsg: ChatMessage = {
          id: Date.now().toString(),
          role: 'user',
          content,
          timestamp: new Date().toISOString(),
          hasAttachments: !!(attachments && attachments.length > 0),
          audioData: audioData ? (audioData.startsWith('data:') ? audioData : `data:audio/webm;base64,${audioData}`) : undefined,
        };
        setMessages((msgs) => [...msgs, userMsg]);
      }

      // Prepend page context if running in Chrome extension
      let messageContent = content;
      if (isExtension.current && extensionPageContext.current) {
        const ctx = extensionPageContext.current;
        const parts = [`[Page: ${ctx.url}`];
        if (ctx.title) parts[0] += ` | ${ctx.title}`;
        if (ctx.productName) parts[0] += ` | ${ctx.productName}`;
        if (ctx.price) parts[0] += ` | ${ctx.currency || '$'}${ctx.price}`;
        parts[0] += ']';
        if (ctx.selection) parts.push(`[Selected: ${ctx.selection}]`);
        if (ctx.hasForms) parts.push(`[Page has ${ctx.formCount} form(s)]`);
        messageContent = parts.join('\n') + '\n' + content;

        // Request fresh context for next message
        window.parent?.postMessage({ type: 'bloby:request-context' }, '*');
      }

      const payload: any = { conversationId, content: messageContent };
      if (audioData) {
        payload.audioData = audioData.includes(',') ? audioData.split(',')[1] : audioData;
      }
      if (attachments?.length) {
        payload.attachments = attachments.map((att) => {
          const match = att.preview.match(/^data:([^;]+);base64,(.+)$/);
          return {
            type: att.type,
            name: att.name,
            mediaType: match?.[1] || 'application/octet-stream',
            data: match?.[2] || '',
          };
        });
      }
      ws.send('user:message', payload);
    },
    [ws, conversationId],
  );

  const stopStreaming = useCallback(() => {
    if (!ws) return;
    ws.send('user:stop', { conversationId });
    setStreaming(false);
    setStreamBuffer('');
    streamBufferRef.current = '';
    committedTextLength.current = 0;
    setTools([]);
  }, [ws, conversationId]);

  const clearContext = useCallback(() => {
    // Send clear to server (which broadcasts to all clients + clears Agent SDK session)
    if (ws) ws.send('user:clear-context', {});
    // Optimistic local clear
    setMessages([]);
    setConversationId(null);
    setStreamBuffer('');
    streamBufferRef.current = '';
    committedTextLength.current = 0;
    setStreaming(false);
    setTools([]);
    loaded.current = false;
  }, [ws]);

  return { messages, streaming, streamBuffer, conversationId, tools, hasMore, loadOlder, sendMessage, addPendingAudio, stopStreaming, clearContext };
}
