/**
 * TagInput — WealthX Design System
 *
 * A tokenized, free-form contact-tag input: selected tags render as removable
 * {@link Chip}s, and an inline text field autocompletes against existing tags
 * with a "Create '<x>'" affordance for new ones. Multi-select, de-duplicated
 * (case-insensitive), with an optional `maxTags` cap.
 *
 * Pure & controlled — the host owns the value (`value` / `onChange`) and the
 * autocomplete source (`suggestions`). No data fetching here.
 *
 * Used to tag a contact (ManageContactTagsDialog), tag an imported batch
 * (CSV import) and pick a campaign audience by tag.
 */
import {
  type ReactElement,
  useEffect,
  useId,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { createPortal } from "react-dom";
import { cn } from "@/lib/utils";
import { Chip } from "./chip";

export interface TagInputProps {
  /** Currently selected tags (controlled). */
  value: string[];
  /** Called with the next selection whenever a tag is added or removed. */
  onChange: (next: string[]) => void;
  /** Existing tags to autocomplete against (e.g. the broker's tag library). */
  suggestions?: string[];
  placeholder?: string;
  /** Cap the number of tags. When reached, the field stops accepting input. */
  maxTags?: number;
  /** Allow creating a brand-new tag from the typed text. Defaults to true. */
  allowCreate?: boolean;
  disabled?: boolean;
  /** Accessible label for the text field (falls back to "Add a tag"). */
  "aria-label"?: string;
  id?: string;
  className?: string;
}

const norm = (s: string): string => s.trim().toLowerCase();

export function TagInput({
  value,
  onChange,
  suggestions = [],
  placeholder = "Add a tag…",
  maxTags,
  allowCreate = true,
  disabled = false,
  "aria-label": ariaLabel = "Add a tag",
  id,
  className,
}: TagInputProps): ReactElement {
  const [query, setQuery] = useState("");
  const [open, setOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(0);
  const inputRef = useRef<HTMLInputElement>(null);
  const controlRef = useRef<HTMLDivElement>(null);
  const [menuPos, setMenuPos] = useState<{
    top: number;
    left: number;
    width: number;
  } | null>(null);
  const reactId = useId();
  const listId = `${id ?? reactId}-taglist`;

  const atMax = maxTags != null && value.length >= maxTags;

  const selectedSet = useMemo(() => new Set(value.map(norm)), [value]);

  const filtered = useMemo(
    () =>
      suggestions.filter(
        (s) => !selectedSet.has(norm(s)) && norm(s).includes(norm(query)),
      ),
    [suggestions, selectedSet, query],
  );

  const trimmed = query.trim();
  const canCreate =
    allowCreate &&
    trimmed.length > 0 &&
    !selectedSet.has(norm(trimmed)) &&
    !suggestions.some((s) => norm(s) === norm(trimmed));

  // Options shown in the dropdown: existing matches first, then a create row.
  const options: Array<{ type: "existing" | "create"; label: string }> = [
    ...filtered.map((label) => ({ type: "existing" as const, label })),
    ...(canCreate ? [{ type: "create" as const, label: trimmed }] : []),
  ];

  const addTag = (tag: string): void => {
    const clean = tag.trim();
    if (!clean || disabled || atMax || selectedSet.has(norm(clean))) return;
    onChange([...value, clean]);
    setQuery("");
    setActiveIndex(0);
    setOpen(true);
    inputRef.current?.focus();
  };

  const removeTag = (tag: string): void => {
    if (disabled) return;
    onChange(value.filter((t) => t !== tag));
  };

  const commitActive = (): void => {
    const opt = options[activeIndex];
    if (opt) addTag(opt.label);
    else if (canCreate) addTag(trimmed);
    else if (filtered.length === 1) addTag(filtered[0]);
  };

  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
    switch (e.key) {
      case "Enter":
        if (options.length > 0 || trimmed.length > 0) {
          e.preventDefault();
          commitActive();
        }
        break;
      case "Backspace":
        if (query === "" && value.length > 0) {
          e.preventDefault();
          removeTag(value[value.length - 1]);
        }
        break;
      case "ArrowDown":
        if (options.length > 0) {
          e.preventDefault();
          setOpen(true);
          setActiveIndex((i) => (i + 1) % options.length);
        }
        break;
      case "ArrowUp":
        if (options.length > 0) {
          e.preventDefault();
          setOpen(true);
          setActiveIndex((i) => (i - 1 + options.length) % options.length);
        }
        break;
      case "Escape":
        setOpen(false);
        break;
      default:
        break;
    }
  };

  const showDropdown = open && !disabled && !atMax && options.length > 0;

  // The listbox is portaled to <body> so a dialog's `overflow-y-auto` can't clip
  // it; keep it pinned under the control across scroll/resize while open.
  useLayoutEffect(() => {
    if (!showDropdown) return;
    const measure = (): void => {
      const el = controlRef.current;
      if (!el) return;
      const r = el.getBoundingClientRect();
      setMenuPos({ top: r.bottom + 4, left: r.left, width: r.width });
    };
    measure();
    window.addEventListener("scroll", measure, true);
    window.addEventListener("resize", measure);
    return () => {
      window.removeEventListener("scroll", measure, true);
      window.removeEventListener("resize", measure);
    };
  }, [showDropdown, value.length, query]);

  useEffect(() => {
    if (!showDropdown) setMenuPos(null);
  }, [showDropdown]);

  return (
    <div className={cn("relative", className)} data-slot="tag-input">
      {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
      <div
        ref={controlRef}
        className={cn(
          "flex min-h-9 flex-wrap items-center gap-1.5 border border-input bg-transparent px-2 py-1.5 text-body-small shadow-xs transition-[color,box-shadow] focus-within:border-primary focus-within:ring-[3px] focus-within:ring-primary/20",
          disabled && "pointer-events-none opacity-50",
        )}
        data-slot="tag-input-control"
        onMouseDown={(e) => {
          // Keep focus on the text field when clicking empty space in the box.
          if (e.target === e.currentTarget) {
            e.preventDefault();
            inputRef.current?.focus();
          }
        }}
      >
        {value.map((tag) => (
          <Chip key={tag} variant="secondary" onRemove={() => removeTag(tag)}>
            {tag}
          </Chip>
        ))}
        <input
          ref={inputRef}
          id={id ?? reactId}
          type="text"
          role="combobox"
          aria-expanded={showDropdown}
          aria-controls={listId}
          aria-autocomplete="list"
          aria-label={ariaLabel}
          disabled={disabled || atMax}
          value={query}
          placeholder={atMax ? `Max ${maxTags} tags` : placeholder}
          className="min-w-24 flex-1 bg-transparent py-0.5 text-body-small outline-none placeholder:font-normal placeholder:text-muted-foreground disabled:cursor-not-allowed"
          onChange={(e) => {
            setQuery(e.target.value);
            setActiveIndex(0);
            setOpen(true);
          }}
          onFocus={() => setOpen(true)}
          onBlur={() => setOpen(false)}
          onKeyDown={handleKeyDown}
        />
      </div>

      {showDropdown && menuPos && typeof document !== "undefined"
        ? createPortal(
            <ul
              id={listId}
              role="listbox"
              data-slot="tag-input-listbox"
              style={{
                position: "fixed",
                top: menuPos.top,
                left: menuPos.left,
                width: menuPos.width,
              }}
              className="z-[200] max-h-60 min-w-[12rem] overflow-y-auto border border-border bg-popover p-1 text-popover-foreground shadow-md"
            >
              {options.map((opt, i) => (
                <li
                  key={`${opt.type}-${opt.label}`}
                  role="option"
                  aria-selected={i === activeIndex}
                  className={cn(
                    "flex cursor-default items-center gap-2 px-2 py-1.5 text-body-small select-none",
                    i === activeIndex && "bg-primary/5 text-foreground",
                  )}
                  // onMouseDown (not onClick) so it fires before the input's blur.
                  onMouseDown={(e) => {
                    e.preventDefault();
                    addTag(opt.label);
                  }}
                  onMouseEnter={() => setActiveIndex(i)}
                >
                  {opt.type === "create" ? (
                    <span className="text-muted-foreground">
                      Create{" "}
                      <span className="text-foreground">“{opt.label}”</span>
                    </span>
                  ) : (
                    <span>{opt.label}</span>
                  )}
                </li>
              ))}
            </ul>,
            document.body,
          )
        : null}
    </div>
  );
}
