import { useEffect, useRef, useState, type ReactNode, type RefObject } from "react";
import { useTranslation } from "react-i18next";
import { ArrowDown, ArrowUp, ListFilter, X } from "lucide-react";
import { useDismiss } from "../lib/useDismiss";
import { useScrollFade } from "../lib/useScrollFade";
import { type SortState } from "../lib/columnSort";

// The column-header machinery — the column contract, the sortable header row with its filter
// funnel + popover, and the filter predicate — lifted OUT of DataTable so a non-DataTable body can
// render under the same headers.
//
// Why it lives here and not in DataTable: DataTable owns its filter/sort/page state internally,
// while the playlist manager's body is a drag-reorderable list whose order is the persisted
// server-side one. Two owners of the same ordering fight; composing these pieces instead lets the
// playlist keep its stored order and still get real column headers. Same move as E4 S3's
// ChannelLayoutGrid, which builds a card view straight from the `Column<T>` render closures.

type ColumnFilter<T> =
  | { kind: "text"; get: (row: T) => string }
  | {
      kind: "select";
      options: { value: string; label: string }[];
      test: (row: T, value: string) => boolean;
    }
  | {
      kind: "multi";
      options: { value: string; label: string }[];
      test: (row: T, values: string[]) => boolean;
    };

export interface Column<T> {
  key: string;
  header: string;
  render: (row: T) => ReactNode;
  align?: "left" | "right" | "center";
  width?: string;
  sortable?: boolean;
  sortValue?: (row: T) => string | number;
  filter?: ColumnFilter<T>;
  // Keep the cell on one line so the column auto-sizes to its content (table-layout: auto).
  nowrap?: boolean;
  // Card fallback (below md): `cardPrimary` renders as the card heading (no label); `hideInCard`
  // omits the column; `cardLabel:false` shows the value without its header label.
  cardPrimary?: boolean;
  hideInCard?: boolean;
  cardLabel?: boolean;
}

export type FilterMap = Record<string, string | string[]>;

export function isActive(value: string | string[] | undefined): boolean {
  return Array.isArray(value) ? value.length > 0 : !!value;
}

/** Apply every column's own filter predicate. A column with no filter, or an empty value, passes. */
export function filterRows<T>(rows: T[], columns: Column<T>[], filters: FilterMap): T[] {
  return rows.filter((row) =>
    columns.every((col) => {
      const f = col.filter;
      if (!f) return true;
      const val = filters[col.key];
      if (!isActive(val)) return true;
      if (f.kind === "text") return f.get(row).toLowerCase().includes(String(val).toLowerCase());
      if (f.kind === "select") return f.test(row, String(val));
      return f.test(row, val as string[]);
    }),
  );
}

export const alignClass = (a?: "left" | "right" | "center") =>
  a === "right" ? "text-right" : a === "center" ? "text-center" : "text-left";

// The multi-select filter's option list, capped and scrollable. Its own component because it owns a
// hook — see the FilterPopover note below for why nesting either of these would remount them.
function FilterOptionList({ children }: { children: ReactNode }) {
  const fade = useScrollFade();
  return (
    <div
      ref={fade.ref}
      style={fade.style}
      className="flex flex-col gap-0.5 max-h-60 overflow-y-auto no-scrollbar"
    >
      {children}
    </div>
  );
}

// MODULE LEVEL, deliberately. This used to be declared inside DataTable's body, which made it a NEW
// component type on every render: React unmounted and remounted the popover on each keystroke, so
// the caret jumped to the end and IME composition (accents, any CJK input) broke mid-word. The
// `autoFocus` masked it well enough that it read as "the filter is just twitchy".
function FilterPopover<T>({
  col,
  value,
  onApply,
  panelRef,
}: {
  col: Column<T>;
  value: string | string[] | undefined;
  onApply: (key: string, value: string | string[]) => void;
  panelRef: RefObject<HTMLDivElement>;
}) {
  const { t } = useTranslation();
  const f = col.filter!;
  return (
    <div
      ref={panelRef}
      className="glass-menu absolute left-0 top-full mt-1 z-chrome w-52 rounded-xl p-2 animate-[popIn_0.16s_ease]"
    >
      {f.kind === "text" && (
        <input
          autoFocus
          value={(value as string) ?? ""}
          onChange={(e) => onApply(col.key, e.target.value)}
          placeholder={col.header}
          className="w-full bg-card border border-border rounded-md px-2 py-1 text-xs outline-none focus:border-accent"
        />
      )}
      {f.kind === "select" && (
        <div className="flex flex-col gap-0.5">
          <button
            onClick={() => onApply(col.key, "")}
            className={`text-left text-xs px-2 py-1.5 rounded-md transition ${
              !isActive(value) ? "bg-accent text-accent-fg" : "text-muted hover:bg-card"
            }`}
          >
            {t("datatable.all")}
          </button>
          {f.options.map((o) => (
            <button
              key={o.value}
              onClick={() => onApply(col.key, o.value)}
              className={`text-left text-xs px-2 py-1.5 rounded-md transition ${
                value === o.value ? "bg-accent text-accent-fg" : "text-muted hover:bg-card"
              }`}
            >
              {o.label}
            </button>
          ))}
        </div>
      )}
      {f.kind === "multi" && (
        <FilterOptionList>
          {f.options.length === 0 && (
            <span className="text-xs text-muted px-2 py-1">{t("datatable.noOptions")}</span>
          )}
          {f.options.map((o) => {
            const arr = (value as string[]) ?? [];
            const on = arr.includes(o.value);
            return (
              <button
                key={o.value}
                onClick={() =>
                  onApply(col.key, on ? arr.filter((x) => x !== o.value) : [...arr, o.value])
                }
                className={`flex items-center gap-2 text-left text-xs px-2 py-1.5 rounded-md transition ${
                  on ? "text-fg" : "text-muted hover:bg-card"
                }`}
              >
                <span
                  className={`w-3.5 h-3.5 rounded border flex items-center justify-center ${
                    on ? "bg-accent border-accent text-accent-fg" : "border-border"
                  }`}
                >
                  {on && <X className="w-2.5 h-2.5" />}
                </span>
                {o.label}
              </button>
            );
          })}
        </FilterOptionList>
      )}
      {isActive(value) && (
        <button
          onClick={() => onApply(col.key, f.kind === "multi" ? [] : "")}
          className="mt-1.5 w-full text-xs text-muted hover:text-accent transition"
        >
          {t("datatable.clear")}
        </button>
      )}
    </div>
  );
}

