import { useCallback, useEffect, useState } from "react";
import {
  type CollapseAccessors,
  collapsibleKeys,
  isCollapsedFor,
  parseStoredExpanded,
  serializeExpanded,
  visibleRows,
} from "../utils/collapsibleRows";
import { getPreference, setPreference } from "../services/userPreferenceService";

export interface CollapsibleGroups<T> {
  /** Filters a section's rows down to the ones visible under the current state. */
  visible: (rows: T[]) => T[];
  /** True when this parent row is collapsed (its subtree is hidden). */
  isCollapsed: (row: T) => boolean;
  /** Flip one group between collapsed and expanded, then persist. */
  toggle: (row: T) => void;
  /** Expand every collapsible group found in `rows`, then persist. */
  expandAll: (rows: T[]) => void;
  /** Collapse every collapsible group found in `rows`, then persist. */
  collapseAll: (rows: T[]) => void;
  /** True when `rows` has at least one group and all of them are expanded. */
  allExpanded: (rows: T[]) => boolean;
  /** Number of collapsible groups (parents, any depth) in `rows`. */
  groupCount: (rows: T[]) => number;
}

/**
 * Per-report collapse/expand state for the hierarchical tables (issue #254/#265),
 * generalised to every hierarchy level (issue #288).
 *
 * The persisted value is the set of keys whose state DIFFERS from the default
 * (see `isCollapsedFor`), so `defaultExpanded: false` + an empty set = everything
 * collapsed — the reports' established behaviour — while `defaultExpanded: true`
 * + an empty set = everything expanded.
 *
 * Persistence lives in `user_preferences`, the profile's own SQLite table, so the
 * state is scoped per profile for free and is destroyed with the profile (no
 * localStorage residue surviving `deleteProfile` — a privacy-first requirement).
 * Reads/writes are async; hydration runs in an effect. There is no visible flash
 * because the default state IS what renders first, and hydration can only reveal
 * what the user had opened. `storageKey: null` disables persistence entirely
 * (pure in-memory state, for the category trees).
 *
 * The hook stays context-free — it never calls `useProfile()` — because the
 * `user_preferences` key is already per-profile (it lives in the profile's DB).
 */
export function useCollapsibleGroups<T>(
  storageKey: string | null,
  acc: CollapseAccessors<T>,
  options?: { defaultExpanded?: boolean },
): CollapsibleGroups<T> {
  const defaultExpanded = options?.defaultExpanded ?? false;

  // Keys whose state differs from the default. Initial = default (empty), then an
  // effect hydrates from user_preferences when a storageKey is set.
  const [flipped, setFlipped] = useState<Set<string>>(() => new Set());

  useEffect(() => {
    if (storageKey === null) {
      // No persistence: reset to the default state on (re)mount / key change.
      setFlipped(new Set());
      return;
    }
    let cancelled = false;
    void getPreference(storageKey)
      .then((raw) => {
        if (!cancelled) setFlipped(parseStoredExpanded(raw));
      })
      .catch(() => {
        // Best-effort: a read failure just keeps the default state.
      });
    return () => {
      cancelled = true;
    };
  }, [storageKey]);

  const persist = useCallback(
    (next: Set<string>) => {
      setFlipped(next);
      if (storageKey !== null) {
        void setPreference(storageKey, serializeExpanded(next)).catch(() => {
          // Best-effort: a write failure just means the next session falls back
          // to the default state.
        });
      }
    },
    [storageKey],
  );

  const isCollapsed = useCallback(
    (row: T) => isCollapsedFor(flipped, acc.keyOf(row), defaultExpanded),
    [flipped, acc, defaultExpanded],
  );

  const visible = useCallback((rows: T[]) => visibleRows(rows, acc, isCollapsed), [acc, isCollapsed]);

  const toggle = useCallback(
    (row: T) => {
      const key = acc.keyOf(row);
      const next = new Set(flipped);
      if (next.has(key)) next.delete(key);
      else next.add(key);
      persist(next);
    },
    [flipped, acc, persist],
  );

  // In `defaultExpanded` polarity the flipped set holds the *collapsed* keys, so
  // expanding everything means clearing it; collapsing everything means filling
  // it. The polarity is inverted when the default is collapsed.
  const expandAll = useCallback(
    (rows: T[]) => persist(defaultExpanded ? new Set() : new Set(collapsibleKeys(rows, acc))),
    [acc, persist, defaultExpanded],
  );

  const collapseAll = useCallback(
    (rows: T[]) => persist(defaultExpanded ? new Set(collapsibleKeys(rows, acc)) : new Set()),
    [acc, persist, defaultExpanded],
  );

  const groupCount = useCallback((rows: T[]) => collapsibleKeys(rows, acc).length, [acc]);

  const allExpanded = useCallback(
    (rows: T[]) => {
      const keys = collapsibleKeys(rows, acc);
      return keys.length > 0 && keys.every((k) => !isCollapsedFor(flipped, k, defaultExpanded));
    },
    [flipped, acc, defaultExpanded],
  );

  return { visible, isCollapsed, toggle, expandAll, collapseAll, allExpanded, groupCount };
}
