/**
 * The demand side of the demo book: customers, the people inside them, who on
 * our side owns each account, the deals, what was asked for, and what has been
 * reserved against a block.
 *
 * Every account here is INVENTED — see the integrity note in `./index.ts`.
 *
 * Three rules hold across the whole file, and they are what stop the numbers on
 * screen from contradicting one another.
 *
 * **A deal's value is derived, never asserted.** TCV is the money the hours
 * actually fetch — allocated hours × the price on the allocation, or requested
 * hours × the price we have quoted — and ACV is that annualised. An earlier
 * version wrote the two halves independently, and every deal's stated value
 * disagreed with the revenue implied by its own allocation by between 1.7× and
 * 3.6×. That is the kind of error a reader finds with a calculator in the first
 * minute of a demonstration.
 *
 * **A sell price sits between what the block cost and what the hardware
 * actually fetches.** The spread on this book, against the costs `supply.ts`
 * committed to:
 *
 *   H200  CoreWeave  cost 1.89  sold 2.39  +26%   (Halcyon, 12-month reserved)
 *   H100  Nebius EU  cost 1.71  sold 2.19  +28%   (Verity, EU-resident, certified)
 *   B200  Crusoe     cost 3.05  sold 3.75  +23%   (Northwind, 9-month block)
 *   A100  RunPod     cost 0.96  sold 1.25  +30%   (Tessellate, community pool)
 *
 * Twenty-something per cent on the hour is what a capacity intermediary can
 * defend in this market; the 43% the first draft carried is not, and it made
 * the flagship account look like a rounding error had been left in.
 *
 * **Cost is charged against the whole block, so the spread is not the margin.**
 * After idle hours and research burn — both already paid for — the book clears
 * roughly 5%, which is the number a brokerage actually lives on. The blocks
 * disagree with each other underneath it: the H200 block is 95% sold and makes
 * money, the EU block is 55% sold and does not, and the A100 pool has sold
 * nothing at all. That contrast is the point of the dataset.
 */
import {
  ALLOCATION_STATUSES,
  type AllocationStatus,
  type CustomerSegment,
  type DemandStage,
  type ProductLine,
  type SecurityTier,
  type Team,
  type TeamRole,
} from '@pig/core';
import { and, eq } from 'drizzle-orm';
import {
  accounts,
  activities,
  allocations,
  capacityCommitments,
  capacityRequests,
  contacts,
  dealContacts,
  demandDeals,
  teamMemberships,
  users,
  type NewAllocation,
} from '../../schema/index';
import { accountLastActivityAt, dealLastActivityAt, seedDealActivities } from './activities';
import type { DemoContext } from './index';

function isAllocationStatus(value: string): value is AllocationStatus {
  return (ALLOCATION_STATUSES as readonly string[]).includes(value);
}

// ---------------------------------------------------------------- our people

/**
 * The sellers, and the fact that a record belongs to one of them.
 *
 * `ownerUserId` is indexed on accounts, contacts and deals, and until now the
 * demo book left all three null — so every owner column rendered empty and the
 * owner filter on the calendar could only ever return nothing. These four are
 * invented, exactly like the customers, and none of them is a platform admin:
 * the development user stays the only administrator, because a demo dataset
 * that quietly grants administration is a trap rather than a convenience.
 *
 * Addresses are on a `.invalid` domain, which the DNS root guarantees can never
 * resolve. The base seed refuses to infer an address for a real person; the
 * least this file can do is make sure its invented ones can never reach one.
 */
interface DemoUser {
  readonly key: string;
  readonly name: string;
  readonly email: string;
  readonly handle: string;
  readonly title: string;
  readonly lastSeenDaysAgo: number;
  readonly teams: readonly { team: Team; role: TeamRole; primary?: boolean }[];
}

