import { sql, type SQLWrapper } from 'drizzle-orm'
import type { Schema } from '@platform-modules/db'
import { asBigint, isWithinMagnitudeBound, MAX_BIGINT_DIGITS } from './internal/bigint.js'
import { lockCounter, type CounterKey } from './internal/counter.js'
import {
  EntitlementDeniedError,
  isMeteringError,
  MeteringStorageError,
  MeteringValidationError,
} from './errors.js'
import type {
  EntitlementPolicy,
  MeteringDatabase,
  PolicyStore,
  SubjectRef,
  UsageQuery,
  UsageView,
} from './types.js'

export type UsageDeps<S extends Schema = Record<string, never>> = {
  db: MeteringDatabase<S>
  policies: PolicyStore
}

type SqlRow = Record<string, unknown>
type SqlExecutor = { execute(query: SQLWrapper): Promise<unknown> }

function rows(result: unknown): SqlRow[] {
  return (Array.isArray(result) ? result : (result as { rows?: SqlRow[] }).rows) ?? []
}

function firstRow(result: unknown): SqlRow | undefined {
  return rows(result)[0]
}


function asDate(value: unknown, field: string): Date {
  const date = value instanceof Date ? new Date(value.getTime()) : new Date(String(value))
  if (Number.isNaN(date.getTime())) throw new Error(`metering usage returned invalid ${field}`)
  return date
}

function assertNonEmptyString(value: unknown, field: string): asserts value is string {
  if (typeof value !== 'string' || value.trim() === '') {
    throw new MeteringValidationError(field, 'must be non-empty')
  }
}

function assertDate(value: unknown, field: string): asserts value is Date {
  if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
    throw new MeteringValidationError(field, 'must be a valid Date')
  }
}

function validateSubject(subject: unknown): asserts subject is SubjectRef {
  if (typeof subject !== 'object' || subject === null) {
    throw new MeteringValidationError('query.subject', 'must be an object')
  }
  const value = subject as Record<string, unknown>
  assertNonEmptyString(value.tenantId, 'query.subject.tenantId')
  assertNonEmptyString(value.account, 'query.subject.account')
}

function validateQuery(query: unknown): asserts query is UsageQuery {
  if (typeof query !== 'object' || query === null) {
    throw new MeteringValidationError('query', 'must be an object')
  }
  const value = query as Record<string, unknown>
  validateSubject(value.subject)
  assertNonEmptyString(value.meter, 'query.meter')
  const hasAt = 'at' in value
  const hasPeriodId = 'periodId' in value
  if (hasAt === hasPeriodId) {
    throw new MeteringValidationError('query', 'must specify exactly one of at or periodId')
  }
  if (hasAt) {
    assertDate(value.at, 'query.at')
    return
  }
  assertNonEmptyString(value.periodId, 'query.periodId')
}

function validatePolicy(policy: unknown, meter: string): asserts policy is EntitlementPolicy {
  if (typeof policy !== 'object' || policy === null) {
    throw new MeteringValidationError('policy', 'must be an object')
  }
  const value = policy as Record<string, unknown>
  if (value.meter !== meter) {
    throw new MeteringValidationError('policy.meter', 'does not match requested meter')
  }
  if (typeof value.includedUnits !== 'bigint' || value.includedUnits < 0n) {
    throw new MeteringValidationError('policy.includedUnits', 'must be a non-negative bigint')
  }
  if (!isWithinMagnitudeBound(value.includedUnits)) {
    throw new MeteringValidationError(
      'policy.includedUnits',
      `must be below 10^${MAX_BIGINT_DIGITS}`,
    )
  }
  if (
    value.hardLimit !== undefined &&
    (typeof value.hardLimit !== 'bigint' || value.hardLimit < value.includedUnits)
  ) {
    throw new MeteringValidationError('policy.hardLimit', 'must be >= includedUnits')
  }
  if (typeof value.hardLimit === 'bigint' && !isWithinMagnitudeBound(value.hardLimit)) {
    throw new MeteringValidationError('policy.hardLimit', `must be below 10^${MAX_BIGINT_DIGITS}`)
  }
  if (value.overage !== 'deny' && value.overage !== 'allow') {
    throw new MeteringValidationError('policy.overage', 'must be deny or allow')
  }
  if (typeof value.period !== 'object' || value.period === null) {
    throw new MeteringValidationError('policy.period', 'must be an object')
  }
  const period = value.period as Record<string, unknown>
  assertNonEmptyString(period.id, 'policy.period.id')
  assertDate(period.startsAt, 'policy.period.startsAt')
  assertDate(period.endsAt, 'policy.period.endsAt')
  if (period.startsAt >= period.endsAt) {
    throw new MeteringValidationError('policy.period', 'must be a non-empty interval')
  }
  assertNonEmptyString(value.version, 'policy.version')
}

function keyFor(subject: SubjectRef, meter: string, periodId: string): CounterKey {
  return {
    tenantId: subject.tenantId,
    account: subject.account,
    meter,
    periodId,
  }
}

function maxZero(value: bigint): bigint {
  return value > 0n ? value : 0n
}

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

