"use client";

import { ChevronUpIcon } from "@/assets/icons";
import { cn } from "@/lib/utils";
import { ILabelValue } from "@/types/shared";
import {
  useCallback,
  useEffect,
  useId,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
  type ChangeEvent,
} from "react";
import { createPortal } from "react-dom";
import {
  type FieldErrors,
  type FieldValues,
  type Path,
  type UseFormRegister,
  type UseFormRegisterReturn,
} from "react-hook-form";
import {
  type SelectSearchMode,
  useSelectServerSearch,
} from "./use-select-server-search";

export type { SelectSearchMode } from "./use-select-server-search";

const DROPDOWN_GAP_PX = 8;
const DROPDOWN_MIN_WIDTH_PX = 200;
const DROPDOWN_MAX_HEIGHT_PX = 352;

type DropdownPlacement = "above" | "below";

type DropdownRect = {
  top: number;
  left: number;
  width: number;
  placement: DropdownPlacement;
};

/** Anchor a fixed portal menu to the trigger; avoids native `<select>` popover bugs inside transformed ancestors. */
function measureDropdownRect(trigger: HTMLElement): DropdownRect {
  const r = trigger.getBoundingClientRect();
  const width = Math.max(r.width, DROPDOWN_MIN_WIDTH_PX);

  const vv = window.visualViewport;
  const viewportLeft = vv?.offsetLeft ?? 0;
  const viewportTop = vv?.offsetTop ?? 0;
  const viewportWidth = vv?.width ?? window.innerWidth;
  const viewportHeight = vv?.height ?? window.innerHeight;

  const left = Math.min(
    Math.max(viewportLeft + 8, r.left),
    viewportLeft + viewportWidth - width - 8,
  );

  const spaceBelow = viewportTop + viewportHeight - r.bottom - DROPDOWN_GAP_PX;
  const spaceAbove = r.top - viewportTop - DROPDOWN_GAP_PX;
  const placement: DropdownPlacement =
    spaceBelow >= Math.min(DROPDOWN_MAX_HEIGHT_PX, 160) || spaceBelow >= spaceAbove
      ? "below"
      : "above";

  const top =
    placement === "below"
      ? r.bottom + DROPDOWN_GAP_PX
      : r.top - DROPDOWN_GAP_PX;

  return { top, left, width, placement };
}

/** Match options by label or value; exact / prefix matches rank higher. */
export function filterSelectItems(
  items: readonly ILabelValue[],
  query: string,
): ILabelValue[] {
  const q = query.trim().toLowerCase();
  if (!q) return [...items];

  const scored: { item: ILabelValue; score: number }[] = [];
  for (const item of items) {
    const v = String(item.value).toLowerCase();
    const l = item.label.toLowerCase();
    let score = 0;
    if (v === q) score = 1000;
    else if (v.startsWith(q)) score = 500;
    else if (l.startsWith(q)) score = 400;
    else if (l.includes(q)) score = 200;
    else if (v.includes(q)) score = 100;
    if (score > 0) scored.push({ item, score });
  }

  scored.sort(
    (a, b) =>
      b.score - a.score ||
      a.item.label.localeCompare(b.item.label, undefined, {
        sensitivity: "base",
      }),
  );
  return scored.map((s) => s.item);
}

