/**
 * Allocations — the margin ledger.
 *
 * This is the table PIG exists for. Everything else is scaffolding around it.
 *
 * A `capacity_commitment` is a block of GPU-hours bought from a provider at a
 * known cost. A `demand_deal` is an agreement to sell compute to a customer at
 * a known price. An allocation records that some of a specific block was sold
 * to a specific customer, at a specific price, for a specific window.
 *
 * From that single join, everything the business runs on falls out:
 *
 *   margin        = Σ(allocated hours × price) − (committed hours × cost)
 *   utilisation   = Σ(allocated hours) ÷ committed hours
 *   idle capacity = committed hours − Σ(allocated hours)
 *
 * No generic CRM can compute these, because none of them has a concept of a
 * cost-bearing commitment sitting behind the pipeline. That is the entire
 * argument for building this rather than configuring HubSpot.
 *
 * Note the deliberate asymmetry in the margin formula: cost is charged against
 * the FULL commitment, not merely the hours that sold. Unsold hours on a
 * commitment are already paid for. Charging only the allocated share would
 * report a healthy margin on a block that is bleeding money, which is precisely
 * the failure this system is meant to make impossible.
 */
import {
  index,
  integer,
  numeric,
  pgTable,
  text,
  timestamp,
  uuid,
} from 'drizzle-orm/pg-core';
import { ALLOCATION_STATUSES, GUARANTEE_TYPES } from '@pig/core';
export {
  ALLOCATION_STATUSES,
  CONSUMING_ALLOCATION_STATUSES,
  RESERVING_ALLOCATION_STATUSES,
} from '@pig/core';
export type { AllocationStatus } from '@pig/core';
import { capacityCommitments } from './supply';
import { demandDeals } from './demand';
import { users } from './identity';

export const allocations = pgTable(
  'allocations',
  {
    id: uuid('id').primaryKey().defaultRandom(),

    /** The block being drawn from. Restricted: never orphan a cost record. */
    capacityCommitmentId: uuid('capacity_commitment_id')
      .notNull()
      .references(() => capacityCommitments.id, { onDelete: 'restrict' }),

    /**
     * Who it was sold to. Nullable, because internal research consumption is a
     * real and important allocation with no deal and no revenue behind it.
     * Leaving research burn out of the ledger overstates available capacity and
     * understates true cost — the two mistakes this table prevents.
     */
    demandDealId: uuid('demand_deal_id').references(() => demandDeals.id, {
      onDelete: 'set null',
    }),

    /**
     * Set when the consumer is an internal team rather than a customer.
     * Mutually exclusive with `demandDealId` in practice; enforced in the
     * service layer rather than by constraint, because a research allocation
     * occasionally converts into a customer one and the transition should not
     * require deleting the row and losing its history.
     */
    internalTeam: text('internal_team'),

    /** Hours drawn from the block. */
    gpuHours: numeric('gpu_hours', { precision: 16, scale: 2 }).notNull(),

    /**
     * Sell price in cents per GPU-hour. Zero for internal research
     * consumption — which is meaningful, not missing: the hours cost real money
     * and earn none.
     */
    pricePerGpuHourCents: integer('price_per_gpu_hour_cents').notNull().default(0),
    currency: text('currency').notNull().default('USD'),

    /**
     * The window this allocation occupies. Must sit inside the commitment's own
     * window; capacity cannot be sold before it exists or after it lapses.
     */
    startsAt: timestamp('starts_at', { withTimezone: true }).notNull(),
    endsAt: timestamp('ends_at', { withTimezone: true }).notNull(),

    /**
     * `planned`    — pencilled in against a deal that has not closed
     * `committed`  — contractually promised to the customer
     * `active`     — running now
     * `completed`  — finished and billable
     * `released`   — given back; the hours return to available inventory
     *
     * Only `committed`, `active` and `completed` consume capacity. `planned`
     * allocations are shown separately so a seller can see what the pipeline
     * would do to utilisation without letting forecasts pollute the actuals.
     */
    status: text('status', { enum: ALLOCATION_STATUSES }).notNull().default('planned'),

    /**
     * A capacity HOLD with an expiry.
     *
     * When a seller reserves inventory against a deal that has not closed,
     * that capacity must disappear from everyone else's availability
     * immediately — otherwise two sellers promise the same GPUs and one of
     * them is wrong. Holds expire on a timer so that a stalled deal releases
     * inventory automatically rather than stranding it indefinitely.
     *
     * This single pair of fields is the clearest thing a generic CRM cannot
     * do: it will happily let you create an opportunity for any amount, and
     * nothing anywhere checks whether you can deliver it.
     */
    holdExpiresAt: timestamp('hold_expires_at', { withTimezone: true }),
    /** What else we turned away to keep this hold, in cents. Makes holds honest. */
    holdOpportunityCostCents: integer('hold_opportunity_cost_cents'),

    /**
     * Priority ladder, borrowed from how ad servers arbitrate guaranteed
     * against opportunistic demand — the closest published analogue to
     * arbitrating reserved against spot against internal burn.
     *
     * `guaranteed`   Contractually reserved; displaces everything below
     * `committed`    Reserved share, but not a fixed block
     * `on_demand`    Priced opportunistically
     * `preemptible`  Spot; yields to anything above it
     * `internal`     Research burn; the first thing displaced
     *
     * Lower `priority` integers win. Making this explicit is what lets the
     * system answer "can I actually sell this?" rather than "is something
     * technically free?"
     */
    guaranteeType: text('guarantee_type', { enum: GUARANTEE_TYPES })
      .notNull()
      .default('committed'),
    priority: integer('priority').notNull().default(100),

    /**
     * The compliance gate. An allocation is a specific buyer against specific
     * capacity in a specific jurisdiction, which is exactly the granularity at
     * which export control actually applies — see compliance.ts. Null means
     * unevaluated, which the service layer treats as blocking rather than
     * permissive for any cross-border match.
     */
    complianceDecisionId: uuid('compliance_decision_id'),

    createdByUserId: uuid('created_by_user_id').references(() => users.id, {
      onDelete: 'set null',
    }),
    notes: text('notes'),

    releasedAt: timestamp('released_at', { withTimezone: true }),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (t) => [
    index('allocations_commitment_idx').on(t.capacityCommitmentId),
    index('allocations_deal_idx').on(t.demandDealId),
    index('allocations_status_idx').on(t.status),
    index('allocations_window_idx').on(t.startsAt, t.endsAt),
    /** Sweeping expired holds back into available inventory. */
    index('allocations_hold_expiry_idx').on(t.holdExpiresAt),
  ],
);

export type Allocation = typeof allocations.$inferSelect;
export type NewAllocation = typeof allocations.$inferInsert;
