import { useCallback, useEffect, useMemo, useReducer } from 'react';
import { useTableFilterStore } from '@/stores/table/table-filter.store';
import { OptionType } from '@/components/input/SelectInput';

type TableFilterStateValue =
  | undefined
  | null
  | boolean
  | string
  | string[]
  | number
  | number[]
  | OptionType<number>
  | OptionType<number>[]
  | OptionType<string>
  | OptionType<string>[];

/** Core filter shape (page + pageSize) extended by your custom fields */
export type TableFilterState<
  TExtra extends Record<string, TableFilterStateValue>,
> = {
  page: number;
  pageSize: number;
} & TExtra;

type Action<TExtra extends Record<string, TableFilterStateValue>> =
  | { type: 'SET_PAGE'; page: number }
  | { type: 'SET_PAGE_SIZE'; pageSize: number; resetPage?: boolean }
  | { type: 'SET_FILTERS'; filters: Partial<TExtra>; resetPage?: boolean }
  | {
      type: 'UPDATE_FILTER';
      key: keyof TExtra;
      value: TExtra[keyof TExtra];
      resetPage?: boolean;
    }
  | { type: 'REPLACE_ALL'; next: TableFilterState<TExtra> }
  | { type: 'RESET' };

export type UseTableFilterOptions<
  TExtra extends Record<string, TableFilterStateValue>,
> = {
  /** Initial state; anything you omit falls back to defaults */
  initial?: Partial<TableFilterState<TExtra>>;
  /** Called after any state change */
  onChange?: (state: TableFilterState<TExtra>) => void;
  /** Default page size (if not provided in initial) */
  defaultPageSize?: number;
  /** Optional mapping to rename keys when exporting to URL (e.g., { pageSize: "rows" }) */
  paramMap?: Partial<Record<keyof TableFilterState<TExtra>, string>>;
  /** If true, `toSearchParams`/`toQueryString` will omit values equal to defaults */
  omitDefaultsInUrl?: boolean;
  /** Optional list of state keys that should never be serialized into the URL/query string */
  excludeKeysFromUrl?: Partial<(keyof TableFilterState<TExtra>)[]>;

  persist?: boolean;
  storeName?: string;
};

function clampToInt(n: number, min = 1) {
  const v = Number.isFinite(n) ? Math.floor(n) : min;
  return v < min ? min : v;
}

function createInitialState<
  TExtra extends Record<string, TableFilterStateValue>,
>(opts: UseTableFilterOptions<TExtra> | undefined): TableFilterState<TExtra> {
  const defaults = {
    page: 1,
    pageSize: opts?.defaultPageSize ?? 10,
  } as TableFilterState<TExtra>;

  return {
    ...defaults,
    ...(opts?.initial as object),
  } as TableFilterState<TExtra>;
}

function serializeValue(v: unknown): string | null {
  if (v === undefined || v === null) return null;

  if (v instanceof Date) return v.toISOString();

  if (v instanceof Object && (v as OptionType).value)
    return String((v as OptionType).value);

  if (Array.isArray(v))
    return v
      .map((x) => serializeValue(x))
      .filter((x) => x !== null)
      .join(',');

  const t = typeof v;

  if (t === 'string' || t === 'number' || t === 'boolean') return String(v);

  try {
    return JSON.stringify(v);
  } catch {
    return null;
  }
}

function shallowEqual<T extends TableFilterStateValue>(
  a: T | undefined | null,
  b: T | undefined | null
): boolean {
  return JSON.stringify(a) === JSON.stringify(b);
}

export function useTableFilter<
  TExtra extends Record<string, TableFilterStateValue>,
