/**
 * Import of a parsed contract fixture (server/assets/shipping/*.json, produced by
 * reference/test-scripts/dhl-contract-parse.mts) into CUST_CarrierRate.
 *
 * planImport() diffs fixture rows against the existing rows of the shipper by
 * rateKey() and never deletes — rows missing from the fixture are reported as
 * `orphans` for manual review. applyImport() POSTs/PUTs with small concurrency.
 *
 * ⚠ The dev `.env` points at the PROD iDempiere: a non-dry-run import from the
 * local dev server writes production data.
 */
import fetchHelper from '../fetchHelper'
import { CARRIER_RATE_MODEL, carrierRateBody, loadCarrierRateRecords, loadCountryMaps, mapCarrierRate } from './carrierRates'
import { rateKey, round2 } from '../../../app/utils/shippingPriceSheet'
import type { CarrierRate, CarrierRateFixture, FixtureRate } from '../../../app/utils/shippingPriceSheet'

export interface PlannedCreate { key: string; rate: FixtureRate; countryId: number | null }
export interface PlannedUpdate { key: string; id: number; countryId: number | null; before: Partial<CarrierRate>; after: Partial<CarrierRate>; changed: string[] }
export interface ImportPlan {
  fixture: string
  shipperId: number
  sections: string[]
  create: PlannedCreate[]
  update: PlannedUpdate[]
  unchanged: number
  orphans: { id: number; serviceCode: string; country: string; rangeFrom: number; description: string }[]
  unresolvedCodes: string[]
  skipped: number
  counts: Record<string, { create: number; update: number; unchanged: number }>
}

export const listFixtures = async (): Promise<{ key: string; fixture: string; shipperId: number; shipperName: string; contractNo: string; validFrom: string; generatedAt: string; rates: number; services: string[] }[]> => {
  const storage = useStorage('assets:server')
  const keys = (await storage.getKeys('shipping')).filter((k) => k.endsWith('.json'))
  const out: any[] = []
  for (const k of keys) {
    try {
      const file = k.replace(/^shipping[:/]/, '').replace(/\.json$/, '')
      const f = await loadFixtureFile(file)
      if (f) out.push({ key: file, fixture: f.fixture, shipperId: f.shipperId, shipperName: f.shipperName, contractNo: f.contractNo, validFrom: f.validFrom, generatedAt: f.generatedAt, rates: f.rates.length, services: f.services.map((s) => s.code) })
    } catch (e) { /* skip unreadable */ }
  }
  return out
}

const loadFixtureFile = async (file: string): Promise<CarrierRateFixture | null> => {
  const safe = String(file || '').replace(/[^a-z0-9\-_.]/gi, '')
  if (!safe) return null
  const storage = useStorage('assets:server')
  const raw = await storage.getItem(`shipping/${safe}.json`) ?? await storage.getItem(`shipping:${safe}.json`)
  if (!raw) return null
  const f = typeof raw === 'string' ? JSON.parse(raw) : raw
  return f && Array.isArray(f.rates) ? f as CarrierRateFixture : null
}

/** Resolve by file name (dhl-contract-2025-07) OR by the fixture's logical name (dhl-2025-07). */
export const loadFixture = async (name: string): Promise<CarrierRateFixture | null> => {
  const direct = await loadFixtureFile(name)
  if (direct) return direct
  for (const fx of await listFixtures()) if (fx.fixture === name) return loadFixtureFile(fx.key)
  return null
}

const COMPARE_FIELDS: (keyof CarrierRate)[] = ['serviceName', 'zone', 'rangeTo', 'basePrice', 'pricePerKg', 'isPercentage', 'contractNo', 'description', 'sourceRef', 'isActive']

const fixtureToRate = (f: CarrierRateFixture, r: FixtureRate, countryId: number | null): Partial<CarrierRate> => {
  const svc = f.services.find((s) => s.code === r.serviceCode)
  return {
    shipperId: f.shipperId,
    serviceCode: r.serviceCode,
    serviceName: svc?.name || r.serviceCode,
    rateType: r.rateType,
    countryId,
    zone: r.zone || '',
    rangeFrom: round2(r.rangeFrom ?? 0),
    rangeTo: round2(r.rangeTo ?? 0),
    basePrice: round2(r.basePrice ?? 0),
    pricePerKg: round2(r.pricePerKg ?? 0),
    isPercentage: r.isPercentage === true,
    contractNo: f.contractNo,
    validFrom: f.validFrom,
    validTo: '',
    description: String(r.description || '').slice(0, 255),
    sourceRef: String(r.sourceRef || '').slice(0, 60),
    isActive: true
  }
}