function demoUsers(prefix: string): readonly DemoUser[] {
  return [
    {
      key: 'ines',
      name: `${prefix}Ines Fabre`,
      email: 'ines.fabre@demo.pig.invalid',
      handle: 'demo-ines',
      title: 'Account executive — labs and research',
      lastSeenDaysAgo: 0,
      teams: [{ team: 'demand', role: 'lead', primary: true }],
    },
    {
      key: 'marcus',
      name: `${prefix}Marcus Oyelaran`,
      email: 'marcus.oyelaran@demo.pig.invalid',
      handle: 'demo-marcus',
      title: 'Account executive — enterprise, EMEA',
      lastSeenDaysAgo: 1,
      teams: [{ team: 'demand', role: 'member', primary: true }],
    },
    {
      // Both teams, deliberately. `teamMemberships` is a join table precisely
      // because in a company this size the person who sources the community
      // pool is also the person who sells burst capacity out of it.
      key: 'wren',
      name: `${prefix}Wren Abbot`,
      email: 'wren.abbot@demo.pig.invalid',
      handle: 'demo-wren',
      title: 'Capacity partnerships',
      lastSeenDaysAgo: 2,
      teams: [
        { team: 'supply', role: 'lead', primary: true },
        { team: 'demand', role: 'member' },
      ],
    },
    {
      key: 'rosalind',
      name: `${prefix}Rosalind Achebe`,
      email: 'rosalind.achebe@demo.pig.invalid',
      handle: 'demo-rosalind',
      title: 'Research programme lead',
      lastSeenDaysAgo: 4,
      teams: [{ team: 'research', role: 'lead', primary: true }],
    },
  ];
}

// ------------------------------------------------------------- the book

/** Roles as `schema/demand.ts` documents them on `deal_contacts`. */
type BuyingRole =
  | 'economic buyer'
  | 'champion'
  | 'technical evaluator'
  | 'procurement'
  | 'legal'
  | 'blocker';

interface ContactSpec {
  readonly name: string;
  readonly title: string;
  readonly decisionMaker?: boolean;
}

interface RequestSpec {
  readonly gpuType: string;
  readonly gpuCount: number;
  readonly fastFabric: boolean;
  /** The ceiling the customer has told us about. The other half of the spread. */
  readonly maxPriceCents: number;
  /**
   * What we have quoted for these hours. Present only where nothing is
   * reserved yet, because it is then the only honest basis for the deal value.
   */
  readonly quotedPriceCents?: number;
  readonly allowedRegions?: readonly string[];
  readonly certifications?: readonly string[];
  readonly minSecurityTier?: SecurityTier;
}

interface AllocationSpec {
  /** Supplier domain, which is how `seedSupply` keys the block it returned. */
  readonly supplier: string;
  /** Share of the block's hours. Cross-checked against the request below. */
  readonly share: number;
  readonly priceCents: number;
  readonly status: string;
  readonly holdDays?: number;
}

interface DealSpec {
  /** Stable handle, so a child deal can name its parent. */
  readonly key: string;
  readonly name: string;
  readonly productLine: ProductLine;
  readonly stage: DemandStage;
  readonly termMonths: number;
  readonly msaExecuted: boolean;
  readonly dpaExecuted: boolean;
  readonly close: { readonly quarter: -1 | 0 | 1; readonly fraction: number };
  /** Set on `closed_won` and `closed_lost`; drives `closedAt`/`closedReason`. */
  readonly closedReason?: string;
  /** The deal this one grew out of. Must appear earlier in the account's list. */
  readonly parent?: string;
  readonly owner?: string;
  readonly primaryContact: string;
  readonly buyingGroup: readonly { readonly contact: string; readonly role: BuyingRole }[];
  readonly request?: RequestSpec;
  readonly allocation?: AllocationSpec;
  /**
   * Hours already delivered, for a deal that closed before the blocks in this
   * book existed. Still hours × price — a closed deal's value is not exempt
   * from the rule, it simply has no live allocation to read it from.
   */
  readonly delivered?: { readonly gpuHours: number; readonly priceCents: number };
}

interface AccountSpec {
  readonly account: string;
  readonly segment: CustomerSegment;
  readonly country: string;
  /** Key into `demoUsers`. Carried down to the account's contacts and deals. */
  readonly owner: string;
  readonly contacts: readonly ContactSpec[];
  readonly deals: readonly DealSpec[];
}

/**
 * Fictional customers.
 *
 * Invented deliberately — see the note at the top of `./index.ts`. Any
 * resemblance to a real company is unintended, and none of these figures
 * describes anyone's actual contract.
 *
 * Built from the prefix rather than declared with it baked in, for the reason
 * given on `DemoContext`: this module must not read a value out of `./index.ts`
 * while that module is still evaluating.
 */
