import { useState, useRef, useEffect, useCallback, useId, useMemo } from "react";
import { useTranslation } from "react-i18next";
import type { Category } from "../../shared/types";

interface CategoryComboboxProps {
  categories: Category[];
  value: number | null;
  onChange: (id: number | null) => void;
  placeholder?: string;
  compact?: boolean;
  ariaLabel?: string;
  /** Extra options shown before the category list (e.g. "All categories", "Uncategorized") */
  extraOptions?: Array<{ value: string; label: string }>;
  /** Called when an extra option is selected */
  onExtraSelect?: (value: string) => void;
  /** Currently active extra option value (for display) */
  activeExtra?: string | null;
}

// Strip accents + lowercase for accent-insensitive matching
function normalize(s: string): string {
  return s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
}

// Compute depth of each category based on parent_id chain
function computeDepths(categories: Category[]): Map<number, number> {
  const byId = new Map<number, Category>();
  for (const c of categories) byId.set(c.id, c);
  const depths = new Map<number, number>();
  function depthOf(id: number, seen: Set<number>): number {
    if (depths.has(id)) return depths.get(id)!;
    if (seen.has(id)) return 0;
    seen.add(id);
    const cat = byId.get(id);
    if (!cat || cat.parent_id == null) {
      depths.set(id, 0);
      return 0;
    }
    const d = depthOf(cat.parent_id, seen) + 1;
    depths.set(id, d);
    return d;
  }
  for (const c of categories) depthOf(c.id, new Set());
  return depths;
}

/**
 * Order a flat list of categories in hierarchical DFS order: each root is
 * emitted immediately followed by its descendants (depth-first, parent before
 * children). Siblings within a group are ordered by `sort_order` ascending,
 * then by `resolveName(cat)` for stable tiebreaking.
 *
 * A plain `ORDER BY sort_order, name` in SQL mixes parents and children from
 * different sub-trees that happen to share the same `sort_order`, producing
 * the scrambled indentation we saw in the by-category report combobox.
 * Doing the DFS client-side keeps rendering correct regardless of query shape.
 *
 * Orphans (category whose parent is missing or inactive / filtered out) are
 * emitted at the end, each treated as a pseudo-root, so nothing disappears.
 */
export function sortHierarchical(
  categories: Category[],
  resolveName: (cat: Category) => string,
): Category[] {
  if (categories.length === 0) return [];

  const ids = new Set<number>();
  for (const c of categories) ids.add(c.id);

  // Group by parent bucket: root (`null`) or parent id.
  const childrenByParent = new Map<number | null, Category[]>();
  const orphans: Category[] = [];
  for (const c of categories) {
    if (c.parent_id == null) {
      const bucket = childrenByParent.get(null) ?? [];
      bucket.push(c);
      childrenByParent.set(null, bucket);
    } else if (ids.has(c.parent_id)) {
      const bucket = childrenByParent.get(c.parent_id) ?? [];
      bucket.push(c);
      childrenByParent.set(c.parent_id, bucket);
    } else {
      orphans.push(c);
    }
  }

  const compare = (a: Category, b: Category) => {
    if (a.sort_order !== b.sort_order) return a.sort_order - b.sort_order;
    return resolveName(a).localeCompare(resolveName(b));
  };
  for (const bucket of childrenByParent.values()) bucket.sort(compare);
  orphans.sort(compare);

  const out: Category[] = [];
  const visited = new Set<number>();
  const visit = (cat: Category) => {
    if (visited.has(cat.id)) return; // defensive against cycles
    visited.add(cat.id);
    out.push(cat);
    const kids = childrenByParent.get(cat.id);
    if (kids) for (const child of kids) visit(child);
  };
  const roots = childrenByParent.get(null) ?? [];
  for (const root of roots) visit(root);
  // Append orphans last, still treated as pseudo-roots so their own children
  // (if any were pulled in) follow them.
  for (const orphan of orphans) visit(orphan);
  return out;
}

