// Per-node field mapping (Wertstrom P6, Baustein 2/5). Translates one parsed
// SimVSM node into a VsmNode (lib/vsm-types.ts) using ONLY fields that
// already exist there — zero schema/migration changes (see README.md
// "Warum keine Migration"). Single-product simplification throughout
// (Capability-Matrix B1 "Standard: 1 Produkt"): only the FIRST row of a
// ProductTable/Produkte table is ever read, matching param-helpers.ts
// `paramFirstTableRow`'s own doc comment.
//
// Every field read here was range-checked against the real corpus before
// being trusted (local-only value scan, not committed): Availability/
// ScrapRate/ReworkRate are already 0–100 percent scale (not a 0–1 fraction),
// and "-1" is SimVSM's own sentinel for "unlimited/unset" on count-like
// fields (CarrierCapacityProduct/maxCarrierNum/OrderLotSize) — every
// count-like read below is guarded `>= 0` so a sentinel is skipped
// (left unset), never imported as a nonsensical negative value.

import type { VsmNode } from '@/lib/vsm-types'
import type { SimVsmParsedNode } from './types'
import { classifyNode } from './mapping-registry'
import { paramBoolean, paramDurationSec, paramFirstTableRow, paramNumber, paramString, rowDurationSec, rowNumber } from './param-helpers'

export interface NodeMapOutcome {
  simvsmKey: string
  simvsmClass: string
  category: string
  nodeName: string
  supported: boolean
  confidence: number
  labelDe: string
  reasonDe?: string
  generalizedNote?: string
  /** true when this is a `customer` node that DID carry an order `Quantity`
   * but no usable `Interval` to normalize it into a Stk/Tag rate — `demand`
   * was therefore deliberately left unset rather than fabricated (see the
   * `entry.targetType === 'customer'` block below). Consumed by report.ts to
   * surface a §14.4 assumption instead of silently under-reporting demand. */
  demandIntervalMissing?: boolean
  vsmNode: VsmNode | null
}

function clampPct(v: number): number {
  return Math.min(100, Math.max(0, v))
}

/** `idFactory` is KEYED by the node's own SimVSM `key` (not a plain `() =>
 * string`) so a caller correlating ids across multiple alternatives of the
 * same file (see streams.ts mapStream's file-scoped nodeIdByKey) can return
 * the SAME id for the SAME key again — a plain zero-arg factory (e.g. in
 * this module's own tests) is still a valid `(key) => string` (it just
 * ignores the argument), so this is a backward-compatible widening, not a
 * breaking change. */