function demandBook(prefix: string): readonly AccountSpec[] {
  return [
    {
      account: `${prefix}Halcyon Research`,
      segment: 'frontier_lab',
      country: 'United States',
      owner: 'ines',
      contacts: [
        { name: 'Dana Whitfield', title: 'Head of Infrastructure', decisionMaker: true },
        { name: 'Marisol Baptiste', title: 'VP Finance', decisionMaker: true },
        { name: 'Tobias Lind', title: 'Staff Research Engineer' },
      ],
      deals: [
        {
          // The land. Won last quarter, and the reason the block beneath it
          // exists at all: a lab buys a quarter before it buys a year.
          key: 'halcyon-burst',
          name: `${prefix}H100 burst — pre-training run`,
          productLine: 'compute_reserved',
          stage: 'closed_won',
          termMonths: 3,
          msaExecuted: true,
          dpaExecuted: true,
          close: { quarter: -1, fraction: 0.3 },
          closedReason:
            'Won. First paid run for this counterparty; the 12-month H200 block was signed off the back of it.',
          primaryContact: 'Dana Whitfield',
          buyingGroup: [{ contact: 'Dana Whitfield', role: 'champion' }],
          // 96 GPUs for 90 days at 2.34, on capacity that has since ended.
          delivered: { gpuHours: 96 * 90 * 24, priceCents: 234 },
        },
        {
          key: 'halcyon-cluster',
          name: `${prefix}Pre-training cluster, 12 months`,
          productLine: 'compute_reserved',
          stage: 'deployment',
          termMonths: 12,
          msaExecuted: true,
          dpaExecuted: true,
          // Slipped: the close date is in the quarter just gone while the deal
          // is still open, so the calendar has a genuinely overdue item.
          close: { quarter: -1, fraction: 0.62 },
          parent: 'halcyon-burst',
          primaryContact: 'Dana Whitfield',
          buyingGroup: [
            { contact: 'Dana Whitfield', role: 'champion' },
            { contact: 'Marisol Baptiste', role: 'economic buyer' },
            { contact: 'Tobias Lind', role: 'technical evaluator' },
          ],
          // 400 GPUs continuously for a year, which is a cluster a lab
          // genuinely rents from a third party. The 256 this once said made
          // the largest account on the book smaller than a Series A's fleet.
          request: { gpuType: 'H200', gpuCount: 400, fastFabric: true, maxPriceCents: 259 },
          // 87% of the CoreWeave block: 424 GPUs' worth of delivered hours
          // against a 400-GPU ask, which is the headroom a 94%-availability
          // fleet needs to keep 400 of them lit.
          allocation: { supplier: 'coreweave.com', share: 0.87, priceCents: 239, status: 'active' },
        },
        {
          // Managed post-training, priced above raw capacity because it carries
          // our engineering rather than only our hours.
          key: 'halcyon-post-training',
          name: `${prefix}Managed post-training run`,
          productLine: 'post_training',
          stage: 'poc',
          termMonths: 3,
          msaExecuted: true,
          dpaExecuted: true,
          close: { quarter: 0, fraction: 0.66 },
          primaryContact: 'Tobias Lind',
          buyingGroup: [
            { contact: 'Tobias Lind', role: 'technical evaluator' },
            { contact: 'Dana Whitfield', role: 'champion' },
          ],
          request: {
            gpuType: 'H200',
            gpuCount: 32,
            fastFabric: true,
            maxPriceCents: 320,
            quotedPriceCents: 289,
          },
        },
      ],
    },
    {
      account: `${prefix}Verity Health AI`,
      segment: 'enterprise',
      country: 'Germany',
      owner: 'marcus',
      contacts: [
        { name: 'Lukas Brenner', title: 'VP Engineering', decisionMaker: true },
        { name: 'Annika Voss', title: 'Head of IT Procurement', decisionMaker: true },
        // The reason `dpaExecuted` is false on the deal below. A data
        // protection officer is a named blocker with a job title, not an
        // unexplained flag.
        { name: 'Dr Elif Sahin', title: 'Data Protection Officer' },
      ],
      deals: [
        {
          key: 'verity-fine-tuning',
          name: `${prefix}EU-resident fine-tuning`,
          productLine: 'post_training',
          stage: 'procurement',
          termMonths: 6,
          msaExecuted: true,
          dpaExecuted: false,
          close: { quarter: 0, fraction: 0.55 },
          primaryContact: 'Lukas Brenner',
          buyingGroup: [
            { contact: 'Lukas Brenner', role: 'champion' },
            { contact: 'Annika Voss', role: 'procurement' },
            { contact: 'Dr Elif Sahin', role: 'legal' },
          ],
          // Data residency: must land in the EU. Drives the Nebius block.
          request: {
            gpuType: 'H100_80GB',
            gpuCount: 64,
            fastFabric: true,
            maxPriceCents: 260,
            allowedRegions: ['eu-north', 'eu-west'],
            certifications: ['ISO 27001', 'SOC 2 Type II'],
          },
          allocation: { supplier: 'nebius.com', share: 0.55, priceCents: 219, status: 'committed' },
        },
        {
          // The one open request the idle half of the EU block could actually
          // serve: same region, same certifications, and 103,680 hours against
          // the ~234,000 sitting unsold on that commitment.
          key: 'verity-inference',
          name: `${prefix}EU inference endpoint`,
          productLine: 'inference',
          stage: 'scoping',
          termMonths: 6,
          msaExecuted: true,
          dpaExecuted: false,
          close: { quarter: 1, fraction: 0.3 },
          primaryContact: 'Lukas Brenner',
          buyingGroup: [{ contact: 'Lukas Brenner', role: 'champion' }],
          request: {
            gpuType: 'H100_80GB',
            gpuCount: 24,
            fastFabric: false,
            maxPriceCents: 235,
            quotedPriceCents: 209,
            allowedRegions: ['eu-north', 'eu-west'],
            certifications: ['ISO 27001'],
          },
        },
      ],
    },
    {
      account: `${prefix}Northwind Robotics`,
      segment: 'applied_ai_startup',
      country: 'United States',
      owner: 'marcus',
      contacts: [
        { name: 'Priya Raghavan', title: 'CTO', decisionMaker: true },
        { name: 'Jonah Reyes', title: 'Head of ML Infrastructure' },
      ],
      deals: [
        {
          // The land-and-expand root. Both deals below hang off it, which is
          // what `parentDealId` is for and what makes the motion visible.
          key: 'northwind-pilot',
          name: `${prefix}A100 inference pilot`,
          productLine: 'inference',
          stage: 'closed_won',
          termMonths: 3,
          msaExecuted: true,
          dpaExecuted: true,
          close: { quarter: -1, fraction: 0.55 },
          closedReason:
            'Won. Delivered inside the customer’s evaluation window; the Blackwell block was signed off the back of it.',
          primaryContact: 'Priya Raghavan',
          buyingGroup: [{ contact: 'Priya Raghavan', role: 'economic buyer' }],
          delivered: { gpuHours: 16 * 90 * 24, priceCents: 129 },
        },
        {
          key: 'northwind-production',
          name: `${prefix}Blackwell production block`,
          productLine: 'compute_reserved',
          stage: 'deployment',
          termMonths: 9,
          msaExecuted: true,
          dpaExecuted: true,
          close: { quarter: 0, fraction: 0.82 },
          parent: 'northwind-pilot',
          primaryContact: 'Priya Raghavan',
          buyingGroup: [
            { contact: 'Priya Raghavan', role: 'economic buyer' },
            { contact: 'Jonah Reyes', role: 'technical evaluator' },
          ],
          request: { gpuType: 'B200', gpuCount: 48, fastFabric: true, maxPriceCents: 410 },
          allocation: { supplier: 'crusoe.ai', share: 0.86, priceCents: 375, status: 'active' },
        },
        {
          // The expansion, and nothing on the book can cover it: the Crusoe
          // block is 86% sold. This is the demand that justifies the 256× H200
          // supply deal sitting in financial diligence.
          key: 'northwind-expansion',
          name: `${prefix}Fleet expansion — 32× B200`,
          productLine: 'compute_reserved',
          stage: 'expansion',
          termMonths: 9,
          msaExecuted: true,
          dpaExecuted: true,
          close: { quarter: 1, fraction: 0.45 },
          parent: 'northwind-pilot',
          primaryContact: 'Priya Raghavan',
          buyingGroup: [
            { contact: 'Priya Raghavan', role: 'economic buyer' },
            { contact: 'Jonah Reyes', role: 'technical evaluator' },
          ],
          request: {
            gpuType: 'B200',
            gpuCount: 32,
            fastFabric: true,
            maxPriceCents: 410,
            // No volume discount on an increment; the blended rate is what the
            // customer negotiates at renewal, not now.
            quotedPriceCents: 379,
          },
        },
      ],
    },
    {
      account: `${prefix}Tessellate Labs`,
      segment: 'applied_ai_startup',
      country: 'United Kingdom',
      owner: 'wren',
      // Single-threaded on a founding engineer who cannot sign. Deliberately
      // the only account with one contact: it is why the deal is at proposal
      // with an unreturned MSA, and the buying-group view should show it.
      contacts: [
        { name: 'Owen Marsh', title: 'Founding Engineer' },
      ],
      deals: [
        {
          key: 'tessellate-burst',
          name: `${prefix}Inference burst capacity`,
          productLine: 'inference',
          stage: 'proposal',
          termMonths: 3,
          msaExecuted: false,
          dpaExecuted: false,
          close: { quarter: 0, fraction: 0.34 },
          primaryContact: 'Owen Marsh',
          buyingGroup: [{ contact: 'Owen Marsh', role: 'champion' }],
          request: {
            gpuType: 'A100_80GB',
            gpuCount: 16,
            fastFabric: false,
            maxPriceCents: 175,
            // States what the pool actually is. A request that silently demands
            // secure_cloud cannot lawfully be served by the community block it
            // is held against, and the matcher is right to say so.
            minSecurityTier: 'community_cloud',
          },
          // A HOLD, not a sale. The deal has not closed, so this reserves
          // capacity without counting as revenue — the distinction the capacity
          // view exists to make visible.
          allocation: {
            supplier: 'runpod.io',
            share: 0.55,
            priceCents: 125,
            status: 'planned',
            holdDays: 12,
          },
        },
      ],
    },
    {
      account: `${prefix}Aurelian Systems`,
      segment: 'enterprise',
      country: 'United States',
      owner: 'ines',
      contacts: [
        { name: 'Meredith Cole', title: 'Director, ML Platform', decisionMaker: true },
        { name: 'Gregory Nkemdirim', title: 'Deputy General Counsel' },
        { name: 'Sandra Ipsen', title: 'VP Information Security' },
      ],
      deals: [
        {
          key: 'aurelian-committed',
          name: `${prefix}Multi-year committed capacity`,
          productLine: 'compute_reserved',
          stage: 'legal',
          termMonths: 24,
          msaExecuted: false,
          dpaExecuted: false,
          close: { quarter: 1, fraction: 0.38 },
          primaryContact: 'Meredith Cole',
          buyingGroup: [
            { contact: 'Meredith Cole', role: 'champion' },
            { contact: 'Gregory Nkemdirim', role: 'legal' },
            { contact: 'Sandra Ipsen', role: 'blocker' },
          ],
          // Quoted under Halcyon's rate: 24 months of committed volume is worth
          // a better number than 12, and this is the deal that would justify
          // the next block. Nothing is reserved — it is still in legal, which
          // is correct: capacity does not move before paper does.
          request: {
            gpuType: 'H200',
            gpuCount: 128,
            fastFabric: true,
            maxPriceCents: 245,
            quotedPriceCents: 229,
          },
        },
      ],
    },
    {
      account: `${prefix}Quillon AI`,
      segment: 'applied_ai_startup',
      country: 'Canada',
      owner: 'rosalind',
      contacts: [
        { name: 'Sofia Trentini', title: 'Head of Research', decisionMaker: true },
      ],
      deals: [
        {
          // The loss, with the reason a seller would actually write down.
          // Recorded rather than deleted: a book with no losses in it has no
          // win rate, and the next person to meet this account starts blind.
          key: 'quillon-reserved',
          name: `${prefix}Reserved H100 block — 12 months`,
          productLine: 'compute_reserved',
          stage: 'closed_lost',
          termMonths: 12,
          msaExecuted: false,
          dpaExecuted: false,
          close: { quarter: -1, fraction: 0.8 },
          closedReason:
            'Lost on price. A hyperscaler’s committed-use discount landed 19% under our quote; matching it would have put the hours below the cost we pay upstream for the block.',
          primaryContact: 'Sofia Trentini',
          buyingGroup: [{ contact: 'Sofia Trentini', role: 'economic buyer' }],
          delivered: { gpuHours: 64 * 360 * 24, priceCents: 209 },
        },
        {
          // The consolation, and how the relationship stayed alive.
          key: 'quillon-evaluations',
          name: `${prefix}Evaluation harness pilot`,
          productLine: 'evaluations',
          stage: 'qualification',
          termMonths: 3,
          msaExecuted: false,
          dpaExecuted: false,
          close: { quarter: 1, fraction: 0.74 },
          primaryContact: 'Sofia Trentini',
          buyingGroup: [{ contact: 'Sofia Trentini', role: 'economic buyer' }],
          // Sixteen L40S for the harness itself — small, cheap and nothing like
          // the block they bought elsewhere.
          request: {
            gpuType: 'L40S',
            gpuCount: 16,
            fastFabric: false,
            maxPriceCents: 115,
            quotedPriceCents: 98,
          },
        },
      ],
    },
  ];
}

