import { and, eq } from 'drizzle-orm'
import type { Cart } from '@platform-modules/commerce-cart'
import { product, variant, variantPrice } from '@platform-modules/commerce-catalog'
import {
  claimForCharge,
  createOrder,
  getOrderById,
  isOrderNotChargeableError as isOrdersNotChargeableError,
  OrderIntegrityError,
  OrderValidationError,
  type NewOrder,
  type NewOrderLine,
  type NewVendorSplit,
  type Order,
  type OrdersSchema,
} from '@platform-modules/commerce-orders'
import { settleCharge, type ChargeResult } from '@platform-modules/billing'
import type { Querier, Transaction } from '@platform-modules/db'
import {
  EU_VAT_SCHEDULES,
  IL_VAT_SCHEDULE,
  resolveVatRate,
  type VatScheduleEntry,
} from '@platform-modules/tax/rates-table'
import { buildChargeKey } from './charge-key.js'
import {
  CheckoutValidationError,
  OrderNotChargeableError,
  PaymentFailedError,
} from './errors.js'
import { checkoutSession } from './schema.js'
import { settleCheckout } from './settle.js'
import type {
  CatalogEntry,
  CatalogSnapshot,
  CheckoutDeps,
  CheckoutInput,
  PricedLine,
} from './types.js'
import { validateCheckout } from './validate.js'

type Phase1Action = 'charge' | 'resume' | 'paid'

function isChargeIntentPendingError(e: unknown): boolean {
  return (
    typeof e === 'object' &&
    e !== null &&
    (e as { code?: unknown }).code === 'CHARGE_INTENT_PENDING'
  )
}

function resolveCart(input: CheckoutInput): Cart {
  if (input.cart) {
    return input.cart
  }
  throw new CheckoutValidationError('EMPTY_CART')
}

function resolveVatSchedule(country: string): readonly VatScheduleEntry[] {
  if (country === 'IL') {
    return IL_VAT_SCHEDULE
  }
  const eu = EU_VAT_SCHEDULES[country]
  if (eu) {
    return eu
  }
  throw new CheckoutValidationError('UNKNOWN_COUNTRY')
}

function hostTodayIso(): string {
  return new Date().toISOString().slice(0, 10)
}

function isProductAvailable(
  status: string,
  availableFrom: Date | null,
  availableUntil: Date | null,
  now: Date,
): boolean {
  if (status !== 'active') {
    return false
  }
  if (availableFrom !== null && availableFrom > now) {
    return false
  }
  if (availableUntil !== null && availableUntil <= now) {
    return false
  }
  return true
}

async function loadCatalogEntry(
  db: Querier<OrdersSchema>,
  variantId: string,
  currency: string,
  now: Date,
): Promise<CatalogEntry | undefined> {
  const [variantRow] = await db.select().from(variant).where(eq(variant.id, variantId))
  if (!variantRow) {
    return undefined
  }

  const [productRow] = await db
    .select()
    .from(product)
    .where(eq(product.id, variantRow.productId))
  if (!productRow) {
    return undefined
  }

  const [priceRow] = await db
    .select()
    .from(variantPrice)
    .where(and(eq(variantPrice.variantId, variantId), eq(variantPrice.currency, currency)))

  if (!priceRow) {
    return undefined
  }

  return {
    available: isProductAvailable(
      productRow.status,
      productRow.availableFrom,
      productRow.availableUntil,
      now,
    ),
    price: {
      amount: priceRow.amount,
      currency: priceRow.currency,
      priceMode: priceRow.priceMode,
    },
    kind: productRow.kind,
    title: productRow.title,
    vendorId: productRow.vendorId,
  }
}

async function buildCatalogSnapshot(
  db: Querier<OrdersSchema>,
  cart: Cart,
  currency: string,
): Promise<CatalogSnapshot> {
  const now = new Date()
  const snapshot = new Map<string, CatalogEntry>()
  const variantIds = [...new Set(cart.lines.map((line) => line.variantId))]

  for (const variantId of variantIds) {
    const entry = await loadCatalogEntry(db, variantId, currency, now)
    if (entry) {
      snapshot.set(variantId, entry)
    }
  }

  return snapshot
}

