import { Fragment } from 'preact';
import { useEffect, useRef, useState } from 'preact/hooks';
import { t } from '../core/i18n';
import type { Message, WidgetState } from '../core/store';
import { BlockList } from './blocks';

type Props = {
    messages: Message[];
    starterPrompts?: string[];
    onRetry?: (failedAssistantId: string) => void;
    onPromptClick?: (prompt: string) => void;
    onLeadCapture?: () => void;
    onApplyCoupon?: (code: string) => Promise<{ ok: boolean; message?: string }>;
    /**
     * Fired when the visitor taps a `<suggestions>` chip rendered under
     * the assistant's last reply. Bar routes the prompt back through its
     * `ask()` so the chip text becomes the next visitor turn.
     */
    onAsk?: (prompt: string) => void;
    /**
     * Server-emitted progress hint for the pending bubble: 'searching'
     * while retrieval runs, 'thinking' while the LLM composes. Bar
     * passes this through from the store so the indicator copy reflects
     * what's actually happening before the first token lands.
     */
    currentStage?: WidgetState['currentStage'];
};

type Citation = { id: number; url: string | null };

/**
 * Replace inline [N] tokens in the assistant's reply with clickable
 * anchors that open the matching citation URL. Falls back to plain
 * text when the citation has no url, or when the [N] doesn't match
 * any known citation (defensive — the LLM occasionally invents one).
 */
function renderWithCitations(text: string, citations: Citation[]) {
    if (!citations || citations.length === 0) {
        return text;
    }

    const byId = new Map(citations.map((c) => [c.id, c.url] as const));
    const parts: (string | preact.JSX.Element)[] = [];
    const regex = /\[(\d+)\]/g;
    let lastIndex = 0;
    let match: RegExpExecArray | null;

    while ((match = regex.exec(text)) !== null) {
        if (match.index > lastIndex) {
            parts.push(text.slice(lastIndex, match.index));
        }

        const id = Number(match[1]);
        const url = byId.get(id);

        if (url) {
            parts.push(
                <a
                    key={`cite-${match.index}`}
                    href={url}
                    target="_blank"
                    rel="noopener noreferrer"
                    title={url}
                    style={{
                        color: 'inherit',
                        textDecoration: 'none',
                        background: 'rgba(99, 102, 241, 0.15)',
                        borderRadius: 4,
                        padding: '0 4px',
                        fontSize: '0.85em',
                        fontWeight: 600,
                        margin: '0 1px',
                    }}
                >
                    [{id}]
                </a>,
            );
        } else {
            parts.push(match[0]);
        }

        lastIndex = match.index + match[0].length;
    }

    if (lastIndex < text.length) {
        parts.push(text.slice(lastIndex));
    }

    return parts.map((p, i) => <Fragment key={i}>{p}</Fragment>);
}

/**
 * Pick a (chars-per-tick, ms-per-tick) pair based on how far the
 * displayed text is behind the actual streamed content. The further
 * behind, the more aggressively we catch up — we don't want a long
 * reply to leave the visitor staring at a half-typed message ten
 * seconds after the stream is done.
 */
function typewriterPace(charsBehind: number): { step: number; delay: number } {
    if (charsBehind > 240) {
        return { step: 32, delay: 8 };
    }

    if (charsBehind > 60) {
        return { step: 4, delay: 12 };
    }

    return { step: 1, delay: 22 };
}

type AssistantBubbleProps = {
    message: Message;
    onLeadCapture?: () => void;
    onApplyCoupon?: (code: string) => Promise<{ ok: boolean; message?: string }>;
    onAsk?: (prompt: string) => void;
    /**
     * True when this assistant message is the trailing message in the
     * thread. Forwarded to `BlockList` → `SuggestionChips` so chips only
     * render on the latest reply (they vanish once the visitor sends
     * the next turn).
     */
    isTrailing?: boolean;
    /** See Messages props. Drives the pre-token indicator copy. */
    currentStage?: WidgetState['currentStage'];
};

function stageLabel(stage: WidgetState['currentStage']): string {
    if (stage === 'searching') {
        return t('Searching your site…');
    }

    if (stage === 'thinking') {
        return t('Thinking…');
    }

    return t('thinking…');
}

