import React, { useCallback, useEffect, useRef, useState } from "react";
import { Search, X } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";

import { useTrackQueryStore } from "@/stores/track-query";
import { facetsApi, type FacetValue } from "@/api/facets";
import { tracks } from "@/api/tracks";
import { SearchDropdown, type ChipField } from "@/components/SearchDropdown";

const FIELD_TO_CHIP: Record<string, ChipField> = {
  genre: "genres",
  mood: "moods",
  producer: "producers",
  key: "keys",
};

// Pure helper — extracts a single COMPLETED `field:value` chip token from the
// box text. Only fires when there is a trailing delimiter (space) so partial
// typing like `genre:tr` isn't absorbed mid-word. Supports a quoted value
// (`producer:"young chop"`). `bpm:` / `has:` are NOT chip fields and return
// null. Returns the matched chip field + value and the remaining free text
// (with the token removed), or null when no completed chip token is present.
export function extractCompletedChipToken(
  text: string,
): { field: ChipField; value: string; rest: string } | null {
  // token: field:value followed by at least one space (the delimiter).
  // value is either a double-quoted string or a run of non-space chars.
  const re = /(genre|mood|producer|key):("([^"]*)"|[^\s]+)\s/i;
  const m = re.exec(text);
  if (!m) return null;
  const field = FIELD_TO_CHIP[m[1].toLowerCase()];
  if (!field) return null;
  const raw = m[3] !== undefined ? m[3] : m[2];
  const value = raw.trim();
  if (!value) return null;
  const rest = (text.slice(0, m.index) + text.slice(m.index + m[0].length))
    .replace(/\s+/g, " ")
    .trim();
  return { field, value, rest };
}

