/**
 * The quarterly calendar — a projection, not a table.
 *
 * Everything with a date on it already lives somewhere: contracts expire,
 * obligations fall due, commitments open and close, holds lapse, export
 * authorisations run out. This service reads those columns where they are and
 * emits one common shape. Nothing here is stored, and nothing here can drift
 * from the record it describes.
 *
 * Three things shape the implementation.
 *
 * **One query per source, each with its own date predicate and its own
 * limit.** The convention elsewhere in this API is a flat `.limit(300)`
 * ordered by `updated_at`, with the caller filtering by date in the browser —
 * which means the deals actually closing this quarter are not guaranteed to be
 * in the response at all. That is precisely the bug this endpoint exists to
 * fix, so every predicate is server-side and every source is bounded
 * independently rather than competing for one budget.
 *
 * **Totals are separate aggregate queries.** If the header counted the rows in
 * the list it would under-report the moment any source truncated, and a
 * quarterly figure that silently shrinks is worse than no figure. The counts
 * are exact even when the list is cut short.
 *
 * **Renewal comes from `renewalAlarm()`.** The rule — expiry minus notice
 * days, only when auto-renewal is on — is defined once, in the contracts
 * service. The SQL below narrows candidates with the same arithmetic so the
 * scan stays bounded, but every date and every state on an emitted event comes
 * from calling that function. If the rule changes, it changes there.
 */
import {
  and,
  asc,
  count,
  eq,
  gt,
  gte,
  isNotNull,
  isNull,
  lt,
  or,
  sql,
} from 'drizzle-orm';
import {
  calendarEventId,
  completableSpanState,
  eventState,
  quarterOf,
  spanState,
  type CalendarEvent,
  type CalendarEventKind,
  type Quarter,
} from '@pig/core';
import {
  accounts,
  allocations,
  calendarEntries,
  capacityCommitments,
  complianceArtifacts,
  contractObligations,
  contracts,
  demandDeals,
  exportAuthorizations,
  supplyDeals,
  users,
  type Database,
} from '@pig/db';
import { renewalAlarm } from './contracts';

/** Per-source ceiling. Generous enough that a real quarter never reaches it. */
const DEFAULT_SOURCE_LIMIT = 500;

export interface CalendarQuery {
  from: Date;
  /** Exclusive. Quarters are half-open so consecutive ones do not double-count. */
  to: Date;
  kinds?: readonly CalendarEventKind[];
  accountId?: string;
  ownerUserId?: string;
  fiscalYearStartMonth?: number;
  timeZone?: string;
  sourceLimit?: number;
}

export interface CalendarTotals {
  /**
   * Σ acv × probability for deals whose expected close date falls in range.
   * The number a GTM lead reads first, and nothing in PIG computed it before.
   */
  weightedPipelineCents: number;
  closingCount: number;
  renewalCount: number;
  obligationCount: number;
  expiringAuthorizationCount: number;
}

export interface CalendarProjection {
  from: string;
  to: string;
  quarter: Quarter;
  events: CalendarEvent[];
  /** True when any single source hit its limit; the totals are still exact. */
  truncated: boolean;
  totals: CalendarTotals;
}

/**
 * Where the front end should go when an event is clicked.
 *
 * Most pages are still flat, so the page is the load-bearing half and the
 * query parameter is a hint the detail sheet can honour once one exists.
 */
function href(page: string, param: string, id: string): string {
  return `/${page}?${param}=${id}`;
}

/**
 * Accounts are the exception: `/accounts/:id` is a real detail route.
 *
 * A compliance deadline is read on the Overview, where the row names the
 * counterparty and the control says "Review". Sending that to `/accounts` with
 * an id nothing reads dropped the reader in front of twenty-three unfiltered
 * rows and left them to find the one the alert had just named.
 */
function accountHref(id: string): string {
  return `/accounts/${id}`;
}

/** Drizzle returns numeric columns as strings; `probability` is one of them. */
function numeric(value: string | null): number | null {
  if (value === null) return null;
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : null;
}

export class CalendarService {
  constructor(
    private readonly db: Database,
    private readonly clock: () => Date = () => new Date(),
  ) {}

  /**
   * The reader's own quarter boundary.
   *
   * `users.timezone` is settable through PATCH /api/me/preferences and until
   * now was read by nothing at all. A quarter is a local-midnight question, so
   * this is the first place it genuinely matters — and UTC remains the honest
   * fallback for a user who has never set one.
   */
  async timeZoneFor(userId: string): Promise<string> {
    const [row] = await this.db
      .select({ timezone: users.timezone })
      .from(users)
      .where(eq(users.id, userId))
      .limit(1);
    return row?.timezone ?? 'UTC';
  }