/**
 * Forecast confidence by stage.
 *
 * A closed deal forecasts at certainty or at nothing, whatever its stage would
 * otherwise imply. `expansion` is deliberately mid-table rather than late:
 * this pipeline puts it after `deployment`, but an expansion is a fresh
 * opportunity with a warm start, not a deal about to sign.
 */
const STAGE_PROBABILITY: Record<DemandStage, number> = {
  qualification: 0.1,
  legal: 0.35,
  scoping: 0.4,
  proposal: 0.45,
  procurement: 0.6,
  poc: 0.7,
  deployment: 0.9,
  expansion: 0.5,
  closed_won: 1,
  closed_lost: 0,
};

/** ACV is the term value annualised. TCV is what the hours actually fetch. */
function annualise(tcvCents: number, termMonths: number): { acvCents: number; tcvCents: number } {
  return { acvCents: Math.round((tcvCents * 12) / termMonths), tcvCents };
}

/** What a block reservation is worth, resolved before the deal is written. */
interface Reservation {
  readonly commitmentId: string;
  readonly gpuHours: number;
  readonly revenueCents: number;
  readonly startsAt: Date;
  readonly endsAt: Date;
}

/**
 * Resolve the allocation before the deal row is inserted.
 *
 * The lookup used to run after the insert, which is how the two halves came to
 * be written independently in the first place: the deal was already in the
 * database by the time anyone knew what its hours were worth. Hoisting it is
 * what makes a derived value possible at all.
 */
