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

import {
  floatingItemClassName,
  floatingItemFocusClassName,
} from "@/components/ui/floating-surface";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { useLogoFallback } from "@/hooks/useLogoFallback";
import { inferProviderFromModelName, providerBrand } from "@/lib/provider-brand";
import { cn } from "@/lib/utils";

const pickerWidthClassName = "w-[min(18rem,calc(100vw-2rem))]";
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 = 200;

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;
}

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();
}

function compactModelName(model?: string | null): string | null {
  const value = model?.trim();
  if (!value) return null;
  return value.split("/").at(-1) || value;
}

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;
  onRequestComposerFocus?: () => void;
  provider?: string | null;
  providerLabel?: string | null;
  needsSetup?: boolean;
  fallbackModelName?: string | null;
  isHero: boolean;
  onClick?: () => void;
}

export function ModelPresetBadge({
  label,
  modelDetail,
  modelPreset,
  modelPresets = [],
  onPresetChange,
  onRequestComposerFocus,
  provider,
  providerLabel,
  needsSetup = false,
  fallbackModelName,
  isHero,
  onClick,
}: ModelPresetBadgeProps) {
  const { t } = useTranslation();
  const [open, setOpen] = useState(false);
  const [motion, setMotion] = useState<PresetMotion | null>(null);
  const [motionWidth, setMotionWidth] = useState<number | null>(null);
  const gestureRef = useRef<PresetGesture | null>(null);
  const suppressClickRef = useRef(false);
  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 fallbackPreset = fallbackModelName
    ? modelPresets.find((preset) => preset.model?.trim() === fallbackModelName.trim())
    : undefined;
  const fallbackDisplayLabel = fallbackPreset?.name
    || fallbackModelName?.trim().split(/[/:]/).pop()
    || null;
  const displayLabel = fallbackDisplayLabel || label;
  const displayModelDetail = fallbackPreset
    ? fallbackPreset.model
    : fallbackModelName
      ? null
      : modelDetail;
  const displayProvider = fallbackPreset?.provider
    || (fallbackModelName ? inferProviderFromModelName(fallbackModelName) : provider);
  const presets = !activeName
    ? modelPresets
    : listedIndex < 0
      ? [activePreset, ...modelPresets]
      : modelPresets.map((preset, index) => index === listedIndex ? activePreset : preset);
  const opensSetup = Boolean(onClick);
  const canSwitch = !opensSetup && 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 switchModelLabel = t("thread.composer.switchModel", {
    defaultValue: "Switch model for this chat",
  });

  const selectPreset = (name: string) => {
    setOpen(false);
    if (name !== activeName) onPresetChange?.(name);
    requestAnimationFrame(() => onRequestComposerFocus?.());
  };

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

  const clearMotion = () => {
    setMotion(null);
    setMotionWidth(null);
  };

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

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

  const 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,
    });
  };

  const handlePointerDown = (event: PointerEvent<HTMLButtonElement>) => {
    if (!canSwitch || gestureRef.current || motion) 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;
      setMotionWidth(Math.round(gesture.target.getBoundingClientRect().width) || null);
      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;
  };

  const handlePointerMove = (event: PointerEvent<HTMLButtonElement>) => {
    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);
  };

  const finishGesture = (event: PointerEvent<HTMLButtonElement>, 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) {
      clearMotion();
      return;
    }
    suppressClickRef.current = true;
    const selected = presets[wrapIndex(gesture.baseIndex + gesture.step, presets.length)];
    setMotion((current) => current && { ...current, remainder: 0, settling: true });
    if (selected && selected.name !== activeName) selectPreset(selected.name);
  };

  const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
    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) selectPreset(next.name);
  };

  const pill = (
    <PresetPill
      label={displayLabel}
      modelDetail={displayModelDetail}
      provider={displayProvider}
      providerLabel={fallbackModelName ? null : providerLabel}
      needsSetup={needsSetup}
      fallbackModelName={fallbackModelName}
      fallbackFromLabel={fallbackModelName ? label : null}
      isHero={isHero}
    />
  );

  if (!canSwitch) {
    const Container = opensSetup ? "button" : "span";
    return (
      <Container
        aria-label={fallbackModelName ? `${displayLabel} (fallback from ${label})` : label}
        type={opensSetup ? "button" : undefined}
        onClick={opensSetup ? onClick : undefined}
        className={cn(
          "thread-composer-model-badge group inline-flex w-fit min-w-0 max-w-[min(18rem,44vw)] appearance-none border-0 bg-transparent p-0 shadow-none",
          opensSetup && "cursor-pointer focus-visible:outline-none",
          isHero ? "h-8" : "h-9",
        )}
      >
        {pill}
      </Container>
    );
  }

  return (
    <Popover
      open={open}
      onOpenChange={(nextOpen) => {
        setOpen(nextOpen);
        if (!nextOpen) requestAnimationFrame(() => onRequestComposerFocus?.());
      }}
    >
      <PopoverTrigger asChild>
        <button
          type="button"
          data-switching={motion ? "true" : undefined}
          aria-label={label}
          aria-expanded={open}
          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()}
          onKeyDown={handleKeyDown}
          onClickCapture={(event) => {
            if (!suppressClickRef.current) return;
            suppressClickRef.current = false;
            event.preventDefault();
            event.stopPropagation();
          }}
          style={{
            touchAction: "manipulation",
            width: motionWidth ? `${motionWidth}px` : undefined,
          }}
          className={cn(
            "thread-composer-model-badge group relative inline-flex w-fit min-w-0 max-w-[min(18rem,44vw)] cursor-pointer appearance-none border-0 bg-transparent p-0 shadow-none focus-visible:outline-none",
            motion && "z-10 cursor-grabbing",
            !motion && "cursor-grab",
            isHero ? "h-8" : "h-9",
          )}
        >
          {motion ? (
            <>
              <span data-testid="composer-model-pill-layout" className="invisible inline-flex h-full shrink-0" aria-hidden>
                {pill}
              </span>
              <span
                data-testid="composer-model-pill-viewport"
                className={cn(
                  "composer-model-pill-viewport pointer-events-none absolute -left-2 right-0 overflow-hidden bg-transparent",
                  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-[calc(100%-0.5rem)] flex-col items-end gap-1 will-change-transform"
                  onTransitionEnd={(event) => {
                    if (motion.settling && event.currentTarget === event.target) clearMotion();
                  }}
                  style={{
                    paddingTop: isHero ? "10px" : "12px",
                    transform: `translate3d(0, ${-pillStride * (2 + motion.remainder)}px, 0)`,
                  }}
                >
                  {PILL_OFFSETS.map((offset) => {
                    const preset = presets[wrapIndex(motion.index + offset, presets.length)];
                    return (
                      <PresetPill
                        key={motion.index + offset}
                        label={preset.name}
                        modelDetail={preset.model}
                        provider={preset.provider}
                        isHero={isHero}
                        offset={offset}
                        scale={motion.settling ? 1 : dockScale(offset - motion.remainder)}
                      />
                    );
                  })}
                </span>
              </span>
            </>
          ) : pill}
        </button>
      </PopoverTrigger>
      <PopoverContent
        align="end"
        side="top"
        sideOffset={10}
        role="dialog"
        aria-label={switchModelLabel}
        onOpenAutoFocus={(event) => {
          event.preventDefault();
          const content = event.currentTarget;
          if (!(content instanceof HTMLElement)) return;
          const selected = content.querySelector<HTMLElement>(
            '[role="option"][aria-selected="true"]',
          );
          selected?.focus();
        }}
        className={cn(
          pickerWidthClassName,
          "origin-[var(--radix-popover-content-transform-origin)] p-1.5 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-bottom-1 data-[state=open]:slide-in-from-bottom-1 duration-200 ease-out motion-reduce:animate-none",
        )}
      >
        <div
          role="listbox"
          aria-label={switchModelLabel}
          className="max-h-[min(16rem,var(--radix-popover-content-available-height))] overflow-y-auto py-1 scrollbar-thin scrollbar-track-transparent"
        >
          {presets.map((preset) => (
            <PresetOption
              key={preset.name}
              preset={preset}
              selected={preset.name === activeName}
              onSelect={selectPreset}
            />
          ))}
        </div>
      </PopoverContent>
    </Popover>
  );
}