  async project(query: CalendarQuery): Promise<CalendarProjection> {
    const now = this.clock();
    const timeZone = query.timeZone ?? 'UTC';
    const fiscalYearStartMonth = query.fiscalYearStartMonth ?? 0;
    const limit = query.sourceLimit ?? DEFAULT_SOURCE_LIMIT;
    const wanted = query.kinds?.length ? new Set(query.kinds) : null;
    const wants = (kind: CalendarEventKind): boolean => !wanted || wanted.has(kind);

    const collected: { events: CalendarEvent[]; truncated: boolean }[] = await Promise.all([
      wants('expected_close') ? this.expectedClose(query, now, limit) : empty(),
      wants('contract_effective')
        ? this.contractDate(query, now, limit, 'contract_effective')
        : empty(),
      wants('contract_expiry')
        ? this.contractDate(query, now, limit, 'contract_expiry')
        : empty(),
      wants('contract_executed')
        ? this.contractDate(query, now, limit, 'contract_executed')
        : empty(),
      wants('renewal_notice') ? this.renewalNotices(query, now, limit) : empty(),
      wants('obligation_due') ? this.obligations(query, now, limit) : empty(),
      wants('capacity_window') ? this.capacityWindows(query, now, limit) : empty(),
      wants('allocation_window') ? this.allocationWindows(query, now, limit) : empty(),
      wants('hold_expiry') ? this.holdExpiries(query, now, limit) : empty(),
      wants('supply_available_from') ? this.supplyAvailability(query, now, limit) : empty(),
      wants('authorization_expiry') ? this.authorizationExpiries(query, now, limit) : empty(),
      wants('artifact_expiry') ? this.artifactExpiries(query, now, limit) : empty(),
      wants('calendar_entry') ? this.entries(query, now, limit) : empty(),
    ]);

    const events = collected
      .flatMap((source) => source.events)
      .sort((a, b) => a.startsAt.localeCompare(b.startsAt) || a.id.localeCompare(b.id));

    return {
      from: query.from.toISOString(),
      to: query.to.toISOString(),
      quarter: quarterOf(query.from, fiscalYearStartMonth, timeZone),
      events,
      truncated: collected.some((source) => source.truncated),
      totals: await this.totals(query),
    };
  }

  // ------------------------------------------------------------------ totals

  /**
   * Counted in SQL rather than off the event list, so a truncated source
   * cannot quietly shrink a quarterly figure. The kind filter is deliberately
   * ignored here: narrowing the list to one kind should not blank the header
   * the reader is narrowing against.
   */
  private async totals(query: CalendarQuery): Promise<CalendarTotals> {
    const { from, to, accountId, ownerUserId } = query;

    const [pipeline, renewals, obligations, authorizations] = await Promise.all([
      this.db
        .select({
          /**
           * A closed-won deal forecasts at certainty and a closed-lost one at
           * nothing, whatever `probability` still says; an open deal with no
           * forecast contributes nothing rather than its full value, because
           * an unfilled field is not a prediction of 100%.
           */
          weightedCents: sql<string>`coalesce(sum(round(${demandDeals.acvCents} * (case
            when ${demandDeals.stage} = 'closed_won' then 1
            when ${demandDeals.stage} = 'closed_lost' then 0
            else coalesce(${demandDeals.probability}, 0) end))), 0)`,
          closing: sql<number>`count(*) filter (where ${demandDeals.stage} <> 'closed_lost')::int`,
        })
        .from(demandDeals)
        .where(
          and(
            gte(demandDeals.expectedCloseDate, from),
            lt(demandDeals.expectedCloseDate, to),
            accountId ? eq(demandDeals.accountId, accountId) : undefined,
            ownerUserId ? eq(demandDeals.ownerUserId, ownerUserId) : undefined,
          ),
        ),
      this.db
        .select({ value: count() })
        .from(contracts)
        .where(this.renewalPredicate(query)),
      this.db
        .select({ value: count() })
        .from(contractObligations)
        .innerJoin(contracts, eq(contracts.id, contractObligations.contractId))
        .where(
          and(
            gte(contractObligations.dueAt, from),
            lt(contractObligations.dueAt, to),
            // Outstanding only. A count that includes work already done reads
            // as a backlog that is not there.
            isNull(contractObligations.completedAt),
            accountId ? eq(contracts.accountId, accountId) : undefined,
            ownerUserId ? eq(contractObligations.ownerUserId, ownerUserId) : undefined,
          ),
        ),
      // An expiring authorisation has no owner column, so an owner filter can
      // only ever exclude it — reporting zero rather than the whole book.
      ownerUserId
        ? Promise.resolve([{ value: 0 }])
        : this.db
            .select({ value: count() })
            .from(exportAuthorizations)
            .where(
              and(
                gte(exportAuthorizations.expiresAt, from),
                lt(exportAuthorizations.expiresAt, to),
                accountId ? eq(exportAuthorizations.accountId, accountId) : undefined,
              ),
            ),
    ]);

    return {
      weightedPipelineCents: Math.round(Number(pipeline[0]?.weightedCents ?? 0)),
      closingCount: pipeline[0]?.closing ?? 0,
      renewalCount: renewals[0]?.value ?? 0,
      obligationCount: obligations[0]?.value ?? 0,
      expiringAuthorizationCount: authorizations[0]?.value ?? 0,
    };
  }

