"use client";

import { useEffect, useId, useMemo, useRef, useState } from "react";

// A text field that also offers what has already been typed before. The list
// opens on focus, collapses to whatever matches as the user keeps typing, and
// closes on Escape, on blur or once something is picked — so it never gets in
// the way of typing a brand new value.
//
// In `multi` mode the field holds a comma-separated list: only the token being
// typed is matched, and picking an option appends it instead of replacing the
// whole field. That is how the library's search tags are edited.
export default function Combobox({
  label,
  value,
  options,
  onChange,
  placeholder,
  maxLength,
  required,
  multi = false,
  hint,
}: {
  label: string;
  value: string;
  options: readonly string[];
  onChange: (value: string) => void;
  placeholder?: string;
  maxLength?: number;
  required?: boolean;
  multi?: boolean;
  hint?: string;
}) {
  const [open, setOpen] = useState(false);
  const [highlight, setHighlight] = useState(0);
  const listId = useId();
  const wrapper = useRef<HTMLLabelElement | null>(null);

  const [head, token] = useMemo(() => {
    if (!multi) return ["", value];
    const separator = value.lastIndexOf(",");
    return separator < 0
      ? ["", value]
      : [value.slice(0, separator + 1), value.slice(separator + 1)];
  }, [multi, value]);

  const chosen = useMemo(
    () =>
      new Set(
        multi
          ? value
              .split(",")
              .map((entry) => entry.trim().toLowerCase())
              .filter(Boolean)
          : [],
      ),
    [multi, value],
  );

  const matches = useMemo(() => {
    const needle = token.trim().toLowerCase();
    return options
      .filter((option) => !chosen.has(option.toLowerCase()))
      .filter((option) => !needle || option.toLowerCase().includes(needle))
      .slice(0, 12);
  }, [chosen, options, token]);

  useEffect(() => {
    setHighlight(0);
  }, [token, open]);

  // A click anywhere else closes the list; a blur alone would fire before the
  // click on an option registers.
  useEffect(() => {
    if (!open) return;
    function onPointerDown(event: MouseEvent) {
      if (!wrapper.current?.contains(event.target as Node)) setOpen(false);
    }
    document.addEventListener("mousedown", onPointerDown);
    return () => document.removeEventListener("mousedown", onPointerDown);
  }, [open]);

  function choose(option: string) {
    onChange(multi ? `${head}${head ? " " : ""}${option}, ` : option);
    setOpen(false);
  }

  return (
    <label className="combobox" ref={wrapper}>
      {label}
      <div className="combobox-field">
        <input
          value={value}
          maxLength={maxLength}
          required={required}
          placeholder={placeholder}
          autoComplete="off"
          role="combobox"
          aria-expanded={open}
          aria-controls={listId}
          onFocus={() => setOpen(true)}
          onChange={(event) => {
            onChange(event.target.value);
            setOpen(true);
          }}
          onKeyDown={(event) => {
            if (event.key === "Escape") {
              setOpen(false);
              return;
            }
            if (!matches.length) return;
            if (event.key === "ArrowDown") {
              event.preventDefault();
              setOpen(true);
              setHighlight((current) => (current + 1) % matches.length);
            } else if (event.key === "ArrowUp") {
              event.preventDefault();
              setHighlight((current) => (current - 1 + matches.length) % matches.length);
            } else if (event.key === "Enter" && open) {
              event.preventDefault();
              choose(matches[highlight] ?? matches[0]);
            }
          }}
        />
        {options.length > 0 && (
          <button
            type="button"
            className="combobox-toggle"
            aria-label={open ? "Zatvori listu" : "Prikaži listu"}
            onClick={() => setOpen((current) => !current)}
          >
            ⌄
          </button>
        )}
        {open && matches.length > 0 && (
          <ul className="combobox-list" id={listId} role="listbox">
            {matches.map((option, index) => (
              <li key={option}>
                <button
                  type="button"
                  role="option"
                  aria-selected={index === highlight}
                  className={index === highlight ? "active" : ""}
                  onMouseEnter={() => setHighlight(index)}
                  onClick={() => choose(option)}
                >
                  {option}
                </button>
              </li>
            ))}
          </ul>
        )}
      </div>
      {hint && <small>{hint}</small>}
    </label>
  );
}
