// packages/commerce-catalog-react/src/revive.ts
import type {
  Page,
  Product,
  Variant,
  VariantPrice,
} from '@platform-modules/commerce-catalog'
import { ProductWireError } from './errors.js'

function asObject(v: unknown, ctx: string): Record<string, unknown> {
  if (typeof v !== 'object' || v === null) {
    throw new ProductWireError(`${ctx}: expected an object, got ${v === null ? 'null' : typeof v}`)
  }
  // An own __proto__/prototype/constructor from wire JSON would ride the spreads
  // below into the revived object and 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 reviveAmount(v: unknown, ctx: string): bigint {
  if (typeof v === 'bigint') return v
  if (typeof v === 'number') {
    // JSON.parse silently rounds numeric literals ≥ 2^53 — a raw out-of-range number is
    // already corrupt; require string-encoding for exactness (the string branch is exact at any magnitude).
    if (!Number.isSafeInteger(v)) throw new ProductWireError(`${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') {
    // Length pre-check bounds BigInt-parse cost (attacker-length digit strings); 21 digits > any real minor-unit.
    if (v.length > 22) {
      throw new ProductWireError(`${ctx}: amount string exceeds 21 digits (length ${v.length})`)
    }
    // Strict integer-string only: BigInt('') / BigInt('  ') silently yield 0n
    // and BigInt('0x10') yields 16n — the spec forbids a silent 0n / mis-parse at
    // this trust boundary (§5: never a silent NaN/0n/Invalid Date). Reject anything
    // that is not an optionally-signed run of ≤21 decimal digits before parsing.
    if (!/^-?\d{1,21}$/.test(v)) {
      throw new ProductWireError(`${ctx}: amount is not a valid integer minor-unit string (≤21 digits): ${JSON.stringify(v)}`)
    }
    return BigInt(v)
  }
  throw new ProductWireError(`${ctx}: amount missing or wrong type (${typeof v})`)
}

function reviveRequiredDate(v: unknown, field: string): Date {
  if (v instanceof Date) return v
  if (typeof v === 'string' || typeof v === 'number') {
    const d = new Date(v)
    if (Number.isNaN(d.getTime())) throw new ProductWireError(`reviveProduct: ${field} is not a valid date: ${String(v)}`)
    return d
  }
  throw new ProductWireError(`reviveProduct: ${field} is required`)
}

function reviveOptionalDate(v: unknown, field: string): Date | null | undefined {
  if (v === undefined) return undefined
  if (v === null) return null
  return reviveRequiredDate(v, field)
}

function revivePrices(v: unknown, vi: number): VariantPrice[] {
  if (v === undefined || v === null) return []
  if (!Array.isArray(v)) throw new ProductWireError(`reviveProduct: variants[${vi}].prices must be an array`)
  return v.map((p, pi) => {
    const pr = asObject(p, `reviveProduct: variants[${vi}].prices[${pi}]`)
    return { ...(pr as unknown as VariantPrice), amount: reviveAmount(pr.amount, `variants[${vi}].prices[${pi}]`) }
  })
}

function reviveVariants(v: unknown): Variant[] {
  if (v === undefined || v === null) return []
  if (!Array.isArray(v)) throw new ProductWireError('reviveProduct: variants must be an array')
  return v.map((variant, i) => {
    const r = asObject(variant, `reviveProduct: variants[${i}]`)
    return { ...(r as unknown as Variant), prices: revivePrices(r.prices, i) }
  })
}

/** Wire JSON → typed Product (deep). Reconstructs bigint amounts + the four Date fields. §5 */
export function reviveProduct(raw: unknown): Product {
  const r = asObject(raw, 'reviveProduct')
  return {
    ...(r as unknown as Product),
    availableFrom: reviveOptionalDate(r.availableFrom, 'availableFrom'),
    availableUntil: reviveOptionalDate(r.availableUntil, 'availableUntil'),
    createdAt: reviveRequiredDate(r.createdAt, 'createdAt'),
    updatedAt: reviveRequiredDate(r.updatedAt, 'updatedAt'),
    variants: reviveVariants(r.variants),
  }
}

function reviveCount(v: unknown, field: string): number {
  // Counts are JSON-faithful numbers (§3/§5 stringify only Money + Date over the
  // wire, never counts), so a non-number is corrupt wire — reject it, never
  // silently coerce ''/null → 0 (the count-analog of the forbidden silent 0n
  // amount). Number('')/Number(null) are 0 and would pass a bare isFinite.
  // Non-negative safe integer — same JSON-rounding footgun as amounts (≥2^53).
  if (typeof v !== 'number' || !Number.isSafeInteger(v) || v < 0) {
    throw new ProductWireError(`reviveProductPage: ${field} must be a non-negative safe integer, got ${String(v)}`)
  }
  return v
}

/** Wire JSON → typed Page<Product>, revival mapped over items. §5 */
export function reviveProductPage(raw: unknown): Page<Product> {
  const r = asObject(raw, 'reviveProductPage')
  if (!Array.isArray(r.items)) throw new ProductWireError('reviveProductPage: items must be an array')
  return {
    items: r.items.map((it) => reviveProduct(it)),
    total: reviveCount(r.total, 'total'),
    page: reviveCount(r.page, 'page'),
    pageSize: reviveCount(r.pageSize, 'pageSize'),
  }
}