  // ----------------------------------------------------------------- sources

  private async expectedClose(query: CalendarQuery, now: Date, limit: number) {
    const rows = await this.db
      .select({ deal: demandDeals, accountName: accounts.name })
      .from(demandDeals)
      .leftJoin(accounts, eq(accounts.id, demandDeals.accountId))
      .where(
        and(
          gte(demandDeals.expectedCloseDate, query.from),
          lt(demandDeals.expectedCloseDate, query.to),
          query.accountId ? eq(demandDeals.accountId, query.accountId) : undefined,
          query.ownerUserId ? eq(demandDeals.ownerUserId, query.ownerUserId) : undefined,
        ),
      )
      .orderBy(asc(demandDeals.expectedCloseDate))
      .limit(limit + 1);

    return bounded(rows, limit, ({ deal, accountName }) => {
      const at = deal.expectedCloseDate!;
      const probability = numeric(deal.probability);
      return {
        id: calendarEventId('demand_deal', deal.id, 'expectedCloseDate'),
        kind: 'expected_close' as const,
        title: deal.name,
        startsAt: at.toISOString(),
        endsAt: null,
        isSpan: false,
        state: eventState({ at, now, completedAt: deal.closedAt }),
        accountId: deal.accountId,
        accountName,
        ownerUserId: deal.ownerUserId,
        amountCents: deal.acvCents,
        currency: deal.currency,
        recordType: 'demand_deal',
        recordId: deal.id,
        href: href('demand', 'deal', deal.id),
        meta: {
          stage: deal.stage,
          probability,
          productLine: deal.productLine,
          weightedCents:
            deal.acvCents !== null && probability !== null
              ? Math.round(deal.acvCents * probability)
              : null,
        },
      };
    });
  }

  private async contractDate(
    query: CalendarQuery,
    now: Date,
    limit: number,
    kind: 'contract_effective' | 'contract_expiry' | 'contract_executed',
  ) {
    const column =
      kind === 'contract_effective'
        ? contracts.effectiveAt
        : kind === 'contract_expiry'
          ? contracts.expiresAt
          : contracts.executedAt;
    const field =
      kind === 'contract_effective'
        ? 'effectiveAt'
        : kind === 'contract_expiry'
          ? 'expiresAt'
          : 'executedAt';
    const label =
      kind === 'contract_effective'
        ? 'takes effect'
        : kind === 'contract_expiry'
          ? 'expires'
          : 'executed';

    const rows = await this.db
      .select({ contract: contracts, accountName: accounts.name })
      .from(contracts)
      .leftJoin(accounts, eq(accounts.id, contracts.accountId))
      .where(
        and(
          gte(column, query.from),
          lt(column, query.to),
          query.accountId ? eq(contracts.accountId, query.accountId) : undefined,
          query.ownerUserId ? eq(contracts.ownerUserId, query.ownerUserId) : undefined,
        ),
      )
      .orderBy(asc(column))
      .limit(limit + 1);

    return bounded(rows, limit, ({ contract, accountName }) => {
      const at = contract[field]!;
      return {
        id: calendarEventId('contract', contract.id, field),
        kind,
        title: `${contract.title} ${label}`,
        startsAt: at.toISOString(),
        endsAt: null,
        isSpan: false,
        // An executed date is a fact about the past, not an errand: it is
        // recorded as done so it does not sit in the overdue list forever.
        state:
          kind === 'contract_executed'
            ? ('done' as const)
            : eventState({ at, now, completedAt: contract.terminatedAt }),
        accountId: contract.accountId,
        accountName,
        ownerUserId: contract.ownerUserId,
        amountCents: contract.valueCents,
        currency: contract.currency,
        recordType: 'contract',
        recordId: contract.id,
        href: href('contracts', 'contract', contract.id),
        meta: {
          contractType: contract.type,
          status: contract.status,
          side: contract.side,
          terminatedAt: contract.terminatedAt?.toISOString() ?? null,
        },
      };
    });
  }

