import type {
  BuyerRef,
  Order,
  OrderLine,
  Page,
  PriceMode,
  ProductKind,
  VendorSplit,
} from '@platform-modules/commerce-orders'
import { isOrderStatus } from '@platform-modules/commerce-orders'
import { OrderWireError } from './errors.js'

const PRICE_MODES: readonly PriceMode[] = ['inclusive', 'exclusive']
const PRODUCT_KINDS: readonly ProductKind[] = ['physical', 'digital', 'voucher']
const FUNDERS: readonly VendorSplit['funder'][] = ['platform', 'vendor']

function asObject(v: unknown, ctx: string): Record<string, unknown> {
  if (typeof v !== 'object' || v === null) {
    throw new OrderWireError(`${ctx}: expected an object, got ${v === null ? 'null' : typeof v}`)
  }
  // fulfillmentState.steps is embedded verbatim in the revived Order — an own
  // __proto__/prototype/constructor from wire JSON would pollute a later Object.assign target; strip.
  const o = v as Record<string, unknown>
  for (const k of ['__proto__', 'prototype', 'constructor']) {
    if (Object.prototype.hasOwnProperty.call(o, k)) delete o[k]
  }
  return o
}

function asString(v: unknown, ctx: string): string {
  if (typeof v !== 'string') throw new OrderWireError(`${ctx}: expected a string, got ${typeof v}`)
  return v
}

function asNullableString(v: unknown, ctx: string): string | null {
  if (v === null) return null
  return asString(v, ctx)
}

function asPositiveInt(v: unknown, ctx: string): number {
  if (typeof v !== 'number' || !Number.isInteger(v) || v <= 0) {
    throw new OrderWireError(`${ctx}: expected a positive integer, got ${String(v)}`)
  }
  return v
}

/** Mirror catalog-react reviveAmount verbatim: exact minor-unit bigint, no silent 0n/NaN. */
function reviveMinor(v: unknown, ctx: string): bigint {
  if (typeof v === 'bigint') return v
  if (typeof v === 'number') {
    if (!Number.isSafeInteger(v)) {
      throw new OrderWireError(`${ctx}: amount must be a safe-integer minor-unit value (values ≥ 2^53 must be string-encoded), got ${v}`)
    }
    return BigInt(v)
  }
  if (typeof v === 'string') {
    if (v.length > 22) {
      throw new OrderWireError(`${ctx}: amount string exceeds 21 digits (length ${v.length})`)
    }
    if (!/^-?\d{1,21}$/.test(v)) {
      throw new OrderWireError(`${ctx}: amount is not a valid integer minor-unit string (≤21 digits): ${JSON.stringify(v)}`)
    }
    return BigInt(v)
  }
  throw new OrderWireError(`${ctx}: amount missing or wrong type (${typeof v})`)
}

function reviveBuyerRef(v: unknown): BuyerRef {
  const o = asObject(v, 'buyerRef')
  if (typeof o.userId === 'string') return { userId: o.userId }
  if (typeof o.guestEmail === 'string') return { guestEmail: o.guestEmail }
  throw new OrderWireError('buyerRef: expected { userId } or { guestEmail }')
}

function reviveLine(v: unknown, i: number): OrderLine {
  const o = asObject(v, `lines[${i}]`)
  const kind = asString(o.kind, `lines[${i}].kind`)
  if (!(PRODUCT_KINDS as readonly string[]).includes(kind)) {
    throw new OrderWireError(`lines[${i}].kind: unknown ProductKind ${JSON.stringify(kind)}`)
  }
  return {
    id: asString(o.id, `lines[${i}].id`),
    orderId: asString(o.orderId, `lines[${i}].orderId`),
    variantId: asString(o.variantId, `lines[${i}].variantId`),
    kind: kind as ProductKind,
    qty: asPositiveInt(o.qty, `lines[${i}].qty`),
    unitPrice: reviveMinor(o.unitPrice, `lines[${i}].unitPrice`),
    lineTotal: reviveMinor(o.lineTotal, `lines[${i}].lineTotal`),
    vendorId: asNullableString(o.vendorId, `lines[${i}].vendorId`),
  }
}

function reviveSplit(v: unknown, i: number): VendorSplit {
  const o = asObject(v, `splits[${i}]`)
  const funder = asString(o.funder, `splits[${i}].funder`)
  if (!(FUNDERS as readonly string[]).includes(funder)) {
    throw new OrderWireError(`splits[${i}].funder: unknown funder ${JSON.stringify(funder)}`)
  }
  return {
    id: asString(o.id, `splits[${i}].id`),
    orderId: asString(o.orderId, `splits[${i}].orderId`),
    vendorId: asNullableString(o.vendorId, `splits[${i}].vendorId`),
    amount: reviveMinor(o.amount, `splits[${i}].amount`),
    funder: funder as VendorSplit['funder'],
  }
}

export function reviveOrder(raw: unknown): Order {
  const o = asObject(raw, 'reviveOrder')
  const status = asString(o.status, 'status')
  if (!isOrderStatus(status)) throw new OrderWireError(`status: unknown OrderStatus ${JSON.stringify(status)}`)
  const priceMode = asString(o.priceMode, 'priceMode')
  if (!(PRICE_MODES as readonly string[]).includes(priceMode)) {
    throw new OrderWireError(`priceMode: unknown PriceMode ${JSON.stringify(priceMode)}`)
  }
  if (!Array.isArray(o.lines)) throw new OrderWireError('lines: expected an array')
  if (!Array.isArray(o.splits)) throw new OrderWireError('splits: expected an array')
  const fulfillment = asObject(o.fulfillmentState, 'fulfillmentState')
  const steps = asObject(fulfillment.steps, 'fulfillmentState.steps')
  return {
    id: asString(o.id, 'id'),
    buyerRef: reviveBuyerRef(o.buyerRef),
    status,
    currency: asString(o.currency, 'currency'),
    priceMode: priceMode as PriceMode,
    subtotal: reviveMinor(o.subtotal, 'subtotal'),
    tax: reviveMinor(o.tax, 'tax'),
    discount: reviveMinor(o.discount, 'discount'),
    total: reviveMinor(o.total, 'total'),
    lines: o.lines.map((l, i) => reviveLine(l, i)),
    splits: o.splits.map((s, i) => reviveSplit(s, i)),
    fulfillmentState: { steps: steps as Order['fulfillmentState']['steps'] },
  }
}

export function reviveOrderPage(raw: unknown): Page<Order> {
  const o = asObject(raw, 'reviveOrderPage')
  if (!Array.isArray(o.items)) throw new OrderWireError('items: expected an array')
  if (o.nextCursor !== null && typeof o.nextCursor !== 'string') {
    throw new OrderWireError(`nextCursor: expected string | null, got ${typeof o.nextCursor}`)
  }
  return {
    items: o.items.map((it) => reviveOrder(it)),
    nextCursor: o.nextCursor,
  }
}
