import { createContext, useContext, createSignal, createEffect, onCleanup, Show, For, JSXElement } from "solid-js";
import { Icon } from "./Icons.tsx";
import { ButtonUI, BUTTON_COLOR_WHITE, BUTTON_COLOR_BLUE } from "./Buttons.tsx";

export interface TutorialStep {
    title?: string;
    content: JSXElement;
    target?: string | (() => HTMLElement | null) | null;
    placement?: string;
    offset?: number;
    onEnter?: () => void;
    onLeave?: () => void;
}

interface TutorialProviderProps {
    steps: TutorialStep[];
    spotlightPadding?: number;
    onEnd?: () => void;
    children?: JSXElement;
}

interface TutorialContextValue {
    isActive: boolean;
    currentStepIndex: number;
    totalSteps: number;
    currentStep: TutorialStep | null;
    start: (stepIndex?: number) => void;
    end: () => void;
    next: () => void;
    previous: () => void;
    goTo: (stepIndex: number) => void;
}

interface TutorialInternalContextValue {
    isActive: () => boolean;
    currentStepIndex: () => number;
    totalSteps: () => number;
    currentStep: () => TutorialStep | null;
    targetRect: () => DOMRect | null;
    start: (stepIndex?: number) => void;
    end: () => void;
    next: () => void;
    previous: () => void;
    goTo: (stepIndex: number) => void;
}

const TutorialContext = createContext<TutorialContextValue | null>(null);
const TutorialInternalContext = createContext<TutorialInternalContextValue | null>(null);

const _TUTORIAL_ANIMATION_DURATION = 100;

export function useTutorial(): TutorialContextValue {
    const context = useContext(TutorialContext);
    if (!context) {
        throw new Error("useTutorial must be used within a TutorialProvider");
    }
    return context;
}

function useTutorialInternal(): TutorialInternalContextValue {
    return useContext(TutorialInternalContext)!;
}

interface PopoverPosition {
    top: number;
    left: number;
    placement: string;
}

function calculatePopoverPosition(targetRect: DOMRect, popoverRect: DOMRect, placement: string, offset: number): PopoverPosition {
    const parts = placement.split("-");
    const basePlacement = parts[0];
    const alignment = parts[1] || "center";

    let top = 0;
    let left = 0;
    let finalPlacement = placement;
    const padding = 16;

    switch (basePlacement) {
        case "top":
            top = targetRect.top - popoverRect.height - offset;
            break;
        case "bottom":
            top = targetRect.bottom + offset;
            break;
        case "left":
            left = targetRect.left - popoverRect.width - offset;
            break;
        case "right":
            left = targetRect.right + offset;
            break;
    }

    if (basePlacement === "top" || basePlacement === "bottom") {
        switch (alignment) {
            case "start":
                left = targetRect.left;
                break;
            case "end":
                left = targetRect.right - popoverRect.width;
                break;
            default:
                left = targetRect.left + (targetRect.width - popoverRect.width) / 2;
        }
    } else {
        switch (alignment) {
            case "start":
                top = targetRect.top;
                break;
            case "end":
                top = targetRect.bottom - popoverRect.height;
                break;
            default:
                top = targetRect.top + (targetRect.height - popoverRect.height) / 2;
        }
    }

    const viewportHeight = window.innerHeight;
    const viewportWidth = window.innerWidth;

    if (basePlacement === "bottom" && top + popoverRect.height > viewportHeight - padding) {
        const flippedTop = targetRect.top - popoverRect.height - offset;
        if (flippedTop >= padding) {
            top = flippedTop;
            finalPlacement = placement.replace("bottom", "top");
        }
    } else if (basePlacement === "top" && top < padding) {
        const flippedTop = targetRect.bottom + offset;
        if (flippedTop + popoverRect.height <= viewportHeight - padding) {
            top = flippedTop;
            finalPlacement = placement.replace("top", "bottom");
        }
    } else if (basePlacement === "right" && left + popoverRect.width > viewportWidth - padding) {
        const flippedLeft = targetRect.left - popoverRect.width - offset;
        if (flippedLeft >= padding) {
            left = flippedLeft;
            finalPlacement = placement.replace("right", "left");
        }
    } else if (basePlacement === "left" && left < padding) {
        const flippedLeft = targetRect.right + offset;
        if (flippedLeft + popoverRect.width <= viewportWidth - padding) {
            left = flippedLeft;
            finalPlacement = placement.replace("left", "right");
        }
    }

    if (left < padding) {
        left = padding;
    } else if (left + popoverRect.width > viewportWidth - padding) {
        left = viewportWidth - popoverRect.width - padding;
    }

    if (top < padding) {
        top = padding;
    } else if (top + popoverRect.height > viewportHeight - padding) {
        top = viewportHeight - popoverRect.height - padding;
    }

    return { top, left, placement: finalPlacement };
}