type PropsType<T extends FieldValues> = {
  name: Path<T>;
  label: string;
  items: ILabelValue[];
  prefixIcon?: React.ReactNode;
  onChange?: (e: ChangeEvent<HTMLSelectElement>) => void;
  className?: string;
  register?: UseFormRegister<T>;
  errors?: FieldErrors<T>;
  required?: boolean;
  clearable?: boolean;
  onClear?: () => void;
  value?: string;
  dataCy?: string;
  disabled?: boolean;
  /** When true, replaces the native picker with a searchable list (label + value). */
  searchable?: boolean;
  /** Placeholder for the search field inside the dropdown (searchable only). */
  searchPlaceholder?: string;
  /**
   * `"client"` — filter `items` in the browser (default).
   * `"server"` — debounced React Query request via `searchFn` (requires `searchable`).
   */
  searchMode?: SelectSearchMode;
  /** Debounce delay (ms) before a server search runs. Default: `300`. */
  searchDebounceMs?: number;
  /** React Query key prefix; debounced search term is appended. Required when `searchMode` is `"server"`. */
  searchQueryKey?: readonly unknown[];
  /** Loads options for the debounced search term. Required when `searchMode` is `"server"`. */
  searchFn?: (query: string) => Promise<ILabelValue[]>;
  /**
   * External lazy-load mode: the parent owns `items`, searching and pagination.
   * Providing `onSearch` and/or `onLoadMore` opts in — the dropdown renders a search
   * box, streams `items` as-is and requests more on scroll. Pairs with an infinite query.
   */
  onSearch?: (query: string) => void;
  /** Fetch the next page; called when the option list is scrolled near its end. */
  onLoadMore?: () => void;
  /** Whether more pages are available to lazy-load. */
  hasMore?: boolean;
  /** Initial / search fetch in progress (external mode). */
  isLoading?: boolean;
  /** Next-page fetch in progress (external mode). */
  isLoadingMore?: boolean;
} & (
  | { placeholder?: string; defaultValue?: string }
  | { placeholder: string; defaultValue?: string }
) &
  Partial<UseFormRegisterReturn>;