export default function CategoryCombobox({
  categories,
  value,
  onChange,
  placeholder = "",
  compact = false,
  ariaLabel,
  extraOptions,
  onExtraSelect,
  activeExtra,
}: CategoryComboboxProps) {
  const { t } = useTranslation();
  const [open, setOpen] = useState(false);
  const [query, setQuery] = useState("");
  const [highlightIndex, setHighlightIndex] = useState(0);
  const inputRef = useRef<HTMLInputElement>(null);
  const listRef = useRef<HTMLUListElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const baseId = useId();
  const listboxId = `${baseId}-listbox`;
  const optionId = (i: number) => `${baseId}-option-${i}`;

  const depths = useMemo(() => computeDepths(categories), [categories]);

  // Resolve the display name for a category: seed rows carry an i18n_key that
  // we translate with name as defaultValue; user-created rows just use name.
  const displayName = useCallback(
    (c: Category) => (c.i18n_key ? t(c.i18n_key, { defaultValue: c.name }) : c.name),
    [t]
  );

  // Re-order the (potentially sort_order-globally-sorted) input into proper
  // hierarchical DFS order so parents always precede their children and
  // siblings stay grouped under the same ancestor.
  const orderedCategories = useMemo(
    () => sortHierarchical(categories, displayName),
    [categories, displayName],
  );

  const selectedCategory = orderedCategories.find((c) => c.id === value);
  const displayLabel =
    activeExtra != null
      ? extraOptions?.find((o) => o.value === activeExtra)?.label ?? ""
      : selectedCategory
        ? displayName(selectedCategory)
        : "";

  const normalizedQuery = normalize(query);
  const filtered = query
    ? orderedCategories.filter((c) => normalize(displayName(c)).includes(normalizedQuery))
    : orderedCategories;

  const filteredExtras = extraOptions
    ? query
      ? extraOptions.filter((o) => normalize(o.label).includes(normalizedQuery))
      : extraOptions
    : [];

  const totalItems = filteredExtras.length + filtered.length;

  useEffect(() => {
    if (open && listRef.current) {
      const el = listRef.current.children[highlightIndex] as HTMLElement | undefined;
      el?.scrollIntoView({ block: "nearest" });
    }
  }, [highlightIndex, open]);

  useEffect(() => {
    if (!open) return;
    const handler = (e: MouseEvent) => {
      if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
        setOpen(false);
        setQuery("");
      }
    };
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, [open]);

  const selectItem = useCallback(
    (index: number) => {
      if (index < filteredExtras.length) {
        onExtraSelect?.(filteredExtras[index].value);
      } else {
        const cat = filtered[index - filteredExtras.length];
        if (cat) onChange(cat.id);
      }
      setOpen(false);
      setQuery("");
      inputRef.current?.blur();
    },
    [filteredExtras, filtered, onChange, onExtraSelect]
  );

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (!open) {
      if (e.key === "ArrowDown" || e.key === "Enter") {
        e.preventDefault();
        setOpen(true);
        setHighlightIndex(0);
      }
      return;
    }

    switch (e.key) {
      case "ArrowDown":
        e.preventDefault();
        setHighlightIndex((i) => (i + 1) % totalItems);
        break;
      case "ArrowUp":
        e.preventDefault();
        setHighlightIndex((i) => (i - 1 + totalItems) % totalItems);
        break;
      case "Enter":
        e.preventDefault();
        if (totalItems > 0) selectItem(highlightIndex);
        break;
      case "Escape":
        e.preventDefault();
        setOpen(false);
        setQuery("");
        inputRef.current?.blur();
        break;
    }
  };

  const py = compact ? "py-1" : "py-2";
  const px = compact ? "px-2" : "px-3";

  const activeId = open && totalItems > 0 ? optionId(highlightIndex) : undefined;

  return (
    <div ref={containerRef} className="relative">
      <input
        ref={inputRef}
        type="text"
        role="combobox"
        aria-label={ariaLabel}
        aria-expanded={open}
        aria-controls={listboxId}
        aria-autocomplete="list"
        aria-activedescendant={activeId}
        value={open ? query : displayLabel}
        placeholder={placeholder || displayLabel}
        onChange={(e) => {
          setQuery(e.target.value);
          setHighlightIndex(0);
          if (!open) setOpen(true);
        }}
        onFocus={() => {
          setOpen(true);
          setQuery("");
          setHighlightIndex(0);
        }}
        onKeyDown={handleKeyDown}
        className={`w-full ${px} ${py} text-sm rounded-lg border border-[var(--border)] bg-[var(--background)] text-[var(--foreground)] placeholder:text-[var(--muted-foreground)] focus:outline-none focus:ring-1 focus:ring-[var(--primary)]`}
      />
      {open && totalItems > 0 && (
        <ul
          ref={listRef}
          id={listboxId}
          role="listbox"
          className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-lg border border-[var(--border)] bg-[var(--card)] shadow-lg"
        >
          {filteredExtras.map((opt, i) => (
            <li
              key={`extra-${opt.value}`}
              id={optionId(i)}
              role="option"
              aria-selected={i === highlightIndex}
              onMouseDown={(e) => e.preventDefault()}
              onClick={() => selectItem(i)}
              onMouseEnter={() => setHighlightIndex(i)}
              className={`${px} ${py} text-sm cursor-pointer ${
                i === highlightIndex
                  ? "bg-[var(--primary)] text-white"
                  : "text-[var(--foreground)] hover:bg-[var(--muted)]"
              }`}
            >
              {opt.label}
            </li>
          ))}
          {filtered.map((cat, i) => {
            const idx = filteredExtras.length + i;
            const depth = depths.get(cat.id) ?? 0;
            const indent = depth > 0 ? "  ".repeat(depth) : "";
            return (
              <li
                key={cat.id}
                id={optionId(idx)}
                role="option"
                aria-selected={idx === highlightIndex}
                onMouseDown={(e) => e.preventDefault()}
                onClick={() => selectItem(idx)}
                onMouseEnter={() => setHighlightIndex(idx)}
                className={`${px} ${py} text-sm cursor-pointer ${
                  idx === highlightIndex
                    ? "bg-[var(--primary)] text-white"
                    : "text-[var(--foreground)] hover:bg-[var(--muted)]"
                }`}
                style={depth > 0 ? { paddingLeft: `calc(${compact ? "0.5rem" : "0.75rem"} + ${depth * 1}rem)` } : undefined}
              >
                <span className="whitespace-pre">{indent}</span>
                {displayName(cat)}
              </li>
            );
          })}
        </ul>
      )}
    </div>
  );
}
