import { sql } from 'drizzle-orm'
import type { Schema } from '@platform-modules/db'
import { isWithinMagnitudeBound, MAX_BIGINT_DIGITS } from './internal/bigint.js'
import { applyDelta, lockCounter, type CounterKey } from './internal/counter.js'
import {
  findByIdempotencyKey,
  fingerprint,
  type FingerprintInput,
  type Reservation as StoredReservation,
} from './internal/idempotency.js'
import {
  EntitlementDeniedError,
  IdempotencyConflictError,
  MeteringStorageError,
  MeteringValidationError,
  QuotaExhaustedError,
  isMeteringError,
} from './errors.js'
import { resolve, type ResolveDeps } from './resolve.js'
import type { Clock, EntitlementPolicy, MeteringDatabase, Reservation, SubjectRef } from './types.js'

export type ReserveDeps<S extends Schema = Record<string, never>> = ResolveDeps & {
  db: MeteringDatabase<S>
  clock?: Clock
}

type ReserveInput = {
  subject: SubjectRef
  meter: string
  units: bigint
  idempotencyKey: string
  expiresAt: Date
}

type SqlRow = Record<string, unknown>

function firstRow(result: unknown): SqlRow | undefined {
  const rows = (Array.isArray(result) ? result : (result as { rows?: SqlRow[] }).rows) ?? []
  return rows[0]
}

function isValidDate(value: unknown): value is Date {
  return value instanceof Date && Number.isFinite(value.getTime())
}

function assertInput(input: ReserveInput): void {
  if (typeof input !== 'object' || input === null) {
    throw new MeteringValidationError('input', 'must be an object')
  }
  if (typeof input.meter !== 'string' || input.meter.trim().length === 0) {
    throw new MeteringValidationError('meter', 'must be non-empty')
  }
  if (typeof input.units !== 'bigint' || input.units <= 0n) {
    throw new MeteringValidationError('units', 'must be a positive bigint')
  }
  if (!isWithinMagnitudeBound(input.units)) {
    throw new MeteringValidationError('units', `must be below 10^${MAX_BIGINT_DIGITS}`)
  }
  if (typeof input.subject !== 'object' || input.subject === null) {
    throw new MeteringValidationError('subject', 'must be an object')
  }
  if (typeof input.subject.tenantId !== 'string' || input.subject.tenantId.trim().length === 0) {
    throw new MeteringValidationError('subject.tenantId', 'must be non-empty')
  }
  if (typeof input.subject.account !== 'string' || input.subject.account.trim().length === 0) {
    throw new MeteringValidationError('subject.account', 'must be non-empty')
  }
  if (typeof input.idempotencyKey !== 'string' || input.idempotencyKey.trim().length === 0) {
    throw new MeteringValidationError('idempotencyKey', 'must be non-empty')
  }
  if (!isValidDate(input.expiresAt)) {
    throw new MeteringValidationError('expiresAt', 'must be a valid Date')
  }
}

function assertClockValue(value: Date): Date {
  if (!isValidDate(value)) throw new MeteringValidationError('clock', 'must return a valid Date')
  return value
}

function counterKey(input: ReserveInput, policy: EntitlementPolicy): CounterKey {
  return {
    tenantId: input.subject.tenantId,
    account: input.subject.account,
    meter: input.meter,
    periodId: policy.period.id,
  }
}

function idempotencyInput(input: ReserveInput, policy: EntitlementPolicy): FingerprintInput {
  return {
    subject: input.subject,
    meter: input.meter,
    units: input.units,
    expiresAt: input.expiresAt,
    periodId: policy.period.id,
  }
}

function effectiveCeiling(policy: EntitlementPolicy): bigint | null {
  if (policy.overage === 'deny') return policy.includedUnits
  return policy.hardLimit ?? null
}

function remaining(ceiling: bigint | null, committed: bigint, reserved: bigint): bigint | null {
  if (ceiling === null) return null
  return ceiling > committed + reserved ? ceiling - committed - reserved : 0n
}

