import { useEffect, useMemo, useState } from "react";
import {
  getSignupAggregate,
  type AccountStatusScope,
} from "@/lib/features/promo-code-campaigns/query";
import { listPromoCodes } from "@/lib/features/promo-codes/query";
import type {
  PromoCode,
  PromoCodeListQueryParams,
} from "@/lib/features/promo-codes/types";

export type PromoSignupRow = PromoCode & {
  _registered: number;
  _loggedIn: number;
};

const codeOf = (p: PromoCode): string => String(p.code ?? "").trim();

// Fetch every promo code matching the base query (all pages).
async function fetchAllPromoCodes(
  query: PromoCodeListQueryParams,
  signal?: AbortSignal,
): Promise<PromoCode[]> {
  const all: PromoCode[] = [];
  const pageSize = 200;
  let offset = 0;
  while (offset < 10000) {
    if (signal?.aborted) throw new DOMException("Cancelled", "AbortError");
    const res = await listPromoCodes({ ...query, limit: pageSize, offset });
    if (!res.success) {
      throw new Error(res.message || "Failed to load promo codes");
    }
    const items = res.data?.data ?? [];
    all.push(...items);
    const pg = res.data?.pagination;
    const hasNext = pg?.hasNext ?? items.length === pageSize;
    if (items.length < pageSize || !hasNext) break;
    offset += pageSize;
  }
  return all;
}

/**
 * Dataset-wide promo-code + signup data for the signup window/country business
 * filters. Shared by the Reports → Promo Codes builder and the Promo Codes list
 * page so both filter the WHOLE matching set (not just one server page).
 *
 * Returns rows enriched with window/country-scoped Registered & Logged-In
 * counts (NOT filtered/sorted/paginated — the caller applies its own policy)
 * plus the country options present in the window.
 *
 * Pass a STABLE `baseQuery` (memoize it) to avoid refetch loops.
 */
export function usePromoCodeSignupData(opts: {
  baseQuery: PromoCodeListQueryParams;
  window: { from?: string; to?: string };
  country: string[];
  /*
   * The account-status scope the page is showing.
   *
   * Passed in rather than read from the filter hook here: this hook is a data
   * source and the filter belongs to the page, but the counts it returns land in
   * the same table as figures that *are* scoped — so without it the table reports
   * every status while the tiles above it report ACTIVE.
   */
  scope?: AccountStatusScope;
  /** `JSON.stringify(scope)`, so the refetch depends on the value not the object. */
  scopeKey?: string;
  enabled?: boolean;
  // Bump to force a re-fetch (e.g. after a create / delete).
  refreshKey?: number;
}): {
  loading: boolean;
  error: string | null;
  rows: PromoSignupRow[];
  countryOptions: string[];
} {
  const {
    baseQuery,
    window: win,
    country,
    scope,
    scopeKey = "",
    enabled = true,
    refreshKey = 0,
  } = opts;
  const [allCodes, setAllCodes] = useState<PromoCode[]>([]);
  const [aggRows, setAggRows] = useState<
    Array<{
      code: string;
      country: string | null;
      signups: number;
      loggedIn: number;
    }>
  >([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Serialize the base query so the effect only re-runs on a real change.
  const baseKey = `${JSON.stringify(baseQuery)}:${refreshKey}`;

  // 1) All codes for the base query.
  useEffect(() => {
    if (!enabled) {
      setAllCodes([]);
      return;
    }
    let cancelled = false;
    setLoading(true);
    void (async () => {
      try {
        const codes = await fetchAllPromoCodes(baseQuery);
        if (cancelled) return;
        setAllCodes(codes);
        setError(null);
      } catch (e) {
        if (cancelled) return;
        setError(e instanceof Error ? e.message : "Failed to load promo codes");
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => {
      cancelled = true;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [enabled, baseKey]);

  // 2) Windowed signup aggregate for those codes.
  useEffect(() => {
    if (!enabled) {
      setAggRows([]);
      return;
    }
    const codeStrings = allCodes.map(codeOf).filter(Boolean);
    if (codeStrings.length === 0) {
      setAggRows([]);
      return;
    }
    let cancelled = false;
    void (async () => {
      const res = await getSignupAggregate(codeStrings, {
        from: win.from,
        to: win.to,
        ...scope,
      });
      if (cancelled || !res.success) return;
      setAggRows(res.data);
    })();
    return () => {
      cancelled = true;
    };
    // `scopeKey` rather than `scope`: a fresh object every render would refetch on
    // every render, and comparing by value is what makes that impossible.
  }, [enabled, allCodes, win.from, win.to, scopeKey]);

  const countryOptions = useMemo(() => {
    const set = new Set<string>();
    for (const r of aggRows) {
      const c = (r.country ?? "").trim();
      if (c) set.add(c);
    }
    return Array.from(set).sort();
  }, [aggRows]);

  // Per-code totals, optionally restricted to the selected countries.
  const statsByCode = useMemo(() => {
    const m = new Map<string, { signups: number; loggedIn: number }>();
    const countrySet = country.length ? new Set(country) : null;
    for (const r of aggRows) {
      if (countrySet && !(r.country && countrySet.has(r.country))) continue;
      const key = String(r.code).toUpperCase();
      const cur = m.get(key) ?? { signups: 0, loggedIn: 0 };
      cur.signups += Number(r.signups || 0);
      cur.loggedIn += Number(r.loggedIn || 0);
      m.set(key, cur);
    }
    return m;
  }, [aggRows, country]);

  const rows = useMemo<PromoSignupRow[]>(
    () =>
      allCodes.map((p) => {
        const s = statsByCode.get(String(p.code ?? "").toUpperCase());
        return {
          ...p,
          _registered: s?.signups ?? 0,
          _loggedIn: s?.loggedIn ?? 0,
        };
      }),
    [allCodes, statsByCode],
  );

  return { loading, error, rows, countryOptions };
}