const same = (a: any, b: any) => {
  if (typeof a === 'number' || typeof b === 'number') return round2(Number(a || 0)) === round2(Number(b || 0))
  if (typeof a === 'boolean' || typeof b === 'boolean') return !!a === !!b
  return String(a ?? '') === String(b ?? '')
}

export const planImport = async (event: any, token: string, fixture: CarrierRateFixture, sections?: string[]): Promise<ImportPlan> => {
  const countries = await loadCountryMaps(event, token)
  const wanted = new Set((sections || []).filter(Boolean))
  const existingRecs = await loadCarrierRateRecords(event, token, { shipperId: fixture.shipperId, includeInactive: true })
  const existing = existingRecs.map((r) => mapCarrierRate(r, countries.byId))
  const byKey = new Map<string, CarrierRate>()
  for (const e of existing) byKey.set(rateKey(e), e)

  const plan: ImportPlan = { fixture: fixture.fixture, shipperId: fixture.shipperId, sections: [...wanted], create: [], update: [], unchanged: 0, orphans: [], unresolvedCodes: [], skipped: 0, counts: {} }
  const unresolved = new Set<string>()
  const seen = new Set<string>()
  const bump = (code: string, k: 'create' | 'update' | 'unchanged') => { plan.counts[code] = plan.counts[code] || { create: 0, update: 0, unchanged: 0 }; plan.counts[code][k]++ }

  for (const r of fixture.rates) {
    if (wanted.size && !wanted.has(r.serviceCode)) continue
    let countryId: number | null = null
    if (r.countryCode) {
      countryId = countries.byCode.get(String(r.countryCode).toUpperCase()) ?? null
      if (!countryId) { unresolved.add(String(r.countryCode)); plan.skipped++; continue }
    }
    const after = fixtureToRate(fixture, r, countryId)
    const key = rateKey({ shipperId: fixture.shipperId, serviceCode: r.serviceCode, countryId, rateType: r.rateType, rangeFrom: after.rangeFrom, validFrom: fixture.validFrom, description: after.description, zone: after.zone })
    seen.add(key)
    const cur = byKey.get(key)
    if (!cur) { plan.create.push({ key, rate: r, countryId }); bump(r.serviceCode, 'create'); continue }
    const changed = COMPARE_FIELDS.filter((f) => !same((cur as any)[f], (after as any)[f]))
    if (!changed.length) { plan.unchanged++; bump(r.serviceCode, 'unchanged'); continue }
    const before: any = {}, diff: any = {}
    for (const f of changed) { before[f] = (cur as any)[f]; diff[f] = (after as any)[f] }
    plan.update.push({ key, id: cur.id, countryId, before, after: diff, changed })
    bump(r.serviceCode, 'update')
  }
  for (const e of existing) {
    if (wanted.size && !wanted.has(e.serviceCode)) continue
    if (!seen.has(rateKey(e))) plan.orphans.push({ id: e.id, serviceCode: e.serviceCode, country: e.country, rangeFrom: e.rangeFrom, description: e.description })
  }
  plan.unresolvedCodes = [...unresolved].sort()
  return plan
}

export interface ImportResult { created: number; updated: number; failed: { key: string; message: string }[] }

const pool = async <T>(items: T[], limit: number, fn: (item: T) => Promise<void>) => {
  let i = 0
  const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
    while (i < items.length) { const item = items[i++]; await fn(item) }
  })
  await Promise.all(workers)
}

export const applyImport = async (event: any, token: string, fixture: CarrierRateFixture, plan: ImportPlan): Promise<ImportResult> => {
  const result: ImportResult = { created: 0, updated: 0, failed: [] }
  const msg = (e: any) => e?.data?.detail || e?.data?.title || e?.message || String(e)
  await pool(plan.create, 3, async (c) => {
    try {
      const body = carrierRateBody(fixtureToRate(fixture, c.rate, c.countryId), true)
      await fetchHelper(event, CARRIER_RATE_MODEL, 'POST', token, body)
      result.created++
    } catch (e: any) { result.failed.push({ key: c.key, message: msg(e) }) }
  })
  await pool(plan.update, 3, async (u) => {
    try {
      const body = carrierRateBody(u.after as any, false)
      await fetchHelper(event, `${CARRIER_RATE_MODEL}/${u.id}`, 'PUT', token, body)
      result.updated++
    } catch (e: any) { result.failed.push({ key: u.key, message: msg(e) }) }
  })
  return result
}