  /**
   * The SQL narrows; `renewalAlarm()` decides.
   *
   * The predicate repeats the expiry-minus-notice arithmetic only to keep the
   * scan bounded — the alternative is loading every auto-renewing contract in
   * the book. Every date and state that reaches a caller comes from the shared
   * function, so there is still exactly one definition of the rule.
   */
  private renewalPredicate(query: CalendarQuery) {
    return and(
      eq(contracts.isAutoRenew, true),
      isNotNull(contracts.noticeDays),
      isNotNull(contracts.expiresAt),
      // A terminated contract will not renew, so its notice date is not a
      // deadline anyone should be chased about.
      isNull(contracts.terminatedAt),
      // The bounds are bound as ISO text and cast, not as `Date`: drizzle types
      // parameters from the column in a comparison, and a raw template has no
      // column to learn from, so postgres-js receives a Date it cannot encode
      // and the whole request 500s. Found by calling the endpoint.
      sql`${contracts.expiresAt} - make_interval(days => ${contracts.noticeDays}) >= ${query.from.toISOString()}::timestamptz`,
      sql`${contracts.expiresAt} - make_interval(days => ${contracts.noticeDays}) < ${query.to.toISOString()}::timestamptz`,
      query.accountId ? eq(contracts.accountId, query.accountId) : undefined,
      query.ownerUserId ? eq(contracts.ownerUserId, query.ownerUserId) : undefined,
    );
  }

  private async renewalNotices(query: CalendarQuery, now: Date, limit: number) {
    const rows = await this.db
      .select({ contract: contracts, accountName: accounts.name })
      .from(contracts)
      .leftJoin(accounts, eq(accounts.id, contracts.accountId))
      .where(this.renewalPredicate(query))
      .orderBy(asc(contracts.expiresAt))
      .limit(limit + 1);

    return bounded(rows, limit, ({ contract, accountName }) => {
      const alarm = renewalAlarm(contract, now);
      const at = alarm.renewalNoticeAt!;
      return {
        id: calendarEventId('contract', contract.id, 'renewalNoticeAt'),
        kind: 'renewal_notice' as const,
        title: `Renewal notice — ${contract.title}`,
        startsAt: at.toISOString(),
        endsAt: null,
        isSpan: false,
        // 'expired' means the window to give notice has gone; the notice date
        // itself is simply late until then.
        state:
          alarm.renewalState === 'expired'
            ? ('overdue' as const)
            : eventState({ at, now }),
        accountId: contract.accountId,
        accountName,
        ownerUserId: contract.ownerUserId,
        amountCents: contract.valueCents,
        currency: contract.currency,
        recordType: 'contract',
        recordId: contract.id,
        href: href('contracts', 'contract', contract.id),
        meta: {
          renewalState: alarm.renewalState,
          expiresAt: contract.expiresAt?.toISOString() ?? null,
          noticeDays: contract.noticeDays,
          side: contract.side,
        },
      };
    });
  }

