import {
  useEffect,
  useLayoutEffect,
  useRef,
  useState,
  type KeyboardEvent,
  type PointerEvent,
} from "react";
import { CircleHelp, Sparkles } from "lucide-react";

import { useLogoFallback } from "@/hooks/useLogoFallback";
import { inferProviderFromModelName, providerBrand } from "@/lib/provider-brand";
import { cn } from "@/lib/utils";

export interface ModelPresetOption {
  name: string;
  model?: string | null;
  provider?: string | null;
}

interface ModelPresetBadgeProps {
  label: string;
  modelDetail?: string | null;
  modelPreset?: string | null;
  modelPresets?: ModelPresetOption[];
  onPresetChange?: (name: string) => void;
  provider?: string | null;
  providerLabel?: string | null;
  needsSetup?: boolean;
  fallbackModelName?: string | null;
  isHero: boolean;
  onClick?: () => void;
}

interface PresetGesture {
  active: boolean;
  baseIndex: number;
  latestY: number;
  pointerId: number;
  startY: number;
  step: number;
  target: HTMLElement;
  timer: ReturnType<typeof setTimeout> | null;
}

interface PresetMotion {
  index: number;
  remainder: number;
  settling: boolean;
}

const LONG_PRESS_MS = 400;
const PRESS_SLOP_PX = 8;
const PILL_GAP_PX = 4;
const PILL_OFFSETS = [-2, -1, 0, 1, 2] as const;
const HANDOFF_THRESHOLD = 0.56;
const DOCK_MAX_SCALE = 1.08;
const DOCK_RADIUS = 1.5;
const SETTLE_MS = 180;

function wrapIndex(index: number, length: number): number {
  return ((index % length) + length) % length;
}

function dockScale(distanceFromFocus: number): number {
  const distance = Math.abs(distanceFromFocus);
  if (distance >= DOCK_RADIUS) return 1;
  const influence = (1 + Math.cos(Math.PI * distance / DOCK_RADIUS)) / 2;
  return 1 + (DOCK_MAX_SCALE - 1) * influence;
}

function stepWithHysteresis(raw: number, current: number): number {
  let next = current;
  while (raw > next + HANDOFF_THRESHOLD) next += 1;
  while (raw < next - HANDOFF_THRESHOLD) next -= 1;
  return next;
}

function preventTouchScroll(event: TouchEvent) {
  if (event.cancelable) event.preventDefault();
}