function mapValidationError(
  result: Extract<ReturnType<typeof validateCheckout>, { ok: false }>,
): never {
  throw new CheckoutValidationError(result.error, result.stale)
}

// The billing seam carries amounts as number — a total past MAX_SAFE_INTEGER
// would silently charge a rounded amount (same cap as executeRefund's).
function assertChargeableTotal(total: bigint): void {
  if (total > BigInt(Number.MAX_SAFE_INTEGER)) {
    throw new OrderValidationError('total')
  }
}

function aggregateOrderAmounts(lines: PricedLine[]): {
  subtotal: bigint
  tax: bigint
  total: bigint
} {
  let subtotal = 0n
  let tax = 0n
  let total = 0n

  for (const line of lines) {
    const qty = BigInt(line.qty)
    subtotal += line.unitNet * qty
    tax += line.vat * qty
    total += line.lineTotal
  }

  if (subtotal + tax !== total) {
    throw new OrderIntegrityError('total')
  }

  assertChargeableTotal(total)

  return { subtotal, tax, total }
}

function buildOrderLines(
  cart: Cart,
  pricedLines: PricedLine[],
  catalog: CatalogSnapshot,
  currency: string,
): NewOrderLine[] {
  const pricedByVariant = new Map(pricedLines.map((line) => [line.variantId, line]))

  return cart.lines.map((line) => {
    const priced = pricedByVariant.get(line.variantId)
    const entry = catalog.get(line.variantId)
    if (!priced || !entry) {
      throw new CheckoutValidationError('STALE_ITEMS')
    }

    return {
      variantId: line.variantId,
      kind: entry.kind,
      qty: line.qty,
      unitPrice: priced.unitNet,
      lineTotal: priced.unitNet * BigInt(line.qty),
      currency,
      // Money attribution reads the CATALOG's vendorId, never the cart line's —
      // a cart line is client-shaped (cart's addLine action carries vendorId),
      // so trusting it would let a forged cart route a split to any vendor.
      vendorId: entry.vendorId ?? null,
    }
  })
}

function buildVendorSplits(
  cart: Cart,
  pricedLines: PricedLine[],
  catalog: CatalogSnapshot,
): NewVendorSplit[] {
  const pricedByVariant = new Map(pricedLines.map((line) => [line.variantId, line]))
  const totals = new Map<string | null, bigint>()

  for (const line of cart.lines) {
    const priced = pricedByVariant.get(line.variantId)
    const entry = catalog.get(line.variantId)
    if (!priced || !entry) {
      throw new CheckoutValidationError('STALE_ITEMS')
    }
    const vendorId = entry.vendorId ?? null
    totals.set(vendorId, (totals.get(vendorId) ?? 0n) + priced.lineTotal)
  }

  // funder drives refund/clawback attribution (orders div 4): a vendor split is
  // absorbed by the vendor, the platform bucket by the platform.
  return [...totals.entries()].map(([vendorId, amount]) => ({
    vendorId,
    amount,
    funder: vendorId === null ? ('platform' as const) : ('vendor' as const),
  }))
}

function assertBuyerOwnsOrder(order: Order, buyerUserId: string): void {
  if (!('userId' in order.buyerRef) || order.buyerRef.userId !== buyerUserId) {
    throw new OrderNotChargeableError(order.id)
  }
}

async function resolvePhase1Action(
  tx: Transaction<OrdersSchema>,
  order: Order,
  buyerUserId: string,
): Promise<{ action: Phase1Action; order: Order }> {
  let current = order

  while (true) {
    assertBuyerOwnsOrder(current, buyerUserId)

    switch (current.status) {
      case 'pending': {
        try {
          const claimed = await claimForCharge(tx, current.id)
          return { action: 'charge', order: claimed }
        } catch (e) {
          if (!isOrdersNotChargeableError(e)) {
            throw e
          }
          const reloaded = await getOrderById(tx, current.id, { isAdmin: true })
          if (!reloaded) {
            throw new OrderNotChargeableError(current.id)
          }
          current = reloaded
          continue
        }
      }
      case 'charging':
        return { action: 'resume', order: current }
      case 'paid':
        return { action: 'paid', order: current }
      default:
        throw new OrderNotChargeableError(current.id)
    }
  }
}