  /**
   * Every obligation on every contract, in one query.
   *
   * Obligations were reachable only inside GET /api/contracts/:id, so a
   * quarter of them meant one request per contract. They are the dated things
   * most likely to be missed, which makes that the wrong place for them to be.
   */
  private async obligations(query: CalendarQuery, now: Date, limit: number) {
    const rows = await this.db
      .select({
        obligation: contractObligations,
        contract: contracts,
        accountName: accounts.name,
      })
      .from(contractObligations)
      .innerJoin(contracts, eq(contracts.id, contractObligations.contractId))
      .leftJoin(accounts, eq(accounts.id, contracts.accountId))
      .where(
        and(
          gte(contractObligations.dueAt, query.from),
          lt(contractObligations.dueAt, query.to),
          query.accountId ? eq(contracts.accountId, query.accountId) : undefined,
          query.ownerUserId
            ? eq(contractObligations.ownerUserId, query.ownerUserId)
            : undefined,
        ),
      )
      .orderBy(asc(contractObligations.dueAt))
      .limit(limit + 1);

    return bounded(rows, limit, ({ obligation, contract, accountName }) => ({
      id: calendarEventId('contract_obligation', obligation.id, 'dueAt'),
      kind: 'obligation_due' as const,
      title: obligation.title,
      startsAt: obligation.dueAt.toISOString(),
      endsAt: null,
      isSpan: false,
      state: eventState({
        at: obligation.dueAt,
        now,
        completedAt: obligation.completedAt,
      }),
      accountId: contract.accountId,
      accountName,
      ownerUserId: obligation.ownerUserId,
      amountCents: null,
      currency: null,
      recordType: 'contract_obligation',
      recordId: obligation.id,
      href: href('contracts', 'contract', contract.id),
      meta: {
        obligationKind: obligation.kind,
        contractId: contract.id,
        contractTitle: contract.title,
        completedAt: obligation.completedAt?.toISOString() ?? null,
      },
    }));
  }

  /**
   * Commitment windows, split on the capacity shape where one is present.
   *
   * A commitment ramps and steps — it is not a rectangle — and `shape` is
   * authoritative over `startsAt`/`endsAt` when set. Drawing one bar across
   * the whole term shows a seller capacity in a month it does not exist in,
   * which is exactly the mistake the shape column was added to prevent.
   */
  private async capacityWindows(query: CalendarQuery, now: Date, limit: number) {
    // No owner column anywhere on the supply chain of custody, so an owner
    // filter cannot be satisfied and must exclude the source outright.
    if (query.ownerUserId) return { events: [], truncated: false };

    const rows = await this.db
      .select({ commitment: capacityCommitments, accountName: accounts.name })
      .from(capacityCommitments)
      .leftJoin(accounts, eq(accounts.id, capacityCommitments.accountId))
      .where(
        and(
          lt(capacityCommitments.startsAt, query.to),
          gt(capacityCommitments.endsAt, query.from),
          query.accountId ? eq(capacityCommitments.accountId, query.accountId) : undefined,
        ),
      )
      .orderBy(asc(capacityCommitments.startsAt))
      .limit(limit + 1);

    const truncated = rows.length > limit;
    if (truncated) rows.length = limit;

    const events: CalendarEvent[] = [];
    for (const { commitment, accountName } of rows) {
      const base = {
        kind: 'capacity_window' as const,
        isSpan: true,
        accountId: commitment.accountId,
        accountName,
        ownerUserId: null,
        amountCents: null,
        currency: commitment.currency,
        recordType: 'capacity_commitment',
        recordId: commitment.id,
        href: href('capacity', 'commitment', commitment.id),
      };

      const shape = commitment.shape;
      const subSpans =
        shape && shape.intervals.length >= 2 && shape.quantities.length >= 1
          ? shape.intervals.slice(0, -1).map((boundary, index) => ({
              index,
              startsAt: new Date(boundary),
              endsAt: new Date(shape.intervals[index + 1]!),
              gpuCount: shape.quantities[index] ?? commitment.gpuCount,
            }))
          : [
              {
                index: null,
                startsAt: commitment.startsAt,
                endsAt: commitment.endsAt,
                gpuCount: commitment.gpuCount,
              },
            ];

      for (const span of subSpans) {
        if (Number.isNaN(span.startsAt.getTime()) || Number.isNaN(span.endsAt.getTime())) {
          continue;
        }
        if (span.startsAt >= query.to || span.endsAt <= query.from) continue;
        events.push({
          ...base,
          id: calendarEventId(
            'capacity_commitment',
            commitment.id,
            span.index === null ? 'window' : `shape.${span.index}`,
          ),
          title:
            span.index === null
              ? commitment.name
              : `${commitment.name} — ${span.gpuCount}× ${commitment.gpuType}`,
          startsAt: span.startsAt.toISOString(),
          endsAt: span.endsAt.toISOString(),
          state: commitment.terminatedAt
            ? ('done' as const)
            : spanState({ startsAt: span.startsAt, endsAt: span.endsAt, now }),
          meta: {
            gpuType: commitment.gpuType,
            gpuCount: span.gpuCount,
            envelopeGpuCount: commitment.gpuCount,
            shaped: span.index !== null,
            costPerGpuHourCents: commitment.costPerGpuHourCents,
            terminatedAt: commitment.terminatedAt?.toISOString() ?? null,
          },
        });
      }
    }

    return { events, truncated };
  }