interface SpotlightOverlayProps {
    // Solid's `h` auto-invokes zero-arg function props on read — these
    // are plain values inside the component body, not accessors.
    targetRect: DOMRect | null;
    hasTarget: boolean;
    padding: number;
    onclick?: () => void;
}

interface AnimatedRect {
    left: number;
    top: number;
    width: number;
    height: number;
}

function SpotlightOverlay(props: SpotlightOverlayProps) {
    const [borderRadius, setBorderRadius] = createSignal(3.2);
    const [animatedRect, setAnimatedRect] = createSignal<AnimatedRect | null>(null);
    const [overlayOpacity, setOverlayOpacity] = createSignal(0);

    createEffect(() => {
        const cssValue = getComputedStyle(document.documentElement).getPropertyValue("--radius-default").trim();
        if (cssValue) {
            const remValue = parseFloat(cssValue);
            if (!isNaN(remValue)) {
                const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
                setBorderRadius(remValue * rootFontSize);
            }
        }
    });

    createEffect(() => {
        requestAnimationFrame(() => setOverlayOpacity(1));
    });

    createEffect(() => {
        const target = props.targetRect;
        if (target) {
            setAnimatedRect({
                left: target.left,
                top: target.top,
                width: target.width,
                height: target.height,
            });
        } else {
            setAnimatedRect(null);
        }
    });

    const pad = () => props.padding ?? 8;
    const baseTransition = `all ${_TUTORIAL_ANIMATION_DURATION}ms cubic-bezier(0.4, 0, 0.2, 1)`;

    return (
        <Show when={props.hasTarget && animatedRect()} fallback={
            <div class="fixed inset-0 bg-black/50 z-150" onclick={() => props.onclick?.()} style={{
                    opacity: overlayOpacity(),
                    transition: `opacity ${_TUTORIAL_ANIMATION_DURATION}ms ease-out`,
                }}/>
        }>
            <div class="fixed z-150 pointer-events-none rounded-default" style={(() => {
                    const rect = animatedRect()!;
                    return {
                        left: (rect.left - pad()) + "px",
                        top: (rect.top - pad()) + "px",
                        width: (rect.width + pad() * 2) + "px",
                        height: (rect.height + pad() * 2) + "px",
                        "border-radius": borderRadius() + "px",
                        "box-shadow": `0 0 0 9999px rgba(0, 0, 0, ${0.5 * overlayOpacity()})`,
                        transition: baseTransition,
                    };
                })()}>
                <div class="fixed inset-0 -z-10 cursor-pointer" onclick={() => props.onclick?.()}/>
            </div>
        </Show>
    );
}

interface PopoverArrowProps {
    // Solid's `h` auto-invokes zero-arg function props on read.
    placement: string;
}

function PopoverArrow(props: PopoverArrowProps) {
    const basePlacement = () => props.placement.split("-")[0];

    const arrowStyles: Record<string, object> = {
        top: {
            bottom: "-8px", left: "50%", transform: "translateX(-50%)",
            "border-left": "8px solid transparent", "border-right": "8px solid transparent", "border-top": "8px solid white",
        },
        bottom: {
            top: "-8px", left: "50%", transform: "translateX(-50%)",
            "border-left": "8px solid transparent", "border-right": "8px solid transparent", "border-bottom": "8px solid white",
        },
        left: {
            right: "-8px", top: "50%", transform: "translateY(-50%)",
            "border-top": "8px solid transparent", "border-bottom": "8px solid transparent", "border-left": "8px solid white",
        },
        right: {
            left: "-8px", top: "50%", transform: "translateY(-50%)",
            "border-top": "8px solid transparent", "border-bottom": "8px solid transparent", "border-right": "8px solid white",
        },
    };

    const borderArrowStyles: Record<string, object> = {
        top: {
            bottom: "-9px", left: "50%", transform: "translateX(-50%)",
            "border-left": "9px solid transparent", "border-right": "9px solid transparent", "border-top": "9px solid #e5e5e5",
        },
        bottom: {
            top: "-9px", left: "50%", transform: "translateX(-50%)",
            "border-left": "9px solid transparent", "border-right": "9px solid transparent", "border-bottom": "9px solid #e5e5e5",
        },
        left: {
            right: "-9px", top: "50%", transform: "translateY(-50%)",
            "border-top": "9px solid transparent", "border-bottom": "9px solid transparent", "border-left": "9px solid #e5e5e5",
        },
        right: {
            left: "-9px", top: "50%", transform: "translateY(-50%)",
            "border-top": "9px solid transparent", "border-bottom": "9px solid transparent", "border-right": "9px solid #e5e5e5",
        },
    };

    return [
        <div class="absolute w-0 h-0" style={{
            ...borderArrowStyles[basePlacement()],
            width: "0",
            height: "0",
            transition: `all ${_TUTORIAL_ANIMATION_DURATION}ms cubic-bezier(0.4, 0, 0.2, 1)`,
        }}/>,
        <div class="absolute w-0 h-0" style={{
            ...arrowStyles[basePlacement()],
            transition: `all ${_TUTORIAL_ANIMATION_DURATION}ms cubic-bezier(0.4, 0, 0.2, 1)`,
        }}/>,
    ];
}

