import {
    useCallback,
    useEffect,
    useLayoutEffect,
    useMemo,
    useRef,
    useState,
    type PointerEvent,
    type ReactNode,
    type TouchEvent,
    type TransitionEvent
} from 'react';

type SwipeTabsProps<T extends string | number> = {
    tabs: T[];
    activeTab: T;
    onTabChange: (tab: T) => void;
    renderPanel: (tab: T) => ReactNode;
    className?: string;
    panelClassName?: string;
    swipeDisabled?: boolean;
    threshold?: number;
    renderWindow?: number;
    scrollToTopOnChange?: boolean;
    onSwipePastStart?: () => void;
    // For 1:1 sidebar gesture tracking
    onSwipePastStartProgress?: (progress: number) => void;
    onSwipePastStartDragStart?: () => void;
    onSwipePastStartDragEnd?: (shouldOpen: boolean) => void;
    sidebarWidth?: number; // Width of sidebar for progress calculation
};

const DRAG_START_DISTANCE = 3;
const EDGE_RESISTANCE = 0.35;
const DEFAULT_THRESHOLD = 0.2;
const DEFAULT_RENDER_WINDOW = 1;

const findTouchById = (touches: React.TouchList, id: number): React.Touch | null => {
    for (let i = 0; i < touches.length; i += 1) {
        const touch = touches.item(i);
        if (touch && touch.identifier === id) {
            return touch;
        }
    }
  return null;
};

const resolveSwipeTarget = (target: EventTarget | null): Element | null => {
    if (!target) return null;
    const element = target as Element;
    if (typeof element.closest === 'function') return element;
    const node = target as Node & { parentElement?: Element | null };
    return node.parentElement ?? null;
};