function AssistantBubble({
    message,
    onLeadCapture,
    onApplyCoupon,
    onAsk,
    isTrailing,
    currentStage,
}: AssistantBubbleProps) {
    const target = message.content.length;
    // Capture at mount whether this message was streaming. Hydrated
    // history from /init lands here non-pending and renders instantly;
    // freshly-streamed turns mount with pending=true and animate.
    const [animate] = useState(() => message.pending === true);
    const [displayed, setDisplayed] = useState(animate ? 0 : target);

    useEffect(() => {
        if (displayed >= target) {
            return;
        }

        const { step, delay } = typewriterPace(target - displayed);
        const id = window.setTimeout(() => {
            setDisplayed((d) => Math.min(target, d + step));
        }, delay);

        return () => window.clearTimeout(id);
    }, [displayed, target]);

    const visible = message.content.slice(0, displayed);
    const stillTyping = displayed < target;
    const showCursor = message.pending === true || stillTyping;

    return (
        <div
            style={{
                padding: '10px 14px',
                borderRadius: 12,
                background: message.error ? '#fef2f2' : '#f3f4f6',
                color: message.error ? '#991b1b' : '#0f172a',
                border: message.error ? '1px solid #fecaca' : 'none',
                fontSize: 14,
                whiteSpace: 'pre-wrap',
            }}
        >
            {message.pending && message.content === '' ? (
                <em style={{ opacity: 0.7 }}>
                    {isTrailing ? stageLabel(currentStage) : t('thinking…')}
                </em>
            ) : (
                <>
                    {renderWithCitations(visible, message.citations ?? [])}
                    {showCursor && (
                        <span
                            aria-hidden="true"
                            style={{
                                display: 'inline-block',
                                width: 2,
                                height: '1em',
                                marginLeft: 2,
                                background: '#0f172a',
                                verticalAlign: '-0.15em',
                                animation:
                                    'pitchbar-cursor 1s steps(2, end) infinite',
                            }}
                        />
                    )}
                </>
            )}
            {message.blocks && message.blocks.length > 0 && (
                <BlockList
                    blocks={message.blocks}
                    onLeadCapture={onLeadCapture}
                    onApplyCoupon={onApplyCoupon}
                    onAsk={onAsk}
                    isTrailing={isTrailing}
                />
            )}
            {!stillTyping &&
                message.citations &&
                message.citations.length > 0 && (
                    <div
                        style={{
                            marginTop: 6,
                            fontSize: 11,
                            opacity: 0.7,
                        }}
                    >
                        {t('Sources')}:{' '}
                        {message.citations.map((c, idx) => (
                            <a
                                key={idx}
                                href={c.url ?? '#'}
                                target="_blank"
                                rel="noopener noreferrer"
                                style={{
                                    color: 'inherit',
                                    textDecoration: 'underline',
                                    marginRight: 6,
                                }}
                            >
                                [{c.id}]
                            </a>
                        ))}
                    </div>
                )}
        </div>
    );
}