  private async allocationWindows(query: CalendarQuery, now: Date, limit: number) {
    if (query.ownerUserId) return { events: [], truncated: false };

    const rows = await this.db
      .select({
        allocation: allocations,
        commitmentName: capacityCommitments.name,
        dealName: demandDeals.name,
        accountId: demandDeals.accountId,
        accountName: accounts.name,
      })
      .from(allocations)
      .leftJoin(
        capacityCommitments,
        eq(capacityCommitments.id, allocations.capacityCommitmentId),
      )
      .leftJoin(demandDeals, eq(demandDeals.id, allocations.demandDealId))
      .leftJoin(accounts, eq(accounts.id, demandDeals.accountId))
      .where(
        and(
          lt(allocations.startsAt, query.to),
          gt(allocations.endsAt, query.from),
          query.accountId ? eq(demandDeals.accountId, query.accountId) : undefined,
        ),
      )
      .orderBy(asc(allocations.startsAt))
      .limit(limit + 1);

    return bounded(rows, limit, (row) => {
      const { allocation } = row;
      const gpuHours = numeric(allocation.gpuHours) ?? 0;
      return {
        id: calendarEventId('allocation', allocation.id, 'window'),
        kind: 'allocation_window' as const,
        title:
          row.dealName ??
          (allocation.internalTeam
            ? `Internal — ${allocation.internalTeam}`
            : (row.commitmentName ?? 'Allocation')),
        startsAt: allocation.startsAt.toISOString(),
        endsAt: allocation.endsAt.toISOString(),
        isSpan: true,
        state:
          allocation.releasedAt !== null
            ? ('done' as const)
            : spanState({
                startsAt: allocation.startsAt,
                endsAt: allocation.endsAt,
                now,
              }),
        accountId: row.accountId ?? null,
        accountName: row.accountName ?? null,
        ownerUserId: null,
        // Revenue over the window, in cents — hours are fractional, money is not.
        amountCents: Math.round(gpuHours * allocation.pricePerGpuHourCents),
        currency: allocation.currency,
        recordType: 'allocation',
        recordId: allocation.id,
        href: href('capacity', 'allocation', allocation.id),
        meta: {
          status: allocation.status,
          guaranteeType: allocation.guaranteeType,
          gpuHours,
          internalTeam: allocation.internalTeam,
          commitmentId: allocation.capacityCommitmentId,
          releasedAt: allocation.releasedAt?.toISOString() ?? null,
        },
      };
    });
  }

  /**
   * A hold expiring is the one date on this calendar that changes what can be
   * sold: the moment it passes, the capacity returns to everyone else's
   * availability. It has never been visible anywhere.
   */
  private async holdExpiries(query: CalendarQuery, now: Date, limit: number) {
    if (query.ownerUserId) return { events: [], truncated: false };

    const rows = await this.db
      .select({
        allocation: allocations,
        dealName: demandDeals.name,
        accountId: demandDeals.accountId,
        accountName: accounts.name,
      })
      .from(allocations)
      .leftJoin(demandDeals, eq(demandDeals.id, allocations.demandDealId))
      .leftJoin(accounts, eq(accounts.id, demandDeals.accountId))
      .where(
        and(
          gte(allocations.holdExpiresAt, query.from),
          lt(allocations.holdExpiresAt, query.to),
          query.accountId ? eq(demandDeals.accountId, query.accountId) : undefined,
        ),
      )
      .orderBy(asc(allocations.holdExpiresAt))
      .limit(limit + 1);

    return bounded(rows, limit, (row) => {
      const at = row.allocation.holdExpiresAt!;
      return {
        id: calendarEventId('allocation', row.allocation.id, 'holdExpiresAt'),
        kind: 'hold_expiry' as const,
        title: `Hold expires — ${row.dealName ?? 'unassigned capacity'}`,
        startsAt: at.toISOString(),
        endsAt: null,
        isSpan: false,
        state: eventState({ at, now, completedAt: row.allocation.releasedAt }),
        accountId: row.accountId ?? null,
        accountName: row.accountName ?? null,
        ownerUserId: null,
        // What was turned away to keep the hold. Makes the deadline honest.
        amountCents: row.allocation.holdOpportunityCostCents,
        currency: row.allocation.currency,
        recordType: 'allocation',
        recordId: row.allocation.id,
        href: href('capacity', 'allocation', row.allocation.id),
        meta: {
          status: row.allocation.status,
          gpuHours: numeric(row.allocation.gpuHours),
          commitmentId: row.allocation.capacityCommitmentId,
        },
      };
    });
  }

