import {
  ALLOCATION_STATUSES,
  CONSUMING_ALLOCATION_STATUSES,
  RESERVING_ALLOCATION_STATUSES,
} from '@pig/core';
import type {
  AllocationStatus,
  GuaranteeType,
  GpuSocket,
  InterconnectType,
  SecurityTier,
} from '@pig/core';
import type {
  Allocation,
  CapacityCommitment,
  Database,
  DemandDeal,
  NewCapacityCommitment,
} from '@pig/db';
import { allocations, capacityCommitments, demandDeals } from '@pig/db';
import { eq } from 'drizzle-orm';
import type { Principal } from '../lib/auth';
import { MutationError } from '../lib/mutation';
import { quantityAt, type CommitmentShape } from './capacity';

export type CapacityWriteTransaction = Parameters<Parameters<Database['transaction']>[0]>[0];

export interface CommitmentCapacity {
  id: string;
  gpuCount: number;
  startsAt: Date;
  endsAt: Date;
  totalGpuHours: number;
  shape: CommitmentShape | null;
  oversubscriptionPct: number;
  terminatedAt: Date | null;
}

export interface ReservationCapacity {
  id?: string;
  gpuHours: number;
  startsAt: Date;
  endsAt: Date;
  status: AllocationStatus;
  holdExpiresAt: Date | null;
}

export interface CapacityViolation {
  code: 'outside_commitment_window' | 'total_capacity_exceeded' | 'shape_capacity_exceeded';
  message: string;
  at?: Date;
  reserved?: number;
  available?: number;
}

const EPSILON = 1e-7;

function isLiveReservation(reservation: ReservationCapacity, now: Date): boolean {
  if (!(RESERVING_ALLOCATION_STATUSES as readonly string[]).includes(reservation.status)) {
    return false;
  }
  return !(
    reservation.status === 'planned' &&
    reservation.holdExpiresAt !== null &&
    reservation.holdExpiresAt <= now
  );
}

function reservationRate(reservation: ReservationCapacity): number {
  const durationHours =
    (reservation.endsAt.getTime() - reservation.startsAt.getTime()) / 3_600_000;
  return durationHours > 0 ? reservation.gpuHours / durationHours : Number.POSITIVE_INFINITY;
}

/**
 * Checks both the contracted hour budget and every instantaneous shape interval.
 * The row lock used by the caller turns this pure check into a concurrency-safe
 * invariant rather than an optimistic preflight that two requests can both pass.
 */
export function findCapacityViolation(
  commitment: CommitmentCapacity,
  existing: readonly ReservationCapacity[],
  candidate: ReservationCapacity | null,
  now: Date,
): CapacityViolation | null {
  const reservations = [...existing, ...(candidate ? [candidate] : [])].filter((reservation) =>
    isLiveReservation(reservation, now),
  );

  for (const reservation of reservations) {
    if (
      reservation.startsAt < commitment.startsAt ||
      reservation.endsAt > commitment.endsAt ||
      reservation.endsAt <= reservation.startsAt
    ) {
      return {
        code: 'outside_commitment_window',
        message: 'The allocation window must sit inside the commitment window.',
      };
    }
  }

  const multiplier = 1 + commitment.oversubscriptionPct / 100;
  const allowedGpuHours = commitment.totalGpuHours * multiplier;
  const reservedGpuHours = reservations.reduce((sum, reservation) => sum + reservation.gpuHours, 0);
  if (reservedGpuHours - allowedGpuHours > EPSILON) {
    return {
      code: 'total_capacity_exceeded',
      message: 'The allocation would exceed the commitment GPU-hour budget.',
      reserved: reservedGpuHours,
      available: allowedGpuHours,
    };
  }

  const boundaries = new Set<number>([
    commitment.startsAt.getTime(),
    commitment.endsAt.getTime(),
    ...(commitment.shape?.intervals.map((boundary) => Date.parse(boundary)) ?? []),
  ]);
  for (const reservation of reservations) {
    boundaries.add(reservation.startsAt.getTime());
    boundaries.add(reservation.endsAt.getTime());
  }

  const ordered = [...boundaries]
    .filter(
      (boundary) =>
        Number.isFinite(boundary) &&
        boundary >= commitment.startsAt.getTime() &&
        boundary <= commitment.endsAt.getTime(),
    )
    .sort((a, b) => a - b);

  for (let index = 0; index < ordered.length - 1; index++) {
    const intervalStart = ordered[index]!;
    const intervalEnd = ordered[index + 1]!;
    if (intervalEnd <= intervalStart) continue;

    const overlapping = reservations.filter(
      (reservation) =>
        reservation.startsAt.getTime() < intervalEnd &&
        reservation.endsAt.getTime() > intervalStart,
    );
    if (overlapping.length === 0) continue;

    const at = new Date(intervalStart + (intervalEnd - intervalStart) / 2);
    const reserved = overlapping.reduce(
      (sum, reservation) => sum + reservationRate(reservation),
      0,
    );
    const available = quantityAt(commitment.shape, commitment.gpuCount, at) * multiplier;
    if (reserved - available > EPSILON) {
      return {
        code: 'shape_capacity_exceeded',
        message: 'The allocation would exceed available GPUs during part of its window.',
        at,
        reserved,
        available,
      };
    }
  }

  return null;
}

