import { sql } from 'drizzle-orm'
import type { Transaction } from '@platform-modules/db'
import {
  FulfillmentValidationError,
  VoucherAlreadyRedeemedError,
  VoucherExpiredError,
  VoucherWrongVendorError,
} from '../errors.js'
import type { FulfillmentSchema } from '../schema.js'
import { assertUuid, type Voucher } from '../types.js'
import { decideRedemption } from './decide.js'

type SqlRow = Record<string, unknown>

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

function rowToVoucher(row: SqlRow): Voucher {
  const expiresAt = row.expires_at
  const redeemedAt = row.redeemed_at
  const createdAt = row.created_at
  return {
    id: String(row.id),
    orderId: String(row.order_id),
    lineId: String(row.line_id),
    unitIndex: Number(row.unit_index),
    vendorId: row.vendor_id == null ? null : String(row.vendor_id),
    state: String(row.state) as Voucher['state'],
    expiresAt:
      expiresAt == null
        ? null
        : expiresAt instanceof Date
          ? expiresAt
          : new Date(String(expiresAt)),
    redeemedAt:
      redeemedAt == null
        ? null
        : redeemedAt instanceof Date
          ? redeemedAt
          : new Date(String(redeemedAt)),
    createdAt:
      createdAt instanceof Date ? createdAt : new Date(String(createdAt)),
  }
}

function throwForDecision(
  voucherId: string,
  scanningVendorId: string,
  reason: 'ALREADY_REDEEMED' | 'EXPIRED' | 'WRONG_VENDOR' | 'INVALID',
): never {
  if (reason === 'ALREADY_REDEEMED') {
    throw new VoucherAlreadyRedeemedError(voucherId)
  }
  if (reason === 'EXPIRED') {
    throw new VoucherExpiredError(voucherId)
  }
  if (reason === 'WRONG_VENDOR') {
    throw new VoucherWrongVendorError({ voucherId, scanningVendorId })
  }
  throw new FulfillmentValidationError('voucher')
}

function classifyRedeemFailure(
  voucherId: string,
  scanningVendorId: string,
  serverNow: Date,
  row: SqlRow,
): never {
  const state = String(row.state)
  const vendorId = row.vendor_id == null ? null : String(row.vendor_id)
  const expiresAtRaw = row.expires_at
  const expiresAt =
    expiresAtRaw == null
      ? null
      : expiresAtRaw instanceof Date
        ? expiresAtRaw
        : new Date(String(expiresAtRaw))

  if (state === 'REDEEMED') {
    throw new VoucherAlreadyRedeemedError(voucherId)
  }
  if (expiresAt !== null && expiresAt <= serverNow) {
    throw new VoucherExpiredError(voucherId)
  }
  if (vendorId !== null && vendorId !== scanningVendorId) {
    throw new VoucherWrongVendorError({ voucherId, scanningVendorId })
  }
  throw new VoucherAlreadyRedeemedError(voucherId)
}

export async function redeemVoucher(
  tx: Transaction<FulfillmentSchema>,
  input: { voucherId: string; scanningVendorId: string },
): Promise<Voucher> {
  assertUuid(input.voucherId, 'voucherId')

  const selectRes = await tx.execute(sql`
    SELECT id, order_id, line_id, unit_index, vendor_id, state, expires_at, redeemed_at, created_at,
           NOW() AS server_now
    FROM voucher
    WHERE id = ${input.voucherId}::uuid
  `)
  const snapshot = firstRow(selectRes)
  if (!snapshot) {
    throw new FulfillmentValidationError('voucherId')
  }

  const serverNow = new Date(String(snapshot.server_now))
  const current = rowToVoucher(snapshot)
  const advisory = decideRedemption(current.state, {
    currentState: current.state,
    at: serverNow,
    scanningVendorId: input.scanningVendorId,
    expiresAt: current.expiresAt,
    vendorId: current.vendorId,
  })
  if (!advisory.ok) {
    throwForDecision(input.voucherId, input.scanningVendorId, advisory.reason)
  }

  const updateRes = await tx.execute(sql`
    UPDATE voucher
    SET state = 'REDEEMED', redeemed_at = NOW()
    WHERE id = ${input.voucherId}::uuid
      AND state = 'UNREDEEMED'
      AND (expires_at IS NULL OR expires_at > NOW())
      AND (vendor_id IS NULL OR vendor_id = ${input.scanningVendorId})
    RETURNING id, order_id, line_id, unit_index, vendor_id, state, expires_at, redeemed_at, created_at
  `)

  const updated = firstRow(updateRes)
  if (updated) {
    return rowToVoucher(updated)
  }

  const rereadRes = await tx.execute(sql`
    SELECT id, order_id, line_id, unit_index, vendor_id, state, expires_at, redeemed_at, created_at,
           NOW() AS server_now
    FROM voucher
    WHERE id = ${input.voucherId}::uuid
  `)
  const reread = firstRow(rereadRes)
  if (!reread) {
    throw new FulfillmentValidationError('voucherId')
  }
  const rereadServerNow = new Date(String(reread.server_now))
  classifyRedeemFailure(input.voucherId, input.scanningVendorId, rereadServerNow, reread)
}
