import { sql } from 'drizzle-orm'
import type { Order, OrderLine } from '@platform-modules/commerce-orders'
import { recordStep } from '@platform-modules/commerce-orders'
import type { Transaction } from '@platform-modules/db'
import { grantDigitalAccess } from './digital/grant.js'
import {
  FulfillmentValidationError,
  UnfulfillableLineError,
  isUnfulfillableLineError,
} from './errors.js'
import { createShipment } from './physical/shipment.js'
import type { FulfillmentPorts } from './seams.js'
import type { FulfillmentDbSchema } from './schema.js'
import {
  ownerKeyOf,
  type FulfillmentKind,
  type FulfillmentOverall,
  type FulfillmentResult,
  type LineFulfillmentOutcome,
} from './types.js'
import { issueVoucher } from './voucher/issue.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 allRows(res: unknown): SqlRow[] {
  return (Array.isArray(res) ? res : (res as { rows?: SqlRow[] }).rows) ?? []
}

function isFulfillmentKind(kind: string): kind is FulfillmentKind {
  return kind === 'digital' || kind === 'voucher' || kind === 'physical'
}

function errorMessage(error: unknown): string {
  if (error instanceof Error) {
    return error.message
  }
  return String(error)
}

async function loadDigitalGrantId(
  tx: Transaction<FulfillmentDbSchema>,
  order: Order,
  line: OrderLine,
): Promise<string> {
  const ownerKey = ownerKeyOf(order.buyerRef)
  const res = await tx.execute(sql`
    SELECT id
    FROM access_grant
    WHERE order_id = ${order.id}::uuid
      AND item_id = ${line.variantId}
      AND owner_key = ${ownerKey}
  `)
  const row = firstRow(res)
  if (!row) {
    throw new FulfillmentValidationError('grant')
  }
  return String(row.id)
}

async function loadVoucherIds(
  tx: Transaction<FulfillmentDbSchema>,
  order: Order,
  line: OrderLine,
): Promise<string[]> {
  const res = await tx.execute(sql`
    SELECT id
    FROM voucher
    WHERE order_id = ${order.id}::uuid
      AND line_id = ${line.id}
    ORDER BY unit_index ASC
  `)
  const ids = allRows(res).map((row) => String(row.id))
  if (ids.length === 0) {
    throw new FulfillmentValidationError('voucher')
  }
  return ids
}

async function loadShipmentId(
  tx: Transaction<FulfillmentDbSchema>,
  order: Order,
  line: OrderLine,
): Promise<string> {
  const lineIdsJson = JSON.stringify([line.id])
  const res = await tx.execute(sql`
    SELECT id
    FROM shipment
    WHERE order_id = ${order.id}::uuid
      AND line_ids @> ${lineIdsJson}::jsonb
    ORDER BY created_at ASC
    LIMIT 1
  `)
  const row = firstRow(res)
  if (!row) {
    throw new FulfillmentValidationError('shipment')
  }
  return String(row.id)
}

async function loadReplayOutcome(
  tx: Transaction<FulfillmentDbSchema>,
  order: Order,
  line: OrderLine,
): Promise<LineFulfillmentOutcome> {
  switch (line.kind) {
    case 'digital':
      return {
        lineId: line.id,
        kind: 'digital',
        grantId: await loadDigitalGrantId(tx, order, line),
      }
    case 'voucher':
      return {
        lineId: line.id,
        kind: 'voucher',
        voucherIds: await loadVoucherIds(tx, order, line),
      }
    case 'physical':
      return {
        lineId: line.id,
        kind: 'physical',
        shipmentId: await loadShipmentId(tx, order, line),
      }
    default:
      throw new UnfulfillableLineError({
        orderId: order.id,
        lineId: line.id,
        kind: line.kind,
      })
  }
}