async function upsertCheckoutSession(
  db: Querier<OrdersSchema>,
  orderId: string,
  clientSecret: string,
): Promise<void> {
  const now = new Date()
  await db
    .insert(checkoutSession)
    .values({
      orderId,
      clientSecret,
      createdAt: now,
      updatedAt: now,
    })
    .onConflictDoUpdate({
      target: checkoutSession.orderId,
      set: {
        clientSecret,
        updatedAt: now,
      },
    })
}

async function readCheckoutSession(
  db: Querier<OrdersSchema>,
  orderId: string,
): Promise<string | undefined> {
  const [row] = await db
    .select()
    .from(checkoutSession)
    .where(eq(checkoutSession.orderId, orderId))
  return row?.clientSecret
}

export async function startCheckout<S extends OrdersSchema>(
  deps: CheckoutDeps<S>,
  input: CheckoutInput,
): Promise<{ orderId: string; clientSecret?: string }> {
  const cart = resolveCart(input)
  const vatRate = resolveVatRate(resolveVatSchedule(input.buyerCountry), hostTodayIso())
  const catalog = await buildCatalogSnapshot(
    deps.db as unknown as Querier<OrdersSchema>,
    cart,
    input.currency,
  )

  const validation = validateCheckout({
    cart,
    catalog,
    currency: input.currency,
    priceMode: input.priceMode,
    vatRate,
  })
  if (!validation.ok) {
    mapValidationError(validation)
  }

  const { subtotal, tax, total } = aggregateOrderAmounts(validation.lines)
  const newOrder: NewOrder = {
    idempotencyKey: input.idempotencyKey,
    buyerRef: input.buyerRef,
    currency: input.currency,
    priceMode: input.priceMode,
    subtotal,
    tax,
    discount: 0n,
    total,
    lines: buildOrderLines(cart, validation.lines, catalog, input.currency),
    splits: buildVendorSplits(cart, validation.lines, catalog),
  }

  const { action, order } = await deps.db.transaction(async (tx) => {
    const ordersTx = tx as unknown as Transaction<OrdersSchema>
    const created = await createOrder(ordersTx, newOrder)
    return resolvePhase1Action(ordersTx, created, input.buyerRef.userId)
  })

  if (action === 'paid') {
    return { orderId: order.id }
  }

  if (action === 'resume') {
    const clientSecret = await readCheckoutSession(
      deps.db as unknown as Querier<OrdersSchema>,
      order.id,
    )
    return clientSecret
      ? { orderId: order.id, clientSecret }
      : { orderId: order.id }
  }

  // Provider call only: a throw here may mean the card WAS charged, so it must
  // never blind-fail the order. Wrap as PaymentFailedError (INV-2b). settleCheckout
  // and upsertCheckoutSession run OUTSIDE this try — their throws (notably
  // FulfillmentIncompleteError, the INV-1 webhook-redelivery heal trigger) must
  // surface unwrapped to the retrying buyer, not be mislabeled as a 402.
  assertChargeableTotal(order.total)

  let result: ChargeResult
  try {
    result = await settleCharge(
      {
        chargeKey: buildChargeKey(order.id),
        amount: Number(order.total),
        currency: order.currency,
      },
      deps,
    )
  } catch (e) {
    if (isChargeIntentPendingError(e)) {
      return { orderId: order.id }
    }
    throw new PaymentFailedError(e)
  }

  if (result.kind === 'requires_client_action') {
    await upsertCheckoutSession(
      deps.db as unknown as Querier<OrdersSchema>,
      order.id,
      result.clientSecret,
    )
    return { orderId: order.id, clientSecret: result.clientSecret }
  }

  await settleCheckout(deps, order.id, result.providerRef, result.amount)
  return { orderId: order.id }
}