export function commitmentCapacityError(commitment: CommitmentCapacity): string | null {
  const durationHours =
    (commitment.endsAt.getTime() - commitment.startsAt.getTime()) / 3_600_000;
  if (durationHours <= 0) return 'Commitment end must be after its start.';
  if (commitment.oversubscriptionPct < 0) return 'Oversubscription cannot be negative.';

  const shape = commitment.shape;
  if (!shape) {
    if (commitment.totalGpuHours - durationHours * commitment.gpuCount > EPSILON) {
      return 'Total GPU-hours cannot exceed the flat commitment envelope.';
    }
    return null;
  }

  if (shape.intervals.length < 2 || shape.quantities.length !== shape.intervals.length - 1) {
    return 'Shape quantities must have exactly one entry per interval.';
  }

  const boundaries = shape.intervals.map((boundary) => Date.parse(boundary));
  if (boundaries.some((boundary) => !Number.isFinite(boundary))) {
    return 'Shape intervals must be valid ISO-8601 timestamps.';
  }
  if (
    boundaries[0] !== commitment.startsAt.getTime() ||
    boundaries[boundaries.length - 1] !== commitment.endsAt.getTime()
  ) {
    return 'Shape intervals must cover the full commitment window.';
  }
  for (let index = 0; index < boundaries.length - 1; index++) {
    if (boundaries[index + 1]! <= boundaries[index]!) {
      return 'Shape intervals must be strictly ascending.';
    }
  }
  if (
    shape.quantities.some(
      (quantity) => !Number.isInteger(quantity) || quantity < 0 || quantity > commitment.gpuCount,
    )
  ) {
    return 'Shape quantities must be whole GPUs within the commitment envelope.';
  }

  const shapedGpuHours = shape.quantities.reduce(
    (sum, quantity, index) =>
      sum + ((boundaries[index + 1]! - boundaries[index]!) / 3_600_000) * quantity,
    0,
  );
  if (commitment.totalGpuHours - shapedGpuHours > EPSILON) {
    return 'Total GPU-hours cannot exceed the capacity described by the shape.';
  }
  return null;
}