/** The sticky `<thead>`: one sortable/filterable cell per column.
 *
 * Sticky-header notes (kept from DataTable, where this look was designed): the header stays put at
 * the top of the page scroller while rows scroll under it. `bg-bg` on each cell stops rows showing
 * through; the bottom border rides a `box-shadow` because a sticky `<tr>`'s own border scrolls away
 * with the collapsed table box in some browsers. The `::after` paints a short fade just BELOW the
 * header so rows dissolve as they slide under it while the header itself stays crisp — which is why
 * pages using this pass `PageShell fadeTop={false}`. */
export function ColumnHead<T>({
  columns,
  sort,
  onToggleSort,
  filters,
  onFilter,
}: {
  columns: Column<T>[];
  sort: SortState;
  onToggleSort: (key: string) => void;
  filters: FilterMap;
  onFilter: (key: string, value: string | string[]) => void;
}) {
  const { t } = useTranslation();
  const [openFilter, setOpenFilter] = useState<string | null>(null);
  const panelRef = useRef<HTMLDivElement | null>(null);
  // The funnel that opened the panel. `useDismiss` must know about it too, or the trigger's own
  // mousedown closes the panel and the click that follows immediately reopens it — clicking the
  // funnel a second time then looks like it does nothing. Also the thing to hand focus back to.
  const triggerRef = useRef<HTMLButtonElement | null>(null);
  const restoreFocus = useRef(false);
  useDismiss(!!openFilter, () => setOpenFilter(null), [panelRef, triggerRef]);

  // Escape (or an outside click) used to unmount the focused input and strand focus on <body>,
  // so the next Tab restarted from the top of the page.
  useEffect(() => {
    if (openFilter) {
      restoreFocus.current = true;
    } else if (restoreFocus.current) {
      restoreFocus.current = false;
      triggerRef.current?.focus();
    }
  }, [openFilter]);

  return (
    <thead className="sticky top-0 z-base after:pointer-events-none after:absolute after:inset-x-0 after:top-full after:h-4 after:bg-[linear-gradient(to_bottom,var(--bg),transparent)] after:content-['']">
      <tr className="text-muted [&>th]:shadow-[inset_0_-1px_0_var(--border)]">
        {columns.map((col) => {
          const isSorted = sort?.key === col.key;
          const filterOn = isActive(filters[col.key]);
          return (
            <th
              key={col.key}
              style={col.width ? { width: col.width } : undefined}
              className={`relative font-medium px-2 py-2 whitespace-nowrap bg-bg ${alignClass(col.align)}`}
              aria-sort={
                col.sortable
                  ? isSorted
                    ? sort!.dir === "asc"
                      ? "ascending"
                      : "descending"
                    : "none"
                  : undefined
              }
            >
              <span className="inline-flex items-center gap-1">
                {/* Only a sortable header is a control. A non-sortable one used to render a
                    focusable no-op button, which put unlabeled dead stops in the tab order —
                    two of them per playlist table, whose grip and actions headers are empty. */}
                {col.sortable ? (
                  <button
                    onClick={() => onToggleSort(col.key)}
                    className="inline-flex items-center gap-1 hover:text-fg cursor-pointer"
                  >
                    {col.header}
                    {isSorted &&
                      sort &&
                      (sort.dir === "asc" ? (
                        <ArrowUp className="w-3 h-3" />
                      ) : (
                        <ArrowDown className="w-3 h-3" />
                      ))}
                  </button>
                ) : (
                  <span>{col.header}</span>
                )}
                {col.filter && (
                  <button
                    // Captured at open time rather than bound with `ref`, so it survives the close
                    // (a `ref` scoped to the open column nulls out before focus can go back).
                    onClick={(e) => {
                      triggerRef.current = e.currentTarget;
                      setOpenFilter((o) => (o === col.key ? null : col.key));
                    }}
                    aria-label={t("datatable.filter")}
                    aria-expanded={openFilter === col.key}
                    aria-haspopup="dialog"
                    // Generous padding: the icon alone was a 14x14 hit target, the smallest in the app.
                    className={`-m-1.5 p-1.5 transition ${
                      filterOn ? "text-accent" : "text-muted/60 hover:text-fg"
                    }`}
                  >
                    <ListFilter className="w-3.5 h-3.5" />
                  </button>
                )}
              </span>
              {openFilter === col.key && col.filter && (
                <FilterPopover
                  col={col}
                  value={filters[col.key]}
                  onApply={onFilter}
                  panelRef={panelRef}
                />
              )}
            </th>
          );
        })}
      </tr>
    </thead>
  );
}