  private async supplyAvailability(query: CalendarQuery, now: Date, limit: number) {
    const rows = await this.db
      .select({ deal: supplyDeals, accountName: accounts.name })
      .from(supplyDeals)
      .leftJoin(accounts, eq(accounts.id, supplyDeals.accountId))
      .where(
        and(
          gte(supplyDeals.availableFrom, query.from),
          lt(supplyDeals.availableFrom, query.to),
          query.accountId ? eq(supplyDeals.accountId, query.accountId) : undefined,
          query.ownerUserId ? eq(supplyDeals.ownerUserId, query.ownerUserId) : undefined,
        ),
      )
      .orderBy(asc(supplyDeals.availableFrom))
      .limit(limit + 1);

    return bounded(rows, limit, ({ deal, accountName }) => {
      const at = deal.availableFrom!;
      return {
        id: calendarEventId('supply_deal', deal.id, 'availableFrom'),
        kind: 'supply_available_from' as const,
        title: `Capacity available — ${deal.name}`,
        startsAt: at.toISOString(),
        endsAt: null,
        isSpan: false,
        state: eventState({ at, now, completedAt: deal.closedAt }),
        accountId: deal.accountId,
        accountName,
        ownerUserId: deal.ownerUserId,
        amountCents: null,
        currency: null,
        recordType: 'supply_deal',
        recordId: deal.id,
        href: href('supply', 'deal', deal.id),
        meta: {
          stage: deal.stage,
          gpuType: deal.gpuType,
          gpuCount: deal.gpuCount,
          targetCostPerGpuHourCents: deal.targetCostPerGpuHourCents,
        },
      };
    });
  }

  /**
   * An expired export authorisation silently converts lawful business into
   * unlawful business. The schema says so and indexes the column for it, and
   * until this endpoint nothing in PIG read it — no endpoint, no screen.
   */
  private async authorizationExpiries(query: CalendarQuery, now: Date, limit: number) {
    if (query.ownerUserId) return { events: [], truncated: false };

    const rows = await this.db
      .select({ authorization: exportAuthorizations, accountName: accounts.name })
      .from(exportAuthorizations)
      .leftJoin(accounts, eq(accounts.id, exportAuthorizations.accountId))
      .where(
        and(
          gte(exportAuthorizations.expiresAt, query.from),
          lt(exportAuthorizations.expiresAt, query.to),
          query.accountId ? eq(exportAuthorizations.accountId, query.accountId) : undefined,
        ),
      )
      .orderBy(asc(exportAuthorizations.expiresAt))
      .limit(limit + 1);

    return bounded(rows, limit, ({ authorization, accountName }) => {
      const at = authorization.expiresAt!;
      return {
        id: calendarEventId('export_authorization', authorization.id, 'expiresAt'),
        kind: 'authorization_expiry' as const,
        title: `Export authorisation expires — ${accountName ?? 'account'}`,
        startsAt: at.toISOString(),
        endsAt: null,
        isSpan: false,
        // Never 'done': an authorisation is not something anyone completes,
        // and marking a lapsed one finished is the failure mode itself.
        state: eventState({ at, now }),
        accountId: authorization.accountId,
        accountName,
        ownerUserId: null,
        amountCents: null,
        currency: null,
        recordType: 'export_authorization',
        recordId: authorization.id,
        href: accountHref(authorization.accountId),
        meta: {
          authorizationType: authorization.authorizationType,
          reference: authorization.reference,
          // Rules in flux for this counterparty: re-verify, do not trust the date.
          volatile: authorization.volatile,
          evidenceUrl: authorization.evidenceUrl,
          verifiedByUserId: authorization.verifiedByUserId,
        },
      };
    });
  }