export interface CommitmentWriteInput {
  accountId: string;
  siteId?: string | null;
  supplyDealId?: string | null;
  name: string;
  gpuType: string;
  socket?: GpuSocket | null;
  gpuCount: number;
  interconnectType?: InterconnectType;
  securityTier?: SecurityTier;
  startsAt: string;
  endsAt: string;
  totalGpuHours: number;
  costPerGpuHourCents: number;
  currency?: string;
  shape?: CommitmentShape | null;
  colocateWith?: string[];
  isContiguous?: boolean;
  minimumSpendCents?: number | null;
  isAutoRenew?: boolean;
  noticeDays?: number | null;
  takeOrPayFloorPct?: number | null;
  prepaidPct?: number | null;
  prepaidAmountCents?: number | null;
  usefulLifeYears?: number | null;
  salvageValuePct?: number | null;
  depreciationStartAt?: string | null;
  costOfCapitalBps?: number | null;
  financingInstrument?: string | null;
  oversubscriptionPct?: number;
  notes?: string | null;
  terminatedAt?: string | null;
}

export interface AllocationWriteInput {
  capacityCommitmentId: string;
  demandDealId: string;
  gpuHours: number;
  pricePerGpuHourCents: number;
  currency?: string;
  startsAt: string;
  endsAt: string;
  status: (typeof CONSUMING_ALLOCATION_STATUSES)[number];
  guaranteeType?: GuaranteeType;
  priority?: number;
  complianceDecisionId?: string | null;
  notes?: string | null;
}

export interface HoldWriteInput
  extends Omit<AllocationWriteInput, 'status' | 'pricePerGpuHourCents'> {
  pricePerGpuHourCents?: number;
  holdExpiresAt: string;
  holdOpportunityCostCents?: number | null;
}

export interface ReleaseWriteInput {
  reason?: string;
}

export interface CommitmentWriteResult {
  commitment: CapacityCommitment;
}

export interface AllocationWriteResult {
  allocation: Allocation;
  commitment: Pick<CapacityCommitment, 'id' | 'name'>;
  deal: Pick<DemandDeal, 'id' | 'accountId'>;
}

export interface ReleaseWriteResult {
  allocation: Allocation;
  accountId?: string;
  previousStatus: AllocationStatus;
}

function toCapacity(commitment: CapacityCommitment): CommitmentCapacity {
  return {
    id: commitment.id,
    gpuCount: commitment.gpuCount,
    startsAt: commitment.startsAt,
    endsAt: commitment.endsAt,
    totalGpuHours: Number(commitment.totalGpuHours),
    shape: commitment.shape,
    oversubscriptionPct: Number(commitment.oversubscriptionPct),
    terminatedAt: commitment.terminatedAt,
  };
}

function toReservation(allocation: Allocation): ReservationCapacity {
  return {
    id: allocation.id,
    gpuHours: Number(allocation.gpuHours),
    startsAt: allocation.startsAt,
    endsAt: allocation.endsAt,
    status: allocation.status,
    holdExpiresAt: allocation.holdExpiresAt,
  };
}

function assertCommitmentValid(commitment: CommitmentCapacity): void {
  const error = commitmentCapacityError(commitment);
  if (error) throw new MutationError('invalid_commitment', error, 400);
}

function assertCapacityAvailable(
  commitment: CommitmentCapacity,
  existing: readonly Allocation[],
  candidate: ReservationCapacity | null,
  now: Date,
): void {
  const violation = findCapacityViolation(
    commitment,
    existing.map(toReservation),
    candidate,
    now,
  );
  if (!violation) return;
  throw new MutationError(violation.code, violation.message, 409);
}

export class CapacityWriteService {
  constructor(private readonly tx: CapacityWriteTransaction) {}

  private async lockCommitment(id: string): Promise<CapacityCommitment> {
    const [commitment] = await this.tx
      .select()
      .from(capacityCommitments)
      .where(eq(capacityCommitments.id, id))
      .limit(1)
      .for('update');
    if (!commitment) throw MutationError.notFound('Capacity commitment');
    return commitment;
  }

  private async reservations(commitmentId: string): Promise<Allocation[]> {
    return this.tx
      .select()
      .from(allocations)
      .where(eq(allocations.capacityCommitmentId, commitmentId));
  }