function PresetOption({
  preset,
  selected,
  onSelect,
}: {
  preset: ModelPresetOption;
  selected: boolean;
  onSelect: (name: string) => void;
}) {
  const detail = compactModelName(preset.model);
  return (
    <button
      type="button"
      role="option"
      aria-label={preset.name}
      aria-selected={selected}
      onClick={() => onSelect(preset.name)}
      className={cn(
        floatingItemClassName,
        floatingItemFocusClassName,
        "flex min-h-9 w-full cursor-pointer gap-2.5 px-2.5 py-1.5 text-left hover:bg-muted/55",
        selected && "bg-muted/55 text-foreground",
      )}
    >
      <PresetProviderIcon
        label={preset.name}
        modelDetail={detail}
        provider={preset.provider}
        isHero={false}
      />
      <span className="flex min-w-0 flex-1 items-baseline gap-1.5 overflow-hidden whitespace-nowrap">
        <span className="shrink-0 text-[13px] font-medium text-foreground">{preset.name}</span>
        {detail && detail !== preset.name ? (
          <span className="truncate text-[12px] text-muted-foreground">{detail}</span>
        ) : null}
      </span>
      {selected ? <Check className="h-4 w-4 shrink-0 text-foreground/80" aria-hidden /> : null}
    </button>
  );
}