export function ModelPresetBadge({
  label,
  modelDetail,
  modelPreset,
  modelPresets = [],
  onPresetChange,
  provider,
  providerLabel,
  needsSetup = false,
  fallbackModelName,
  isHero,
  onClick,
}: ModelPresetBadgeProps) {
  const activeName = modelPreset?.trim() || "";
  const listedIndex = modelPresets.findIndex((preset) => preset.name === activeName);
  const activePreset: ModelPresetOption = {
    ...(listedIndex >= 0 ? modelPresets[listedIndex] : undefined),
    name: activeName,
    model: modelDetail ?? modelPresets[listedIndex]?.model,
    provider: provider || modelPresets[listedIndex]?.provider,
  };
  const presets = !activeName
    ? modelPresets
    : listedIndex < 0
      ? [activePreset, ...modelPresets]
      : modelPresets.map((preset, index) => index === listedIndex ? activePreset : preset);
  const interactive = Boolean(onClick);
  const canSwitch = !interactive && Boolean(onPresetChange) && activeName !== "" && presets.length > 1;
  const currentIndex = Math.max(0, presets.findIndex((preset) => preset.name === activeName));
  const pillHeight = isHero ? 32 : 36;
  const pillStride = pillHeight + PILL_GAP_PX;
  const [motion, setMotion] = useState<PresetMotion | null>(null);
  const gestureRef = useRef<PresetGesture | null>(null);

  function clearGesture() {
    const gesture = gestureRef.current;
    if (gesture?.timer) clearTimeout(gesture.timer);
    if (gesture?.active) gesture.target.removeEventListener("touchmove", preventTouchScroll);
    gestureRef.current = null;
  }

  useEffect(() => {
    if (!canSwitch) {
      clearGesture();
      setMotion(null);
    }
    return clearGesture;
  }, [canSwitch]);

  useEffect(() => {
    if (!motion?.settling) return;
    const timer = setTimeout(() => setMotion(null), SETTLE_MS + 80);
    return () => clearTimeout(timer);
  }, [motion?.settling]);

  function updateMotion(gesture: PresetGesture, clientY: number) {
    const raw = -(clientY - gesture.startY) / pillStride;
    gesture.step = stepWithHysteresis(raw, gesture.step);
    setMotion({ index: gesture.baseIndex + gesture.step, remainder: raw - gesture.step, settling: false });
  }

  function handlePointerDown(event: PointerEvent<HTMLElement>) {
    if (!canSwitch || gestureRef.current || motion || event.isPrimary === false) return;
    if (event.pointerType === "mouse" && event.button !== 0) return;
    const gesture: PresetGesture = {
      active: false,
      baseIndex: currentIndex,
      latestY: event.clientY,
      pointerId: event.pointerId,
      startY: event.clientY,
      step: 0,
      target: event.currentTarget,
      timer: null,
    };
    gesture.timer = setTimeout(() => {
      if (gestureRef.current !== gesture) return;
      gesture.active = true;
      updateMotion(gesture, gesture.latestY);
      gesture.target.addEventListener("touchmove", preventTouchScroll, { passive: false });
      try {
        gesture.target.setPointerCapture(gesture.pointerId);
      } catch { /* The pointer may already have ended. */ }
    }, LONG_PRESS_MS);
    gestureRef.current = gesture;
  }

  function handlePointerMove(event: PointerEvent<HTMLElement>) {
    const gesture = gestureRef.current;
    if (!gesture || gesture.pointerId !== event.pointerId) return;
    gesture.latestY = event.clientY;
    if (!gesture.active) {
      if (Math.abs(event.clientY - gesture.startY) > PRESS_SLOP_PX) clearGesture();
      return;
    }
    event.preventDefault();
    updateMotion(gesture, event.clientY);
  }

  function finishGesture(event: PointerEvent<HTMLElement>, commit: boolean) {
    const gesture = gestureRef.current;
    if (!gesture || gesture.pointerId !== event.pointerId) return;
    clearGesture();
    if (event.currentTarget.hasPointerCapture?.(gesture.pointerId)) {
      event.currentTarget.releasePointerCapture?.(gesture.pointerId);
    }
    if (!commit || !gesture.active) {
      setMotion(null);
      return;
    }
    const selected = presets[wrapIndex(gesture.baseIndex + gesture.step, presets.length)];
    setMotion((current) => current && { ...current, remainder: 0, settling: true });
    if (selected && selected.name !== activeName) onPresetChange?.(selected.name);
  }

  function handleKeyDown(event: KeyboardEvent<HTMLElement>) {
    if (!canSwitch) return;
    const targetByKey: Record<string, number> = {
      ArrowUp: currentIndex - 1,
      ArrowDown: currentIndex + 1,
      Home: 0,
      End: presets.length - 1,
    };
    const target = targetByKey[event.key];
    if (target === undefined) return;
    event.preventDefault();
    const next = presets[wrapIndex(target, presets.length)];
    if (next?.name !== activeName) onPresetChange?.(next.name);
  }

  const previewIndex = wrapIndex(motion?.index ?? currentIndex, presets.length);
  const previewPreset = presets[previewIndex];
  const Container = interactive || canSwitch ? "button" : "span";
  const trackOffset = motion ? -pillStride * (2 + motion.remainder) : 0;

  return (
    <Container
      data-switching={motion ? "true" : undefined}
      data-settling={motion?.settling ? "true" : undefined}
      aria-label={label}
      aria-orientation={canSwitch ? "vertical" : undefined}
      aria-valuemax={canSwitch ? presets.length - 1 : undefined}
      aria-valuemin={canSwitch ? 0 : undefined}
      aria-valuenow={canSwitch ? previewIndex : undefined}
      aria-valuetext={canSwitch ? previewPreset?.name || label : undefined}
      role={canSwitch ? "spinbutton" : undefined}
      type={interactive || canSwitch ? "button" : undefined}
      onClick={interactive ? onClick : undefined}
      onKeyDown={handleKeyDown}
      onPointerDown={handlePointerDown}
      onPointerMove={handlePointerMove}
      onPointerLeave={(event) => {
        const gesture = gestureRef.current;
        if (gesture && gesture.pointerId === event.pointerId && !gesture.active) clearGesture();
      }}
      onPointerUp={(event) => finishGesture(event, true)}
      onPointerCancel={(event) => finishGesture(event, false)}
      onLostPointerCapture={(event) => finishGesture(event, false)}
      onContextMenu={(event) => {
        if (gestureRef.current?.active) event.preventDefault();
      }}
      onDragStart={(event) => event.preventDefault()}
      style={{ touchAction: canSwitch ? "manipulation" : undefined }}
      className={cn(
        "thread-composer-model-badge group/model-badge relative inline-flex w-fit min-w-0 max-w-[min(18rem,44vw)] justify-end appearance-none border-0 bg-transparent p-0 shadow-none",
        interactive && "cursor-pointer",
        canSwitch && "cursor-grab select-none focus-visible:outline-none",
        motion && "z-10 cursor-grabbing",
        isHero ? "h-8" : "h-9",
      )}
    >
      <PresetPill
        className={motion && "invisible"}
        label={label}
        modelDetail={modelDetail}
        provider={provider}
        providerLabel={providerLabel}
        needsSetup={needsSetup}
        fallbackModelName={fallbackModelName}
        isHero={isHero}
      />
      {motion ? (
        <span
          data-testid="composer-model-pill-viewport"
          className={cn(
            "composer-model-pill-viewport pointer-events-none absolute right-0 w-max max-w-[calc(44vw+0.5rem)] overflow-hidden bg-transparent pl-2 sm:max-w-[18.5rem]",
            isHero ? "-bottom-2.5 -top-2.5" : "-bottom-3 -top-3",
          )}
          aria-hidden
        >
          <span
            data-testid="composer-model-pill-track"
            data-settling={motion.settling ? "true" : undefined}
            className="composer-model-pill-track ml-auto flex w-max max-w-full flex-col items-end gap-1 will-change-transform"
            onTransitionEnd={(event) => {
              if (motion.settling && event.currentTarget === event.target) setMotion(null);
            }}
            style={{
              paddingTop: isHero ? "10px" : "12px",
              transform: `translate3d(0, ${trackOffset}px, 0)`,
            }}
          >
            {PILL_OFFSETS.map((offset) => {
              const virtualIndex = motion.index + offset;
              const preset = presets[wrapIndex(virtualIndex, presets.length)];
              const scale = motion.settling ? 1 : dockScale(offset - motion.remainder);
              return (
                <PresetPill
                  key={virtualIndex}
                  label={preset.name}
                  modelDetail={preset.model}
                  provider={preset.provider}
                  isHero={isHero}
                  offset={offset}
                  scale={scale}
                />
              );
            })}
          </span>
        </span>
      ) : null}
    </Container>
  );
}

