/**
 * Reading reward values off a promo code, and turning them into allocations.
 *
 * Two things this centralises, both previously duplicated and subtly wrong:
 *
 * 1. **Type matching.** `promo_code_rewards.type` is not written consistently —
 *    this console's own form only ever writes "POINTS"/"DISCOUNT", but the table
 *    also holds singular forms. The old check was
 *    `type.toUpperCase() === "POINTS"`, so a row stored as "point" silently
 *    produced `null` and the UI rendered "—" instead of the value. Matching is
 *    now case-insensitive and accepts singular or plural.
 *
 * 2. **The allocation rule.** A member only receives the reward once they have
 *    logged in (`members.first_access_attempt IS TRUE`); a registered member who
 *    has never logged in gets nothing. So the total a code has handed out is
 *    `value × logged-in members`, never `value × registrations`.
 */

/** Shape we can read rewards from — the API sometimes nests under `data`. */
type RewardBearing = {
  rewards?: unknown;
  data?: { rewards?: unknown } | null;
} | null;

type RawReward = { type?: unknown; value?: unknown; name?: unknown };

export type RewardKind = "points" | "discount";

/** Accepted spellings per kind, upper-cased. */
const KIND_ALIASES: Record<RewardKind, string[]> = {
  points: ["POINT", "POINTS", "PT", "PTS"],
  discount: ["DISCOUNT", "DISCOUNTS"],
};

function toNumber(value: unknown): number | null {
  if (typeof value === "number") return Number.isFinite(value) ? value : null;
  if (typeof value === "string") {
    const trimmed = value.trim();
    if (trimmed === "") return null;
    const n = Number(trimmed);
    return Number.isNaN(n) ? null : n;
  }
  return null;
}

/** Every reward on the promo, from whichever nesting the API used. */
function rewardList(promo: RewardBearing): RawReward[] {
  for (const candidate of [promo?.rewards, promo?.data?.rewards]) {
    if (Array.isArray(candidate) && candidate.length > 0) {
      return candidate as RawReward[];
    }
  }
  return [];
}

/**
 * Per-member value of a reward kind, or null when the promo carries none.
 *
 * Null and 0 are meaningfully different: null means "this code has no reward of
 * that kind" (render a dash), 0 means "it has one, worth nothing".
 */
export function rewardValue(
  promo: RewardBearing,
  kind: RewardKind,
): number | null {
  const aliases = KIND_ALIASES[kind];
  for (const reward of rewardList(promo)) {
    const type = (reward?.type ?? "").toString().trim().toUpperCase();
    if (aliases.includes(type)) {
      const value = toNumber(reward?.value);
      if (value !== null) return value;
    }
  }
  return null;
}

/** Both kinds at once, for callers that show a summary. */
export function rewardValues(promo: RewardBearing) {
  return {
    points: rewardValue(promo, "points"),
    discount: rewardValue(promo, "discount"),
  };
}

export interface Allocation {
  /** Per-member reward value, or null when the code has no such reward. */
  perMember: number | null;
  /** Members who signed up with the code. */
  registered: number;
  /** Members who signed up *and* have logged in at least once. */
  loggedIn: number;
  /**
   * Total handed out: `perMember × loggedIn`. Null when the code carries no
   * reward of this kind, so callers can distinguish "none" from "zero".
   */
  total: number | null;
  /** Registrations that have not logged in, so have received nothing yet. */
  pending: number;
}

/**
 * Allocation for one promo code and one reward kind.
 *
 * `loggedIn` is the basis, not `registered` — see the rule in the file header.
 */
export function allocationFor(
  promo: RewardBearing,
  kind: RewardKind,
  counts: { registered: number; loggedIn: number },
): Allocation {
  const perMember = rewardValue(promo, kind);
  const registered = Math.max(0, counts.registered || 0);
  const loggedIn = Math.max(0, Math.min(counts.loggedIn || 0, registered));
  return {
    perMember,
    registered,
    loggedIn,
    total: perMember === null ? null : perMember * loggedIn,
    pending: registered - loggedIn,
  };
}

/** Sums allocations across codes, ignoring codes with no reward of that kind. */
export function sumAllocations(allocations: Allocation[]): {
  total: number;
  codesWithReward: number;
  loggedIn: number;
  registered: number;
} {
  let total = 0;
  let codesWithReward = 0;
  let loggedIn = 0;
  let registered = 0;
  for (const a of allocations) {
    registered += a.registered;
    loggedIn += a.loggedIn;
    if (a.total !== null) {
      total += a.total;
      codesWithReward += 1;
    }
  }
  return { total, codesWithReward, loggedIn, registered };
}