function TutorialPopover() {
    const ctx = useTutorialInternal();
    let popoverRef: HTMLDivElement | undefined;
    const [position, setPosition] = createSignal<{ top: number; left: number } | null>(null);
    const [currentPlacement, setCurrentPlacement] = createSignal("bottom");
    const [displayedPlacement, setDisplayedPlacement] = createSignal("bottom");
    const [isVisible, setIsVisible] = createSignal(false);
    const [isPositioned, setIsPositioned] = createSignal(false);
    const [contentOpacity, setContentOpacity] = createSignal(1);
    const [displayedStep, setDisplayedStep] = createSignal<TutorialStep | null>(ctx.currentStep());
    const [displayedStepIndex, setDisplayedStepIndex] = createSignal(ctx.currentStepIndex());
    const [showArrow, setShowArrow] = createSignal(false);

    let prevStepIndex = ctx.currentStepIndex();
    let isTransitioning = false;

    const placement = () => ctx.currentStep()?.placement ?? "bottom";
    const offset = () => ctx.currentStep()?.offset ?? 16;
    const hasTarget = () => !!ctx.currentStep()?.target;
    const displayHasTarget = () => !!displayedStep()?.target;

    const updatePosition = (immediate: boolean = false) => {
        if (!popoverRef) return;
        const popoverRect = popoverRef.getBoundingClientRect();

        let newTop, newLeft;
        let newPlacement = "bottom";

        if (!hasTarget()) {
            newTop = (window.innerHeight - popoverRect.height) / 2;
            newLeft = (window.innerWidth - popoverRect.width) / 2;
        } else if (ctx.targetRect()) {
            const newPosition = calculatePopoverPosition(ctx.targetRect()!, popoverRect, placement(), offset());
            newTop = newPosition.top;
            newLeft = newPosition.left;
            newPlacement = newPosition.placement;
        } else {
            newTop = (window.innerHeight - popoverRect.height) / 2;
            newLeft = (window.innerWidth - popoverRect.width) / 2;
        }

        setPosition({ top: newTop, left: newLeft });
        setCurrentPlacement(newPlacement);

        if (immediate || !isPositioned()) {
            setDisplayedPlacement(newPlacement);
        }
    };

    createEffect(() => {
        if (!popoverRef) return;

        requestAnimationFrame(() => {
            updatePosition(true);
            setIsPositioned(true);
            requestAnimationFrame(() => {
                setIsVisible(true);
                setTimeout(() => setShowArrow(true), _TUTORIAL_ANIMATION_DURATION);
            });
        });
    });

    createEffect(() => {
        const currentIdx = ctx.currentStepIndex();
        if (prevStepIndex === currentIdx) return;

        prevStepIndex = currentIdx;

        if (isTransitioning) return;
        isTransitioning = true;

        setIsFirstAppearance(false);
        setShowArrow(false);
        setContentOpacity(0);

        setTimeout(() => {
            setDisplayedStep(ctx.currentStep());
            setDisplayedStepIndex(currentIdx);

            requestAnimationFrame(() => {
                updatePosition(true);
                setTimeout(() => {
                    setContentOpacity(1);
                    setTimeout(() => {
                        setShowArrow(true);
                        isTransitioning = false;
                    }, _TUTORIAL_ANIMATION_DURATION / 2);
                }, 50);
            });
        }, _TUTORIAL_ANIMATION_DURATION / 2);
    });

    createEffect(() => {
        ctx.targetRect();
        if (!isTransitioning && isPositioned()) {
            updatePosition();
            setDisplayedPlacement(currentPlacement());
        }
    });

    createEffect(() => {
        if (!isPositioned()) return;

        const handleUpdate = () => {
            if (!isTransitioning) {
                updatePosition();
                setDisplayedPlacement(currentPlacement());
            }
        };

        window.addEventListener("scroll", handleUpdate, true);
        window.addEventListener("resize", handleUpdate);

        onCleanup(() => {
            window.removeEventListener("scroll", handleUpdate, true);
            window.removeEventListener("resize", handleUpdate);
        });
    });

    const isFirstStep = () => displayedStepIndex() === 0;
    const isLastStep = () => displayedStepIndex() === ctx.totalSteps() - 1;

    const [isFirstAppearance, setIsFirstAppearance] = createSignal(true);

    const getPopoverStyle = () => {
        const pos = position();
        if (!pos) {
            return {
                visibility: "hidden" as const,
                top: "-9999px",
                left: "-9999px",
            };
        }

        if (isFirstAppearance()) {
            return {
                top: pos.top + "px",
                left: pos.left + "px",
                opacity: isVisible() ? 1 : 0,
                transform: isVisible() ? "scale(1)" : "scale(0.95)",
                transition: `opacity ${_TUTORIAL_ANIMATION_DURATION}ms cubic-bezier(0.4, 0, 0.2, 1), transform ${_TUTORIAL_ANIMATION_DURATION}ms cubic-bezier(0.4, 0, 0.2, 1)`,
            };
        }

        return {
            top: pos.top + "px",
            left: pos.left + "px",
            opacity: isVisible() ? 1 : 0,
            transform: isVisible() ? "scale(1)" : "scale(0.95)",
            transition: `opacity ${_TUTORIAL_ANIMATION_DURATION}ms cubic-bezier(0.4, 0, 0.2, 1), transform ${_TUTORIAL_ANIMATION_DURATION}ms cubic-bezier(0.4, 0, 0.2, 1), top ${_TUTORIAL_ANIMATION_DURATION}ms cubic-bezier(0.4, 0, 0.2, 1), left ${_TUTORIAL_ANIMATION_DURATION}ms cubic-bezier(0.4, 0, 0.2, 1)`,
        };
    };

    const getContentStyle = () => ({
        opacity: contentOpacity(),
        transition: `opacity ${_TUTORIAL_ANIMATION_DURATION / 2}ms ease-out`,
    });

    return (
        <div ref={(el: HTMLDivElement) => popoverRef = el} class="fixed z-200 bg-surface rounded-default shadow-lg border border-line max-w-sm" style={getPopoverStyle()}>
            <Show when={displayHasTarget() && showArrow()}>
                <PopoverArrow placement={displayedPlacement()}/>
            </Show>

            <div style={getContentStyle()}>
                <div class="flex items-center justify-between p-4 pb-2">
                    <div class="flex items-center gap-2">
                        <Show when={displayedStep()?.title}>
                            <span class="font-medium text-ink">{displayedStep()?.title}</span>
                        </Show>
                        <span class="text-ss text-ink-muted">
                            {(displayedStepIndex() + 1) + " of " + ctx.totalSteps()}
                        </span>
                    </div>
                    <button onclick={() => ctx.end()} class="cursor-pointer text-ink-faint bg-transparent border-0 p-0 leading-none transition-colors hover:text-ink-soft">
                        <Icon icon="xmark" size={18}/>
                    </button>
                </div>

                <div class="px-4 pb-4 text-sm text-ink">
                    {displayedStep()?.content}
                </div>

                <div class="flex items-center justify-between px-4 pb-4 gap-2">
                    <div>
                        <Show when={!isFirstStep()}>
                            <ButtonUI color={BUTTON_COLOR_WHITE} small onclick={() => ctx.previous()}>
                                Previous
                            </ButtonUI>
                        </Show>
                    </div>
                    <div class="flex gap-2">
                        <Show when={isLastStep()} fallback={
                            <ButtonUI color={BUTTON_COLOR_BLUE} small onclick={() => ctx.next()}>
                                Next
                            </ButtonUI>
                        }>
                            <ButtonUI color={BUTTON_COLOR_BLUE} small onclick={() => ctx.end()}>
                                Finish
                            </ButtonUI>
                        </Show>
                    </div>
                </div>

                <Show when={ctx.totalSteps() > 1}>
                    <div class="flex justify-center gap-1.5 pb-3">
                        <For each={Array.from({ length: ctx.totalSteps() })}>{(_, i) => (
                            <div class={"w-2 h-2 rounded-full transition-all duration-300 ease-in-out " + (i() === displayedStepIndex() ? "bg-sky-600 scale-110" : "bg-surface-strong")}/>
                        )}</For>
                    </div>
                </Show>
            </div>
        </div>
    );
}

