"use client";

import { useCallback, useEffect, useId, useRef, useState } from "react";
import { Check, Server as ServerIcon } from "lucide-react";

import type { StreamingProviderDescriptor } from "@/services/providers/types";
import { cn } from "@/lib/utils";

/**
 * Standalone streaming-server selector.
 *
 * This is the `Server: ▾` control the player page shows beside the Back button.
 * It is intentionally a sibling of the player rather than a child of any header,
 * and it is a controlled component so the player owns which provider is active
 * and can re-resolve the source when it changes.
 *
 * Why this is a listbox and not a `<select>`
 *
 * A native `<select>` cannot be styled to match the rest of the player: the
 * option list is drawn by the operating system, so the dark pill, the type and
 * the rounded corners of the trigger stop the moment the menu opens. This is the
 * usual reason to hand-roll one, so the cost is worth naming — a hand-rolled
 * listbox owns its own keyboard model, and getting that wrong makes the control
 * unusable without a mouse. What is implemented here:
 *
 *   • `role="listbox"` with `role="option"` children, so the roles are correct
 *     even though the markup is `<button>` + `<li>`.
 *   • Roving focus: the list owns focus once open, Arrow/Home/End move the
 *     active option, Enter/Space choose it, Escape closes without choosing.
 *   • Focus returns to the trigger on close, so a keyboard user is never dumped
 *     back at the top of the document.
 *   • The trigger is a real `<button>` with `aria-haspopup`/`aria-expanded`/
 *     `aria-controls`, so it is reachable by Tab and announces its state.
 *   • Pointer dismissal, and dismissal on Tab, so the list cannot be left
 *     stranded open over the video.
 *   • `disabled` while a resolve is in flight, matching the player's own state.
 *
 * The list is rendered only while open. An always-mounted list would put
 * focusable `role="option"` elements in the tab order while the control looked
 * closed, which is worse than a name missing from the server-rendered HTML.
 */