  private async demandDeal(id: string): Promise<Pick<DemandDeal, 'id' | 'accountId'>> {
    const [deal] = await this.tx
      .select({ id: demandDeals.id, accountId: demandDeals.accountId })
      .from(demandDeals)
      .where(eq(demandDeals.id, id))
      .limit(1);
    if (!deal) throw MutationError.notFound('Demand deal');
    return deal;
  }

  async createCommitment(input: CommitmentWriteInput, now: Date): Promise<CommitmentWriteResult> {
    const capacity: CommitmentCapacity = {
      id: 'new',
      gpuCount: input.gpuCount,
      startsAt: new Date(input.startsAt),
      endsAt: new Date(input.endsAt),
      totalGpuHours: input.totalGpuHours,
      shape: input.shape ?? null,
      oversubscriptionPct: input.oversubscriptionPct ?? 0,
      terminatedAt: null,
    };
    assertCommitmentValid(capacity);

    const [commitment] = await this.tx
      .insert(capacityCommitments)
      .values({
        accountId: input.accountId,
        siteId: input.siteId,
        supplyDealId: input.supplyDealId,
        name: input.name,
        gpuType: input.gpuType,
        socket: input.socket,
        gpuCount: input.gpuCount,
        interconnectType: input.interconnectType,
        securityTier: input.securityTier,
        startsAt: capacity.startsAt,
        endsAt: capacity.endsAt,
        totalGpuHours: String(input.totalGpuHours),
        costPerGpuHourCents: input.costPerGpuHourCents,
        currency: input.currency,
        shape: input.shape,
        colocateWith: input.colocateWith,
        isContiguous: input.isContiguous,
        minimumSpendCents: input.minimumSpendCents,
        isAutoRenew: input.isAutoRenew,
        noticeDays: input.noticeDays,
        takeOrPayFloorPct:
          input.takeOrPayFloorPct === undefined || input.takeOrPayFloorPct === null
            ? input.takeOrPayFloorPct
            : String(input.takeOrPayFloorPct),
        prepaidPct:
          input.prepaidPct === undefined || input.prepaidPct === null
            ? input.prepaidPct
            : String(input.prepaidPct),
        prepaidAmountCents: input.prepaidAmountCents,
        usefulLifeYears:
          input.usefulLifeYears === undefined || input.usefulLifeYears === null
            ? input.usefulLifeYears
            : String(input.usefulLifeYears),
        salvageValuePct:
          input.salvageValuePct === undefined || input.salvageValuePct === null
            ? input.salvageValuePct
            : String(input.salvageValuePct),
        depreciationStartAt:
          input.depreciationStartAt === undefined
            ? undefined
            : input.depreciationStartAt === null
              ? null
              : new Date(input.depreciationStartAt),
        costOfCapitalBps: input.costOfCapitalBps,
        financingInstrument: input.financingInstrument,
        oversubscriptionPct: String(input.oversubscriptionPct ?? 0),
        notes: input.notes,
        createdAt: now,
        updatedAt: now,
      })
      .returning();
    if (!commitment) throw new MutationError('write_failed', 'Commitment was not created.', 409);
    return { commitment };
  }

