// ==========================================
// ChatPane Kit — useChat Hook
// ==========================================
// Core chat state machine: messages, channel CRUD, send/receive,
// model selection. Independently importable.
//
// v0.37.10: Added regenerate(), deleteMessage(), pagination.
//
// Usage:
//   const chat = useChat({ onChannelChange });
//   chat.sendMessage('Hello');

import { useStream } from './use-stream.js';

const { useState, useRef, useCallback, useEffect } = window.hooks;

const PAGE_SIZE = 50;

/** Extract <think>…</think> blocks from raw content.
 *  Returns { content, thinking } with tags stripped. */
function extractThinking(raw) {
    if (!raw || !raw.includes('<think>')) return { content: raw, thinking: '' };
    let thinking = '';
    const content = raw.replace(/<think>([\s\S]*?)<\/think>/g, (_, t) => {
        thinking += (thinking ? '\n' : '') + t.trim();
        return '';
    });
    return { content: content.trim(), thinking };
}

/** Apply extractThinking to an array of message objects. */
function hydrateThinking(msgs) {
    return msgs.map(m => {
        const { content, thinking } = extractThinking(m.content);
        return thinking ? { ...m, content, thinking } : m;
    });
}

/**
 * @param {{ initialChannelId?: string, getContext?: () => {path: string, content: string}|null, onChannelChange?: (id: string) => void }} opts
 */
