// A4 Timeline-Sequenz (execution-prompt §8.4/§8.9, Referenzwertstrom-Auftrag
// Kais §8) — Wertstrom-Referenzbeispiel (KAR-878).
//
// `computeTimelineLadder` (timeline-ladder.ts) already produces the
// AUTHORITATIVE aggregate totals (DLZ, VA-Zeit, PCE, …) — this module does
// NOT recompute or duplicate any of those sums (Kais §17.12 "keine
// redundanten, voneinander abweichenden Kennzahlen"). It answers a DIFFERENT
// question: "in what ORDER, and above/below which baseline, does each node's
// own time contribution belong" — the data a classic VSM zigzag/Treppen-
// Zeitlinie needs to draw itself, which `computeTimelineLadder`'s flat
// category sums cannot answer alone.
//
// Ordering: Kahn's topological sort over materialFlow connections only
// (`kind !== 'information'`), same technique vsm-auto-layout.ts's topoOrder
// already uses (reimplemented locally, see `topoOrderExcluding`'s own doc —
// lib/vsm-engine must stay independent of components/). A plain DFS was
// tried first and REJECTED: this reference wertstrom has a genuine merge
// point (Gehäusevorbereitung is fed by BOTH a FIFO buffer from
// Leiterplattenbestückung AND the Aluminiumgehäuse-Eingangssupermarkt) — a
// DFS eagerly descends whichever incoming path it meets first and does NOT
// wait for a node's OTHER predecessors, silently producing the wrong
// chronological order for everything reachable only via the path not taken
// yet. Kahn's algorithm only marks a node ready once every (in-graph)
// predecessor has fired, which is what a sequential timeline actually needs.
//
// The cycle problem (EOL→Nacharbeit→EOL) is solved differently here than a
// plain DFS would: rework-loop nodes (see below) are identified FIRST and
// entirely EXCLUDED from the graph before Kahn's algorithm ever runs, so the
// back-edge that would otherwise deadlock two mutually-waiting nodes never
// exists in the sorted graph at all. `vsm-auto-layout.ts`'s own topoOrder has
// no such exclusion and DOES deadlock on exactly this shape — every node
// transitively after the cycle would fall into ITS "leftover, original array
// order" bucket instead of a real position. This is a real, pre-existing
// latent gap in vsm-auto-layout.ts, newly exercised by this reference
// wertstrom's rework loop — not fixed there in this PR (a shared, tested,
// wider-blast-radius module), see PRODUCT_SPEC.md/CHANGELOG.md "verbleibende
// Einschränkungen".
//
// Rework/reprocessing loops (Kais §7 "kleiner Rückfluss zur Nacharbeit"): a
// node whose ONLY connections are exactly one incoming and one outgoing
// materialFlow edge is a pure loop appendage — it has no independent role in
// the sequential flow (100% of units do NOT pass through it, unlike every
// other node) — IF one of two shapes holds: (a) both edges connect to/from
// the SAME host node P (a literal P→N→P cycle), or (b) the node's incoming
// edge comes from a host P and its outgoing edge lands on a node H that P
// ALSO reaches via its OWN direct edge — a parallel side-path that
// re-converges downstream instead of looping back (Referenzwertstrom-
// Fixrunde, KAR-878: the ACTUAL seed topology is EOL → Nacharbeit →
// Prüf-/Freigabepuffer, alongside a direct EOL → Prüf-/Freigabepuffer edge —
// shape (a)'s strict "same host on both ends" never matched this, so the
// node fell through into the ordinary sequence undetected; shape (b) is the
// generalization that catches it WITHOUT weakening the check into a false
// positive on a plain pass-through node, since that requires the direct
// P→H shortcut edge to actually exist). Either shape is EXCLUDED from the
// main sequence's width-proportional blocks (which would otherwise imply it
// happens to every unit) and returned separately as a `ReworkLoopAnnotation`,
// attached to its host — "visuell verständlich, aber nicht mit dem normalen
// Hauptmaterialfluss verwechselt" (Kais §7).
//
// Nodes wired up EXCLUSIVELY via information-kind edges (pure
// Informationsfluss-/Steuerungsknoten, e.g. a PPS-Element connected only via
// `kind: 'information'` edges) are excluded from the sequence entirely (see
// the `isInformationOnlyNode` filter in `computeTimelineSequence` below) —
// they carry no physical material-flow step for this timeline to place
// anywhere, and previously fell through into the generic process/machine/
// timevalue branch (rendering as a chronologically meaningless "?" block
// wherever their array index happened to sort them, e.g. after the final
// customer delivery — Referenzwertstrom-Fixrunde, KAR-878). A node with NO
// connections at all (a genuinely disconnected island) is a different,
// pre-existing case and is NOT excluded by this check — see
// `isInformationOnlyNode`'s own doc comment.
//
// P8.2a (KAR-878/KAR-986, Baustein 1 "isVaNode-unknown-Alignment"): a
// process/machine/timevalue node with explicit `vaClass === 'unknown'`
// ("Ungeklärt") used to render as the SAME `'non-value-add-process'`
// category as a genuinely NVA-classified node (the old binary `isVaNode`) —
// a declared residual gap (see `resolveVaBadgeState`'s K8-Fix doc comment,
// components/wertstrom/vsm-config.ts, and `computeVaClassBreakdown`'s own
// separate `unknown` bucket, components/wertstrom/vsm-metrics.ts). Closed
// here via a new `'unknown-process'` category — `classifyVaState` below is
// now tri-state; every already-classified node keeps byte-identical
// behavior.
//
// P8.2a-Review Fix-Runde (K1, KAR-878/KAR-986): Baustein 1 above only closed
// the EXPLICIT `vaClass === 'unknown'` literal — a node that was NEVER
// classified at all (neither `vaClass` nor `isValueAdded` ever set) still
// rendered as `'non-value-add-process'` unchanged, the same misrepresentation
// this paragraph describes, for a structurally equivalent input. Closed now
// — see `classifyVaState`'s own doc comment below for the full priority
// order and the declared divergence from `computeVaClassBreakdown`'s
// four-way split (PRODUCT_SPEC.md's K1 addendum).