export function mapSimVsmNode(node: SimVsmParsedNode, idFactory: (key: string) => string): NodeMapOutcome {
  const entry = classifyNode(node.simvsmClass)

  if (!entry || !entry.supported) {
    return {
      simvsmKey: node.key,
      simvsmClass: node.simvsmClass,
      category: node.category,
      nodeName: node.nodeName,
      supported: false,
      confidence: 0,
      // Review-Fix C17 (adversarial review PR #360): a genuinely unknown
      // class (`entry` undefined — not in NODE_MAPPING_REGISTRY at all, as
      // opposed to a KNOWN-but-unsupported one like noteVSM, which already
      // has a real, maintained German labelDe) used to fall back to the raw
      // camelCase SimVSM class name as the PRIMARY user-facing label — an
      // internal registry identifier leaking into the simple view (§14.1).
      // The class name is still available in reasonDe below (details, not
      // the primary label).
      labelDe: entry?.labelDe ?? 'Nicht unterstütztes Element',
      reasonDe: entry?.reasonDe ?? `Unbekannte SimVSM-Klasse "${node.simvsmClass}" — im Mapping-Register nicht erfasst.`,
      vsmNode: null,
    }
  }

  const vsmNode: VsmNode = {
    id: idFactory(node.key),
    type: entry.targetType,
    x: 0,
    y: 0,
    name: node.nodeName || entry.labelDe,
    provenance: 'imported',
  }

  if (entry.targetInventoryKind) vsmNode.inventoryKind = entry.targetInventoryKind
  // P8.2a (Baustein 2, "PPS als echtes Datenfeld", KAR-878/KAR-986): mirrors
  // targetInventoryKind directly above — see mapping-registry.ts's
  // productionControl entry.
  if (entry.targetIsPps) vsmNode.isPps = true

  const availability = paramNumber(node.params, 'Availability')
  if (availability !== undefined) vsmNode.availabilityPct = clampPct(availability)

  const numWorkers = paramNumber(node.params, 'NumberOfWorkers')
  if (numWorkers !== undefined && numWorkers >= 0) vsmNode.numWorkers = numWorkers

  const mttrSec = paramDurationSec(node.params, 'MTTR')
  if (mttrSec !== undefined && mttrSec >= 0) vsmNode.mttrMin = mttrSec / 60

  const isValueAdding = paramBoolean(node.params, 'isValueAdding')
  if (isValueAdding !== undefined) {
    vsmNode.isValueAdded = isValueAdding
  } else if (node.simvsmClass === 'productionControl') {
    // productionControl has no isValueAdding parameter in SimVSM at all (it
    // is a coordination box, not a manufacturing step) — the generalization
    // onto `type: 'process'` (see mapping-registry.ts) only makes sense with
    // this explicit false, so vsm-engine's VA/NVA sums don't silently count
    // a PPS box as value-adding.
    vsmNode.isValueAdded = false
  }

  const comment = paramString(node.params, 'comment')
  if (comment) vsmNode.notes = comment

  // `productRow` is SimVSM's generic "ProductTable"/"Produkte" first-row —
  // both a process node's OWN cycle-time/scrap data AND a customer node's
  // ORDER row happen to use this same table shape (and even the same
  // "PlannedCycleTime" column name), but the two are fields from completely
  // different fachlich concepts. Reading the row itself is type-agnostic
  // (harmless — inventory/transport/supplier nodes simply have no matching
  // table and get `undefined`), but every FIELD extracted from it below is
  // gated to the node type it actually applies to.
  const productRow = paramFirstTableRow(node.params, 'ProductTable') ?? paramFirstTableRow(node.params, 'Produkte') ?? paramFirstTableRow(node.params, 'product')

  let demandIntervalMissing = false

  if (entry.targetType === 'process') {
    const cycleTimeSec = rowDurationSec(productRow, 'PlannedCycleTime')
    if (cycleTimeSec !== undefined && cycleTimeSec >= 0) vsmNode.cycleTimeSec = cycleTimeSec

    const scrapRate = rowNumber(productRow, 'ScrapRate')
    if (scrapRate !== undefined && scrapRate >= 0) vsmNode.scrapRate = clampPct(scrapRate)
  }

  if (entry.targetType === 'customer') {
    // VsmNode.demand is contractually "Stk/Tag" (lib/vsm-engine/internal/
    // inventory-coverage.ts) — SimVSM's "Quantity" is an order quantity PER
    // "Interval" (a duration in seconds), not inherently a daily figure.
    // Normalize: demand = Quantity × (86400 / IntervalSec). If Interval is
    // absent or not a usable (>0) duration, demand stays UNSET — never
    // fabricate a "Stk/Tag" number out of an unknown time basis (null≠0
    // doctrine). `demandIntervalMissing` tells report.ts to surface this as
    // a declared assumption instead of a silently-missing value.
    //
    // `quantity` (Bestandsmenge, a DIFFERENT concept — see VsmNode.quantity/
    // inventory-coverage.ts) is deliberately NEVER filled from this order
    // row either: an order quantity is not an inventory stock level, and
    // SimVSM's customer node carries no stock-level field at all — leaving
    // it unset is honest, not a regression (no other source ever fed it a
    // real stock figure before this fix).
    const quantity = rowNumber(productRow, 'Quantity')
    const intervalSec = rowDurationSec(productRow, 'Interval')
    if (quantity !== undefined && quantity >= 0) {
      if (intervalSec !== undefined && intervalSec > 0) {
        vsmNode.demand = quantity * (86400 / intervalSec)
      } else {
        demandIntervalMissing = true
      }
    }
  }

  if (entry.targetType === 'inventory') {
    const capacity = paramNumber(node.params, 'Capacity')
    const maxCarrierNum = paramNumber(node.params, 'maxCarrierNum')
    const resolvedCapacity = capacity !== undefined && capacity >= 0 ? capacity : maxCarrierNum !== undefined && maxCarrierNum >= 0 ? maxCarrierNum : undefined
    if (resolvedCapacity !== undefined) vsmNode.inventoryMaxQuantity = resolvedCapacity
  }

  if (entry.targetType === 'transport') {
    const transportSec = paramDurationSec(node.params, 'TransportationTime')
    if (transportSec !== undefined && transportSec >= 0) vsmNode.transportTimeSec = transportSec
  }

  return {
    simvsmKey: node.key,
    simvsmClass: node.simvsmClass,
    category: node.category,
    nodeName: node.nodeName,
    supported: true,
    confidence: entry.confidence,
    labelDe: entry.labelDe,
    generalizedNote: entry.generalizedNote,
    demandIntervalMissing: demandIntervalMissing || undefined,
    vsmNode,
  }
}