  async updateCommitment(
    id: string,
    input: Partial<CommitmentWriteInput>,
    now: Date,
  ): Promise<CommitmentWriteResult> {
    const current = await this.lockCommitment(id);
    const existing = await this.reservations(id);
    const prospective: CommitmentCapacity = {
      id,
      gpuCount: input.gpuCount ?? current.gpuCount,
      startsAt: input.startsAt ? new Date(input.startsAt) : current.startsAt,
      endsAt: input.endsAt ? new Date(input.endsAt) : current.endsAt,
      totalGpuHours: input.totalGpuHours ?? Number(current.totalGpuHours),
      shape: input.shape === undefined ? current.shape : input.shape,
      oversubscriptionPct:
        input.oversubscriptionPct ?? Number(current.oversubscriptionPct),
      terminatedAt:
        input.terminatedAt === undefined
          ? current.terminatedAt
          : input.terminatedAt
            ? new Date(input.terminatedAt)
            : null,
    };
    assertCommitmentValid(prospective);
    assertCapacityAvailable(prospective, existing, null, now);
    if (
      prospective.terminatedAt &&
      existing.some((allocation) => isLiveReservation(toReservation(allocation), now))
    ) {
      throw new MutationError(
        'commitment_in_use',
        'Release live allocations before terminating the commitment.',
        409,
      );
    }

    const changes: Partial<NewCapacityCommitment> = { updatedAt: now };
    const assign = <Key extends keyof CommitmentWriteInput>(
      key: Key,
      value: NewCapacityCommitment[keyof NewCapacityCommitment],
    ) => {
      if (input[key] !== undefined) {
        (changes as Record<string, unknown>)[key] = value;
      }
    };
    assign('accountId', input.accountId);
    assign('siteId', input.siteId);
    assign('supplyDealId', input.supplyDealId);
    assign('name', input.name);
    assign('gpuType', input.gpuType);
    assign('socket', input.socket);
    assign('gpuCount', input.gpuCount);
    assign('interconnectType', input.interconnectType);
    assign('securityTier', input.securityTier);
    assign('startsAt', prospective.startsAt);
    assign('endsAt', prospective.endsAt);
    assign('totalGpuHours', String(prospective.totalGpuHours));
    assign('costPerGpuHourCents', input.costPerGpuHourCents);
    assign('currency', input.currency);
    assign('shape', input.shape);
    assign('colocateWith', input.colocateWith);
    assign('isContiguous', input.isContiguous);
    assign('minimumSpendCents', input.minimumSpendCents);
    assign('isAutoRenew', input.isAutoRenew);
    assign('noticeDays', input.noticeDays);
    assign(
      'takeOrPayFloorPct',
      input.takeOrPayFloorPct === null ? null : String(input.takeOrPayFloorPct),
    );
    assign('prepaidPct', input.prepaidPct === null ? null : String(input.prepaidPct));
    assign('prepaidAmountCents', input.prepaidAmountCents);
    assign('usefulLifeYears', input.usefulLifeYears === null ? null : String(input.usefulLifeYears));
    assign('salvageValuePct', input.salvageValuePct === null ? null : String(input.salvageValuePct));
    assign(
      'depreciationStartAt',
      input.depreciationStartAt ? new Date(input.depreciationStartAt) : null,
    );
    assign('costOfCapitalBps', input.costOfCapitalBps);
    assign('financingInstrument', input.financingInstrument);
    assign('oversubscriptionPct', String(prospective.oversubscriptionPct));
    assign('notes', input.notes);
    assign('terminatedAt', prospective.terminatedAt);

    const [commitment] = await this.tx
      .update(capacityCommitments)
      .set(changes)
      .where(eq(capacityCommitments.id, id))
      .returning();
    if (!commitment) throw MutationError.notFound('Capacity commitment');
    return { commitment };
  }

  async createAllocation(
    input: AllocationWriteInput,
    principal: Principal,
    now: Date,
  ): Promise<AllocationWriteResult> {
    const commitment = await this.lockCommitment(input.capacityCommitmentId);
    if (commitment.terminatedAt) {
      throw new MutationError('commitment_terminated', 'The commitment is terminated.', 409);
    }
    const deal = await this.demandDeal(input.demandDealId);
    const existing = await this.reservations(commitment.id);
    const candidate: ReservationCapacity = {
      gpuHours: input.gpuHours,
      startsAt: new Date(input.startsAt),
      endsAt: new Date(input.endsAt),
      status: input.status,
      holdExpiresAt: null,
    };
    assertCapacityAvailable(toCapacity(commitment), existing, candidate, now);

    const [allocation] = await this.tx
      .insert(allocations)
      .values({
        capacityCommitmentId: commitment.id,
        demandDealId: deal.id,
        gpuHours: String(input.gpuHours),
        pricePerGpuHourCents: input.pricePerGpuHourCents,
        currency: input.currency,
        startsAt: candidate.startsAt,
        endsAt: candidate.endsAt,
        status: input.status,
        guaranteeType: input.guaranteeType,
        priority: input.priority,
        complianceDecisionId: input.complianceDecisionId,
        createdByUserId: principal.userId,
        notes: input.notes,
        createdAt: now,
        updatedAt: now,
      })
      .returning();
    if (!allocation) throw new MutationError('write_failed', 'Allocation was not created.', 409);
    return { allocation, commitment, deal };
  }