import type { VsmConnection, VsmNode } from '@/lib/vsm-types'
import { computeInventoryCoverage } from './inventory-coverage'
import { isStationNode, type MetricExplain } from './types'
import { type ShiftModelInput } from './work-time'

export type TimelineBlockLevel = 'upper' | 'lower'
/** P8.2a (KAR-878/KAR-986, Baustein 1 "isVaNode-unknown-Alignment"):
 * `'unknown-process'` is NEW — a process/machine/timevalue node with
 * explicit `vaClass === 'unknown'` ("Ungeklärt") used to render as
 * `'non-value-add-process'` (the old binary `isVaNode`), i.e. a segment a
 * consumer would color/label as NVA even though the user explicitly did NOT
 * confirm that judgement. A consumer MUST render this category neutrally
 * (e.g. a grey tone), never the NVA color — see vsm-canvas-timeline.tsx's
 * CATEGORY_COLOR/CATEGORY_LABEL. */
export type TimelineBlockCategory = 'value-add' | 'non-value-add-process' | 'unknown-process' | 'wait' | 'inventory' | 'transport'

export interface TimelineBlock {
  nodeId: string
  nodeName: string
  nodeType: VsmNode['type']
  level: TimelineBlockLevel
  category: TimelineBlockCategory
  /** `null` = not computable from today's data (missing quantity/demand/
   * shiftModel/time field) — never fabricated. A caller renders this as a
   * "?" marker, never a 0-width silent drop. */
  seconds: number | null
  /** Rüstzeit on this SAME node — carried as a non-width-contributing
   * annotation only (Kais §8: "Rüstzeiten … dürfen nicht pauschal zur
   * Bearbeitungszeit eines einzelnen Produkts addiert werden") — never added
   * into `seconds`. */
  setupTimeSec?: number
}

export interface ReworkLoopAnnotation {
  hostNodeId: string
  hostNodeName: string
  loopNodeId: string
  loopNodeName: string
  /** The loop node's own cycleTimeSec (e.g. "durchschnittliche
   * Nacharbeitszeit") — `null` if not set. */
  cycleTimeSec: number | null
}

export interface TimelineSequenceResult {
  /** One entry per visited node, in material-flow order — a node with both
   * process time AND a set `waitTimeSec` contributes TWO entries (its own
   * lower block, then an upper 'wait' block right after). Pure loop-
   * appendage nodes (see module doc) are never included here. */
  blocks: TimelineBlock[]
  reworkLoops: ReworkLoopAnnotation[]
  explain: MetricExplain
}

function isMaterialFlow(c: VsmConnection): boolean {
  return c.kind === undefined || c.kind === 'materialFlow'
}

/** A node whose only connections are exactly {1 incoming, 1 outgoing}
 * materialFlow edge, matching one of the two rework-loop shapes documented
 * in the module doc above (same-host cycle, or a parallel side-path the
 * host also reaches directly). A plain pass-through node (e.g. a transport
 * step strictly between two processes, no shortcut edge) matches NEITHER
 * shape and is correctly left in the main sequence — see this module's
 * `__tests__/timeline-sequence.test.ts` negative test. */