export function SearchInput(): React.JSX.Element {
  const { t } = useTranslation();
  const navigate = useNavigate();
  const setText = useTrackQueryStore((s) => s.setText);

  const [focused, setFocused] = useState(false);
  const [box, setBox] = useState("");
  const inputRef = useRef<HTMLInputElement | null>(null);
  const debounceRef = useRef<number | null>(null);

  // Dropdown data
  const [recent, setRecent] = useState<string[]>([]);
  const [topProducers, setTopProducers] = useState<FacetValue[]>([]);
  const [topGenres, setTopGenres] = useState<FacetValue[]>([]);
  const [topKeys, setTopKeys] = useState<FacetValue[]>([]);
  const [recentlyAdded, setRecentlyAdded] = useState<{ id: number; title: string }[]>([]);

  const dropdownVisible = focused && box.trim().length === 0;

  // Append a value to a chip filter array (dedupe).
  const appendChip = useCallback((field: ChipField, value: string): void => {
    const state = useTrackQueryStore.getState();
    const current = state.filters[field];
    if (current.includes(value)) return;
    const next = [...current, value];
    if (field === "producers") state.setProducerFilter(next);
    else if (field === "genres") state.setGenreFilter(next);
    else if (field === "moods") state.setMoodFilter(next);
    else state.setKeyFilter(next);
  }, []);

  // Debounced free-text → setText.
  const scheduleSetText = useCallback(
    (value: string): void => {
      if (debounceRef.current != null) window.clearTimeout(debounceRef.current);
      debounceRef.current = window.setTimeout(() => {
        setText(value);
      }, 250);
    },
    [setText],
  );

  // Handle a new box value: absorb any completed chip token(s), then schedule
  // setText for the remaining free text.
  const applyBox = useCallback(
    (raw: string): void => {
      let text = raw;
      // Absorb every completed chip token currently present.
      for (;;) {
        const tok = extractCompletedChipToken(text);
        if (!tok) break;
        appendChip(tok.field, tok.value);
        text = tok.rest;
      }
      setBox(text);
      scheduleSetText(text);
    },
    [appendChip, scheduleSetText],
  );

  // Load dropdown data when it becomes visible. Guarded against close/unmount.
  useEffect(() => {
    if (!dropdownVisible) return;
    let alive = true;
    void (async () => {
      const [r, prod, gen, keys, added] = await Promise.all([
        facetsApi.recent().catch(() => []),
        facetsApi.top("producer").catch(() => []),
        facetsApi.top("genre").catch(() => []),
        facetsApi.top("key").catch(() => []),
        tracks.list({ sort_by: "created_at", sort_dir: "desc" }).catch(() => []),
      ]);
      if (!alive) return;
      setRecent(r);
      setTopProducers(prod);
      setTopGenres(gen);
      setTopKeys(keys);
      setRecentlyAdded(added.slice(0, 5).map((t) => ({ id: t.id, title: t.title })));
    })();
    return () => {
      alive = false;
    };
  }, [dropdownVisible]);

  const closeAndClear = useCallback((): void => {
    if (debounceRef.current != null) window.clearTimeout(debounceRef.current);
    setBox("");
    setText("");
    setFocused(false);
  }, [setText]);

  // ⌘F to focus the (always-visible) search box.
  useEffect(() => {
    function onKey(e: KeyboardEvent): void {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "f") {
        e.preventDefault();
        inputRef.current?.focus();
      }
    }
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  // Clear debounce on unmount.
  useEffect(() => {
    return () => {
      if (debounceRef.current != null) window.clearTimeout(debounceRef.current);
    };
  }, []);

  function onInputKeyDown(e: React.KeyboardEvent<HTMLInputElement>): void {
    // While an IME composition is in progress (e.g. typing English via a
    // Chinese IME), the Enter that confirms the candidate fires keydown with
    // isComposing=true. Acting on it here would mutate the controlled value
    // mid-composition and race the IME commit, duplicating the text
    // ("regalia" → "regalia regalia"). Ignore all keys while composing — the
    // first Enter confirms the IME, a second (non-composing) Enter submits.
    if (e.nativeEvent.isComposing) return;
    if (e.key === "Escape") {
      e.preventDefault();
      closeAndClear();
      inputRef.current?.blur();
      return;
    }
    if (e.key === "Enter") {
      e.preventDefault();
      // Absorb any trailing completed token (Enter acts as a delimiter too).
      applyBox(box.endsWith(" ") ? box : box + " ");
      const query = box.trim();
      if (query.length > 0) {
        void facetsApi.pushRecent(query).catch(() => undefined);
      }
      setFocused(false);
    }
  }

  return (
    <div className="relative w-[460px] max-w-[46vw]">
      <div className="flex items-center gap-2.5 bg-bg-elevated border border-border-subtle hover:border-text-tertiary focus-within:border-text-secondary rounded-full px-3.5 py-2 transition-colors">
        <Search size={16} className="text-text-tertiary shrink-0" />
        <input
          ref={inputRef}
          value={box}
          onChange={(e) => applyBox(e.target.value)}
          onFocus={() => setFocused(true)}
          // Delay blur so dropdown button clicks register before it hides.
          onBlur={() => setTimeout(() => setFocused(false), 150)}
          onKeyDown={onInputKeyDown}
          placeholder={t("search.placeholder")}
          className="flex-1 bg-transparent text-[15px] text-text-primary focus:outline-none"
        />
        {box.length > 0 && (
          <button
            type="button"
            onClick={closeAndClear}
            className="text-text-tertiary hover:text-text-primary shrink-0"
            aria-label={t("search.clearAria")}
          >
            <X size={14} />
          </button>
        )}
      </div>

      {dropdownVisible && (
        <SearchDropdown
          recent={recent}
          topProducers={topProducers}
          topGenres={topGenres}
          topKeys={topKeys}
          recentlyAdded={recentlyAdded}
          onPickQuery={(query) => {
            if (debounceRef.current != null) {
              window.clearTimeout(debounceRef.current);
              debounceRef.current = null;
            }
            setBox(query);
            setText(query);
            setFocused(false);
            inputRef.current?.focus();
          }}
          onPickChip={(field, value) => {
            if (debounceRef.current != null) {
              window.clearTimeout(debounceRef.current);
              debounceRef.current = null;
            }
            appendChip(field, value);
            setFocused(false);
          }}
          onOpenTrack={(id) => {
            navigate(`/tracks/${id}/edit`);
            closeAndClear();
          }}
        />
      )}
    </div>
  );
}
