import { and, eq, sql } from 'drizzle-orm'
import type { RefundResult } from '@platform-modules/billing'
import type { Transaction, TransactionalDatabase } from '@platform-modules/db'
import {
  OrderNotChargeableError,
  OrderNotFoundError,
  OrderValidationError,
  RefundExceedsPaidError,
} from '../errors.js'
import { loadOrderChildren } from '../order-assembly.js'
import { order, refundIntent, type OrdersSchema } from '../schema.js'
import {
  assertUuid,
  type Actor,
  type OrderLine,
  type OrderStatus,
} from '../types.js'
import type { RefundLine, RefundPort } from './types.js'

const REFUNDABLE_STATUSES: ReadonlySet<OrderStatus> = new Set([
  'paid',
  'partially_refunded',
  'fulfilled',
  'completed',
  'unfulfillable',
])

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 sumRefundLines(lines: RefundLine[]): bigint {
  let total = 0n
  for (const line of lines) {
    total += line.amount
  }
  return total
}

export function assertActorMayRefund(
  actor: Actor,
  orderLines: OrderLine[],
  lines: RefundLine[],
  orderId: string,
): void {
  const lineById = new Map(orderLines.map((line) => [line.id, line]))

  for (const refundLine of lines) {
    if (!lineById.has(refundLine.orderLineId)) {
      throw new OrderNotFoundError(orderId)
    }
  }

  if (actor.isAdmin === true) {
    return
  }

  if (
    typeof actor.vendorId === 'string' &&
    actor.vendorId.length > 0 &&
    lines.every((refundLine) => {
      const line = lineById.get(refundLine.orderLineId)!
      return line.vendorId === actor.vendorId && line.vendorId !== null
    })
  ) {
    return
  }

  throw new OrderNotFoundError(orderId)
}

function assertRefundableStatus(status: OrderStatus, orderId: string): void {
  if (!REFUNDABLE_STATUSES.has(status)) {
    throw new OrderNotChargeableError(orderId)
  }
}

export async function recomputeOrderRefundStatus(
  tx: Transaction<OrdersSchema>,
  orderId: string,
): Promise<void> {
  // Lock the order row FIRST so concurrent settle tx2s serialize here. Without
  // FOR UPDATE two settles each read SUM(executed) under their own snapshot
  // (neither sees the other's uncommitted intent), both write the same stale
  // 'partially_refunded', and a fully-refunded order stays partial (lost update
  // on the denormalized order.status — money stays correct, the cap reads
  // refund_intent not status, but the status is wrong). Lock → fresh SUM → write
  // is the same MVCC discipline the inventory reserve path uses.
  const orderRes = await tx.execute(sql`
    SELECT total
    FROM "order"
    WHERE id = ${orderId}::uuid
    FOR UPDATE
  `)
  const orderRow = firstRow(orderRes)
  if (!orderRow) {
    return
  }

  const sumRes = await tx.execute(sql`
    SELECT COALESCE(SUM(amount), 0) AS refunded
    FROM refund_intent
    WHERE order_id = ${orderId}::uuid
      AND status = 'executed'
  `)
  const sumRow = firstRow(sumRes)
  const refunded = BigInt(String(sumRow?.refunded ?? 0))
  const total = BigInt(String(orderRow.total))

  const newStatus: OrderStatus = refunded === total ? 'refunded' : 'partially_refunded'
  const now = new Date()

  await tx
    .update(order)
    .set({ status: newStatus, updatedAt: now })
    .where(eq(order.id, orderId))
}

export type ClaimedRefundIntent = {
  intentId: string
  seq: number
  refundKey: string
  chargeRef: string
  thisRefund: bigint
}