export function ServerSelector({
  providers,
  value,
  onChange,
  busy = false,
  className,
}: {
  providers: StreamingProviderDescriptor[];
  value: string | null;
  onChange: (id: string) => void;
  /** Disables the control while a source is being resolved. */
  busy?: boolean;
  className?: string;
}) {
  const listId = useId();
  const rootRef = useRef<HTMLDivElement>(null);
  const triggerRef = useRef<HTMLButtonElement>(null);
  const optionRefs = useRef<Array<HTMLLIElement | null>>([]);
  const [open, setOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(0);

  /*
    The highlighted entry is the one that is actually playing, not merely the
    first. While the list is open this tracks the keyboard instead, so a stored
    preference survives the visitor arrowing past it and closing with Escape.
  */
  const selectedIndex = Math.max(
    0,
    providers.findIndex((p) => p.id === (value ?? providers[0]?.id)),
  );
  const active = providers[selectedIndex];

  const openList = useCallback(
    (focusIndex = selectedIndex) => {
      if (busy) return;
      setOpen(true);
      setActiveIndex(focusIndex);
    },
    [busy, selectedIndex],
  );

  const closeList = useCallback((returnFocus: boolean) => {
    setOpen(false);
    if (returnFocus) triggerRef.current?.focus();
  }, []);

  const choose = useCallback(
    (id: string) => {
      onChange(id);
      closeList(true);
    },
    [closeList, onChange],
  );

  // Focus follows the active option, once the options are actually mounted.
  useEffect(() => {
    if (!open) return;
    optionRefs.current[activeIndex]?.focus();
  }, [activeIndex, open]);

  // A pointer press anywhere else closes the list without choosing.
  useEffect(() => {
    if (!open) return;
    function onPointerDown(event: PointerEvent | MouseEvent) {
      if (!rootRef.current?.contains(event.target as Node)) setOpen(false);
    }
    document.addEventListener("pointerdown", onPointerDown);
    return () => document.removeEventListener("pointerdown", onPointerDown);
  }, [open]);

  /*
    No servers configured means there is nothing to select, so this returns `null`
    rather than a disabled "No servers" chip. A control that cannot change
    anything is not a control, and an empty badge above the player is just noise
    over the video. The "nothing to play yet" state is already communicated
    inside the player area by `VideoPlayer`'s placeholder.

    This has to come *after* every hook above: an early return placed among them
    would make the number of hooks depend on the provider list, so a deployment
    that removed its last server would run a different hook sequence than one
    with servers and React would refuse to keep the two consistent.
  */
  if (providers.length === 0 || !active) return null;

  const last = providers.length - 1;

  function onListKeyDown(event: React.KeyboardEvent<HTMLUListElement>) {
    switch (event.key) {
      case "ArrowDown":
        event.preventDefault();
        setActiveIndex((i) => (i >= last ? 0 : i + 1));
        return;
      case "ArrowUp":
        event.preventDefault();
        setActiveIndex((i) => (i <= 0 ? last : i - 1));
        return;
      case "Home":
        event.preventDefault();
        setActiveIndex(0);
        return;
      case "End":
        event.preventDefault();
        setActiveIndex(last);
        return;
      case "Escape":
        // Close without changing servers, and put focus back where it came from.
        event.preventDefault();
        event.stopPropagation();
        closeList(true);
        return;
      case "Enter":
      case " ":
        event.preventDefault();
        choose(providers[activeIndex]!.id);
        return;
      case "Tab":
        // Let focus leave, but do not leave the list stranded open.
        setOpen(false);
        return;
      default:
    }
  }

  function onTriggerKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) {
    if (event.key === "ArrowDown" || event.key === "ArrowUp") {
      event.preventDefault();
      // ArrowUp from the trigger lands on the end, the way a list does.
      openList(event.key === "ArrowUp" ? providers.length - 1 : selectedIndex);
    }
  }

  return (
    <div ref={rootRef} className={cn("relative", className)}>
      <button
        ref={triggerRef}
        type="button"
        aria-label="Streaming server"
        aria-haspopup="listbox"
        aria-expanded={open}
        aria-controls={open ? listId : undefined}
        disabled={busy}
        onClick={() => (open ? closeList(false) : openList())}
        onKeyDown={onTriggerKeyDown}
        className={cn(
          "inline-flex h-9 items-center gap-1.5 rounded-pill bg-black/55 pl-3 pr-2.5 text-[12px] font-semibold text-white/90 ring-1 ring-white/15 backdrop-blur-md transition-colors hover:bg-black/75",
          "disabled:cursor-not-allowed disabled:opacity-60",
        )}
      >
        <ServerIcon className="size-3.5 shrink-0" />
        <span className="sr-only sm:not-sr-only">Server</span>
        <span className="max-w-[110px] truncate sm:max-w-[130px]">{active.name}</span>
        <svg
          aria-hidden
          viewBox="0 0 12 12"
          className={cn(
            "size-3 shrink-0 text-white/60 transition-transform",
            open && "rotate-180",
          )}
          fill="none"
          stroke="currentColor"
          strokeWidth={1.6}
        >
          <path d="M3 4.5 6 7.5 9 4.5" />
        </svg>
      </button>

      {open ? (
        <ul
          id={listId}
          role="listbox"
          aria-label="Streaming servers"
          onKeyDown={onListKeyDown}
          className={cn(
            // Opens downward from the trigger and is left-aligned to it, because
            // the control itself now sits on the left of the stage.
            "absolute left-0 top-[calc(100%+0.375rem)] z-30 min-w-[13rem] overflow-hidden rounded-xl",
            "border border-line bg-surface-2/95 p-1 shadow-2xl shadow-black/50 backdrop-blur-md",
          )}
        >
          {providers.map((provider, index) => {
            const isSelected = provider.id === active.id;
            return (
              <li
                key={provider.id}
                ref={(node) => {
                  optionRefs.current[index] = node;
                }}
                role="option"
                aria-selected={isSelected}
                data-provider={provider.id}
                tabIndex={index === activeIndex ? 0 : -1}
                onClick={() => choose(provider.id)}
                className={cn(
                  "flex cursor-pointer items-center justify-between gap-2 rounded-lg px-2.5 py-2 text-[12px] font-semibold outline-none transition-colors",
                  index === activeIndex ? "bg-white/10 text-ink" : "text-ink-muted hover:bg-white/6",
                )}
              >
                <span className="truncate">
                  {provider.name}
                  {provider.badge ? ` (${provider.badge})` : ""}
                </span>
                {isSelected ? <Check className="size-3.5 shrink-0 text-brand-300" /> : null}
              </li>
            );
          })}
        </ul>
      ) : null}
    </div>
  );
}
