/**
 * Margin arithmetic — the reason PIG exists.
 *
 * A two-sided compute business buys capacity in blocks (a commitment) and sells
 * it in slices (allocations against deals). The spread between what a
 * GPU-hour cost and what it sold for, net of the hours nobody bought, is the
 * business. These functions are deliberately pure and unit-tested: every
 * dashboard number and every agent answer resolves through them, so an error
 * here is an error everywhere.
 *
 * All money is handled in minor units (cents) as integers. Floating-point
 * currency in a system that reports margin is a defect waiting to be found by
 * an accountant.
 */

/** A block of capacity purchased from a supplier. */
export interface CommitmentInput {
  /** Total GPU-hours contracted over the term. */
  gpuHours: number;
  /** What we pay per GPU-hour, in cents. */
  costPerGpuHourCents: number;
}

/** A slice of that block sold to a customer. */
export interface AllocationInput {
  /** GPU-hours allocated to a demand deal. */
  gpuHours: number;
  /** What the customer pays per GPU-hour, in cents. */
  pricePerGpuHourCents: number;
}

export interface MarginResult {
  /** Hours bought. */
  committedGpuHours: number;
  /** Hours sold. */
  allocatedGpuHours: number;
  /** Hours bought and not sold. This is the number that hurts. */
  idleGpuHours: number;
  /** Share of committed capacity that is sold, 0–1. */
  utilisation: number;
  /** Total paid to the supplier, cents. */
  costCents: number;
  /** Total billed to customers, cents. */
  revenueCents: number;
  /**
   * Revenue minus the FULL cost of the commitment — not merely the cost of the
   * hours that sold. Unsold hours on a commitment are already paid for, so
   * charging only allocated cost would flatter the number and hide the very
   * problem this system exists to surface.
   */
  grossMarginCents: number;
  /** Gross margin as a share of revenue, 0–1. Null when there is no revenue. */
  grossMarginPct: number | null;
  /** Effective blended margin per GPU-hour sold, in cents. Null if nothing sold. */
  marginPerAllocatedGpuHourCents: number | null;
}

export function computeMargin(
  commitment: CommitmentInput,
  allocations: readonly AllocationInput[],
): MarginResult {
  const committedGpuHours = commitment.gpuHours;
  const allocatedGpuHours = allocations.reduce((sum, a) => sum + a.gpuHours, 0);

  // Over-allocation is possible and legitimate: capacity can be oversubscribed
  // deliberately, on the assumption not every buyer uses their full reservation.
  // Clamping idle at zero keeps the figure meaningful when that happens.
  const idleGpuHours = Math.max(0, committedGpuHours - allocatedGpuHours);

  const costCents = Math.round(committedGpuHours * commitment.costPerGpuHourCents);
  const revenueCents = allocations.reduce(
    (sum, a) => sum + Math.round(a.gpuHours * a.pricePerGpuHourCents),
    0,
  );

  const grossMarginCents = revenueCents - costCents;

  return {
    committedGpuHours,
    allocatedGpuHours,
    idleGpuHours,
    utilisation: committedGpuHours > 0 ? allocatedGpuHours / committedGpuHours : 0,
    costCents,
    revenueCents,
    grossMarginCents,
    grossMarginPct: revenueCents > 0 ? grossMarginCents / revenueCents : null,
    marginPerAllocatedGpuHourCents:
      allocatedGpuHours > 0 ? grossMarginCents / allocatedGpuHours : null,
  };
}

/**
 * The break-even sell price for the remaining unsold hours on a commitment.
 *
 * This is the number a seller actually wants mid-quarter: "the block is half
 * sold and already paid for — what must I get for the rest to come out even?"
 * It falls as more of the block sells, which is why it is computed against
 * remaining hours rather than the whole commitment.
 *
 * Returns null when the block is fully allocated: there is nothing left to
 * price, and dividing by zero hours would produce a confident-looking
 * Infinity.
 */
export function breakEvenPricePerGpuHourCents(
  commitment: CommitmentInput,
  allocations: readonly AllocationInput[],
): number | null {
  const m = computeMargin(commitment, allocations);
  if (m.idleGpuHours <= 0) return null;
  const uncoveredCents = m.costCents - m.revenueCents;
  // Already in profit: any further sale is upside, so the floor is zero rather
  // than a negative price, which would be nonsense to display.
  if (uncoveredCents <= 0) return 0;
  return uncoveredCents / m.idleGpuHours;
}

/**
 * Aggregate margin across many commitments — a book-level view.
 *
 * Deliberately sums the underlying cents rather than averaging the per-block
 * percentages: an average of ratios weights a tiny block equally with a huge
 * one and produces a number that is wrong in the direction of whichever blocks
 * happen to be small.
 */
export function aggregateMargin(
  books: readonly { commitment: CommitmentInput; allocations: readonly AllocationInput[] }[],
): MarginResult {
  const results = books.map((b) => computeMargin(b.commitment, b.allocations));

  const committedGpuHours = results.reduce((s, r) => s + r.committedGpuHours, 0);
  const allocatedGpuHours = results.reduce((s, r) => s + r.allocatedGpuHours, 0);
  const idleGpuHours = results.reduce((s, r) => s + r.idleGpuHours, 0);
  const costCents = results.reduce((s, r) => s + r.costCents, 0);
  const revenueCents = results.reduce((s, r) => s + r.revenueCents, 0);
  const grossMarginCents = revenueCents - costCents;

  return {
    committedGpuHours,
    allocatedGpuHours,
    idleGpuHours,
    utilisation: committedGpuHours > 0 ? allocatedGpuHours / committedGpuHours : 0,
    costCents,
    revenueCents,
    grossMarginCents,
    grossMarginPct: revenueCents > 0 ? grossMarginCents / revenueCents : null,
    marginPerAllocatedGpuHourCents:
      allocatedGpuHours > 0 ? grossMarginCents / allocatedGpuHours : null,
  };
}

/** Format cents as a currency string for display. */
export function formatCents(cents: number, currency = 'USD'): string {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency,
    maximumFractionDigits: 2,
  }).format(cents / 100);
}
