/**
 * Mapping upstream availability listings into PIG's inventory rows.
 *
 * The schema was written to mirror the upstream field names, so this is mostly
 * a rename rather than a transformation. The interesting parts are the two
 * places where a judgement is required.
 *
 * **Node totals.** Upstream reports `gpuMemory` and `prices.onDemand` for the
 * whole node, while PIG compares inventory per GPU. Normalising here prevents
 * larger nodes from appearing to have more expensive GPUs, and `raw` keeps the
 * original totals for reconciliation.
 *
 * **Money.** PIG stores integer cents, because floating-point currency in a
 * system that reports margin is a defect waiting to be found by an accountant.
 * Rounding happens exactly once, here, after normalising to a per-GPU price.
 *
 * **Interconnect.** The single most commercially loaded field, since it decides
 * whether capacity can train or only serve. Upstream sends free text with
 * inconsistent casing, and an unrecognised value maps to `Unknown` rather than
 * being optimistically read as Ethernet — claiming a cluster has no fast fabric
 * when it does loses a deal, but the reverse sells a customer something that
 * will not work.
 */
import type { InterconnectType, SecurityTier, StockStatus } from '@pig/core';
import type { PrimeGpuListing } from './client';

export interface MappedListing {
  externalCloudId: string | null;
  providerSlug: string | null;
  gpuType: string;
  socket: string | null;
  gpuCount: number;
  gpuMemoryGb: number | null;
  vcpu: number | null;
  memoryGb: number | null;
  diskGb: number | null;
  internetMbps: number | null;
  interconnectGbps: number | null;
  interconnectType: InterconnectType;
  region: string | null;
  country: string | null;
  securityTier: SecurityTier;
  stockStatus: StockStatus;
  isSpot: boolean;
  provisioningMinutes: number | null;
  prepaidHours: string | null;
  onDemandPriceCents: number | null;
  communityPriceCents: number | null;
  priceIsVariable: boolean;
  currency: string;
  images: string[];
  raw: Record<string, unknown>;
}

export function mapListing(listing: PrimeGpuListing): MappedListing | null {
  // Without a GPU type and a count there is nothing sellable to record.
  if (!listing.gpuType || !listing.gpuCount) return null;

  return {
    externalCloudId: listing.cloudId ?? null,
    providerSlug: listing.provider ?? null,
    gpuType: listing.gpuType,
    socket: normaliseSocket(listing.socket),
    gpuCount: listing.gpuCount,
    gpuMemoryGb: perGpu(listing.gpuMemory, listing.gpuCount),
    vcpu: unwrapCount(listing.vcpu),
    memoryGb: unwrapCount(listing.memory),
    diskGb: listing.disk?.defaultCount ?? null,
    internetMbps: listing.internetSpeed ?? null,
    interconnectGbps: listing.interconnect ?? null,
    interconnectType: normaliseInterconnect(listing.interconnectType),
    region: listing.region ?? null,
    country: listing.country ?? null,
    securityTier: listing.security === 'community_cloud' ? 'community_cloud' : 'secure_cloud',
    stockStatus: normaliseStock(listing.stockStatus),
    isSpot: listing.isSpot ?? false,
    provisioningMinutes: listing.provisioningTime ?? null,
    prepaidHours: listing.prepaidTime != null ? String(listing.prepaidTime) : null,
    onDemandPriceCents: toCents(perGpu(listing.prices?.onDemand, listing.gpuCount)),
    communityPriceCents: toCents(listing.prices?.communityPrice),
    priceIsVariable: listing.prices?.isVariable ?? false,
    currency: listing.prices?.currency ?? 'USD',
    images: listing.images ?? [],
    raw: listing.raw,
  };
}

/**
 * Dollars to integer cents.
 *
 * Multiplying by 100 in floating point then truncating loses a cent on values
 * like 2.43 (which is 2.4299999... in binary), so this rounds rather than
 * truncates. At GPU-hour scale one cent compounds into real money across
 * millions of hours.
 */
export function toCents(dollars: number | null | undefined): number | null {
  if (dollars == null || !Number.isFinite(dollars)) return null;
  return Math.round(dollars * 100);
}

function perGpu(total: number | null | undefined, gpuCount: number): number | null {
  if (total == null || !Number.isFinite(total)) return null;
  return total / gpuCount;
}

function unwrapCount(value: unknown): number | null {
  if (typeof value === 'number') return value;
  if (value && typeof value === 'object') {
    const c = (value as { defaultCount?: unknown }).defaultCount;
    if (typeof c === 'number') return c;
  }
  return null;
}

function normaliseSocket(socket: string | undefined): string | null {
  if (!socket) return null;
  const upper = socket.toUpperCase().replace(/[\s_-]/g, '');
  if (upper === 'PCIE') return 'PCIe';
  const sxm = /^SXM([2-6])$/.exec(upper);
  if (sxm) return `SXM${sxm[1]}`;
  // Unknown sockets are dropped rather than stored, since the column is an
  // enum. The verbatim value survives in `raw`.
  return null;
}

/**
 * Interconnect, conservatively.
 *
 * `Unknown` is the safe default. Guessing Ethernet would understate real
 * capacity; guessing InfiniBand would sell a training customer a cluster that
 * cannot train. Neither error is acceptable, so an unrecognised value stays
 * explicitly unknown and a human resolves it.
 */
function normaliseInterconnect(value: string | undefined): InterconnectType {
  if (!value) return 'Unknown';
  const v = value.toLowerCase().replace(/[\s_-]/g, '');
  if (v.includes('infiniband') || v === 'ib') return 'Infiniband';
  if (v.includes('roce')) return 'RoCE';
  if (v.includes('nvlink') || v.includes('nvl')) return 'NVLink';
  if (v.includes('ethernet') || v.includes('eth')) return 'Ethernet';
  return 'Unknown';
}

function normaliseStock(value: string | undefined): StockStatus {
  switch ((value ?? '').toLowerCase()) {
    case 'available':
      return 'Available';
    case 'low':
      return 'Low';
    case 'medium':
      return 'Medium';
    case 'high':
      return 'High';
    case 'unavailable':
      return 'Unavailable';
    default:
      // An unknown stock signal is treated as unavailable rather than
      // available: it is better to under-promise inventory than to have a
      // seller offer capacity that turns out not to exist.
      return 'Unavailable';
  }
}
