import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { FEATURE_KEYS, useFeatureFlag } from "@/hooks/use-feature";
import { ThreadActionBar } from "./components/thread-action-bar"
import { ThreadMessage } from "./components/thread-message"
import { ThreadEvent, isCondensed, groupSystemEvents, CollapsedEventsGroup } from "./components/thread-event"
import { ThreadEventInput } from "./components/thread-event-input"
import { useMailboxContext, TimelineItem, isThreadEvent } from "@/features/providers/mailbox"
import useRead from "@/features/message/use-read"
import useMentionRead from "@/features/message/use-mention-read"
import { useDebounceCallback } from "@/hooks/use-debounce-callback"
import { useIsSharedContext } from "@/hooks/use-is-shared-context"
import { useVisibilityObserver } from "@/hooks/use-visibility-observer"
import { MailboxRoleChoices, Message, Thread, ThreadEvent as ThreadEventModel } from "@/features/api/gen/models"
import { Icon, IconType, Spinner } from "@gouvfr-lasuite/ui-kit"
import { Banner } from "@/features/ui/components/banner"
import { SKIP_LINK_TARGET_ID } from "@/features/ui/components/skip-link"
import { useTranslation } from "react-i18next"
import { ThreadViewLabelsList } from "./components/thread-view-labels-list"
import { ThreadSummary } from "./components/thread-summary";
import { ThreadViewEmpty } from "./components/thread-view-empty";
import clsx from "clsx";
import ThreadViewProvider, { useThreadViewContext } from "./provider";
import useSpam from "@/features/message/use-spam";
import ViewHelper from "@/features/utils/view-helper";

/**
 * Fallback height (px) used when measuring the sticky header before the
 * DOM ref is populated. Matches the rendered header height closely enough
 * for the IntersectionObserver to ignore content hidden behind it.
 */
const STICKY_HEADER_FALLBACK_HEIGHT = 125;

type MessageWithDraftChild = Message & {
    draft_message?: Message;
}

type ThreadViewComponentProps = {
    threadItems: readonly TimelineItem[],
    mailboxId: string,
    thread: Thread,
    showTrashedMessages: boolean,
    setShowTrashedMessages: (show: boolean) => void,
    stats: { trashed: number, archived: number, total: number },
    showIMInput: boolean,
}