function findReworkLoopNodeIds(nodes: VsmNode[], connections: VsmConnection[]): Map<string, ReworkLoopAnnotation> {
  const nodeById = new Map(nodes.map((n) => [n.id, n]))
  const result = new Map<string, ReworkLoopAnnotation>()
  for (const n of nodes) {
    const touching = connections.filter((c) => isMaterialFlow(c) && (c.fromNodeId === n.id || c.toNodeId === n.id))
    if (touching.length !== 2) continue
    const incoming = touching.filter((c) => c.toNodeId === n.id)
    const outgoing = touching.filter((c) => c.fromNodeId === n.id)
    if (incoming.length !== 1 || outgoing.length !== 1) continue
    const hostId = incoming[0].fromNodeId
    const downstreamId = outgoing[0].toNodeId
    const isSameHostCycle = hostId === downstreamId
    const isParallelSidePath =
      !isSameHostCycle && connections.some((c) => isMaterialFlow(c) && c.fromNodeId === hostId && c.toNodeId === downstreamId)
    if (!isSameHostCycle && !isParallelSidePath) continue
    const host = nodeById.get(hostId)
    if (!host) continue
    result.set(n.id, {
      hostNodeId: host.id,
      hostNodeName: host.name,
      loopNodeId: n.id,
      loopNodeName: n.name,
      cycleTimeSec: n.cycleTimeSec ?? null,
    })
  }
  return result
}

/** A node that HAS at least one connection, but NONE of them are
 * materialFlow — a pure Informationsfluss-/Steuerungsknoten (e.g. a
 * PPS-Element wired up exclusively via `kind: 'information'` edges) — see
 * module doc. Deliberately NOT the same as "zero materialFlow edges" alone:
 * a node with NO connections at all (a genuinely disconnected island, e.g. a
 * freshly-added process not yet wired up) is a different, pre-existing case
 * that must still appear on the timeline (`__tests__/timeline-sequence.test.ts`
 * "a disconnected island node is still included") — only a node actively
 * wired up EXCLUSIVELY through information-kind edges is excluded here. */
function isInformationOnlyNode(nodeId: string, connections: VsmConnection[]): boolean {
  const touching = connections.filter((c) => c.fromNodeId === nodeId || c.toNodeId === nodeId)
  return touching.length > 0 && touching.every((c) => !isMaterialFlow(c))
}

/** P8.2a-Review Fix-Runde (K1, KAR-878/KAR-986): FULL mirror of
 * timeline-ladder.ts's own `classifyVaState` (reimplemented locally per this
 * module's own "lib/vsm-engine stays independent of components/" doctrine,
 * see module doc — and of `resolveVaBadgeState`'s, vsm-config.ts, three-way
 * canvas-badge priority): `vaClass` decides when set (`'unknown'` its own
 * result, never folded into `'nonVa'`); absent `vaClass` falls back to the
 * legacy `isValueAdded` boolean; and — closed by THIS fix, not Baustein 1 —
 * neither field set is ALSO `'unknown'`, never a silent `'nonVa'`. Baustein 1
 * ("isVaNode-unknown-Alignment") only closed the explicit-literal half of
 * this; a never-classified node (reachable e.g. via `lib/simvsm-import` for a
 * SimVSM process class without an `isValueAdding` parameter) still rendered
 * as `'non-value-add-process'` below until now — the same misrepresentation
 * the K8-Fix canvas badge already fixed for this exact input. Every node
 * with an explicit `vaClass` or explicit `isValueAdded` keeps byte-identical
 * behavior. Deliberate, DECLARED divergence from `computeVaClassBreakdown`'s
 * (vsm-metrics.ts) four-way derivedVa/derivedNonVa split — see
 * timeline-ladder.ts's `classifyVaState` doc comment / PRODUCT_SPEC.md's K1
 * addendum for the full reasoning (not duplicated here). */
function classifyVaState(n: VsmNode): 'va' | 'unknown' | 'nonVa' {
  if (n.vaClass === 'va') return 'va'
  if (n.vaClass === 'unknown') return 'unknown'
  if (n.vaClass) return 'nonVa'
  if (n.isValueAdded === true) return 'va'
  if (n.isValueAdded === false) return 'nonVa'
  return 'unknown'
}