function PresetPill({
  className,
  label,
  modelDetail,
  provider,
  providerLabel,
  needsSetup = false,
  fallbackModelName,
  isHero,
  offset,
  scale,
}: {
  className?: string | false | null;
  label: string;
  modelDetail?: string | null;
  provider?: string | null;
  providerLabel?: string | null;
  needsSetup?: boolean;
  fallbackModelName?: string | null;
  isHero: boolean;
  offset?: number;
  scale?: number;
}) {
  const labelRef = useRef<HTMLSpanElement | null>(null);
  const [labelOverflows, setLabelOverflows] = useState(false);
  const inferredProvider = needsSetup
    ? null
    : provider || inferProviderFromModelName(modelDetail || label);
  const brand = providerBrand(inferredProvider);
  const { logoUrl, onLogoError, onLogoLoad } = useLogoFallback(brand?.logoUrls);
  const title = [...new Set([label, modelDetail, providerLabel].filter(Boolean))].join(" · ");
  const logoTestId = offset !== undefined
    ? undefined
    : needsSetup
      ? "composer-model-setup-icon"
      : `composer-model-logo${inferredProvider ? `-${inferredProvider}` : ""}`;

  useLayoutEffect(() => {
    const node = labelRef.current;
    if (!node) return;
    const update = () => setLabelOverflows(node.scrollWidth > node.clientWidth + 1);
    update();
    const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(update);
    observer?.observe(node);
    return () => observer?.disconnect();
  }, [label]);

  return (
    <span
      data-fallback={fallbackModelName ? "true" : undefined}
      data-preset-offset={offset}
      title={fallbackModelName || title || undefined}
      className={cn(
        "composer-model-badge composer-model-pill inline-flex h-full w-fit max-w-full min-w-0 shrink-0 items-center rounded-full border border-border/55 bg-card font-medium text-foreground/70",
        offset === undefined && "shadow-[0_2px_8px_rgba(15,23,42,0.045)]",
        "transition-[color,background-color,border-color,transform] duration-150 ease-out group-focus-visible/model-badge:ring-2 group-focus-visible/model-badge:ring-ring/45",
        needsSetup && "border-amber-500/35 bg-amber-50/70 text-amber-900 dark:bg-amber-500/10 dark:text-amber-200",
        isHero ? "gap-1.5 px-2.5 text-[12px]" : "gap-2 px-3 text-[12.5px]",
        offset !== undefined && "composer-model-pill-dock",
        className,
      )}
      style={scale === undefined ? undefined : {
        height: `${isHero ? 32 : 36}px`,
        transform: `scale(${scale.toFixed(4)})`,
        zIndex: Math.round(scale * 100),
      }}
    >
      <span
        data-testid={logoTestId}
        className={cn(
          "grid shrink-0 place-items-center overflow-hidden",
          needsSetup ? "text-amber-800 dark:text-amber-200" : "rounded-full border bg-background",
          isHero ? "h-4 w-4" : "h-[18px] w-[18px]",
        )}
        style={{
          borderColor: !needsSetup && brand ? `${brand.color}28` : undefined,
          boxShadow: !needsSetup && brand ? `inset 0 0 0 1px ${brand.color}18` : undefined,
        }}
        aria-hidden
      >
        {needsSetup ? (
          <CircleHelp className={cn(isHero ? "h-3 w-3" : "h-3.5 w-3.5")} strokeWidth={1.8} />
        ) : logoUrl ? (
          <img
            src={logoUrl}
            alt=""
            draggable={false}
            decoding="async"
            loading="lazy"
            className={cn("object-contain", isHero ? "h-3 w-3" : "h-3.5 w-3.5")}
            onLoad={onLogoLoad}
            onError={onLogoError}
          />
        ) : brand ? (
          <span
            className={cn(
              "grid h-full w-full place-items-center rounded-full text-white",
              isHero ? "text-[7.5px]" : "text-[8px]",
            )}
            style={{ backgroundColor: brand.color }}
          >
            {brand.initials.slice(0, 2)}
          </span>
        ) : (
          <Sparkles className="h-3 w-3 text-muted-foreground/65" />
        )}
      </span>
      <span
        ref={labelRef}
        className={cn(
          "thread-composer-model-label min-w-0 overflow-hidden whitespace-nowrap text-center",
          labelOverflows && "thread-composer-model-label-fade",
        )}
      >
        {label}
      </span>
    </span>
  );
}