export function Messages({
    messages,
    starterPrompts,
    onRetry,
    onPromptClick,
    onLeadCapture,
    onApplyCoupon,
    onAsk,
    currentStage,
}: Props) {
    const containerRef = useRef<HTMLDivElement | null>(null);
    const contentRef = useRef<HTMLDivElement | null>(null);
    const lastSeenIdRef = useRef<string | undefined>(undefined);
    const lastMessageId = messages[messages.length - 1]?.id;

    // Force-scroll to the bottom every time a new message lands (id of
    // the trailing message changes). Covers three cases that the
    // ResizeObserver below can miss or shouldn't fire on:
    //
    //   1. First mount with hydrated history — visitor opens the bar
    //      and should land on the LATEST turn, not scrolled up to the
    //      first message in their thread.
    //   2. Visitor sends a turn — the new user bubble + pending
    //      assistant placeholder must always be visible, even if the
    //      visitor had scrolled up to read older history.
    //   3. Retry — old failed bubble removed, new assistant placeholder
    //      mounts; should anchor to the new turn.
    //
    // The ResizeObserver effect handles streaming-token growth and is
    // intentionally near-bottom-gated so we don't yank the visitor down
    // mid-read.
    useEffect(() => {
        if (!lastMessageId || lastMessageId === lastSeenIdRef.current) {
            return;
        }

        lastSeenIdRef.current = lastMessageId;
        const scroller = containerRef.current;

        if (scroller) {
            scroller.scrollTop = scroller.scrollHeight;
        }
    }, [lastMessageId]);

    // ResizeObserver replaces the per-message-length scroll effect:
    // typewriter advances grow the inner content's height without
    // changing the messages array, so a deps-based effect would miss
    // them. Observing the content div catches every height change
    // (typewriter, citations footer revealing, retry chip mounting).
    useEffect(() => {
        const scroller = containerRef.current;
        const content = contentRef.current;

        if (!scroller || !content) {
            return;
        }

        const onResize = () => {
            const distanceFromBottom =
                scroller.scrollHeight -
                scroller.scrollTop -
                scroller.clientHeight;

            // Only auto-scroll if the visitor was already near the
            // bottom — don't yank them down if they've scrolled up.
            if (distanceFromBottom < 96) {
                scroller.scrollTop = scroller.scrollHeight;
            }
        };

        const ro = new ResizeObserver(onResize);
        ro.observe(content);

        return () => ro.disconnect();
    }, []);

    if (messages.length === 0) {
        const hasPrompts =
            starterPrompts !== undefined &&
            starterPrompts.length > 0 &&
            onPromptClick !== undefined;

        return (
            <div
                role="log"
                style={{
                    padding: hasPrompts ? '24px 16px' : 32,
                    textAlign: 'center',
                    color: '#6b7280',
                    fontSize: 14,
                }}
            >
                <div style={{ marginBottom: hasPrompts ? 12 : 0 }}>
                    {t('Ask anything about this site.')}
                </div>
                {hasPrompts && (
                    <div
                        style={{
                            display: 'flex',
                            flexWrap: 'wrap',
                            gap: 6,
                            justifyContent: 'center',
                        }}
                    >
                        {starterPrompts.map((prompt, i) => (
                            <button
                                key={`${prompt}-${i}`}
                                type="button"
                                onClick={() => onPromptClick(prompt)}
                                style={{
                                    padding: '6px 12px',
                                    borderRadius: 999,
                                    border: '1px solid #e5e7eb',
                                    background: 'white',
                                    color: '#0f172a',
                                    fontSize: 13,
                                    cursor: 'pointer',
                                    maxWidth: '100%',
                                    whiteSpace: 'normal',
                                    textAlign: 'left',
                                }}
                            >
                                {prompt}
                            </button>
                        ))}
                    </div>
                )}
            </div>
        );
    }

    return (
        <div
            ref={containerRef}
            role="log"
            aria-live="polite"
            style={{
                padding: 16,
                maxHeight: 360,
                overflowY: 'auto',
            }}
        >
            <div
                ref={contentRef}
                style={{
                    display: 'flex',
                    flexDirection: 'column',
                    gap: 10,
                }}
            >
                {messages.map((m, idx) => {
                    const isUser = m.role === 'user';
                    const isHuman = m.role === 'human-agent';
                    const isAssistant = m.role === 'assistant';
                    const isTrailing = idx === messages.length - 1;

                    return (
                        <div
                            key={m.id}
                            style={{
                                display: 'flex',
                                flexDirection: 'column',
                                alignSelf: isUser ? 'flex-end' : 'flex-start',
                                maxWidth: '85%',
                            }}
                        >
                            {isHuman && (
                                <span
                                    style={{
                                        fontSize: 10,
                                        color: '#059669',
                                        fontWeight: 600,
                                        marginBottom: 2,
                                        textTransform: 'uppercase',
                                        letterSpacing: 0.4,
                                    }}
                                >
                                    {t('live agent')}
                                </span>
                            )}
                            {isAssistant ? (
                                <AssistantBubble
                                    message={m}
                                    onLeadCapture={onLeadCapture}
                                    onApplyCoupon={onApplyCoupon}
                                    onAsk={onAsk}
                                    isTrailing={isTrailing}
                                    currentStage={currentStage}
                                />
                            ) : (
                                <div
                                    style={{
                                        padding: '10px 14px',
                                        borderRadius: 12,
                                        background: isUser
                                            ? '#111827'
                                            : '#ecfdf5',
                                        color: isUser ? 'white' : '#0f172a',
                                        border: isHuman
                                            ? '1px solid #a7f3d0'
                                            : 'none',
                                        fontSize: 14,
                                        whiteSpace: 'pre-wrap',
                                    }}
                                >
                                    {m.content}
                                </div>
                            )}
                            {m.error && onRetry && (
                                <button
                                    type="button"
                                    onClick={() => onRetry(m.id)}
                                    style={{
                                        alignSelf: 'flex-start',
                                        marginTop: 4,
                                        padding: '4px 10px',
                                        borderRadius: 999,
                                        border: '1px solid #fecaca',
                                        background: 'white',
                                        color: '#991b1b',
                                        fontSize: 12,
                                        cursor: 'pointer',
                                    }}
                                >
                                    ↻ {t('Retry')}
                                </button>
                            )}
                        </div>
                    );
                })}
            </div>
        </div>
    );
}