>(options?: UseTableFilterOptions<TExtra>) {
  if (options?.persist && !options?.storeName) {
    throw new Error(
      'storeName is required if persist is true in useTableFilter!'
    );
  }

  const storeName = options?.storeName ?? '';
  const persistedState = useTableFilterStore(
    useCallback(
      (storeState) =>
        storeName
          ? (storeState.data[storeName] as Partial<TableFilterState<TExtra>>)
          : undefined,
      [storeName]
    )
  );
  const setTableData = useTableFilterStore(
    (storeState) => storeState.setTableData
  );

  const defaults = useMemo(() => {
    return createInitialState<TExtra>(options);
  }, [options]);

  const initialState = useMemo(
    () =>
      ({
        ...defaults,
        ...(persistedState as object),
      }) as TableFilterState<TExtra>,
    [defaults, persistedState]
  );

  const [state, dispatch] = useReducer(
    (
      s: TableFilterState<TExtra>,
      a: Action<TExtra>
    ): TableFilterState<TExtra> => {
      switch (a.type) {
        case 'SET_PAGE':
          return { ...s, page: clampToInt(a.page) };
        case 'SET_PAGE_SIZE': {
          const pageSize = clampToInt(a.pageSize);
          const page = a.resetPage ? 1 : s.page;

          return { ...s, pageSize, page };
        }
        case 'SET_FILTERS': {
          const page = a.resetPage ? 1 : s.page;

          return { ...s, ...a.filters, page };
        }
        case 'UPDATE_FILTER': {
          const page = a.resetPage ? 1 : s.page;

          return {
            ...s,
            [a.key]: a.value,
            page,
          } as TableFilterState<TExtra>;
        }
        case 'REPLACE_ALL':
          return {
            ...a.next,
            page: clampToInt(a.next.page),
            pageSize: clampToInt(a.next.pageSize),
          };
        case 'RESET':
          return defaults;
        default:
          return s;
      }
    },
    initialState
  );

  useEffect(() => {
    if (!options?.persist || !storeName) {
      return;
    }

    setTableData(storeName, state);
  }, [options?.persist, setTableData, state, storeName]);

  const onChange = options?.onChange;
  useEffect(() => {
    if (onChange) onChange(state);
  }, [state, onChange]);

  // Helpers (stable)
  const setPage = useCallback((page: number) => {
    dispatch({ type: 'SET_PAGE', page });
  }, []);

  const setPageSize = useCallback((pageSize: number, resetPage = true) => {
    dispatch({ type: 'SET_PAGE_SIZE', pageSize, resetPage });
  }, []);

  const setFilters = useCallback(
    (filters: Partial<TExtra>, resetPage = true) => {
      dispatch({ type: 'SET_FILTERS', filters, resetPage });
    },
    []
  );

  const updateFilter = useCallback(
    <K extends keyof TExtra>(key: K, value: TExtra[K], resetPage = false) => {
      dispatch({ type: 'UPDATE_FILTER', key, value, resetPage });
    },
    [dispatch]
  );

  const replaceAll = useCallback((next: TableFilterState<TExtra>) => {
    dispatch({ type: 'REPLACE_ALL', next });
  }, []);

  const reset = useCallback(() => dispatch({ type: 'RESET' }), []);

  const core = useMemo(
    () => ({ page: state.page, pageSize: state.pageSize }),
    [state.page, state.pageSize]
  );

  const extras = useMemo(() => {
    const stateWithExtras = state as TableFilterState<
      Record<string, TableFilterStateValue>
    >;
    const rest = Object.fromEntries(
      Object.entries(stateWithExtras).filter(
        ([key]) => key !== 'page' && key !== 'pageSize'
      )
    );
    return rest as TExtra;
  }, [state]);

  /** Map a key using paramMap (if provided) */
  const mapKey = useCallback(
    (key: string) => {
      const m = options?.paramMap as Record<string, string> | undefined;

      if (key === 'pageSize' && ((m && !m[key]) || !m)) return 'limit';

      return (m && m[key]) || key;
    },
    [options?.paramMap]
  );

  /** Build URLSearchParams from current state */
  const toSearchParams = useCallback(() => {
    const params = new URLSearchParams();
    const source = state as Record<string, TableFilterStateValue>;
    const baseline = options?.omitDefaultsInUrl ? defaults : null;
    const excludedKeys = new Set<string>(
      (options?.excludeKeysFromUrl as string[] | undefined) ?? []
    );

    for (const key of Object.keys(source)) {
      if (excludedKeys.has(key)) continue;

      const value = source[key];
      if (value === undefined || value === null) continue;

      if (baseline && shallowEqual(value, baseline[key])) {
        continue;
      }

      const mapped = mapKey(key);
      const serialized = serializeValue(value);
      if (serialized !== null) params.set(mapped, serialized);
    }
    return params;
  }, [
    state,
    defaults,
    options?.omitDefaultsInUrl,
    options?.excludeKeysFromUrl,
    mapKey,
  ]);

  /** Build query string (prefixed with '?', or empty string if none) */
  const toQueryString = useCallback(() => {
    const sp = toSearchParams();
    const s = sp.toString();
    return s ? `?${s}` : '';
  }, [toSearchParams]);

  return {
    /** Full state (page, pageSize, and extras) */
    state,
    /** Convenience accessors */
    page: state.page,
    pageSize: state.pageSize,
    filters: extras,
    /** Setters */
    setPage,
    setPageSize,
    setFilters,
    updateFilter,
    replaceAll,
    reset,
    /** Sometimes handy to have just the core pair */
    core,
    /** URL helpers */
    toSearchParams,
    toQueryString,
  };
}