  async createHold(
    input: HoldWriteInput,
    principal: Principal,
    now: Date,
  ): Promise<AllocationWriteResult> {
    const holdExpiresAt = new Date(input.holdExpiresAt);
    if (holdExpiresAt <= now) {
      throw new MutationError('hold_already_expired', 'A new hold must expire in the future.', 400);
    }
    const commitment = await this.lockCommitment(input.capacityCommitmentId);
    if (commitment.terminatedAt) {
      throw new MutationError('commitment_terminated', 'The commitment is terminated.', 409);
    }
    const deal = await this.demandDeal(input.demandDealId);
    const existing = await this.reservations(commitment.id);
    const candidate: ReservationCapacity = {
      gpuHours: input.gpuHours,
      startsAt: new Date(input.startsAt),
      endsAt: new Date(input.endsAt),
      status: 'planned',
      holdExpiresAt,
    };
    assertCapacityAvailable(toCapacity(commitment), existing, candidate, now);

    const [allocation] = await this.tx
      .insert(allocations)
      .values({
        capacityCommitmentId: commitment.id,
        demandDealId: deal.id,
        gpuHours: String(input.gpuHours),
        pricePerGpuHourCents: input.pricePerGpuHourCents ?? 0,
        currency: input.currency,
        startsAt: candidate.startsAt,
        endsAt: candidate.endsAt,
        status: 'planned',
        holdExpiresAt,
        holdOpportunityCostCents: input.holdOpportunityCostCents,
        guaranteeType: input.guaranteeType,
        priority: input.priority,
        complianceDecisionId: input.complianceDecisionId,
        createdByUserId: principal.userId,
        notes: input.notes,
        createdAt: now,
        updatedAt: now,
      })
      .returning();
    if (!allocation) throw new MutationError('write_failed', 'Hold was not created.', 409);
    return { allocation, commitment, deal };
  }

  async releaseAllocation(
    id: string,
    _input: ReleaseWriteInput,
    now: Date,
  ): Promise<ReleaseWriteResult> {
    const [reference] = await this.tx
      .select({ capacityCommitmentId: allocations.capacityCommitmentId })
      .from(allocations)
      .where(eq(allocations.id, id))
      .limit(1);
    if (!reference) throw MutationError.notFound('Allocation');
    await this.lockCommitment(reference.capacityCommitmentId);

    const [current] = await this.tx
      .select()
      .from(allocations)
      .where(eq(allocations.id, id))
      .limit(1)
      .for('update');
    if (!current) throw MutationError.notFound('Allocation');
    if (current.status === 'released') {
      throw new MutationError('already_released', 'The allocation is already released.', 409);
    }
    if (current.status === 'completed') {
      throw new MutationError(
        'completed_allocation',
        'Completed billable usage cannot be released.',
        409,
      );
    }

    const [allocation] = await this.tx
      .update(allocations)
      .set({ status: 'released', releasedAt: now, holdExpiresAt: null, updatedAt: now })
      .where(eq(allocations.id, id))
      .returning();
    if (!allocation) throw MutationError.notFound('Allocation');

    const deal = current.demandDealId
      ? await this.demandDeal(current.demandDealId)
      : undefined;
    return { allocation, accountId: deal?.accountId, previousStatus: current.status };
  }
}

export const VALID_ALLOCATION_STATUSES = ALLOCATION_STATUSES;