function publicReservation(stored: StoredReservation): Reservation {
  const { fingerprint: _fingerprint, ...reservation } = stored
  return reservation
}

function insertResultId(result: unknown): string | undefined {
  const id = firstRow(result)?.id
  return id === undefined || id === null ? undefined : String(id)
}

export async function reserve<S extends Schema = Record<string, never>>(
  deps: ReserveDeps<S>,
  input: ReserveInput,
): Promise<Reservation> {
  assertInput(input)

  let at: Date
  try {
    at = assertClockValue(deps.clock?.() ?? new Date())
  } catch (error) {
    if (isMeteringError(error)) throw error
    throw new MeteringStorageError('reserve.clock', error)
  }
  const policy = await resolve({ policies: deps.policies }, input.subject, input.meter, at)
  if (policy === null) {
    throw new EntitlementDeniedError({ subject: input.subject, meter: input.meter, at })
  }

  const key = counterKey(input, policy)
  const requestFingerprint = fingerprint(idempotencyInput(input, policy))

  try {
    return await deps.db.transaction(async (tx) => {
      const position = await lockCounter(tx, key)
      const existing = await findByIdempotencyKey(tx, input.subject.tenantId, input.idempotencyKey)
      if (existing) {
        if (existing.fingerprint !== requestFingerprint) {
          throw new IdempotencyConflictError({
            tenantId: input.subject.tenantId,
            idempotencyKey: input.idempotencyKey,
          })
        }
        return publicReservation(existing)
      }

      const ceiling = effectiveCeiling(policy)
      const capacityRemaining = remaining(ceiling, position.committed, position.reserved)
      const includedRemaining = remaining(policy.includedUnits, position.committed, position.reserved) ?? 0n
      if (capacityRemaining !== null && input.units > capacityRemaining) {
        throw new QuotaExhaustedError({
          requested: input.units,
          includedRemaining,
          capacityRemaining,
        })
      }
      if (!isWithinMagnitudeBound(position.reserved + input.units)) {
        throw new MeteringValidationError(
          'reserved',
          `result must be below 10^${MAX_BIGINT_DIGITS}`,
        )
      }

      const id = crypto.randomUUID()
      const inserted = await tx.execute(sql`
        INSERT INTO metering_reservation (
          id, tenant_id, idempotency_key, fingerprint, account, meter, period_id,
          period_starts_at, period_ends_at, reserved_units, status,
          applied_included_units, applied_hard_limit, applied_overage, policy_version, expires_at
        ) VALUES (
          ${id}, ${input.subject.tenantId}, ${input.idempotencyKey}, ${requestFingerprint},
          ${input.subject.account}, ${input.meter}, ${policy.period.id},
          ${policy.period.startsAt.toISOString()}::timestamptz,
          ${policy.period.endsAt.toISOString()}::timestamptz,
          ${input.units}, 'reserved', ${policy.includedUnits}, ${policy.hardLimit ?? null},
          ${policy.overage}, ${policy.version}, ${input.expiresAt.toISOString()}::timestamptz
        )
        ON CONFLICT (tenant_id, idempotency_key) DO NOTHING
        RETURNING id
      `)

      if (insertResultId(inserted) === undefined) {
        const winner = await findByIdempotencyKey(tx, input.subject.tenantId, input.idempotencyKey)
        if (!winner) throw new MeteringStorageError('reserve.idempotency', new Error('winner row missing'))
        if (winner.fingerprint !== requestFingerprint) {
          throw new IdempotencyConflictError({
            tenantId: input.subject.tenantId,
            idempotencyKey: input.idempotencyKey,
          })
        }
        return publicReservation(winner)
      }

      await applyDelta(tx, key, { reserved: input.units })
      const stored = await findByIdempotencyKey(tx, input.subject.tenantId, input.idempotencyKey)
      if (!stored) throw new MeteringStorageError('reserve.insert', new Error('inserted row missing'))
      return publicReservation(stored)
    })
  } catch (error) {
    if (isMeteringError(error)) throw error
    throw new MeteringStorageError('reserve', error)
  }
}