export function SwipeTabs<T extends string | number>({
    tabs,
    activeTab,
    onTabChange,
    renderPanel,
    className,
    panelClassName,
    swipeDisabled = false,
    threshold = DEFAULT_THRESHOLD,
    renderWindow = DEFAULT_RENDER_WINDOW,
    scrollToTopOnChange = true,
    onSwipePastStart,
    onSwipePastStartProgress,
    onSwipePastStartDragStart,
    onSwipePastStartDragEnd,
    sidebarWidth = 280
}: SwipeTabsProps<T>) {
    const containerRef = useRef<HTMLDivElement>(null);
    const trackRef = useRef<HTMLDivElement>(null);
    const activeIndex = useMemo(() => {
        const index = tabs.indexOf(activeTab);
        return index >= 0 ? index : 0;
    }, [tabs, activeTab]);

    const [displayIndex, setDisplayIndex] = useState(activeIndex);
    const displayIndexRef = useRef(displayIndex);
    const widthRef = useRef(0);
    const baseOffsetRef = useRef(0);
    const dragOffsetRef = useRef(0);
    const rafRef = useRef(0);
    const isDraggingRef = useRef(false);
    const hasMountedRef = useRef(false);
    const transitionRef = useRef<string | null>(null);
    const transformRef = useRef<string | null>(null);
    const pendingIndexRef = useRef<number | null>(null);
    const isAnimatingRef = useRef(false);
    const needsResetRef = useRef(false);
    const hasScrolledRef = useRef(false);
    const dragRef = useRef({
        pointerId: null as number | null,
        touchId: null as number | null,
        startX: 0,
        startY: 0,
        startIndex: 0,
        isActive: false,
        isDragging: false
    });

    const shouldIgnoreSwipe = useCallback((target: EventTarget | null) => {
        const element = resolveSwipeTarget(target);
        if (!element) return false;
        // Don't ignore role="button" - we handle tap vs swipe via delayed pointer capture
        if (element.closest(
            '[data-swipe-ignore="true"], button, a, input, select, textarea, [role="link"], [role="switch"], [contenteditable="true"]'
        )) {
            return true;
        }
        const label = element.closest('label');
        if (label && label.querySelector('input, select, textarea, button')) {
            return true;
        }
        return false;
    }, []);

    const applyTransform = useCallback((offset: number, animate: boolean) => {
        const track = trackRef.current;
        if (!track) return;
        const translateX = baseOffsetRef.current + offset;
        const transform = `translate3d(${translateX}px, 0, 0)`;
        if (transformRef.current !== transform) {
            track.style.transform = transform;
            transformRef.current = transform;
        }
        const transition = animate
            ? 'transform 280ms cubic-bezier(0.2, 0.7, 0.2, 1)'
            : 'none';
        if (transitionRef.current !== transition) {
            track.style.transition = transition;
            transitionRef.current = transition;
        }
    }, []);

    const measureWidth = useCallback(() => {
        const width = containerRef.current?.getBoundingClientRect().width || 0;
        if (width && width !== widthRef.current) {
            widthRef.current = width;
            baseOffsetRef.current = -width;
            applyTransform(dragOffsetRef.current, false);
        }
    }, [applyTransform]);

    useLayoutEffect(() => {
        measureWidth();
        const node = containerRef.current;
        if (!node) return undefined;
        let observer: ResizeObserver | null = null;
        if (typeof ResizeObserver !== 'undefined') {
            observer = new ResizeObserver(() => measureWidth());
            observer.observe(node);
        } else if (typeof window !== 'undefined') {
            window.addEventListener('resize', measureWidth);
        }
        return () => {
            observer?.disconnect();
            if (typeof window !== 'undefined') {
                window.removeEventListener('resize', measureWidth);
            }
        };
    }, [measureWidth]);

    useLayoutEffect(() => {
        displayIndexRef.current = displayIndex;
        if (!needsResetRef.current) return;
        needsResetRef.current = false;
        dragOffsetRef.current = 0;
        applyTransform(0, false);
    }, [applyTransform, displayIndex]);

    useEffect(() => {
        return () => {
            if (rafRef.current) {
                cancelAnimationFrame(rafRef.current);
            }
        };
    }, []);

    useEffect(() => {
        if (!scrollToTopOnChange) return;
        if (!hasScrolledRef.current) {
            hasScrolledRef.current = true;
            return;
        }
        if (typeof window === 'undefined') return;
        window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
    }, [activeTab, scrollToTopOnChange]);

    const resetToIndex = useCallback(
        (index: number) => {
            displayIndexRef.current = index;
            setDisplayIndex(index);
            dragOffsetRef.current = 0;
            applyTransform(0, false);
        },
        [applyTransform]
    );

    const animateToIndex = useCallback(
        (index: number) => {
            if (index === displayIndexRef.current) {
                dragOffsetRef.current = 0;
                applyTransform(0, true);
                return;
            }
            const delta = index - displayIndexRef.current;
            const width = widthRef.current || 0;
            if (Math.abs(delta) !== 1 || width === 0) {
                resetToIndex(index);
                return;
            }
            pendingIndexRef.current = index;
            isAnimatingRef.current = true;
            dragOffsetRef.current = 0;
            applyTransform(delta > 0 ? -width : width, true);
        },
        [applyTransform, resetToIndex]
    );

    useLayoutEffect(() => {
        if (isDraggingRef.current || isAnimatingRef.current) return;
        if (!hasMountedRef.current) {
            hasMountedRef.current = true;
            resetToIndex(activeIndex);
            return;
        }
        if (activeIndex !== displayIndexRef.current) {
            const distance = Math.abs(activeIndex - displayIndexRef.current);
            if (distance === 1) {
                animateToIndex(activeIndex);
            } else {
                resetToIndex(activeIndex);
            }
        }
    }, [activeIndex, animateToIndex, resetToIndex]);

    const scheduleDragUpdate = useCallback(() => {
        if (rafRef.current) return;
        rafRef.current = requestAnimationFrame(() => {
            rafRef.current = 0;
            applyTransform(dragOffsetRef.current, false);
        });
    }, [applyTransform]);

    const setDraggingClass = useCallback((value: boolean) => {
        const node = containerRef.current;
        if (!node) return;
        node.classList.toggle('swipe-tabs--dragging', value);
    }, []);

    const cancelAnimationForDrag = useCallback(() => {
        if (!isAnimatingRef.current) return;
        const pendingIndex = pendingIndexRef.current;
        pendingIndexRef.current = null;
        isAnimatingRef.current = false;
        needsResetRef.current = false;
        const targetIndex = pendingIndex ?? activeIndex;
        resetToIndex(targetIndex);
    }, [activeIndex, resetToIndex]);

    const startDrag = useCallback(
        (x: number, y: number, pointerId: number | null, touchId: number | null) => {
            if (swipeDisabled || tabs.length <= 1) return;
            if (isAnimatingRef.current) {
                cancelAnimationForDrag();
            }
            measureWidth();
            dragRef.current.pointerId = pointerId;
            dragRef.current.touchId = touchId;
            dragRef.current.startX = x;
            dragRef.current.startY = y;
            dragRef.current.startIndex = displayIndexRef.current;
            dragRef.current.isActive = true;
            dragRef.current.isDragging = false;
            dragOffsetRef.current = 0;
        },
        [cancelAnimationForDrag, measureWidth, swipeDisabled, tabs.length]
    );

    const sidebarDragStartedRef = useRef(false);
    const sidebarDragProgressRef = useRef(0);

    const updateDrag = useCallback(
        (x: number, y: number, preventDefault: () => void, pointerType?: string) => {
            const state = dragRef.current;
            if (!state.isActive) return;
            const dx = x - state.startX;
            const dy = y - state.startY;

            if (!state.isDragging) {
                if (Math.abs(dx) < DRAG_START_DISTANCE && Math.abs(dy) < DRAG_START_DISTANCE) {
                    return;
                }
                if (Math.abs(dx) <= Math.abs(dy)) {
                    return;
                }
                state.isDragging = true;
                isDraggingRef.current = true;
                setDraggingClass(true);
                // Capture pointer now that drag has started
                if (state.pointerId !== null && containerRef.current?.setPointerCapture) {
                    try {
                        containerRef.current.setPointerCapture(state.pointerId);
                    } catch {
                        // Ignore capture failures
                    }
                }
            }

            if (!state.isDragging) return;

            const width = widthRef.current || 1;
            let offset = dx;
            const atStart = state.startIndex <= 0;
            const atEnd = state.startIndex >= tabs.length - 1;
            // Don't apply resistance at start if onSwipePastStart is defined (sidebar can open)
            const hasVirtualTabAtStart = !!onSwipePastStart || !!onSwipePastStartProgress;

            // Calculate sidebar drag progress when swiping right on first tab
            const isSidebarSwipe = atStart && dx > 0 && hasVirtualTabAtStart;
            if (isSidebarSwipe) {
                // Notify drag start
                if (!sidebarDragStartedRef.current) {
                    sidebarDragStartedRef.current = true;
                    onSwipePastStartDragStart?.();
                }
                // Calculate progress (0 to 1) based on sidebar width
                const progress = Math.min(1, dx / sidebarWidth);
                sidebarDragProgressRef.current = progress;
                onSwipePastStartProgress?.(progress);
                // Don't move the page - only the sidebar moves
                offset = 0;
            } else if ((atStart && dx > 0) || (atEnd && dx < 0)) {
                // Apply resistance at edges when not doing sidebar swipe
                offset = dx * EDGE_RESISTANCE;
            }

            offset = Math.max(Math.min(offset, width), -width);
            dragOffsetRef.current = offset;
            scheduleDragUpdate();

            if (pointerType !== 'mouse') {
                preventDefault();
            }
        },
        [onSwipePastStart, onSwipePastStartProgress, onSwipePastStartDragStart, scheduleDragUpdate, sidebarWidth, tabs.length]
    );

    const finishDrag = useCallback(
        (cancelled: boolean) => {
            const state = dragRef.current;
            if (!state.isActive) return;

            state.isActive = false;
            state.pointerId = null;
            state.touchId = null;

            const wasDragging = state.isDragging;
            state.isDragging = false;
            isDraggingRef.current = false;
            setDraggingClass(false);

            // Handle sidebar drag end
            const wasSidebarDragging = sidebarDragStartedRef.current;
            const sidebarProgress = sidebarDragProgressRef.current;
            sidebarDragStartedRef.current = false;
            sidebarDragProgressRef.current = 0;

            if (!wasDragging) return;

            if (rafRef.current) {
                cancelAnimationFrame(rafRef.current);
                rafRef.current = 0;
            }

            const width = widthRef.current || 1;
            const dx = dragOffsetRef.current;
            const thresholdPx = width * threshold;
            let targetIndex = state.startIndex;

            // Check if this was a sidebar gesture (using the flag, not dx which is 0 for sidebar swipes)
            const shouldOpenSidebar = !cancelled && sidebarProgress > 0.3; // 30% threshold

            if (wasSidebarDragging) {
                // Call the new drag end callback with whether to open
                onSwipePastStartDragEnd?.(shouldOpenSidebar);

                // Also call legacy callback if sidebar should open
                if (shouldOpenSidebar && onSwipePastStart) {
                    onSwipePastStart();
                }
            } else if (!cancelled && Math.abs(dx) > thresholdPx) {
                const direction = dx < 0 ? 1 : -1;
                targetIndex = Math.min(Math.max(state.startIndex + direction, 0), tabs.length - 1);

                // If swiping right on first tab without new callbacks, call onSwipePastStart
                if (state.startIndex === 0 && dx > 0 && onSwipePastStart && !onSwipePastStartDragEnd) {
                    onSwipePastStart();
                }
            }

            dragOffsetRef.current = 0;
            if (targetIndex === state.startIndex) {
                applyTransform(0, true);
            } else {
                pendingIndexRef.current = targetIndex;
                isAnimatingRef.current = true;
                applyTransform(targetIndex > state.startIndex ? -width : width, true);
            }

            if (targetIndex !== state.startIndex) {
                const nextTab = tabs[targetIndex];
                if (nextTab !== activeTab) {
                    onTabChange(nextTab);
                }
            }
        },
        [activeTab, applyTransform, onTabChange, onSwipePastStart, onSwipePastStartDragEnd, setDraggingClass, sidebarWidth, tabs, threshold]
    );

    const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {
        if (swipeDisabled || shouldIgnoreSwipe(event.target)) return;
        if (event.pointerType === 'mouse' && event.button !== 0) return;
        if (dragRef.current.isActive) return;
        startDrag(event.clientX, event.clientY, event.pointerId, null);
        // Don't capture pointer immediately - let taps work normally
        // Capture will happen in updateDrag when drag actually starts
    };

    const handlePointerMove = (event: PointerEvent<HTMLDivElement>) => {
        const state = dragRef.current;
        if (!state.isActive || state.pointerId !== event.pointerId) return;
        const nativeEvent = event.nativeEvent as globalThis.PointerEvent;
        const coalesced = typeof nativeEvent.getCoalescedEvents === 'function'
            ? nativeEvent.getCoalescedEvents()
            : [];
        const latest = coalesced.length > 0 ? coalesced[coalesced.length - 1] : nativeEvent;
        updateDrag(
            latest.clientX,
            latest.clientY,
            () => {
                if (event.nativeEvent.cancelable) {
                    event.preventDefault();
                }
            },
            event.pointerType
        );
    };

    const handlePointerUp = (event: PointerEvent<HTMLDivElement>) => {
        if (dragRef.current.pointerId !== event.pointerId) return;
        finishDrag(false);
        if (containerRef.current?.hasPointerCapture?.(event.pointerId)) {
            containerRef.current.releasePointerCapture(event.pointerId);
        }
    };

    const handlePointerCancel = (event: PointerEvent<HTMLDivElement>) => {
        if (dragRef.current.pointerId !== event.pointerId) return;
        finishDrag(true);
        if (containerRef.current?.hasPointerCapture?.(event.pointerId)) {
            containerRef.current.releasePointerCapture(event.pointerId);
        }
    };

    const handleTouchStart = (event: TouchEvent<HTMLDivElement>) => {
        if (swipeDisabled || shouldIgnoreSwipe(event.target)) return;
        if (dragRef.current.isActive) return;
        const touch = event.changedTouches[0];
        if (!touch) return;
        startDrag(touch.clientX, touch.clientY, null, touch.identifier);
    };

    const handleTouchMove = (event: TouchEvent<HTMLDivElement>) => {
        const state = dragRef.current;
        if (!state.isActive || state.touchId === null) return;
        const touch = findTouchById(event.changedTouches, state.touchId);
        if (!touch) return;
        updateDrag(touch.clientX, touch.clientY, () => {
            if (event.nativeEvent.cancelable) {
                event.preventDefault();
            }
        });
    };

    const handleTouchEnd = (event: TouchEvent<HTMLDivElement>) => {
        const state = dragRef.current;
        if (!state.isActive || state.touchId === null) return;
        const touch = findTouchById(event.changedTouches, state.touchId);
        if (!touch) return;
        finishDrag(false);
    };

    const handleTouchCancel = (event: TouchEvent<HTMLDivElement>) => {
        const state = dragRef.current;
        if (!state.isActive || state.touchId === null) return;
        const touch = findTouchById(event.changedTouches, state.touchId);
        if (!touch) return;
        finishDrag(true);
    };

    const handleTransitionEnd = (event: TransitionEvent<HTMLDivElement>) => {
        if (event.propertyName !== 'transform') return;
        if (!isAnimatingRef.current) return;
        const nextIndex = pendingIndexRef.current;
        pendingIndexRef.current = null;
        isAnimatingRef.current = false;
        if (nextIndex === null) return;
        dragOffsetRef.current = 0;
        displayIndexRef.current = nextIndex;
        needsResetRef.current = true;
        setDisplayIndex(nextIndex);
    };

    const swipeEnabled = !swipeDisabled && tabs.length > 1;
    const rootClassName = [
        'swipe-tabs',
        swipeEnabled ? 'swipe-tabs--snap' : 'swipe-tabs--static',
        className
    ]
        .filter(Boolean)
        .join(' ');
    const panelInnerClasses = ['swipe-tabs-panel-inner', panelClassName].filter(Boolean).join(' ');
    const usePointerEvents = typeof window !== 'undefined' && 'PointerEvent' in window;

    const renderRadius = Math.max(1, renderWindow);
    const prevIndex = renderRadius >= 1 && displayIndex > 0 ? displayIndex - 1 : null;
    const nextIndex =
        renderRadius >= 1 && displayIndex < tabs.length - 1 ? displayIndex + 1 : null;
    const slotTabs = [
        prevIndex !== null ? { tab: tabs[prevIndex], index: prevIndex } : null,
        { tab: tabs[displayIndex], index: displayIndex },
        nextIndex !== null ? { tab: tabs[nextIndex], index: nextIndex } : null
    ];

    return (
        <div
            ref={containerRef}
            className={rootClassName}
            onPointerDown={usePointerEvents ? handlePointerDown : undefined}
            onPointerMove={usePointerEvents ? handlePointerMove : undefined}
            onPointerUp={usePointerEvents ? handlePointerUp : undefined}
            onPointerCancel={usePointerEvents ? handlePointerCancel : undefined}
            onTouchStart={!usePointerEvents ? handleTouchStart : undefined}
            onTouchMove={!usePointerEvents ? handleTouchMove : undefined}
            onTouchEnd={!usePointerEvents ? handleTouchEnd : undefined}
            onTouchCancel={!usePointerEvents ? handleTouchCancel : undefined}
        >
            <div className="swipe-tabs-track" ref={trackRef} onTransitionEnd={handleTransitionEnd}>
                {slotTabs.map((slot, slotIndex) => {
                    const tab = slot?.tab ?? null;
                    const key = tab !== null ? String(tab) : `empty-${slotIndex}`;
                    return (
                        <section key={key} className="swipe-tabs-panel">
                            <div className={panelInnerClasses}>
                                {tab !== null ? renderPanel(tab) : null}
                            </div>
                        </section>
                    );
                })}
            </div>
        </div>
    );
}