async function reserve(
  context: DemoContext,
  commitmentIds: Map<string, string>,
  spec: AllocationSpec,
): Promise<Reservation | null> {
  const commitmentId = commitmentIds.get(spec.supplier);
  if (!commitmentId) return null;

  const [commitment] = await context.db
    .select()
    .from(capacityCommitments)
    .where(eq(capacityCommitments.id, commitmentId))
    .limit(1);
  if (!commitment) return null;

  const gpuHours = Math.round(Number(commitment.totalGpuHours) * spec.share);
  return {
    commitmentId,
    gpuHours,
    revenueCents: gpuHours * spec.priceCents,
    startsAt: commitment.startsAt,
    endsAt: commitment.endsAt,
  };
}

/**
 * Seed the demand book against the blocks the supply slice bought.
 *
 * `commitmentIds` is handed in rather than looked up: an allocation must point
 * at the block this run created, and re-querying by name would silently pick up
 * a commitment from an earlier run whose hours no longer match.
 */
export async function seedDemand(
  context: DemoContext,
  commitmentIds: Map<string, string>,
): Promise<void> {
  const { db, prefix, at, hours, quarterAt } = context;
  const ownerIds = await seedDemoUsers(context);
  const book = demandBook(prefix);

  // Parent ids by deal key. A child must be inserted after its parent, which
  // for this book means later in the same account's list; anything else is a
  // mistake worth failing loudly on rather than writing a null and moving on.
  const dealIdByKey = new Map<string, string>();
  let dealIndex = 0;

  for (const [accountIndex, entry] of book.entries()) {
    const [existingAccount] = await db
      .select({ id: accounts.id })
      .from(accounts)
      .where(eq(accounts.name, entry.account))
      .limit(1);
    if (existingAccount) continue;

    const accountOwnerId = ownerIds.get(entry.owner) ?? null;

    const [account] = await db
      .insert(accounts)
      .values({
        name: entry.account,
        side: 'demand',
        customerSegment: entry.segment,
        country: entry.country,
        description: 'Fictional company, for demonstration only.',
        source: 'seed',
        confidence: 'confirmed',
        ownerUserId: accountOwnerId,
        lastActivityAt: accountLastActivityAt(context, accountIndex),
      })
      .returning();
    if (!account) continue;

    const contactIdByName = new Map<string, string>();
    for (const person of entry.contacts) {
      const [contact] = await db
        .insert(contacts)
        .values({
          accountId: account.id,
          fullName: person.name,
          title: person.title,
          affiliation: 'staff',
          isDecisionMaker: person.decisionMaker ?? false,
          confidence: 'confirmed',
          source: 'seed',
          ownerUserId: accountOwnerId,
          email: null,
        })
        .returning();
      if (contact) contactIdByName.set(person.name, contact.id);
    }

    for (const spec of entry.deals) {
      const reservation = spec.allocation
        ? await reserve(context, commitmentIds, spec.allocation)
        : null;

      /*
       * The one place a deal's money is decided.
       *
       * Reserved hours price themselves; a request we have quoted against
       * prices itself from the quote; a deal that closed before these blocks
       * existed carries the hours it actually delivered. There is no fourth
       * case, and a deal that reaches one is a deal whose value nobody can
       * check against anything.
       */
      const quoted = spec.request?.quotedPriceCents;
      const termValueCents = reservation
        ? reservation.revenueCents
        : spec.request && quoted != null
          ? Number(hours(spec.request.gpuCount, spec.termMonths * 30, 1)) * quoted
          : spec.delivered
            ? spec.delivered.gpuHours * spec.delivered.priceCents
            : null;
      if (termValueCents === null) {
        throw new Error(`Demo deal ${spec.key} has no basis for its value.`);
      }

      const isClosed = spec.stage === 'closed_won' || spec.stage === 'closed_lost';
      const closedAt = isClosed ? quarterAt(spec.close.quarter, spec.close.fraction) : null;
      const parentDealId = spec.parent ? dealIdByKey.get(spec.parent) : undefined;
      if (spec.parent && !parentDealId) {
        throw new Error(`Demo deal ${spec.key} names a parent (${spec.parent}) not yet seeded.`);
      }

      const [deal] = await db
        .insert(demandDeals)
        .values({
          accountId: account.id,
          name: spec.name,
          productLine: spec.productLine,
          stage: spec.stage,
          // A closed deal stopped moving on the day it closed. Leaving this at
          // its default would date every historic deal to the seed run and make
          // time-in-stage nonsense; the open ones are spread rather than stamped
          // together for the same reason.
          stageChangedAt: closedAt ?? at(-9 - dealIndex * 4),
          ...annualise(termValueCents, spec.termMonths),
          termMonths: spec.termMonths,
          msaExecuted: spec.msaExecuted,
          dpaExecuted: spec.dpaExecuted,
          ownerUserId: ownerIds.get(spec.owner ?? entry.owner) ?? accountOwnerId,
          primaryContactId: contactIdByName.get(spec.primaryContact),
          parentDealId,
          expectedCloseDate: quarterAt(spec.close.quarter, spec.close.fraction),
          closedAt,
          closedReason: spec.closedReason,
          probability: String(STAGE_PROBABILITY[spec.stage]),
          lastActivityAt: closedAt ?? dealLastActivityAt(context, dealIndex),
        })
        .returning();
      if (!deal) continue;

      dealIdByKey.set(spec.key, deal.id);
      dealIndex += 1;

      for (const member of spec.buyingGroup) {
        const contactId = contactIdByName.get(member.contact);
        if (!contactId) continue;
        await db.insert(dealContacts).values({
          demandDealId: deal.id,
          contactId,
          role: member.role,
        });
      }

      if (spec.request) {
        await db.insert(capacityRequests).values({
          demandDealId: deal.id,
          gpuType: spec.request.gpuType,
          gpuCount: spec.request.gpuCount,
          requiresHighSpeedInterconnect: spec.request.fastFabric,
          minInterconnectType: spec.request.fastFabric ? 'Infiniband' : undefined,
          minSecurityTier: spec.request.minSecurityTier ?? 'secure_cloud',
          maxPricePerGpuHourCents: spec.request.maxPriceCents,
          allowedRegions: [...(spec.request.allowedRegions ?? [])],
          requiredCertifications: [...(spec.request.certifications ?? [])],
          // Where capacity is already reserved, the requirement is dated to the
          // window it is being served in. The forward-dated window this used to
          // carry ran past the end of the block behind it, so every covered
          // deal still read as uncovered.
          startsAt: reservation?.startsAt ?? at(15),
          endsAt: reservation?.endsAt ?? at(15 + spec.termMonths * 30),
          totalGpuHours: hours(spec.request.gpuCount, spec.termMonths * 30, 1),
        });
      }

      if (reservation && spec.allocation) {
        if (!isAllocationStatus(spec.allocation.status)) {
          throw new Error(`Invalid demo allocation status: ${spec.allocation.status}`);
        }
        const allocationRow = {
          capacityCommitmentId: reservation.commitmentId,
          demandDealId: deal.id,
          gpuHours: String(reservation.gpuHours),
          pricePerGpuHourCents: spec.allocation.priceCents,
          startsAt: reservation.startsAt,
          endsAt: reservation.endsAt,
          status: spec.allocation.status,
          guaranteeType: spec.allocation.status === 'planned' ? 'committed' : 'guaranteed',
          priority: spec.allocation.status === 'planned' ? 100 : 10,
          holdExpiresAt: spec.allocation.holdDays ? at(spec.allocation.holdDays) : null,
          createdByUserId: accountOwnerId,
          notes: spec.name,
        } satisfies NewAllocation;
        await db.insert(allocations).values(allocationRow);
      }

      if (isClosed && closedAt) {
        /*
         * A closed deal's last entry is its closure, written here rather than
         * through `seedDealActivities` because that helper narrates a deal in
         * flight — "next step agreed" is the wrong thing to say about a deal
         * that has none.
         */
        await db.insert(activities).values({
          type: 'note',
          subject: `${prefix}${spec.stage === 'closed_won' ? 'Closed won' : 'Closed lost'} — ${spec.name.slice(prefix.length)}`,
          body: spec.closedReason,
          accountId: account.id,
          demandDealId: deal.id,
          occurredAt: closedAt,
        });
      } else {
        await seedDealActivities(context, {
          accountId: account.id,
          demandDealId: deal.id,
          contactName: spec.primaryContact,
          stage: spec.stage,
        });
      }
    }
  }

  await seedInternalResearchBurn(context, commitmentIds);
}