export function useChat(opts = {}) {
    const { initialChannelId, getContext, onChannelChange, externalModel } = opts;

    const [messages, setMessages] = useState([]);
    const [channelId, setChannelId] = useState(initialChannelId || null);
    const [sending, setSending] = useState(false);
    const [error, setError] = useState(null);
    const [model, setModel] = useState('');
    const [hasMore, setHasMore] = useState(false);
    const [loadingMore, setLoadingMore] = useState(false);

    const channelRef = useRef(channelId);
    const pageRef = useRef(1);
    const { streamContent, streamThinking, streaming, startStream, stopStream } = useStream();

    // Keep ref in sync
    channelRef.current = channelId;

    // Use external model (from parent surface) when provided
    const effectiveModel = externalModel || model;
    const modelRef = useRef(effectiveModel);
    modelRef.current = effectiveModel;

    // ── Init model from session or first enabled ─────────────
    if (!model && window.sw?.auth?.session?.settings?.model) {
        setTimeout(() => setModel(window.sw.auth.session.settings.model), 0);
    }
    // Auto-load first enabled model if none set
    const modelInitRef = useRef(false);
    if (!effectiveModel && !modelInitRef.current && window.sw?.api?.models?.enabled) {
        modelInitRef.current = true;
        window.sw.api.models.enabled().then(resp => {
            const list = resp.data || [];
            const visible = list.filter(m => !m.hidden);
            if (visible.length && !model && !externalModel) {
                const first = visible[0];
                setModel(first.isPersona ? (first.personaId || first.id) : first.id);
            }
        }).catch(() => {});
    }

    // ── Send Message ────────────────────────
    const sendMessage = useCallback(async (text) => {
        if (!text.trim() || sending) return;
        setSending(true);
        setError(null);

        // Resolve model: use current ref value, or fetch first enabled model on the fly
        let resolvedModel = modelRef.current;
        if (!resolvedModel && window.sw?.api?.models?.enabled) {
            try {
                const resp = await window.sw.api.models.enabled();
                const list = (resp.data || []).filter(m => !m.hidden);
                if (list.length) {
                    const first = list[0];
                    resolvedModel = first.isPersona ? (first.personaId || first.id) : first.id;
                    setModel(resolvedModel);
                }
            } catch (_) {}
        }

        let cid = channelRef.current;

        // Create channel if needed
        if (!cid) {
            try {
                const title = text.slice(0, 50) + (text.length > 50 ? '\u2026' : '');
                const resp = await window.sw.api.channels.create({ title, model_id: resolvedModel, channel_type: 'direct' });
                cid = resp.id;
                channelRef.current = cid;
                setChannelId(cid);
                if (onChannelChange) onChannelChange(cid);
            } catch (e) {
                setError('Failed to create chat: ' + e.message);
                setSending(false);
                return;
            }
        }

        // Append user message optimistically
        const myId = window.sw?.auth?.user?.id;
        const userMsg = { id: 'tmp-' + Date.now(), role: 'user', content: text, user_id: myId };
        setMessages(prev => [...prev, userMsg]);

        try {
            // Build content with optional context injection
            let content = text;
            if (getContext) {
                const ctx = getContext();
                if (ctx) {
                    content = text + '\n\n[Context: Currently editing ' + ctx.path + ']\n```\n' + ctx.content + '\n```';
                }
            }

            // Run pipe pre-send filters (extensions can transform or block)
            if (window.sw?.pipe?._runPre) {
                const pipeCtx = window.sw.pipe._runPre({ content, channel: { id: cid }, model: resolvedModel });
                if (pipeCtx === null) { setSending(false); return; }
                content = pipeCtx.content;
            }

            const data = { channel_id: cid, content, model: resolvedModel };
            const resp = await window.sw.api.channels.complete(data);
            // ai_mode off/mention_only returns JSON, not SSE — skip streaming
            const ct = resp.headers.get('content-type') || '';
            if (ct.includes('application/json')) {
                // Message persisted server-side, no AI response expected
            } else {
                await startStream(resp);
            }
        } catch (e) {
            if (e.name !== 'AbortError') {
                setError('Error: ' + e.message);
            }
        }

        setSending(false);
    }, [sending, effectiveModel, getContext, onChannelChange, startStream]);

    // Finalize: when streaming ends, capture the final content into messages
    const prevStreamingRef = useRef(false);
    if (prevStreamingRef.current && !streaming && streamContent) {
        prevStreamingRef.current = false;
        const msg = { role: 'assistant', content: streamContent };
        if (streamThinking) msg.thinking = streamThinking;
        setMessages(prev => [...prev, msg]);
    }
    prevStreamingRef.current = streaming;

    // ── Regenerate ──────────────────────────
    const regenerate = useCallback(async (msgId) => {
        const cid = channelRef.current;
        if (!cid || !msgId || sending) return;

        setSending(true);
        setError(null);

        try {
            const resp = await window.sw.api.channels.regenerate(cid, msgId, { model: modelRef.current });
            // Remove the old assistant message being regenerated
            setMessages(prev => {
                const idx = prev.findIndex(m => m.id === msgId);
                return idx >= 0 ? prev.slice(0, idx) : prev;
            });
            await startStream(resp);
        } catch (e) {
            if (e.name !== 'AbortError') {
                setError('Regenerate failed: ' + e.message);
            }
        }

        setSending(false);
    }, [sending, effectiveModel, startStream]);

    // ── Delete Message ──────────────────────
    const deleteMessage = useCallback(async (msgId) => {
        // Optimistic remove from UI
        setMessages(prev => prev.filter(m => m.id !== msgId));

        // If API supports delete, fire it (no-op if not available)
        if (window.sw?.api?.channels?.deleteMessage) {
            try {
                await window.sw.api.channels.deleteMessage(channelRef.current, msgId);
            } catch (_) { /* swallow — already removed from UI */ }
        }
    }, []);

    // ── Switch Channel ──────────────────────
    const switchChannel = useCallback(async (id) => {
        setMessages([]);
        setError(null);
        setHasMore(false);
        pageRef.current = 1;
        channelRef.current = id;
        setChannelId(id);
        if (onChannelChange) onChannelChange(id);

        if (!id) return;

        try {
            const resp = await window.sw.api.channels.messages(id, { page: 1, per_page: PAGE_SIZE });
            const data = hydrateThinking(resp.data || resp || []);
            setMessages(data);
            setHasMore(data.length >= PAGE_SIZE);
        } catch (e) {
            // Channel deleted/not found — clear selection
            if ((e.status === 404 || e.message?.includes('404')) && onChannelChange) {
                channelRef.current = null;
                setChannelId(null);
                onChannelChange(null);
                return;
            }
            setError('Failed to load messages: ' + e.message);
        }

        // Mark channel as read
        if (window.sw?.api?.channels?.markRead) {
            window.sw.api.channels.markRead(id).catch(() => {});
        }
    }, [onChannelChange]);

    // ── Load More (pagination) ──────────────
    const loadMore = useCallback(async () => {
        const cid = channelRef.current;
        if (!cid || loadingMore || !hasMore) return;

        setLoadingMore(true);
        const nextPage = pageRef.current + 1;

        try {
            const resp = await window.sw.api.channels.messages(cid, { page: nextPage, per_page: PAGE_SIZE });
            const data = hydrateThinking(resp.data || resp || []);
            if (data.length > 0) {
                pageRef.current = nextPage;
                // Prepend older messages
                setMessages(prev => [...data, ...prev]);
                setHasMore(data.length >= PAGE_SIZE);
            } else {
                setHasMore(false);
            }
        } catch (e) {
            setError('Failed to load older messages: ' + e.message);
        }

        setLoadingMore(false);
    }, [loadingMore, hasMore]);

    // ── New Chat ────────────────────────────
    const newChat = useCallback(() => {
        stopStream();
        setMessages([]);
        setError(null);
        setHasMore(false);
        pageRef.current = 1;
        channelRef.current = null;
        setChannelId(null);
        if (onChannelChange) onChannelChange(null);
    }, [stopStream, onChannelChange]);

    // ── Stop ────────────────────────────────
    const stop = useCallback(() => {
        stopStream();
        setSending(false);
    }, [stopStream]);

    // ── Clear Error ─────────────────────────
    const clearError = useCallback(() => setError(null), []);

    // ── Typing indicators ───────────────────
    const [typingUsers, setTypingUsers] = useState([]); // [{ id, name }]
    const typingTimers = useRef({}); // id → timeout

    // ── WebSocket: listen for messages from other users ──
    useEffect(() => {
        if (!window.sw?.on) return;
        const handler = (evt) => {
            try {
                const data = typeof evt.payload === 'string' ? JSON.parse(evt.payload) : (evt.payload || evt);
                // Only append if it's for the active channel
                if (data.channel_id !== channelRef.current) return;
                // Skip messages from self (already added optimistically)
                const myId = window.sw?.auth?.user?.id;
                if (data.user_id && data.user_id === myId) return;
                const { content: cleanContent, thinking } = extractThinking(data.content);
                const msg = {
                    id: data.id,
                    role: data.role || 'user',
                    content: cleanContent,
                    user_id: data.user_id,
                    participant_id: data.participant_id || data.user_id,
                    participant_type: data.participant_type || 'user',
                    display_name: data.display_name,
                    created_at: data.created_at || new Date().toISOString(),
                };
                if (thinking) msg.thinking = thinking;
                setMessages(prev => prev.some(m => m.id && m.id === msg.id) ? prev : [...prev, msg]);
                // Clear typing for this user/persona when their message arrives
                const pid = data.participant_id || data.user_id;
                if (pid) _clearTyping(pid);
            } catch (_) {}
        };

        // Typing start handler (persona chaining + user typing)
        const typingStartHandler = (evt) => {
            try {
                const data = typeof evt.payload === 'string' ? JSON.parse(evt.payload) : (evt.payload || evt);
                if (data.channel_id !== channelRef.current) return;
                const pid = data.participant_id || data.user_id;
                if (!pid) return;
                const myId = window.sw?.auth?.user?.id;
                if (pid === myId) return;
                const name = data.display_name || pid;
                setTypingUsers(prev => {
                    if (prev.some(t => t.id === pid)) return prev;
                    return [...prev, { id: pid, name }];
                });
                // Auto-clear after 10s (safety net)
                if (typingTimers.current[pid]) clearTimeout(typingTimers.current[pid]);
                typingTimers.current[pid] = setTimeout(() => _clearTyping(pid), 10000);
            } catch (_) {}
        };

        // Typing stop handler
        const typingStopHandler = (evt) => {
            try {
                const data = typeof evt.payload === 'string' ? JSON.parse(evt.payload) : (evt.payload || evt);
                const pid = data.participant_id || data.user_id;
                if (pid) _clearTyping(pid);
            } catch (_) {}
        };

        // message.deleted handler (other clients' deletes)
        const deleteHandler = (evt) => {
            try {
                const data = typeof evt.payload === 'string' ? JSON.parse(evt.payload) : (evt.payload || evt);
                if (data.channel_id !== channelRef.current) return;
                setMessages(prev => prev.filter(m => m.id !== data.id));
            } catch (_) {}
        };

        window.sw.on('message.created', handler);
        window.sw.on('message.deleted', deleteHandler);
        window.sw.on('typing.start', typingStartHandler);
        window.sw.on('typing.stop', typingStopHandler);
        window.sw.on('chat.typing.start', typingStartHandler);
        window.sw.on('chat.typing.stop', typingStopHandler);
        return () => {
            window.sw.off?.('message.created', handler);
            window.sw.off?.('message.deleted', deleteHandler);
            window.sw.off?.('typing.start', typingStartHandler);
            window.sw.off?.('typing.stop', typingStopHandler);
            window.sw.off?.('chat.typing.start', typingStartHandler);
            window.sw.off?.('chat.typing.stop', typingStopHandler);
        };
    }, []);

    function _clearTyping(pid) {
        if (typingTimers.current[pid]) {
            clearTimeout(typingTimers.current[pid]);
            delete typingTimers.current[pid];
        }
        setTypingUsers(prev => prev.filter(t => t.id !== pid));
    }

    // Emit typing event (called from MessageInput via prop)
    const emitTyping = useCallback((isTyping) => {
        const cid = channelRef.current;
        if (!cid || !window.sw?.emit) return;
        const myId = window.sw?.auth?.user?.id;
        const myName = window.sw?.auth?.user?.display_name || window.sw?.auth?.user?.username || '';
        window.sw.emit(isTyping ? 'chat.typing.start' : 'chat.typing.stop', {
            channel_id: cid,
            participant_id: myId,
            participant_type: 'user',
            display_name: myName,
        });
    }, []);

    return {
        messages,
        channelId,
        sending,
        error,
        model,
        setModel,
        sendMessage,
        switchChannel,
        newChat,
        stop,
        clearError,
        // Streaming state (passthrough from useStream)
        streamContent,
        streamThinking,
        streaming,
        // v0.37.10: new methods
        regenerate,
        deleteMessage,
        loadMore,
        hasMore,
        loadingMore,
        // v0.37.14: typing indicators
        typingUsers,
        emitTyping,
    };
}