function PresetPill({
  label,
  modelDetail,
  provider,
  providerLabel,
  needsSetup = false,
  fallbackModelName,
  fallbackFromLabel,
  isHero,
  offset,
  scale,
}: {
  label: string;
  modelDetail?: string | null;
  provider?: string | null;
  providerLabel?: string | null;
  needsSetup?: boolean;
  fallbackModelName?: string | null;
  fallbackFromLabel?: 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 title = [...new Set([label, modelDetail, providerLabel].filter(Boolean))].join(" · ");
  const fallbackTitle = fallbackModelName
    ? `${fallbackFromLabel || label} · using ${fallbackModelName}`
    : title;

  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={fallbackTitle || undefined}
      className={cn(
        "composer-model-badge composer-model-pill inline-flex h-full max-w-full min-w-0 shrink-0 items-center rounded-full border border-border/55 bg-card font-medium text-foreground/70",
        "w-fit",
        "transition-[color,background-color,border-color,transform] duration-150 ease-out group-focus-visible:ring-2 group-focus-visible: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",
      )}
      style={scale === undefined ? undefined : {
        height: `${isHero ? 32 : 36}px`,
        transform: `scale(${scale.toFixed(4)})`,
        zIndex: Math.round(scale * 100),
      }}
    >
      <PresetProviderIcon
        label={label}
        modelDetail={modelDetail}
        provider={inferredProvider}
        needsSetup={needsSetup}
        testId={needsSetup ? "composer-model-setup-icon" : `composer-model-logo${inferredProvider ? `-${inferredProvider}` : ""}`}
        isHero={isHero}
      />
      <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>
  );
}

function PresetProviderIcon({
  label,
  modelDetail,
  provider,
  needsSetup = false,
  testId,
  isHero,
}: {
  label: string;
  modelDetail?: string | null;
  provider?: string | null;
  needsSetup?: boolean;
  testId?: string;
  isHero: boolean;
}) {
  const inferredProvider = needsSetup
    ? null
    : provider || inferProviderFromModelName(modelDetail || label);
  const brand = providerBrand(inferredProvider);
  const { logoUrl, onLogoError, onLogoLoad } = useLogoFallback(brand?.logoUrls);
  return (
    <span
      data-testid={testId}
      className={cn(
        "grid shrink-0 place-items-center",
        needsSetup && "text-amber-800 dark:text-amber-200",
        isHero ? "h-4 w-4" : "h-[18px] w-[18px]",
      )}
      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.5 w-3.5" : "h-[18px] w-[18px]")}
          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>
  );
}