/**
 * The sellers themselves, keyed by the handle the book refers to them by.
 *
 * Guarded on the address rather than the name: `users.email` is unique, so a
 * second run must find the row rather than collide with it. Team memberships
 * are upserted for the same reason.
 */
async function seedDemoUsers(context: DemoContext): Promise<Map<string, string>> {
  const { db, at } = context;
  const ids = new Map<string, string>();

  for (const person of demoUsers(context.prefix)) {
    const [existing] = await db
      .select({ id: users.id })
      .from(users)
      .where(eq(users.email, person.email))
      .limit(1);

    const id =
      existing?.id ??
      (
        await db
          .insert(users)
          .values({
            email: person.email,
            name: person.name,
            handle: person.handle,
            title: person.title,
            isPlatformAdmin: false,
            lastSeenAt: at(-person.lastSeenDaysAgo),
          })
          .returning({ id: users.id })
      )[0]?.id;
    if (!id) continue;

    ids.set(person.key, id);
    for (const membership of person.teams) {
      await db
        .insert(teamMemberships)
        .values({
          userId: id,
          team: membership.team,
          role: membership.role,
          isPrimary: membership.primary ?? false,
        })
        .onConflictDoNothing();
    }
  }

  return ids;
}

/**
 * Internal research burn against the largest block — real cost, no revenue.
 *
 * An allocation with no demand deal behind it, which is the case the margin
 * view has to get right: the hours are gone and nothing was sold for them.
 */
