import { sql, type SQLWrapper } from 'drizzle-orm'
import type { Schema } from '@platform-modules/db'
import {
  isMeteringError,
  MeteringStorageError,
  MeteringValidationError,
  ReservationConflictError,
  ReservationNotFoundError,
} from './errors.js'
import { applyDelta, lockCounter, type CounterKey } from './internal/counter.js'
import { findByIdempotencyKey, type Reservation as StoredReservation } from './internal/idempotency.js'
import type { MeteringDatabase } from './types.js'

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

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

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

function assertReservationId(reservationId: string): void {
  if (typeof reservationId !== 'string' || reservationId.trim() === '') {
    throw new MeteringValidationError('reservationId', 'must be non-empty')
  }
}

function keyFromRow(row: SqlRow): CounterKey {
  return {
    tenantId: String(row.tenant_id),
    account: String(row.account),
    meter: String(row.meter),
    periodId: String(row.period_id),
  }
}

async function findReservationLocator(
  tx: SqlExecutor,
  reservationId: string,
): Promise<{ key: CounterKey; idempotencyKey: string } | undefined> {
  const result = await tx.execute(sql`
    SELECT tenant_id, account, meter, period_id, idempotency_key
    FROM metering_reservation
    WHERE id = ${reservationId}
  `)
  const row = firstRow(result)
  if (!row) return undefined
  return {
    key: keyFromRow(row),
    idempotencyKey: String(row.idempotency_key),
  }
}

function throwStorage(error: unknown, operation: string): never {
  if (isMeteringError(error)) throw error
  throw new MeteringStorageError(operation, error)
}

async function releaseInTransaction(
  tx: SqlExecutor,
  reservationId: string,
): Promise<void> {
  const locator = await findReservationLocator(tx, reservationId)
  if (!locator) throw new ReservationNotFoundError(reservationId)

  await lockCounter(tx, locator.key)
  const reservation = await findByIdempotencyKey(
    tx,
    locator.key.tenantId,
    locator.idempotencyKey,
  )
  if (!reservation || reservation.id !== reservationId) {
    throw new MeteringStorageError('reservation row changed during release', new Error(reservationId))
  }

  if (reservation.status === 'released') return
  if (reservation.status !== 'reserved') {
    throw new ReservationConflictError({
      reservationId,
      detail: `cannot release ${reservation.status} reservation`,
    })
  }

  await applyDelta(tx, locator.key, { reserved: -reservation.reservedUnits })
  const updateResult = await tx.execute(sql`
    UPDATE metering_reservation
    SET status = 'released'
    WHERE id = ${reservationId} AND status = 'reserved'
    RETURNING id
  `)
  if (!firstRow(updateResult)?.id) {
    throw new MeteringStorageError('reservation disappeared during release', new Error(reservationId))
  }
}

export async function release<S extends Schema = Record<string, never>>(
  deps: ReleaseDeps<S>,
  reservationId: string,
): Promise<void> {
  assertReservationId(reservationId)

  try {
    await deps.db.transaction((tx) => releaseInTransaction(tx, reservationId))
  } catch (error) {
    throwStorage(error, 'release')
  }
}