const ThreadViewComponent = ({ threadItems, mailboxId, thread, showTrashedMessages, setShowTrashedMessages, stats, showIMInput }: ThreadViewComponentProps) => {
    const { t } = useTranslation();
    const latestSeenDate = useRef<string | null>(null);
    const stickyContainerRef = useRef<HTMLDivElement>(null);
    const { markAsReadAt } = useRead();
    const [editingEvent, setEditingEvent] = useState<ThreadEventModel | null>(null);
    const { markAsNotSpam } = useSpam();
    const debouncedMarkAsRead = useDebounceCallback((threadId: string, readAt: string) => {
        markAsReadAt({ threadIds: [threadId], readAt });
    }, 150);

    const rootRef = useRef<HTMLDivElement>(null);
    const isAISummaryEnabled = useFeatureFlag(FEATURE_KEYS.AI_SUMMARY);
    const { isReady, reset, hasBeenInitialized, setHasBeenInitialized } = useThreadViewContext();
    // Refs for all unread messages
    const unreadRefs = useRef<Record<string, HTMLElement | null>>({});
    // Refs for thread events with unread mentions
    const mentionRefs = useRef<Record<string, HTMLElement | null>>({});
    const { markMentionsRead } = useMentionRead(thread.id);
    // Collapse runs of 3+ consecutive non-IM events between messages/IMs
    // behind a progressive-disclosure toggle. Short runs stay inline so we
    // don't hide metadata when there's little of it — pattern borrowed from
    // Linear's collapsed issue history.
    const renderItems = useMemo(() => groupSystemEvents(threadItems), [threadItems]);
    // Find all unread message IDs
    const messages = useMemo(() => threadItems.filter(item => item.type === 'message').map(item => item.data as MessageWithDraftChild), [threadItems]);
    const unreadMessageIds = useMemo(() => messages.filter((m) => m.is_unread).map((m) => m.id), [messages]);
    const draftMessageIds = useMemo(() => messages.filter((m) => m.draft_message).map((m) => m.id), [messages]);
    const unreadMentionEventIds = useMemo(() =>
        threadItems
            .filter((item): item is Extract<typeof item, { type: 'event' }> =>
                item.type === 'event' && (item.data as ThreadEventModel).has_unread_mention === true
            )
            .map(item => item.data.id),
        [threadItems]
    );
    /**
     * Walks the timeline once to build a map <rootId, true> for
     * condensed-IM groups that contain at least one unread mention.
     *
     * A "root" is an IM event whose header is rendered (first event
     * of a condensed run). Surfacing the unread-mention badge on the root
     * means that a mention on a condensed sibling — whose own header is
     * hidden — still draws the user's eye via the root's header.
     */
    const unreadMentionGroupMap = useMemo(() => {
        const map = new Map<string, boolean>();
        let currentRootId: string | null = null;
        let prevEvent: ThreadEventModel | null = null;
        for (const item of threadItems) {
            if (!isThreadEvent(item)) {
                currentRootId = null;
                prevEvent = null;
                continue;
            }
            const current = item.data as ThreadEventModel;
            if (!isCondensed(current, prevEvent)) {
                currentRootId = current.id;
            }
            if (current.has_unread_mention && currentRootId) {
                map.set(currentRootId, true);
            }
            prevEvent = current;
        }
        return map;
    }, [threadItems]);

    // Mention IDs accumulated across debounce windows. The intersection
    // observer can fire several times within the 150ms window; using a ref
    // (instead of a value passed to the debounced callback) preserves earlier
    // batches that would otherwise be overwritten by the trailing-edge debounce.
    const pendingMentionIdsRef = useRef<Set<string>>(new Set());
    // Mentions already PATCHed during this thread session. `useMentionRead`
    // intentionally does not update the thread events cache (to keep the
    // "Mentioned" badge visible for the whole session), so an event stays in
    // `unreadMentionEventIds` until the next natural refetch. Without this
    // guard, scrolling back onto a flagged event would re-enqueue it on every
    // visibility pass and trigger redundant PATCH + stats invalidations.
    // Cleared on thread switch via the cleanup effect below.
    const sentMentionIdsRef = useRef<Set<string>>(new Set());
    const flushPendingMentions = useCallback(() => {
        if (pendingMentionIdsRef.current.size === 0) return;
        const ids = [...pendingMentionIdsRef.current];
        pendingMentionIdsRef.current.clear();
        markMentionsRead(ids);
    }, [markMentionsRead]);
    const debouncedFlushMentions = useDebounceCallback(flushPendingMentions, 150);
    const isThreadTrashed = stats.trashed === stats.total;
    const isThreadArchived = stats.archived === stats.total;
    // Talk mode (left/right alternating layout) only makes sense for an actual
    // back-and-forth: at least one received message AND at least one truly sent
    // one. A draft doesn't count as sent, so a lone draft stays full-width.
    const isTalkMode = messages?.some((m) => !m.is_sender) && messages?.some((m) => m.is_sender && !m.is_draft);
    const latestMessage = messages.reduce((acc, message) => {
        if (message!.created_at && acc!.created_at && message!.created_at > acc!.created_at) {
            return message;
        }
        return acc;
    }, messages[0]);

    /**
     * Scroll to the bottom of the thread view.
     */
    const scrollToBottom = useCallback(() => {
        requestAnimationFrame(() => {
            rootRef.current?.scrollTo({
                top: rootRef.current.scrollHeight,
                behavior: 'smooth',
            });
        });
    }, []);

    /**
     * Mark messages as read once they scroll into view. Tracks the latest
     * timestamp seen so a single debounced call covers all messages above it.
     */
    const topOffset = stickyContainerRef.current?.getBoundingClientRect().height || STICKY_HEADER_FALLBACK_HEIGHT;
    useVisibilityObserver({
        enabled: isReady,
        ids: unreadMessageIds,
        refs: unreadRefs,
        rootRef,
        topOffset,
        onVisible: (entry) => {
            const createdAt = entry.target.getAttribute('data-created-at');
            if (!createdAt) return;
            // Track the most recent message scrolled into view
            if (!latestSeenDate.current || new Date(createdAt) > new Date(latestSeenDate.current)) {
                latestSeenDate.current = createdAt;
            }
            debouncedMarkAsRead(thread.id, latestSeenDate.current);
        },
    });

    /**
     * Mark mentions as read when their ThreadEvent scrolls into view.
     * IDs are accumulated through `pendingMentionIdsRef` so several batches
     * within the same debounce window are flushed together.
     */
    useVisibilityObserver({
        enabled: isReady,
        ids: unreadMentionEventIds,
        refs: mentionRefs,
        rootRef,
        topOffset,
        onVisible: (entry) => {
            const eventId = entry.target.getAttribute('data-event-id');
            if (!eventId) return;
            if (sentMentionIdsRef.current.has(eventId)) return;
            sentMentionIdsRef.current.add(eventId);
            pendingMentionIdsRef.current.add(eventId);
            debouncedFlushMentions();
        },
    });

    useEffect(() => {
        if (isReady && !hasBeenInitialized) {
            // Deep-link hash takes precedence over draft/unread heuristics:
            // a user arriving via a shared "#thread-message-{id}" or
            // "#thread-event-{id}" URL expects to land on that exact element,
            // scrolled smoothly into view with a brief highlight.
            const hash = typeof window !== 'undefined' ? window.location.hash : '';
            const hashMatch = hash.match(/^#thread-(message|event)-([\w-]+)$/);
            let selector = `#thread-message-${latestMessage?.id}`;
            let scrollBehavior: ScrollBehavior = 'instant';
            let highlightTargetId: string | null = null;

            if (hashMatch) {
                selector = hash;
                scrollBehavior = 'smooth';
                highlightTargetId = hash.slice(1);
            } else if (draftMessageIds.length > 0) {
                selector = `#thread-message-${draftMessageIds[0]} > .thread-message__reply-form`;
            } else {
                const firstUnreadItem = threadItems.find((item) => {
                    if (item.type === 'message') {
                        return (item.data as MessageWithDraftChild).is_unread;
                    }
                    return (item.data as ThreadEventModel).has_unread_mention === true;
                });
                if (firstUnreadItem) {
                    selector = firstUnreadItem.type === 'message'
                        ? `#thread-message-${firstUnreadItem.data.id}`
                        : `#thread-event-${firstUnreadItem.data.id}`;
                }
            }

            const el = document.querySelector<HTMLElement>(selector);
            if (el) {
                const performScroll = (): boolean => {
                    const refreshed = document.querySelector<HTMLElement>(selector);
                    const target = refreshed ?? el;
                    const root = rootRef.current;
                    if (!root) return false;
                    const targetTop = Math.max(target.offsetTop - 225, 0);
                    const willScroll = Math.abs(targetTop - root.scrollTop) > 1;
                    if (willScroll) {
                        root.scrollTo({ top: targetTop, behavior: scrollBehavior });
                    }
                    return willScroll;
                };

                const startHighlight = () => {
                    if (!highlightTargetId) return;
                    const highlightTarget = document.getElementById(highlightTargetId);
                    if (!highlightTarget) return;
                    const messagesList = rootRef.current?.querySelector('.thread-view__messages-list');
                    highlightTarget.classList.add('thread-view__highlight');
                    messagesList?.classList.add('thread-view__messages-list--focusing');

                    // Sync cleanup to the CSS animation. The timeout
                    // guards prefers-reduced-motion, where no animation fires.
                    let cleaned = false;
                    const cleanup = () => {
                        if (cleaned) return;
                        cleaned = true;
                        highlightTarget.classList.remove('thread-view__highlight');
                        messagesList?.classList.remove('thread-view__messages-list--focusing');
                        highlightTarget.removeEventListener('animationend', cleanup);
                    };
                    highlightTarget.addEventListener('animationend', cleanup);
                    setTimeout(cleanup, 2200);
                };

                // Defer the highlight until the smooth scroll has actually
                // landed. `scrollend` doesn't emit when no scroll happens
                // nor on older browsers, so the 700ms safety acts as a fallback.
                const scheduleHighlight = (willScroll: boolean) => {
                    const root = rootRef.current;
                    if (!root || scrollBehavior !== 'smooth' || !willScroll) {
                        startHighlight();
                        return;
                    }
                    let fired = false;
                    const fire = () => {
                        if (fired) return;
                        fired = true;
                        root.removeEventListener('scrollend', fire);
                        clearTimeout(safety);
                        startHighlight();
                    };
                    root.addEventListener('scrollend', fire);
                    const safety = setTimeout(fire, 700);
                };

                if (hashMatch) {
                    requestAnimationFrame(() => requestAnimationFrame(() => {
                        const willScroll = performScroll();
                        scheduleHighlight(willScroll);
                    }));
                } else {
                    const willScroll = performScroll();
                    scheduleHighlight(willScroll);
                }
                setHasBeenInitialized(true);
            }
        }
    }, [isReady]);

    const handleEventDelete = useCallback((eventId: string) => {
        if (editingEvent?.id === eventId) {
            setEditingEvent(null);
        }
    }, [editingEvent]);

    useEffect(() => () => {
        reset();
        setEditingEvent(null);
        pendingMentionIdsRef.current.clear();
        sentMentionIdsRef.current.clear();
    }, [thread.id]);

    return (
        <div id={SKIP_LINK_TARGET_ID} className={clsx("thread-view", { "thread-view--talk": isTalkMode })} ref={rootRef}>
            <div className="thread-view__sticky-container" ref={stickyContainerRef}>
                <header className="thread-view__header">
                    <div className="thread-view__header__top">
                        <ThreadActionBar canUndelete={isThreadTrashed} canUnarchive={isThreadArchived} />
                        <h2 className="thread-view__subject">
                            {thread.has_starred &&
                                <Icon name="star" type={IconType.FILLED} className="thread-view__subject__star" aria-label={t('Starred')} />
                            }
                            {thread.subject || t('No subject')}
                        </h2>
                    </div>
                </header>
            </div>
            {
                thread.labels.length > 0 && (
                    <ThreadViewLabelsList labels={thread.labels} />
                )
            }
            {isAISummaryEnabled && (
                <ThreadSummary
                    threadId={thread.id}
                    summary={thread.summary}
                    selectedMailboxId={mailboxId}
                    selectedThread={thread}
                />
            )}
            <div className="thread-view__messages-list">
                {thread.is_spam && (
                    <Banner
                        icon={<Icon name="report" type={IconType.OUTLINED} />}
                        type="warning"
                        actions={[{ label: t('Remove report'), onClick: () => markAsNotSpam({ threadIds: [thread.id] }) }]}
                    >
                        <p>{t('This thread has been reported as spam.')}</p>
                    </Banner>
                )}
                {stats.trashed > 0 && !showTrashedMessages && (
                    <Banner
                        icon={<Icon name="delete" type={IconType.OUTLINED} />}
                        type="info" actions={[{
                            label: t('Show'),
                            onClick: () => setShowTrashedMessages(!showTrashedMessages)
                        }]}
                    >
                        {t(
                            '{{count}} messages of this thread have been deleted.',
                            {
                                count: stats.trashed,
                                defaultValue_one: "{{count}} message of this thread has been deleted.",
                            }
                        )}
                    </Banner>
                )}
                {renderItems.map((item, index) => {
                    if (item.kind === 'collapsed_events') {
                        return (
                            <CollapsedEventsGroup
                                key={`collapsed-events-${item.events[0].id}`}
                                events={item.events}
                            />
                        );
                    }
                    if (item.kind === 'event') {
                        const prevItem = index > 0 ? renderItems[index - 1] : null;
                        const prevEvent = prevItem?.kind === 'event' ? prevItem.data : null;
                        const eventData = item.data;
                        return (
                            <ThreadEvent
                                key={`event-${item.data.id}`}
                                event={item.data}
                                onEdit={setEditingEvent}
                                onDelete={handleEventDelete}
                                isCondensed={isCondensed(eventData, prevEvent)}
                                // `hasUnreadMention` is group-aware so the badge surfaces on the
                                // condensed root, but `mentionRef` stays gated by the raw event
                                // flag on purpose: the read is only acknowledged once the bubble
                                // that actually carries the mention scrolls into view, not when
                                // the root header alone is visible.
                                hasUnreadMention={unreadMentionGroupMap.get(item.data.id) ?? false}
                                mentionRef={eventData.has_unread_mention
                                    ? (el: HTMLDivElement | null) => { mentionRefs.current[item.data.id] = el; }
                                    : undefined
                                }
                            />
                        );
                    }
                    const message = item.data as MessageWithDraftChild;
                    const isLatest = latestMessage?.id === message.id;
                    const isUnread = message.is_unread;
                    return (
                        <ThreadMessage
                            key={message.id}
                            message={message}
                            isLatest={isLatest}
                            ref={isUnread ? (el => { unreadRefs.current[message.id] = el; }) : undefined}
                            data-message-id={message.id}
                            data-created-at={message.created_at}
                            draftMessage={message.draft_message}
                        />
                    );
                })}
            </div>
            {showIMInput && (
                <ThreadEventInput
                    threadId={thread.id}
                    editingEvent={editingEvent}
                    onCancelEdit={() => setEditingEvent(null)}
                    onEventCreated={scrollToBottom}
                    containerRef={rootRef}
                />
            )}
        </div>
    )
}

export const ThreadView = () => {
    const isTrashView = ViewHelper.isTrashedView();
    const { selectedMailbox, selectedThread, unmountThreadViewNeeded, messages, threadItems, queryStates } = useMailboxContext();
    const [showTrashedMessages, setShowTrashedMessages] = useState(isTrashView);
    // Nest draft messages under their parent messages
    const messagesWithDraftChildren = useMemo(() => {
        if (!messages) return [];
        const rootMessages: MessageWithDraftChild[] = messages.filter((m) => !m.is_draft || !m.parent_id);
        const draftChildren = messages.filter((m) => m.is_draft && m.parent_id);
        draftChildren.forEach((m) => {
            const parentMessage = rootMessages.find((um) => um.id === m.parent_id);
            if (parentMessage) {
                parentMessage.draft_message = m;
            }
        });
        return rootMessages
    }, [messages]);
    const messagesStats = useMemo(() => ({
        trashed: messagesWithDraftChildren?.filter((m) => m.is_trashed).length || 0,
        archived: messagesWithDraftChildren?.filter((m) => m.is_archived).length || 0,
        total: messagesWithDraftChildren?.length || 0,
    }), [messagesWithDraftChildren]);
    // Show IM input when the user has at least edit rights on the mailbox
    // (editor/sender/admin), regardless of the thread-level role.
    // Still gated to shared contexts (see useIsSharedContext).
    const hasMailboxEditAccess = !!selectedMailbox && (
        selectedMailbox.role === MailboxRoleChoices.editor
        || selectedMailbox.role === MailboxRoleChoices.sender
        || selectedMailbox.role === MailboxRoleChoices.admin
    );
    const isSharedContext = useIsSharedContext();
    const showIMInput = Boolean(isSharedContext && hasMailboxEditAccess);

    // Build filtered timeline items: enrich messages with draft children,
    // apply trash filtering, and keep all events.
    const filteredThreadItems = useMemo(() => {
        if (!threadItems) return [];
        const messagesById = new Map(messagesWithDraftChildren.map((m) => [m.id, m]));
        const showAll = !isTrashView && showTrashedMessages;
        return threadItems.flatMap<TimelineItem>((item) => {
            if (item.type === 'event') return [item];
            const message = messagesById.get(item.data.id);
            if (!message) return [];
            if (!showAll && message.is_trashed !== isTrashView) return [];
            return [{ type: 'message', data: message, created_at: item.created_at }];
        });
    }, [threadItems, messagesWithDraftChildren, isTrashView, showTrashedMessages]);

    const messageIds = filteredThreadItems
        .filter((item): item is Extract<TimelineItem, { type: 'message' }> => item.type === 'message')
        .map(item => item.data.id);

    // A deep-link "#thread-message-{id}" pointing at a trashed message
    // should be treated as a request to show the trash view
    useEffect(() => {
        if (!messages || isTrashView || showTrashedMessages) return;
        const hash = typeof window !== 'undefined' ? window.location.hash : '';
        const match = hash.match(/^#thread-message-([\w-]+)$/);
        if (!match) return;
        const target = messages.find((m) => m.id === match[1]);
        if (target?.is_trashed) {
            setShowTrashedMessages(true);
        }
    }, [messages, isTrashView, showTrashedMessages]);

    useEffect(() => () => {
        setShowTrashedMessages(isTrashView);
    }, [selectedThread]);

    // `unmountThreadViewNeeded` unmounts the heavy thread view (and its
    // auto-mark-as-read observer) synchronously on unselect, before the
    // navigation completes, while still showing the empty placeholder rather
    // than a blank panel.
    if (unmountThreadViewNeeded) return <ThreadViewEmpty />
    if (!selectedMailbox || !selectedThread) return null

    if (queryStates.messages.isLoading || queryStates.threadEvents.isLoading) {
        return (
            <div className="thread-view thread-view--loading">
                <Spinner />
            </div>
        )
    }

    return (
        <ThreadViewProvider threadId={selectedThread!.id} messageIds={messageIds}>
            <ThreadViewComponent
                mailboxId={selectedMailbox!.id}
                thread={selectedThread!}
                threadItems={filteredThreadItems}
                showTrashedMessages={showTrashedMessages}
                setShowTrashedMessages={setShowTrashedMessages}
                stats={messagesStats}
                showIMInput={showIMInput}
            />
        </ThreadViewProvider>
    )
}