async function seedInternalResearchBurn(
  context: DemoContext,
  commitmentIds: Map<string, string>,
): Promise<void> {
  const { db, prefix } = context;

  const coreweave = commitmentIds.get('coreweave.com');
  if (!coreweave) return;

  const [commitment] = await db
    .select()
    .from(capacityCommitments)
    .where(eq(capacityCommitments.id, coreweave))
    .limit(1);
  const RESEARCH_NOTE = `${prefix}Internal research consumption`;
  const [existingResearch] = await db
    .select({ id: allocations.id })
    .from(allocations)
    .where(
      and(
        eq(allocations.capacityCommitmentId, coreweave),
        eq(allocations.notes, RESEARCH_NOTE),
      ),
    )
    .limit(1);

  if (commitment && !existingResearch) {
    const researchAllocationRow = {
      capacityCommitmentId: coreweave,
      internalTeam: 'research',
      gpuHours: String(Math.round(Number(commitment.totalGpuHours) * 0.08)),
      pricePerGpuHourCents: 0,
      startsAt: commitment.startsAt,
      endsAt: commitment.endsAt,
      status: 'active',
      guaranteeType: 'internal',
      priority: 200,
      notes: RESEARCH_NOTE,
    } satisfies NewAllocation;
    await db.insert(allocations).values(researchAllocationRow);
  }
}