  private async artifactExpiries(query: CalendarQuery, now: Date, limit: number) {
    if (query.ownerUserId) return { events: [], truncated: false };

    const rows = await this.db
      .select({ artifact: complianceArtifacts, accountName: accounts.name })
      .from(complianceArtifacts)
      .leftJoin(accounts, eq(accounts.id, complianceArtifacts.accountId))
      .where(
        and(
          gte(complianceArtifacts.expiresAt, query.from),
          lt(complianceArtifacts.expiresAt, query.to),
          query.accountId ? eq(complianceArtifacts.accountId, query.accountId) : undefined,
        ),
      )
      .orderBy(asc(complianceArtifacts.expiresAt))
      .limit(limit + 1);

    return bounded(rows, limit, ({ artifact, accountName }) => {
      const at = artifact.expiresAt!;
      return {
        id: calendarEventId('compliance_artifact', artifact.id, 'expiresAt'),
        kind: 'artifact_expiry' as const,
        title: `${artifact.claim} expires — ${accountName ?? 'account'}`,
        startsAt: at.toISOString(),
        endsAt: null,
        isSpan: false,
        state: eventState({ at, now }),
        accountId: artifact.accountId,
        accountName,
        ownerUserId: null,
        amountCents: null,
        currency: null,
        recordType: 'compliance_artifact',
        recordId: artifact.id,
        href: accountHref(artifact.accountId),
        meta: {
          claim: artifact.claim,
          scope: artifact.scope,
          // Certification versus self-declared alignment decides procurement,
          // so it travels with the deadline rather than being looked up later.
          isCertified: artifact.isCertified,
          soc2Type: artifact.soc2Type,
          verifiedByUserId: artifact.verifiedByUserId,
        },
      };
    });
  }

  private async entries(query: CalendarQuery, now: Date, limit: number) {
    const rows = await this.db
      .select({ entry: calendarEntries, accountName: accounts.name })
      .from(calendarEntries)
      .leftJoin(accounts, eq(accounts.id, calendarEntries.accountId))
      .where(
        and(
          // A dated entry with no end is a point; one with an end is a span,
          // and a span overlaps the window whenever it has not already closed.
          lt(calendarEntries.startsAt, query.to),
          or(
            and(isNull(calendarEntries.endsAt), gte(calendarEntries.startsAt, query.from)),
            and(isNotNull(calendarEntries.endsAt), gt(calendarEntries.endsAt, query.from)),
          ),
          query.accountId ? eq(calendarEntries.accountId, query.accountId) : undefined,
          query.ownerUserId ? eq(calendarEntries.ownerUserId, query.ownerUserId) : undefined,
        ),
      )
      .orderBy(asc(calendarEntries.startsAt))
      .limit(limit + 1);

    return bounded(rows, limit, ({ entry, accountName }) => ({
      id: calendarEventId('calendar_entry', entry.id, 'startsAt'),
      kind: 'calendar_entry' as const,
      title: entry.title,
      startsAt: entry.startsAt.toISOString(),
      endsAt: entry.endsAt?.toISOString() ?? null,
      isSpan: entry.endsAt !== null,
      // Not `spanState`: this is the one projected row type with a completion
      // column, so a closed window is overdue until `completed_at` says
      // otherwise. Whether a missed QBR is flagged must not depend on whether
      // its author happened to type an end time.
      state: entry.endsAt
        ? completableSpanState({
            startsAt: entry.startsAt,
            endsAt: entry.endsAt,
            now,
            completedAt: entry.completedAt,
          })
        : eventState({ at: entry.startsAt, now, completedAt: entry.completedAt }),
      accountId: entry.accountId,
      accountName,
      ownerUserId: entry.ownerUserId,
      amountCents: null,
      currency: null,
      recordType: 'calendar_entry',
      recordId: entry.id,
      href: href('calendar', 'entry', entry.id),
      meta: {
        entryKind: entry.kind,
        allDay: entry.allDay,
        description: entry.description,
        demandDealId: entry.demandDealId,
        supplyDealId: entry.supplyDealId,
        completedAt: entry.completedAt?.toISOString() ?? null,
      },
    }));
  }
}

// ------------------------------------------------------------------- helpers

async function empty(): Promise<{ events: CalendarEvent[]; truncated: boolean }> {
  return { events: [], truncated: false };
}

/**
 * Each source asks for one row more than its budget. Detecting truncation any
 * other way means either a second count query per source or silently returning
 * a partial quarter as if it were whole.
 */
function bounded<Row>(
  rows: Row[],
  limit: number,
  toEvent: (row: Row) => CalendarEvent,
): { events: CalendarEvent[]; truncated: boolean } {
  const truncated = rows.length > limit;
  if (truncated) rows.length = limit;
  return { events: rows.map(toEvent), truncated };
}