/**
 * Kahn's topological sort over materialFlow edges, deterministic tie-break
 * by original array order (same technique vsm-auto-layout.ts's topoOrder
 * uses — reimplemented locally rather than imported, since lib/vsm-engine
 * must stay independent of the components/ layer, see README.md
 * "Architecture decision"). Chosen over a plain DFS specifically because a
 * DFS does NOT wait for every predecessor of a merge point (a node with two
 * incoming edges, e.g. a process fed by both a FIFO buffer AND a second
 * supermarket) before visiting it — it would eagerly descend whichever
 * incoming path it meets first, silently producing the WRONG chronological
 * order for everything reachable only via the predecessor it didn't take
 * yet. Kahn's algorithm only marks a node ready once ALL its (in-graph)
 * predecessors have fired, which is exactly what a sequential timeline
 * needs. `excludedIds` (the rework-loop appendages, already extracted by the
 * caller) are removed from the graph BEFORE sorting — this is what keeps
 * Kahn's algorithm itself deadlock-free on the EOL↔Nacharbeit back-edge
 * (unlike vsm-auto-layout.ts's own topoOrder, which has no such exclusion
 * and DOES deadlock on it, see module doc). Nodes the reduced graph still
 * cannot resolve (an unrelated, unanticipated cycle) are appended at the end
 * in original order — never dropped.
 */
function topoOrderExcluding(nodes: VsmNode[], connections: VsmConnection[], excludedIds: Set<string>): VsmNode[] {
  const ids = nodes.filter((n) => !excludedIds.has(n.id)).map((n) => n.id)
  const idSet = new Set(ids)
  const order = new Map(nodes.map((n, i) => [n.id, i]))
  const edges = connections.filter((c) => isMaterialFlow(c) && idSet.has(c.fromNodeId) && idSet.has(c.toNodeId) && c.fromNodeId !== c.toNodeId)

  const inDegree = new Map<string, number>(ids.map((id) => [id, 0]))
  const outgoing = new Map<string, string[]>(ids.map((id) => [id, []]))
  for (const e of edges) {
    inDegree.set(e.toNodeId, (inDegree.get(e.toNodeId) ?? 0) + 1)
    outgoing.get(e.fromNodeId)?.push(e.toNodeId)
  }

  const byOriginalOrder = (a: string, b: string) => (order.get(a) ?? 0) - (order.get(b) ?? 0)
  const ready = ids.filter((id) => (inDegree.get(id) ?? 0) === 0).sort(byOriginalOrder)
  const resultIds: string[] = []
  const visited = new Set<string>()

  while (ready.length > 0) {
    const id = ready.shift() as string
    if (visited.has(id)) continue
    visited.add(id)
    resultIds.push(id)
    const next = (outgoing.get(id) ?? []).slice().sort(byOriginalOrder)
    for (const n of next) {
      if (visited.has(n)) continue
      const remaining = (inDegree.get(n) ?? 0) - 1
      inDegree.set(n, remaining)
      if (remaining === 0) {
        const idx = ready.findIndex((r) => byOriginalOrder(r, n) > 0)
        if (idx === -1) ready.push(n)
        else ready.splice(idx, 0, n)
      }
    }
  }

  const leftover = ids.filter((id) => !visited.has(id)).sort(byOriginalOrder)
  const nodeById = new Map(nodes.map((n) => [n.id, n]))
  return [...resultIds, ...leftover].map((id) => nodeById.get(id)!)
}

const FORMULA =
  'Reihenfolge = Kahns topologische Sortierung über Materialfluss-Verbindungen (kind ≠ "information"), deterministischer Tie-Break nach Node-Array-Reihenfolge. Reine Nacharbeits-Schleifen-Nodes (genau 1 eingehende + 1 ausgehende Materialfluss-Verbindung, entweder beide zum selben Host-Node ODER ein paralleler Seitenpfad, den der Host auch direkt erreicht) werden VOR der Sortierung aus dem Graphen entfernt (zyklensicher) und separat als ReworkLoopAnnotation ausgewiesen, nicht als Sequenz-Block. Nodes ganz ohne Materialfluss-Verbindung (reine Informationsfluss-/Steuerungsknoten) werden ebenfalls nicht als Sequenz-Block gerechnet. Bestand → obere Ebene (Reichweite via Little\'s Law). Transport → obere Ebene (transportTimeSec). Prozess/Maschine/Zeitwert → untere Ebene (Zykluszeit, wertschöpfend oder nicht), zusätzlich eigener oberer Block bei gesetzter Wartezeit.'