async function dispatchLine(
  tx: Transaction<FulfillmentDbSchema>,
  ports: FulfillmentPorts,
  order: Order,
  line: OrderLine,
): Promise<LineFulfillmentOutcome> {
  if (!isFulfillmentKind(line.kind)) {
    throw new UnfulfillableLineError({
      orderId: order.id,
      lineId: line.id,
      kind: line.kind,
    })
  }

  switch (line.kind) {
    case 'digital': {
      const blobKey = await ports.resolveBlobKey(line)
      if (!blobKey) {
        throw new FulfillmentValidationError('blobKey')
      }
      const grant = await grantDigitalAccess(tx, {
        orderId: order.id,
        itemId: line.variantId,
        ownerKey: ownerKeyOf(order.buyerRef),
        blobKey,
      })
      return { lineId: line.id, kind: 'digital', grantId: grant.id }
    }
    case 'voucher': {
      const vouchers = await issueVoucher(tx, {
        orderId: order.id,
        lineId: line.id,
        qty: line.qty,
        vendorId: line.vendorId,
        expiresAt: null,
      })
      return {
        lineId: line.id,
        kind: 'voucher',
        voucherIds: vouchers.map((voucher) => voucher.id),
      }
    }
    case 'physical': {
      const address = await ports.resolveShippingAddress(order)
      if (!address) {
        throw new FulfillmentValidationError('address')
      }
      const shipment = await createShipment(tx, {
        orderId: order.id,
        lineIds: [line.id],
        address,
      })
      return { lineId: line.id, kind: 'physical', shipmentId: shipment.id }
    }
    default: {
      const _exhaustive: never = line.kind
      throw new UnfulfillableLineError({
        orderId: order.id,
        lineId: line.id,
        kind: String(_exhaustive),
      })
    }
  }
}

async function notifyForOutcome(
  ports: FulfillmentPorts,
  order: Order,
  line: OrderLine,
  outcome: LineFulfillmentOutcome,
): Promise<void> {
  if (outcome.kind === 'digital') {
    await ports.notify({
      kind: 'digital-grant',
      orderId: order.id,
      grantId: outcome.grantId,
      itemId: line.variantId,
    })
    return
  }

  if (outcome.kind === 'voucher') {
    await ports.notify({
      kind: 'voucher-issued',
      orderId: order.id,
      lineId: line.id,
      codes: outcome.voucherIds,
    })
  }
}

async function deliverNotification(
  ports: FulfillmentPorts,
  order: Order,
  line: OrderLine,
  outcome: LineFulfillmentOutcome,
): Promise<void> {
  if (outcome.kind !== 'digital' && outcome.kind !== 'voucher') {
    return
  }

  const stepId = `notify:${line.id}`

  try {
    const existing = await ports.db.execute(sql`
      SELECT 1
      FROM order_step
      WHERE order_id = ${order.id}::uuid
        AND step_id = ${stepId}
      LIMIT 1
    `)
    if (firstRow(existing)) {
      return
    }

    await notifyForOutcome(ports, order, line, outcome)
    await ports.db.transaction(async (tx) => {
      await recordStep(tx, order.id, stepId, { kind: outcome.kind, lineId: line.id })
    })
  } catch {
    // best-effort: marker stays unset → re-attempted on next fulfillOrder
  }
}

function computeOverall(lines: LineFulfillmentOutcome[]): FulfillmentOverall {
  const fulfilledCount = lines.filter((line) => line.kind !== 'unfulfillable').length
  if (fulfilledCount === lines.length) {
    return 'fulfilled'
  }
  if (fulfilledCount === 0) {
    return 'unfulfillable'
  }
  return 'partial'
}

export async function fulfillOrder(
  ports: FulfillmentPorts,
  order: Order,
): Promise<FulfillmentResult> {
  const lineOutcomes: LineFulfillmentOutcome[] = []

  for (const line of order.lines) {
    let outcome: LineFulfillmentOutcome

    try {
      outcome = await ports.db.transaction(async (tx) => {
        const fresh = await recordStep(tx, order.id, `fulfill:${line.id}`, {
          kind: line.kind,
          lineId: line.id,
        })

        if (!fresh) {
          return loadReplayOutcome(tx, order, line)
        }

        return dispatchLine(tx, ports, order, line)
      })
    } catch (error) {
      outcome = isUnfulfillableLineError(error)
        ? { lineId: line.id, kind: 'unfulfillable', error: error.message }
        : { lineId: line.id, kind: 'unfulfillable', error: errorMessage(error) }
      lineOutcomes.push(outcome)
      continue
    }

    lineOutcomes.push(outcome)
    await deliverNotification(ports, order, line, outcome)
  }

  return {
    orderId: order.id,
    overall: computeOverall(lineOutcomes),
    lines: lineOutcomes,
  }
}