function copyPeriod(policy: EntitlementPolicy) {
  return {
    id: policy.period.id,
    startsAt: new Date(policy.period.startsAt.getTime()),
    endsAt: new Date(policy.period.endsAt.getTime()),
  }
}

async function projection(
  tx: SqlExecutor,
  key: CounterKey,
): Promise<{ committed: bigint; reserved: bigint }> {
  const row = firstRow(await tx.execute(sql`
    SELECT
      COALESCE((
        SELECT SUM(units)
        FROM metering_usage_event
        WHERE tenant_id = ${key.tenantId}
          AND account = ${key.account}
          AND meter = ${key.meter}
          AND period_id = ${key.periodId}
          AND kind = 'commit'
      ), 0) AS committed,
      COALESCE((
        SELECT SUM(reserved_units)
        FROM metering_reservation
        WHERE tenant_id = ${key.tenantId}
          AND account = ${key.account}
          AND meter = ${key.meter}
          AND period_id = ${key.periodId}
          AND status = 'reserved'
      ), 0) AS reserved
  `))
  if (!row) throw new Error('metering usage projection returned no row')
  return {
    committed: asBigint(row.committed, 'usage', 'committed'),
    reserved: asBigint(row.reserved, 'usage', 'reserved'),
  }
}

async function historicalPolicy(
  tx: SqlExecutor,
  key: CounterKey,
): Promise<EntitlementPolicy> {
  const row = firstRow(await tx.execute(sql`
    SELECT
      period_id,
      period_starts_at,
      period_ends_at,
      applied_included_units,
      applied_hard_limit,
      applied_overage,
      policy_version
    FROM metering_reservation
    WHERE tenant_id = ${key.tenantId}
      AND account = ${key.account}
      AND meter = ${key.meter}
      AND period_id = ${key.periodId}
    ORDER BY created_at DESC, id DESC
    LIMIT 1
    FOR UPDATE
  `))
  if (!row) {
    throw new MeteringStorageError(
      'historical usage policy snapshot',
      new Error(`period snapshot missing: ${key.periodId}`),
    )
  }

  const hardLimit =
    row.applied_hard_limit === null || row.applied_hard_limit === undefined
      ? undefined
      : asBigint(row.applied_hard_limit, 'usage', 'applied_hard_limit')
  const policy: EntitlementPolicy = {
    meter: key.meter,
    includedUnits: asBigint(row.applied_included_units, 'usage', 'applied_included_units'),
    ...(hardLimit === undefined ? {} : { hardLimit }),
    overage: row.applied_overage as EntitlementPolicy['overage'],
    period: {
      id: String(row.period_id),
      startsAt: asDate(row.period_starts_at, 'period_starts_at'),
      endsAt: asDate(row.period_ends_at, 'period_ends_at'),
    },
    version: String(row.policy_version),
  }
  validatePolicy(policy, key.meter)
  return policy
}

async function readUsage(
  tx: SqlExecutor,
  key: CounterKey,
  policy: EntitlementPolicy,
): Promise<UsageView> {
  await lockCounter(tx, key)
  const { committed, reserved } = await projection(tx, key)
  const used = committed + reserved
  const effectiveCapacity = capacity(policy)
  return {
    period: copyPeriod(policy),
    committed,
    reserved,
    includedRemaining: maxZero(policy.includedUnits - used),
    capacityRemaining:
      effectiveCapacity === null ? null : maxZero(effectiveCapacity - used),
  }
}

async function currentPolicy<S extends Schema>(
  deps: UsageDeps<S>,
  query: Extract<UsageQuery, { at: Date }>,
): Promise<EntitlementPolicy> {
  let policy: EntitlementPolicy | null
  try {
    policy = await deps.policies.resolve(query.subject, query.meter, query.at)
  } catch (error) {
    if (isMeteringError(error)) throw error
    throw new MeteringStorageError('resolve usage policy', error)
  }
  if (policy === null) {
    throw new EntitlementDeniedError({
      subject: query.subject,
      meter: query.meter,
      at: query.at,
    })
  }
  validatePolicy(policy, query.meter)
  return policy
}

export async function usage<S extends Schema = Record<string, never>>(
  deps: UsageDeps<S>,
  query: UsageQuery,
): Promise<UsageView> {
  validateQuery(query)

  try {
    if ('at' in query) {
      const policy = await currentPolicy(deps, query)
      return await deps.db.transaction((tx) =>
        readUsage(tx, keyFor(query.subject, query.meter, policy.period.id), policy),
      )
    }

    const key = keyFor(query.subject, query.meter, query.periodId)
    return await deps.db.transaction(async (tx) => {
      await lockCounter(tx, key)
      const policy = await historicalPolicy(tx, key)
      const { committed, reserved } = await projection(tx, key)
      const used = committed + reserved
      const effectiveCapacity = capacity(policy)
      return {
        period: copyPeriod(policy),
        committed,
        reserved,
        includedRemaining: maxZero(policy.includedUnits - used),
        capacityRemaining:
          effectiveCapacity === null ? null : maxZero(effectiveCapacity - used),
      }
    })
  } catch (error) {
    if (isMeteringError(error)) throw error
    throw new MeteringStorageError('usage', error)
  }
}
