import { useEffect, useRef, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { sortRows, cycleSort, type SortState } from "../lib/columnSort";
import Pager, { hasPagerContent } from "./Pager";
import { PageToolbar } from "./PageShell";
import { ColumnHead, alignClass, filterRows, type Column, type FilterMap } from "./tableHeader";

// A reusable, client-side data table: per-column sort + filter (in the header) + pagination,
// with its sort/filter/page state optionally persisted to localStorage so a reload (F5) keeps
// the view. Columns are declared by the caller; the table is generic over the row type, so
// other modules can reuse it. Below `md` it falls back to a card list built from the same column
// definitions.
//
// The header row itself (sort cell + filter funnel/popover + the filter predicate) lives in
// `tableHeader.tsx` — the playlist manager renders its own drag-reorderable body under the same
// headers without inheriting this component's ordering state.

// Re-exported so the many `import { type Column } from "./DataTable"` call sites keep working.
export type { Column } from "./tableHeader";

interface Persisted {
  sort: SortState;
  filters: FilterMap;
  page: number;
  size?: number;
}

function loadPersist(key?: string): Persisted {
  const empty: Persisted = { sort: null, filters: {}, page: 0 };
  if (!key) return empty;
  try {
    const v = JSON.parse(localStorage.getItem(key) || "{}");
    return { sort: v.sort ?? null, filters: v.filters ?? {}, page: v.page ?? 0, size: v.size };
  } catch {
    return empty;
  }
}

export default function DataTable<T>({
  rows,
  columns,
  rowKey,
  pageSize = 10,
  pageSizeOptions = [10, 25, 50, 100],
  persistKey,
  emptyText,
  rowClassName,
  controlsPosition = "bottom",
  controlsLeading,
  controlsInBand = false,
  controlsBandClassName,
  externalFilter,
  resetFiltersToken,
  sort: sortProp,
  onSortChange,
}: {
  rows: T[];
  columns: Column<T>[];
  rowKey: (row: T) => string;
  pageSize?: number;
  pageSizeOptions?: number[];
  persistKey?: string;
  emptyText?: string;
  rowClassName?: (row: T) => string;
  controlsPosition?: "top" | "bottom" | "both";
  controlsLeading?: ReactNode;
  // Render the controls row (controlsLeading + pager) into the shell's fixed toolbar band instead
  // of inline, so it stays put above the sticky header while the rows scroll. `controlsBandClassName`
  // aligns it with the page's fixed chrome (e.g. the same max-width wrapper).
  controlsInBand?: boolean;
  controlsBandClassName?: string;
  // Set a column's filter from outside (e.g. "focus this channel" deep-links here). Applied
  // whenever its value changes; the user can still clear it from the header afterwards.
  externalFilter?: { key: string; value: string } | null;
  // Bump this to clear all column filters — for navigation intents that mean "show me this
  // set" (e.g. the header's "without full history" link), so a stale persisted filter can't
  // hide the rows the user was just sent to see.
  resetFiltersToken?: number;
  // CONTROLLED sort (optional): when `onSortChange` is given, the caller owns the sort — the table
  // reflects `sort` and reports header clicks instead of keeping its own — so another view of the
  // same rows (the channel manager's cards) can share one sort. Omit both for the default internal,
  // persistKey-backed sort.
  sort?: SortState;
  onSortChange?: (next: SortState) => void;
}) {
  const { t } = useTranslation();
  const initial = loadPersist(persistKey);
  // Sort is controlled when the caller passes onSortChange (it then owns/persists it); otherwise
  // the table keeps its own, seeded from and saved to persistKey.
  const controlledSort = onSortChange !== undefined;
  const [internalSort, setInternalSort] = useState<SortState>(initial.sort);
  const sort = controlledSort ? (sortProp ?? null) : internalSort;
  const [filters, setFilters] = useState<FilterMap>(initial.filters);
  const [page, setPage] = useState(initial.page);
  const [size, setSize] = useState(initial.size ?? pageSize);

  useEffect(() => {
    if (persistKey) localStorage.setItem(persistKey, JSON.stringify({ sort, filters, page, size }));
  }, [persistKey, sort, filters, page, size]);

  useEffect(() => {
    if (!externalFilter) return;
    setFilters((f) => ({ ...f, [externalFilter.key]: externalFilter.value }));
    setPage(0);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [externalFilter?.key, externalFilter?.value]);

  // Clear column filters when the reset token advances. The ref starts at 0 (not at the prop)
  // so a navigation that bumped the token still clears even when it remounts this table fresh
  // (e.g. arriving from another page); a plain reload keeps the token at 0 and the effect is a
  // no-op, so the user's persisted filters survive.
  const resetRef = useRef(0);
  useEffect(() => {
    if (resetFiltersToken === undefined || resetFiltersToken === resetRef.current) return;
    resetRef.current = resetFiltersToken;
    setFilters({});
    setPage(0);
  }, [resetFiltersToken]);

  const sorted = sortRows(filterRows(rows, columns, filters), columns, sort);

  // size <= 0 means "All" — one page with every row.
  const allRows = size <= 0;
  const totalPages = allRows ? 1 : Math.max(1, Math.ceil(sorted.length / size));
  const safePage = Math.min(page, totalPages - 1);
  const paged = allRows ? sorted : sorted.slice(safePage * size, safePage * size + size);

  function toggleSort(key: string) {
    const next = cycleSort(sort, key);
    if (controlledSort) onSortChange!(next);
    else setInternalSort(next);
    setPage(0);
  }
  function applyFilter(key: string, value: string | string[]) {
    setFilters((f) => ({ ...f, [key]: value }));
    setPage(0);
  }

  // The pager + rows-per-page cluster on its own, so it can be rendered a second time at the bottom
  // (controlsPosition "both") WITHOUT repeating the leading controls (e.g. the status chips) — those
  // are header controls and belong only at the top.
  // Stays NULLABLE: the wrappers below decide whether to render a controls row at all, and an always
  // -truthy element would leave an empty `my-3` band (and an empty in-band PageToolbar) on a table
  // with nothing to page. The emptiness rule itself lives in Pager, so the two can't drift apart.
  const pagerNode = hasPagerContent(sorted.length, size) ? (
    <Pager
      page={safePage}
      size={size}
      total={sorted.length}
      pageSizeOptions={pageSizeOptions}
      onPageChange={setPage}
      onSizeChange={(s) => {
        setSize(s);
        setPage(0);
      }}
      sizeLabel={t("datatable.rowsPerPage")}
    />
  ) : null;

  const controls =
    controlsLeading != null || pagerNode ? (
      <div className="flex items-center justify-between gap-3 my-3 text-sm flex-wrap">
        <div className="flex items-center gap-3 flex-wrap">{controlsLeading}</div>
        {pagerNode}
      </div>
    ) : null;

  // Bottom row for "both": just the pager, right-aligned (the leading controls stay at the top).
  const bottomControls = pagerNode ? (
    <div className="flex items-center justify-end gap-3 my-3 text-sm flex-wrap">{pagerNode}</div>
  ) : null;

  const topControls =
    controlsInBand && controls ? (
      <PageToolbar>
        <div className={controlsBandClassName}>{controls}</div>
      </PageToolbar>
    ) : (
      controls
    );

  return (
    <div>
      {(controlsPosition === "top" || controlsPosition === "both") && topControls}
      {/* Wide screens: table. The wrapper isn't clipped so header filter popovers can overflow. */}
      <div className="hidden md:block">
        <table className="w-full border-collapse text-sm">
          <ColumnHead
            columns={columns}
            sort={sort}
            onToggleSort={toggleSort}
            filters={filters}
            onFilter={applyFilter}
          />
          <tbody>
            {paged.map((row) => (
              <tr
                key={rowKey(row)}
                className={`border-b border-border/50 hover:bg-card/40 transition ${
                  rowClassName?.(row) ?? ""
                }`}
              >
                {columns.map((col) => (
                  <td
                    key={col.key}
                    style={col.width ? { width: col.width } : undefined}
                    className={`px-2 py-2 align-middle ${col.nowrap ? "whitespace-nowrap" : ""} ${alignClass(col.align)}`}
                  >
                    {col.render(row)}
                  </td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* Narrow screens: a compact card per row — the primary column as the heading, the
          rest flowing in a single wrapping meta line (no full-width label/value gaps). */}
      <div className="md:hidden flex flex-col gap-2">
        {paged.map((row) => (
          <div
            key={rowKey(row)}
            className={`glass-card rounded-xl p-3 ${rowClassName?.(row) ?? ""}`}
          >
            {columns
              .filter((c) => c.cardPrimary)
              .map((col) => (
                <div key={col.key} className="font-medium">
                  {col.render(row)}
                </div>
              ))}
            <div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 mt-2 text-sm">
              {columns
                .filter((c) => !c.hideInCard && !c.cardPrimary)
                .map((col) => (
                  <span key={col.key} className="inline-flex items-center gap-1">
                    {col.cardLabel !== false && (
                      <span className="text-muted text-xs">{col.header}</span>
                    )}
                    {col.render(row)}
                  </span>
                ))}
            </div>
          </div>
        ))}
      </div>

      {paged.length === 0 && (
        <div className="text-muted py-8 text-center text-sm">
          {emptyText ?? t("datatable.empty")}
        </div>
      )}

      {controlsPosition === "bottom" && controls}
      {controlsPosition === "both" && bottomControls}
    </div>
  );
}