export function Select<T extends FieldValues>(selectProps: PropsType<T>) {
  const hasControlledValue = Object.prototype.hasOwnProperty.call(
    selectProps,
    "value",
  );
  const {
    items,
    label,
    defaultValue,
    placeholder,
    prefixIcon,
    className,
    name,
    errors,
    required,
    onChange,
    onBlur,
    ref,
    clearable = false,
    onClear,
    value: controlledValue,
    dataCy,
    disabled = false,
    searchable = false,
    searchPlaceholder = "Search by label or value…",
    searchMode = "client",
    searchDebounceMs = 300,
    searchQueryKey,
    searchFn,
    onSearch,
    onLoadMore,
    hasMore = false,
    isLoading = false,
    isLoadingMore = false,
    register: _register,
    ...props
  } = selectProps;
  void _register;

  /** Parent-controlled list: search + pagination handled outside, `items` consumed as-is. */
  const isExternal =
    typeof onSearch === "function" || typeof onLoadMore === "function";
  /** Both `searchable` and external mode render the in-dropdown search box. */
  const showSearch = searchable || isExternal;

  if (
    process.env.NODE_ENV !== "production" &&
    searchable &&
    searchMode === "server" &&
    !searchFn
  ) {
    console.warn(
      `[Select "${String(name)}"] searchMode="server" requires searchFn.`,
    );
  }

  const effectiveSearchMode: SelectSearchMode =
    searchable && searchMode === "server" && searchFn ? "server" : "client";
  const id = useId();
  const listboxId = useId();
  const rootRef = useRef<HTMLDivElement>(null);
  const triggerRef = useRef<HTMLButtonElement>(null);
  const searchInputRef = useRef<HTMLInputElement>(null);
  const listRef = useRef<HTMLUListElement>(null);

  const [isOptionSelected, setIsOptionSelected] = useState(!!defaultValue);
  const [value, setValue] = useState(defaultValue || "");
  const [open, setOpen] = useState(false);
  const [query, setQuery] = useState("");
  const [mounted, setMounted] = useState(false);
  const [panelRect, setPanelRect] = useState<DropdownRect | null>(null);

  const error = errors && errors[name];

  useEffect(() => {
    if (hasControlledValue) {
      const nextValue = controlledValue ?? "";
      setValue(nextValue);
      setIsOptionSelected(!!nextValue);
    }
  }, [controlledValue, hasControlledValue]);

  const {
    isServerMode,
    displayItems: serverDisplayItems,
    isSearchLoading,
    isSearchError,
  } = useSelectServerSearch({
    searchMode: effectiveSearchMode,
    searchable,
    open,
    query,
    debounceMs: searchDebounceMs,
    searchQueryKey,
    searchFn,
    items,
    selectedValue: value,
  });

  const listItems = useMemo(() => {
    if (isExternal) return items;
    if (!searchable) return items;
    if (isServerMode && serverDisplayItems) return serverDisplayItems;
    return filterSelectItems(items, query);
  }, [isExternal, searchable, isServerMode, serverDisplayItems, items, query]);

  /** Lazy-load the next page once the option list nears its bottom (external mode). */
  const handleListScroll = useCallback(() => {
    if (!isExternal || !onLoadMore || !hasMore || isLoadingMore) return;
    const el = listRef.current;
    if (!el) return;
    if (el.scrollHeight - el.scrollTop - el.clientHeight < 40) {
      onLoadMore();
    }
  }, [isExternal, onLoadMore, hasMore, isLoadingMore]);

  const selectedLabel = useMemo(() => {
    const hit = listItems.find((i) => String(i.value) === String(value));
    if (hit) return hit.label;
    const fromItems = items.find((i) => String(i.value) === String(value));
    return fromItems?.label ?? (value ? String(value) : "");
  }, [listItems, items, value]);

  const syncPanelPosition = useCallback(() => {
    const el = triggerRef.current;
    if (!el) return;
    setPanelRect(measureDropdownRect(el));
  }, []);

  useEffect(() => {
    setMounted(true);
  }, []);

  useLayoutEffect(() => {
    if (!open) return;
    syncPanelPosition();
    const id = window.requestAnimationFrame(() => {
      window.requestAnimationFrame(() => syncPanelPosition());
    });
    return () => window.cancelAnimationFrame(id);
  }, [open, syncPanelPosition]);

  useEffect(() => {
    if (!open) return;
    const onScrollOrResize = () => syncPanelPosition();
    window.addEventListener("scroll", onScrollOrResize, true);
    window.addEventListener("resize", onScrollOrResize);
    const vv = window.visualViewport;
    vv?.addEventListener("resize", onScrollOrResize);
    vv?.addEventListener("scroll", onScrollOrResize);
    return () => {
      window.removeEventListener("scroll", onScrollOrResize, true);
      window.removeEventListener("resize", onScrollOrResize);
      vv?.removeEventListener("resize", onScrollOrResize);
      vv?.removeEventListener("scroll", onScrollOrResize);
    };
  }, [open, syncPanelPosition]);

  useEffect(() => {
    if (!open) return;
    const onPointerDown = (e: MouseEvent | TouchEvent) => {
      const t = e.target as Node;
      if (rootRef.current?.contains(t)) return;
      const panel = document.getElementById(listboxId);
      if (panel?.contains(t)) return;
      setOpen(false);
      setQuery("");
    };
    document.addEventListener("mousedown", onPointerDown);
    document.addEventListener("touchstart", onPointerDown, { passive: true });
    return () => {
      document.removeEventListener("mousedown", onPointerDown);
      document.removeEventListener("touchstart", onPointerDown);
    };
  }, [open, listboxId]);

  useEffect(() => {
    if (!showSearch || !open) return;
    const idFrame = window.requestAnimationFrame(() => {
      searchInputRef.current?.focus();
      searchInputRef.current?.select();
    });
    return () => window.cancelAnimationFrame(idFrame);
  }, [open, showSearch]);

  const emitChange = (next: string) => {
    if (!hasControlledValue) {
      setValue(next);
    }
    setIsOptionSelected(!!next);
    const synthetic = {
      target: { value: next, name },
      currentTarget: { value: next, name },
    } as unknown as ChangeEvent<HTMLSelectElement>;
    onChange?.(synthetic);
  };

  const handleClear = () => {
    if (!hasControlledValue) {
      setValue("");
    }
    setIsOptionSelected(false);
    onClear?.();
    emitChange("");
  };

  const pickItem = (itemValue: string) => {
    emitChange(itemValue);
    setOpen(false);
    setQuery("");
    triggerRef.current?.focus();
  };

  const dropdownItems = showSearch ? listItems : items;
  /** Mono value chip next to labels only helps the client/server search modes, not external lists. */
  const showItemValue = searchable && !isExternal;

  const dropdown =
    mounted &&
    open &&
    panelRect &&
    typeof document !== "undefined" &&
    createPortal(
      <div
        id={listboxId}
        role="listbox"
        aria-label={label}
        className="z-[1000000] max-h-[min(22rem,calc(100vh-6rem))] overflow-hidden rounded-xl border border-stroke/70 bg-white shadow-[0_18px_50px_-12px_rgba(15,23,42,0.25)] dark:border-white/10 dark:bg-dark-2 dark:shadow-[0_24px_60px_-12px_rgba(0,0,0,0.65)]"
        style={{
          position: "fixed",
          top: panelRect.top,
          left: panelRect.left,
          width: panelRect.width,
          transform:
            panelRect.placement === "above" ? "translateY(-100%)" : undefined,
        }}
      >
        {showSearch ? (
          <div className="border-b border-stroke/50 p-2 dark:border-white/[0.08]">
            <label htmlFor={`${listboxId}-search`} className="sr-only">
              {searchPlaceholder}
            </label>
            <input
              id={`${listboxId}-search`}
              ref={searchInputRef}
              type="search"
              value={query}
              onChange={(e) => {
                setQuery(e.target.value);
                if (isExternal) onSearch?.(e.target.value);
              }}
              onKeyDown={(e) => {
                if (e.key === "Escape") {
                  e.stopPropagation();
                  setOpen(false);
                  setQuery("");
                  triggerRef.current?.focus();
                }
              }}
              placeholder={searchPlaceholder}
              data-cy={dataCy ? `${dataCy}-search` : undefined}
              autoComplete="off"
              spellCheck={false}
              className="w-full rounded-lg border border-stroke/60 bg-white/95 px-3 py-2 text-sm text-dark outline-none ring-primary/20 placeholder:text-dark-5 focus:border-primary/60 focus:ring-2 dark:border-white/10 dark:bg-dark-3/90 dark:text-white dark:placeholder:text-dark-6"
            />
          </div>
        ) : null}
        <ul
          ref={listRef}
          onScroll={isExternal ? handleListScroll : undefined}
          className="max-h-[min(18rem,calc(100vh-10rem))] overflow-y-auto overscroll-contain p-1.5"
        >
          {(isExternal ? isLoading : searchable && isSearchLoading) ? (
            <li className="flex items-center justify-center gap-2 px-3 py-6 text-sm text-dark-5 dark:text-dark-6">
              <span className="inline-flex h-4 w-4 animate-spin rounded-full border-2 border-primary/25 border-t-primary" />
              {isExternal ? "Loading…" : "Searching…"}
            </li>
          ) : !isExternal && searchable && isSearchError ? (
            <li className="px-3 py-6 text-center text-sm text-error">
              Could not load options. Try again.
            </li>
          ) : dropdownItems.length === 0 ? (
            <li className="px-3 py-6 text-center text-sm text-dark-5 dark:text-dark-6">
              {showSearch ? "No matches." : "No options."}
            </li>
          ) : (
            <>
              {dropdownItems.map((item) => {
                const v = String(item.value);
                const isActive = v === String(value);
                return (
                  <li key={v}>
                    <button
                      type="button"
                      role="option"
                      aria-selected={isActive}
                      data-cy={dataCy ? `${dataCy}-option` : undefined}
                      data-value={v}
                      onClick={() => pickItem(v)}
                      className={cn(
                        "flex w-full items-center gap-2 rounded-lg px-3 py-2 text-start text-sm transition",
                        showItemValue ? "justify-between" : "justify-start",
                        isActive
                          ? "bg-primary/12 font-medium text-primary dark:bg-primary/20 dark:text-white"
                          : "text-dark hover:bg-stroke/30 dark:text-white dark:hover:bg-white/[0.06]",
                      )}
                    >
                      <span className="min-w-0 truncate">{item.label}</span>
                      {showItemValue ? (
                        <span className="shrink-0 font-mono text-xs text-dark-5 dark:text-dark-6">
                          {v}
                        </span>
                      ) : null}
                    </button>
                  </li>
                );
              })}
              {isExternal && isLoadingMore ? (
                <li className="flex items-center justify-center py-2">
                  <span className="inline-flex h-4 w-4 animate-spin rounded-full border-2 border-primary/25 border-t-primary" />
                </li>
              ) : null}
            </>
          )}
        </ul>
      </div>,
      document.body,
    );

  return (
    <div ref={rootRef} className={cn("space-y-3", className)}>
        <label
          htmlFor={id}
          className="block text-body-sm font-semibold capitalize text-dark dark:text-white"
        >
          {label}
          {required && <span className="ml-1 select-none text-error">*</span>}
        </label>

        <div className="relative">
          <select
            {...props}
            ref={ref}
            name={name}
            value={value}
            disabled={disabled}
            tabIndex={-1}
            aria-hidden
            data-cy={dataCy}
            className="sr-only"
            onChange={(e) => {
              if (!hasControlledValue) {
                setValue(e.target.value);
              }
              setIsOptionSelected(!!e.target.value);
              onChange?.(e);
            }}
            onBlur={onBlur}
          >
            {placeholder && (
              <option value="" disabled>
                {placeholder}
              </option>
            )}
            {items.map((item) => (
              <option key={String(item.value)} value={String(item.value)}>
                {item.label}
              </option>
            ))}
          </select>

          {prefixIcon && (
            <div className="pointer-events-none absolute left-4 top-1/2 z-[1] -translate-y-1/2">
              {prefixIcon}
            </div>
          )}

          <button
            ref={triggerRef}
            type="button"
            id={id}
            disabled={disabled}
            aria-haspopup="listbox"
            aria-expanded={open}
            aria-controls={open ? listboxId : undefined}
            onClick={() => {
              if (disabled) return;
              setOpen((o) => {
                if (o) setQuery("");
                return !o;
              });
            }}
            onKeyDown={(e) => {
              if (e.key === "Escape" && open) {
                e.preventDefault();
                setOpen(false);
                setQuery("");
              }
            }}
            className={cn(
              "flex w-full min-h-[52px] items-center rounded-xl border border-stroke/70 bg-white/90 px-5.5 py-3 text-start text-body-sm shadow-[0_1px_2px_rgba(16,24,40,0.04),0_8px_20px_rgba(16,24,40,0.04)] outline-none backdrop-blur-[1px] transition-all duration-200 focus:border-primary/70 focus:bg-white focus:shadow-[0_0_0_2px_rgba(59,130,246,0.14),0_10px_24px_rgba(59,130,246,0.08)] dark:border-dark-3/80 dark:bg-dark-2/85 dark:focus:border-primary/80 dark:focus:shadow-[0_0_0_2px_rgba(59,130,246,0.2),0_10px_24px_rgba(15,23,42,0.45)]",
              isOptionSelected && "text-dark dark:text-white",
              !isOptionSelected && "text-dark-5 dark:text-dark-6",
              disabled &&
                "cursor-not-allowed opacity-60 focus:shadow-none dark:focus:shadow-none",
              error &&
                "border-red/80 focus:border-red focus:shadow-[0_0_0_2px_rgba(239,68,68,0.16)] dark:border-red/80",
              prefixIcon && "pl-11.5",
              clearable && value && "pr-20",
              "pr-11",
            )}
          >
            <span className="min-w-0 flex-1 truncate">
              {value ? (
                showItemValue ? (
                  <>
                    <span className="text-dark dark:text-white">
                      {selectedLabel}
                    </span>
                    <span className="ml-2 font-mono text-xs text-dark-5 dark:text-dark-6">
                      {value}
                    </span>
                  </>
                ) : (
                  <span className="text-dark dark:text-white">
                    {selectedLabel}
                  </span>
                )
              ) : (
                placeholder || "Select…"
              )}
            </span>
          </button>

          {clearable && value && (
            <button
              type="button"
              onClick={handleClear}
              data-cy={dataCy ? `${dataCy}-clear` : undefined}
              className="absolute right-10 top-1/2 z-[1] -translate-y-1/2 text-dark-5 transition hover:scale-110 hover:text-dark dark:text-dark-6 dark:hover:text-white"
            >
              <svg
                width="16"
                height="16"
                viewBox="0 0 16 16"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  d="M12 4L4 12M4 4L12 12"
                  stroke="currentColor"
                  strokeWidth="2"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                />
              </svg>
            </button>
          )}

          <ChevronUpIcon className="pointer-events-none absolute right-4 top-1/2 -translate-y-1/2 rotate-180 text-dark-5 dark:text-dark-6" />
        </div>

        {dropdown}

      {error && (
        <p className="text-red mt-1.5 text-sm font-medium">
          {error.message as string}
        </p>
      )}
    </div>
  );
}