export async function claimRefundIntentInTx(
  tx: Transaction<OrdersSchema>,
  orderId: string,
  thisRefund: bigint,
): Promise<ClaimedRefundIntent> {
  const orderRes = await tx.execute(sql`
    SELECT total, charge_ref
    FROM "order"
    WHERE id = ${orderId}::uuid
    FOR UPDATE
  `)
  const orderRow = firstRow(orderRes)
  if (!orderRow) {
    throw new OrderNotFoundError(orderId)
  }

  const chargeRef = orderRow.charge_ref
  if (chargeRef === null || chargeRef === undefined || String(chargeRef).length === 0) {
    throw new OrderNotChargeableError(orderId)
  }

  const amountPaid = BigInt(String(orderRow.total))

  const priorRes = await tx.execute(sql`
    SELECT COALESCE(SUM(amount), 0) AS prior
    FROM refund_intent
    WHERE order_id = ${orderId}::uuid
      AND status IN ('pending', 'executed')
  `)
  const priorRow = firstRow(priorRes)
  const prior = BigInt(String(priorRow?.prior ?? 0))

  if (prior + thisRefund > amountPaid) {
    throw new RefundExceedsPaidError({
      orderId,
      requested: thisRefund,
      alreadyRefunded: prior,
      paid: amountPaid,
    })
  }

  const seqRes = await tx.execute(sql`
    SELECT COALESCE(MAX(seq), 0) + 1 AS next_seq
    FROM refund_intent
    WHERE order_id = ${orderId}::uuid
  `)
  const seqRow = firstRow(seqRes)
  const seq = Number(seqRow?.next_seq ?? 1)

  const intentId = crypto.randomUUID()
  const refundKey = `refund:${intentId}`
  const now = new Date()

  await tx.insert(refundIntent).values({
    id: intentId,
    orderId,
    seq,
    amount: thisRefund,
    status: 'pending',
    refundKey,
    createdAt: now,
    updatedAt: now,
  })

  return {
    intentId,
    seq,
    refundKey,
    chargeRef: String(chargeRef),
    thisRefund,
  }
}

export async function settleRefundIntentInTx(
  tx: Transaction<OrdersSchema>,
  intentId: string,
  orderId: string,
  providerRef: string,
): Promise<boolean> {
  const now = new Date()
  const [settled] = await tx
    .update(refundIntent)
    .set({ status: 'executed', providerRef, updatedAt: now })
    .where(and(eq(refundIntent.id, intentId), eq(refundIntent.status, 'pending')))
    .returning({ id: refundIntent.id })

  if (!settled) {
    return false
  }

  await recomputeOrderRefundStatus(tx, orderId)
  return true
}

export async function executeRefund(
  db: TransactionalDatabase<OrdersSchema>,
  orderId: string,
  lines: RefundLine[],
  refund: RefundPort,
  actor: Actor,
): Promise<RefundResult> {
  assertUuid(orderId, 'orderId')

  if (lines.length === 0) {
    throw new OrderValidationError('lines')
  }

  for (const line of lines) {
    assertUuid(line.orderLineId, 'orderLineId')
  }

  const [orderRow] = await db.select().from(order).where(eq(order.id, orderId))
  if (!orderRow) {
    throw new OrderNotFoundError(orderId)
  }

  const { lines: orderLines } = await loadOrderChildren(db, orderId)
  assertActorMayRefund(actor, orderLines, lines, orderId)
  assertRefundableStatus(orderRow.status, orderId)

  // Per-line positive lower bound (PRE-tx1). A negative/zero line amount poisons
  // the M8 over-refund cap: the cap SUMs refund_intent.amount, so a -60 intent
  // claimed at full cap lets a later +60 over-refund past `paid`. The Σ safe-int
  // assert below cannot catch it — [{100n},{-40n}] sums positive yet carries a
  // malformed negative line. Reject EACH line here, before any tx/claim, so no
  // refund_intent row is ever written for a non-positive amount. DB-side
  // CHECK (amount > 0) is defense-in-depth; this code guard is the primary floor.
  for (const line of lines) {
    if (line.amount <= 0n) {
      throw new OrderValidationError('amount')
    }
  }

  const thisRefund = sumRefundLines(lines)
  if (thisRefund > BigInt(Number.MAX_SAFE_INTEGER)) {
    throw new OrderValidationError('amount')
  }

  const claimed = await db.transaction((tx) => claimRefundIntentInTx(tx, orderId, thisRefund))

  let res: RefundResult
  res = await refund({
    refundKey: claimed.refundKey,
    chargeKey: claimed.chargeRef,
    refundId: claimed.intentId,
    amount: Number(thisRefund),
  })

  if (res.kind === 'refunded') {
    await db.transaction((tx) =>
      settleRefundIntentInTx(tx, claimed.intentId, orderId, res.providerRef),
    )
  }

  return res
}