const DATA_BASIS = 'VsmNode[] + VsmConnection[] (Materialfluss) + optionaler Kundenbedarf [Stk/Tag] + optionales ShiftModelInput (für Bestands-Reichweite).'

/**
 * A4 Timeline-Sequenz. Liefert die pro-Node geordneten Zeitblöcke für eine
 * klassische Zickzack-/Treppen-Zeitlinie (Kais §8) — die Summen/DLZ/PCE
 * bleiben ausschließlich bei `computeTimelineLadder` (nicht dupliziert hier;
 * siehe __tests__/timeline-sequence.test.ts für den Konsistenz-Beweis
 * zwischen beiden Funktionen).
 */
export function computeTimelineSequence(
  nodes: VsmNode[],
  connections: VsmConnection[],
  demandUnitsPerDay?: number,
  shiftModel?: ShiftModelInput,
): TimelineSequenceResult {
  const reworkLoopNodeIds = findReworkLoopNodeIds(nodes, connections)
  const orderedNodes = topoOrderExcluding(nodes, connections, new Set(reworkLoopNodeIds.keys()))

  const blocks: TimelineBlock[] = []
  for (const n of orderedNodes) {
    if (n.type === 'customer' || n.type === 'supplier') continue
    // Kais §5/§8: a node wired up EXCLUSIVELY via information-kind edges
    // (pure Informationsfluss-/Steuerungsknoten, e.g. a PPS-Element) carries
    // no physical material-flow step — it does not belong on a MATERIAL-flow
    // timeline (see module doc + isInformationOnlyNode's own doc comment).
    // A genuinely disconnected island (zero connections) is NOT this case —
    // it still belongs on the timeline, see isInformationOnlyNode's doc.
    if (isInformationOnlyNode(n.id, connections)) continue

    if (n.type === 'inventory') {
      const cov = computeInventoryCoverage(n.quantity, demandUnitsPerDay ? { unitsPerDay: demandUnitsPerDay } : undefined, shiftModel)
      blocks.push({
        nodeId: n.id,
        nodeName: n.name,
        nodeType: n.type,
        level: 'upper',
        category: 'inventory',
        seconds: cov.coverageHours != null ? cov.coverageHours * 3600 : null,
      })
      continue
    }

    if (n.type === 'transport') {
      blocks.push({
        nodeId: n.id,
        nodeName: n.name,
        nodeType: n.type,
        level: 'upper',
        category: 'transport',
        seconds: n.transportTimeSec ?? null,
      })
      continue
    }

    // process | machine | timevalue
    const vaState = classifyVaState(n)
    blocks.push({
      nodeId: n.id,
      nodeName: n.name,
      nodeType: n.type,
      level: 'lower',
      category: vaState === 'va' ? 'value-add' : vaState === 'unknown' ? 'unknown-process' : 'non-value-add-process',
      seconds: n.cycleTimeSec ?? null,
      setupTimeSec: isStationNode(n) ? n.setupTimeSec : undefined,
    })
    if (n.waitTimeSec != null && n.waitTimeSec > 0) {
      blocks.push({
        nodeId: n.id,
        nodeName: n.name,
        nodeType: n.type,
        level: 'upper',
        category: 'wait',
        seconds: n.waitTimeSec,
      })
    }
  }

  return {
    blocks,
    reworkLoops: [...reworkLoopNodeIds.values()],
    explain: {
      formula: FORMULA,
      dataBasis: DATA_BASIS,
      exclusions: [
        'Lineare Reihenfolge — Parallelpfade werden nicht taktkorrekt abgebildet (dieselbe Bestands-Limitation wie computeTimeline/computeTimelineLadder, Gap G6).',
        'Nacharbeits-/Reprozessierungs-Schleifen werden NICHT als Sequenz-Block gerechnet (sie treffen nicht 100% der Einheiten) — siehe reworkLoops.',
        'Nodes ohne jede Materialfluss-Verbindung (reine Informationsfluss-/Steuerungsknoten, z. B. ein PPS-Element) werden NICHT als Sequenz-Block gerechnet — sie betreffen keinen physischen Materialfluss-Schritt.',
        'Rüstzeit (setupTimeSec) ist eine reine Annotation je Block, nie Teil von `seconds`.',
        "P8.2a (Baustein 1): Nodes mit vaClass='unknown' (\"Ungeklärt\") erhalten die eigene Kategorie 'unknown-process' (neutrale Darstellung) statt 'non-value-add-process' — Ehrlichkeits-Doktrin, kein stilles Einsortieren einer ausdrücklich ungeklärten Klassifikation als NVA.",
      ],
    },
  }
}