export function TutorialProvider(props: TutorialProviderProps) {
    const [isActive, setIsActive] = createSignal(false);
    const [currentStepIndex, setCurrentStepIndex] = createSignal(0);
    const [targetRect, setTargetRect] = createSignal<DOMRect | null>(null);

    const totalSteps = () => props.steps.length;
    const currentStep = () => isActive() && props.steps[currentStepIndex()] ? props.steps[currentStepIndex()] : null;

    createEffect(() => {
        if (!isActive() || !currentStep()) {
            setTargetRect(null);
            return;
        }

        const step = currentStep()!;
        if (!step.target) {
            setTargetRect(null);
            return;
        }

        const findTarget = (): HTMLElement | null => {
            if (typeof step.target === "function") {
                return step.target();
            }
            if (typeof step.target === "string") {
                return document.querySelector(step.target);
            }
            return null;
        };

        const updateTargetRect = () => {
            const target = findTarget();
            if (target) {
                setTargetRect(target.getBoundingClientRect());
                target.scrollIntoView({ behavior: "smooth", block: "center" });
            } else {
                setTargetRect(null);
            }
        };

        const timeoutId = setTimeout(updateTargetRect, 50);

        window.addEventListener("scroll", updateTargetRect, true);
        window.addEventListener("resize", updateTargetRect);

        onCleanup(() => {
            clearTimeout(timeoutId);
            window.removeEventListener("scroll", updateTargetRect, true);
            window.removeEventListener("resize", updateTargetRect);
        });
    });

    createEffect(() => {
        if (isActive() && currentStep()?.onEnter) {
            currentStep()!.onEnter!();
        }
    });

    const start = (stepIndex: number = 0) => {
        setCurrentStepIndex(Math.max(0, Math.min(stepIndex, props.steps.length - 1)));
        setIsActive(true);
    };

    const end = () => {
        if (currentStep()?.onLeave) {
            currentStep()!.onLeave!();
        }
        setIsActive(false);
        setCurrentStepIndex(0);
        props.onEnd?.();
    };

    const next = () => {
        if (currentStep()?.onLeave) {
            currentStep()!.onLeave!();
        }
        if (currentStepIndex() < props.steps.length - 1) {
            setCurrentStepIndex(currentStepIndex() + 1);
        } else {
            end();
        }
    };

    const previous = () => {
        if (currentStep()?.onLeave) {
            currentStep()!.onLeave!();
        }
        if (currentStepIndex() > 0) {
            setCurrentStepIndex(currentStepIndex() - 1);
        }
    };

    const goTo = (stepIndex: number) => {
        if (currentStep()?.onLeave) {
            currentStep()!.onLeave!();
        }
        setCurrentStepIndex(Math.max(0, Math.min(stepIndex, props.steps.length - 1)));
    };

    createEffect(() => {
        if (!isActive()) return;

        const handleKeyDown = (e: KeyboardEvent) => {
            if (e.key === "Escape") {
                end();
            } else if (e.key === "ArrowRight" || e.key === "Enter") {
                next();
            } else if (e.key === "ArrowLeft") {
                previous();
            }
        };

        document.addEventListener("keydown", handleKeyDown);
        onCleanup(() => document.removeEventListener("keydown", handleKeyDown));
    });

    const publicValue = (): TutorialContextValue => ({
        isActive: isActive(),
        currentStepIndex: currentStepIndex(),
        totalSteps: props.steps.length,
        currentStep: currentStep(),
        start,
        end,
        next,
        previous,
        goTo,
    });

    const internalValue: TutorialInternalContextValue = {
        isActive,
        currentStepIndex,
        totalSteps,
        currentStep,
        targetRect,
        start,
        end,
        next,
        previous,
        goTo,
    };

    return (
        <TutorialContext.Provider value={publicValue()}>
            <TutorialInternalContext.Provider value={internalValue}>
                {props.children}
                <Show when={isActive() && currentStep()}>
                    <SpotlightOverlay targetRect={targetRect()} hasTarget={!!currentStep()?.target} padding={props.spotlightPadding ?? 8} onclick={() => end()}/>
                    <TutorialPopover/>
                </Show>
            </TutorialInternalContext.Provider>
        </TutorialContext.Provider>
    );
}

interface StartTutorialButtonProps {
    stepIndex?: number;
    class?: string;
    children?: JSXElement;
}

export function StartTutorialButton(props: StartTutorialButtonProps) {
    const { start } = useTutorial();

    return (
        <ButtonUI color={BUTTON_COLOR_BLUE} onclick={() => start(props.stepIndex ?? 0)} class={props.class || ""}>
            {props.children ?? "Start Tutorial"}
        </ButtonUI>
    );
}
