/**
 * Reimport delta engine + selective adoption (QVS-P5, KAR-974).
 * Refs: reports/qaf-value-stream-spec-source.txt Spec 21 ("Reimport and
 * Synchronization"), gap-analysis G8, architecture.md §7 P5.
 *
 * Pure functions only — no DB, no I/O (ADR 024 "Modul-Layout": mirrors
 * mapper.ts's own pure/DB-wired split). `reimport.ts` is the DB-wired
 * counterpart: it loads the 3 inputs below from Supabase and persists
 * `applySyncAdoption`'s result.
 *
 * ── The 3-way comparison ─────────────────────────────────────────────────
 * `computeSyncDelta` compares three nodes[] snapshots that all ultimately
 * trace back to the same value stream:
 *   - `sourceNodes`  — freshly re-mapped from the CURRENT `qaf_manufacturing_step`
 *     rows of the chosen source qaf_file (P1 mapper, called again, right now).
 *   - `snapshotNodes` — `value_stream_imports.import_snapshot.nodes`: the
 *     source state as of the LAST import or reimport (the reconciliation
 *     baseline — never the live document).
 *   - `liveNodes` — `value_stream_maps.nodes`: what is actually in the editor
 *     right now, possibly manually edited (fieldStatus 'modified', P3) or
 *     restructured (nodes added/removed) since the snapshot was taken.
 *
 * ── Step-matching identity (the hard part) ───────────────────────────────
 * snapshotNodes ↔ liveNodes matching is trivial and exact: both arrays
 * originate from the SAME mapper run (a live node is never re-assigned a new
 * `id` by the editor — `vsm-editor.tsx` `updateNode`/`deleteNode` operate by
 * id, never regenerate one), so the snapshot node's own `id` IS the live
 * node's `id`, if it still exists.
 *
 * snapshotNodes ↔ sourceNodes matching is the genuinely hard problem: these
 * two arrays can come from two DIFFERENT `qaf_file` rows (Spec 21: "A newer
 * QAF revision is uploaded" / "The source QAF is replaced") — `qaf_file`
 * rows are immutable after ingest (no code path updates
 * `qaf_manufacturing_step.raw_values` in place), so a genuine source change
 * always means a different `qaf_file_id`, whose `row_index` numbering has no
 * defined correspondence to the old one (rows may have been added/removed/
 * reordered anywhere upstream of a given step). `rowIndex` is therefore NOT a
 * safe cross-file matching key. The only remaining signal is `name`
 * (`prozessbezeichnung`, trimmed, case-sensitive — case-folding risks
 * silently conflating two genuinely different process descriptions):
 *
 *   - A name unique to one side only → a clean NEW_IN_SOURCE / REMOVED_FROM_SOURCE
 *     (no ambiguity: that name never existed / no longer exists at all).
 *   - A name with the SAME count (>1) on both sides → paired by ascending
 *     `rowIndex` WITHIN that name-group, each side sorted independently
 *     (Spec 7: source order is meaningful evidence; count-matched repeats of
 *     an identical name have no other distinguishing signal, so preserved
 *     relative order is the least-guessing tie-break available).
 *   - A name present on BOTH sides but with MISMATCHED counts (e.g. 1 vs 2) →
 *     every node in that name-group on BOTH sides becomes `unmatched`, never
 *     forced into NEW_IN_SOURCE/REMOVED_FROM_SOURCE and never paired by
 *     guessing which one corresponds to which (task instruction: "bei
 *     Ambiguität ehrlich 'unmatched' statt raten").
 *
 * Known, documented limitation (not a bug): a step that was RENAMED in the
 * new source (Spec 21 lists "Process names change" as in-scope) has no
 * stable identity to be recognized by under this scheme — QAF carries no
 * process UUID — so it surfaces as REMOVED_FROM_SOURCE (old name) +
 * NEW_IN_SOURCE (new name) rather than a single step's SOURCE_CHANGED
 * `name` field. A position-proximity fallback ("the one leftover old row and
 * the one leftover new row must be the same step, renamed") was deliberately
 * NOT implemented — that would be exactly the kind of identity guess the
 * task instruction rules out; a wrong guess would misattribute one step's
 * cost/time data onto an unrelated step, which is worse than the honest
 * remove+add split.
 *
 * ── Field-level status ───────────────────────────────────────────────────
 * For a matched (source, snapshot, live) triple, every `VsmNodeEditableFieldKey`
 * (vsm-field-status.ts `EDITABLE_FIELD_KEYS` — the same 14 fields the P3
 * editor already tracks fieldStatus for) gets:
 *   sourceChanged = hasFieldValueChanged(field, snapshotValue, sourceValue)
 *   localChanged  = hasFieldValueChanged(field, snapshotValue, liveValue)
 *                   || liveNode.fieldStatus?.[field] === 'modified'
 * localChanged deliberately ORs in the fieldStatus flag on top of the raw
 * value diff — erring toward over-reporting a local change rather than ever
 * silently treating a flagged edit as clean (Leitplanke: "LOCAL_CHANGED wird
 * NIE still überschrieben"). `name`'s `sourceChanged` is always `false` for
 * any MATCHED pair by construction (matching itself required equal names) —
 * expected, not a bug; `name` stays in the field loop purely so a LOCAL
 * rename is still protected like any other local edit.
 *
 * Step status is the rollup of its field statuses (BOTH_CHANGED if any field
 * is BOTH_CHANGED, OR if some fields are SOURCE_CHANGED while OTHER fields
 * are LOCAL_CHANGED — a step with changes on both axes needs the user's
 * attention regardless of whether it is the same field). A `locallyDeleted`
 * step (snapshot node with no live counterpart) has no local axis to roll up
 * at all — its status is SOURCE_CHANGED/UNCHANGED purely from the
 * source-vs-snapshot comparison, flagged separately via `locallyDeleted`.
 */

import type { QAFFieldKey } from '@/lib/qaf-parser'
import type { VaClass, VsmConnection, VsmNode, VsmNodeEditableFieldKey } from '@/lib/vsm-types'
import { NODE_W } from '@/components/wertstrom/vsm-geometry'
import { EDITABLE_FIELD_KEYS, deriveIsValueAddedFromVaClass, hasFieldValueChanged } from '@/components/wertstrom/vsm-field-status'
import { PRIMARY_FIELD_MAPPINGS } from './mapper'
import type {
  QvsFieldDelta,
  QvsFieldSyncStatus,
  QvsStepDelta,
  QvsSyncAdoptionPlan,
  QvsSyncApplyResult,
  QvsSyncApplySummary,
  QvsSyncDelta,
  QvsSyncStatus,
  QvsSyncSummary,
  QvsUnmatchedStep,
} from './types'

export interface ComputeSyncDeltaInput {
  /** Freshly re-mapped from the CURRENT qaf_manufacturing_step rows of the
   * chosen source qaf_file — every node MUST carry `qafSource` (guaranteed
   * by mapQafRowsToVsmNodes; a node this function receives without one is
   * ignored, defensively, rather than crashing). */
  sourceNodes: readonly VsmNode[]
  /** `value_stream_imports.import_snapshot.nodes`, as persisted. */
  snapshotNodes: readonly VsmNode[]
  /** `value_stream_maps.nodes`, as currently live — including any manually
   * created nodes without `qafSource` (ignored here; out of sync scope). */
  liveNodes: readonly VsmNode[]
}

function nodeName(node: VsmNode): string {
  return node.name.trim()
}

function groupByName(nodes: readonly VsmNode[]): Map<string, VsmNode[]> {
  const groups = new Map<string, VsmNode[]>()
  for (const node of nodes) {
    if (!node.qafSource) continue
    const key = nodeName(node)
    const group = groups.get(key)
    if (group) group.push(node)
    else groups.set(key, [node])
  }
  return groups
}

function byRowIndexAsc(a: VsmNode, b: VsmNode): number {
  return (a.qafSource?.rowIndex ?? 0) - (b.qafSource?.rowIndex ?? 0)
}

function deriveFieldStatus(sourceChanged: boolean, localChanged: boolean): QvsFieldSyncStatus {
  if (sourceChanged && localChanged) return 'BOTH_CHANGED'
  if (sourceChanged) return 'SOURCE_CHANGED'
  if (localChanged) return 'LOCAL_CHANGED'
  return 'UNCHANGED'
}

function computeFieldDeltas(sourceNode: VsmNode, snapshotNode: VsmNode, liveNode: VsmNode | null): QvsFieldDelta[] {
  const deltas: QvsFieldDelta[] = []
  for (const field of EDITABLE_FIELD_KEYS) {
    const snapshotValue = snapshotNode[field]
    const sourceValue = sourceNode[field]
    const liveValue = liveNode ? liveNode[field] : undefined
    const sourceChanged = hasFieldValueChanged(field, snapshotValue, sourceValue)
    const localChanged = liveNode
      ? hasFieldValueChanged(field, snapshotValue, liveValue) || liveNode.fieldStatus?.[field] === 'modified'
      : false
    deltas.push({
      field,
      status: deriveFieldStatus(sourceChanged, localChanged),
      snapshotValue,
      sourceValue,
      liveValue,
    })
  }
  return deltas
}

function rollupStepStatus(fields: readonly QvsFieldDelta[], locallyDeleted: boolean): QvsSyncStatus {
  if (locallyDeleted) {
    const anySourceChange = fields.some((f) => f.status === 'SOURCE_CHANGED' || f.status === 'BOTH_CHANGED')
    return anySourceChange ? 'SOURCE_CHANGED' : 'UNCHANGED'
  }
  const anySource = fields.some((f) => f.status === 'SOURCE_CHANGED' || f.status === 'BOTH_CHANGED')
  const anyLocal = fields.some((f) => f.status === 'LOCAL_CHANGED' || f.status === 'BOTH_CHANGED')
  if (anySource && anyLocal) return 'BOTH_CHANGED'
  if (anySource) return 'SOURCE_CHANGED'
  if (anyLocal) return 'LOCAL_CHANGED'
  return 'UNCHANGED'
}

function emptySummary(): QvsSyncSummary {
  return { unchanged: 0, sourceChanged: 0, localChanged: 0, bothChanged: 0, newInSource: 0, removedFromSource: 0, unmatched: 0 }
}

const SUMMARY_KEY_BY_STATUS: Record<QvsSyncStatus, keyof QvsSyncSummary> = {
  UNCHANGED: 'unchanged',
  SOURCE_CHANGED: 'sourceChanged',
  LOCAL_CHANGED: 'localChanged',
  BOTH_CHANGED: 'bothChanged',
  NEW_IN_SOURCE: 'newInSource',
  REMOVED_FROM_SOURCE: 'removedFromSource',
}

/**
 * Compares a freshly re-mapped QAF source against the value stream's last
 * sync snapshot and its current live nodes — see module header for the full
 * matching/rollup rationale. Deterministic and side-effect-free: the SAME
 * three inputs always produce the SAME `QvsSyncDelta` (real-corpus anchor
 * test: a file diffed against itself is 100% UNCHANGED).
 */
export function computeSyncDelta(input: ComputeSyncDeltaInput): QvsSyncDelta {
  const liveById = new Map<string, VsmNode>()
  for (const node of input.liveNodes) {
    if (node.qafSource) liveById.set(node.id, node)
  }

  const snapshotGroups = groupByName(input.snapshotNodes)
  const sourceGroups = groupByName(input.sourceNodes)
  const allNames = new Set<string>([...snapshotGroups.keys(), ...sourceGroups.keys()])

  const steps: QvsStepDelta[] = []
  const unmatched: QvsUnmatchedStep[] = []

  for (const name of allNames) {
    const snapGroup = snapshotGroups.get(name) ?? []
    const srcGroup = sourceGroups.get(name) ?? []

    if (snapGroup.length === 0) {
      // Clean NEW_IN_SOURCE: this name never existed in the snapshot.
      for (const sourceNode of srcGroup) {
        steps.push({
          stepMatchId: `source:${sourceNode.qafSource!.rowIndex}`,
          status: 'NEW_IN_SOURCE',
          name,
          locallyDeleted: false,
          sourceNode,
          snapshotNode: null,
          liveNode: null,
          fields: [],
        })
      }
      continue
    }

    if (srcGroup.length === 0) {
      // Clean REMOVED_FROM_SOURCE: this name no longer exists anywhere in the new source.
      for (const snapshotNode of snapGroup) {
        const liveNode = liveById.get(snapshotNode.id) ?? null
        steps.push({
          stepMatchId: snapshotNode.id,
          status: 'REMOVED_FROM_SOURCE',
          name,
          locallyDeleted: liveNode === null,
          sourceNode: null,
          snapshotNode,
          liveNode,
          fields: [],
        })
      }
      continue
    }

    if (snapGroup.length === srcGroup.length) {
      // Count-matched — pair by preserved relative order within the group.
      const snapSorted = [...snapGroup].sort(byRowIndexAsc)
      const srcSorted = [...srcGroup].sort(byRowIndexAsc)
      for (let i = 0; i < snapSorted.length; i++) {
        const snapshotNode = snapSorted[i]
        const sourceNode = srcSorted[i]
        const liveNode = liveById.get(snapshotNode.id) ?? null
        const locallyDeleted = liveNode === null
        const fields = computeFieldDeltas(sourceNode, snapshotNode, liveNode)
        steps.push({
          stepMatchId: snapshotNode.id,
          status: rollupStepStatus(fields, locallyDeleted),
          name,
          locallyDeleted,
          sourceNode,
          snapshotNode,
          liveNode,
          fields,
        })
      }
      continue
    }

    // Mismatched counts (>1 on at least one side) — cannot tell which
    // corresponds to which without guessing. Honestly unmatched, both sides.
    for (const snapshotNode of snapGroup) {
      unmatched.push({ side: 'snapshot', rowIndex: snapshotNode.qafSource!.rowIndex, name, reason: 'ambiguous_name_count_mismatch' })
    }
    for (const sourceNode of srcGroup) {
      unmatched.push({ side: 'source', rowIndex: sourceNode.qafSource!.rowIndex, name, reason: 'ambiguous_name_count_mismatch' })
    }
  }

  // Deterministic order: source order first (current authoritative sequence),
  // REMOVED_FROM_SOURCE steps (no sourceNode) after, ordered by snapshot rowIndex.
  steps.sort((a, b) => {
    const aKey = a.sourceNode?.qafSource?.rowIndex
    const bKey = b.sourceNode?.qafSource?.rowIndex
    if (aKey !== undefined && bKey !== undefined) return aKey - bKey
    if (aKey !== undefined) return -1
    if (bKey !== undefined) return 1
    return (a.snapshotNode?.qafSource?.rowIndex ?? 0) - (b.snapshotNode?.qafSource?.rowIndex ?? 0)
  })
  unmatched.sort((a, b) => a.rowIndex - b.rowIndex)

  const summary = emptySummary()
  for (const step of steps) summary[SUMMARY_KEY_BY_STATUS[step.status]]++
  summary.unmatched = unmatched.length

  return { steps, unmatched, summary }
}

// ── Selective adoption ──────────────────────────────────────────────────────

function fieldSelected(plan: QvsSyncAdoptionPlan, stepMatchId: string, field: VsmNodeEditableFieldKey): boolean {
  if (plan.mode === 'all') return true
  if (plan.mode === 'none') return false
  return (plan.selectedFieldAdoptions ?? []).some((s) => s.stepMatchId === stepMatchId && s.field === field)
}

function newStepSelected(plan: QvsSyncAdoptionPlan, stepMatchId: string): boolean {
  if (plan.mode === 'all') return true
  if (plan.mode === 'none') return false
  return (plan.selectedNewSteps ?? []).some((s) => s.stepMatchId === stepMatchId)
}

function conflictResolutionFor(plan: QvsSyncAdoptionPlan, stepMatchId: string, field: VsmNodeEditableFieldKey): 'keep_local' | 'take_source' | undefined {
  return (plan.conflictResolutions ?? []).find((r) => r.stepMatchId === stepMatchId && r.field === field)?.resolution
}

function removedStepAction(plan: QvsSyncAdoptionPlan, stepMatchId: string): 'keep' | 'remove' {
  return (plan.removedStepDecisions ?? []).find((d) => d.stepMatchId === stepMatchId)?.action ?? 'keep'
}

/** Reverse of mapper.ts's `FIELD_MAPPED_TO` — the QAF source field key a
 * given `VsmNodeEditableFieldKey` was extracted from, so an adopted field can
 * refresh its OWN `qafSource.fields[key]` provenance sub-object (cell/
 * original/unit/confidence) to the NEW source's, not just the plain node
 * value. `vaClass` has no entry (derived from `name`, not itself an extracted
 * QAF field — see va-classification.ts) and is intentionally absent, not a
 * gap. Built from the already-exported `PRIMARY_FIELD_MAPPINGS` (mapper.ts) —
 * no new export needed there. */
const TARGET_TO_QAF_FIELD: Partial<Record<VsmNodeEditableFieldKey, QAFFieldKey>> = {
  name: 'prozessbezeichnung',
  ...Object.fromEntries(PRIMARY_FIELD_MAPPINGS.map((m) => [m.target, m.source] as const)),
}

/**
 * Applies one adopted field value onto a live node clone — 'vaClass' also
 * re-derives `isValueAdded` (architecture.md §3.1 compatibility rule: it is
 * ALWAYS derived from vaClass wherever vaClass is set, same as the editor's
 * own `updateVaClass`), and `fieldStatus[field]` resets to 'imported' (the
 * field is fresh-from-source again, no longer a pending local deviation).
 *
 * Also refreshes THIS FIELD's own `qafSource.fields[qafKey]` provenance
 * sub-object (cell/original/unit/confidence) from `sourceNode` — without
 * this, "Quelle anzeigen" would keep showing the OLD cell/value/confidence
 * for a field the user just explicitly told the system to take from the NEW
 * source, actively misleading. Deliberately narrower than a full lineage
 * rewrite: the NODE-level `qafSource.importId`/`sheet`/`rowIndex`/
 * `positionNumber`/`manufacturingStepId` (i.e. "which import first created
 * this step") are left untouched — only the specific adopted field's own
 * provenance entry moves forward. A field with no QAF source key (`vaClass`,
 * derived) or whose new source genuinely has no captured entry for it (e.g.
 * `below_confidence`/absent in the new file) leaves `qafSource.fields`
 * untouched for that key — never fabricates one. */
function applyFieldValue(node: VsmNode, field: VsmNodeEditableFieldKey, value: unknown, sourceNode: VsmNode | null): void {
  ;(node as unknown as Record<VsmNodeEditableFieldKey, unknown>)[field] = value
  if (field === 'vaClass') node.isValueAdded = deriveIsValueAddedFromVaClass(value as VaClass)
  node.fieldStatus = { ...node.fieldStatus, [field]: 'imported' }

  const qafKey = TARGET_TO_QAF_FIELD[field]
  const freshProvenance = qafKey ? sourceNode?.qafSource?.fields?.[qafKey] : undefined
  if (qafKey && freshProvenance && node.qafSource) {
    node.qafSource = { ...node.qafSource, fields: { ...node.qafSource.fields, [qafKey]: freshProvenance } }
  }
}

function linearConnections(nodes: readonly VsmNode[]): VsmConnection[] {
  const connections: VsmConnection[] = []
  for (let i = 0; i < nodes.length - 1; i++) {
    connections.push({ id: crypto.randomUUID(), fromNodeId: nodes[i].id, toNodeId: nodes[i + 1].id })
  }
  return connections
}

/**
 * Applies a `QvsSyncAdoptionPlan` to the current live nodes/connections.
 * Pure and side-effect-free (clones every node it touches). `importId` is
 * stamped onto any newly-adopted NEW_IN_SOURCE node's `qafSource.importId`
 * (reimport.ts passes the value stream's own `value_stream_imports.id` — the
 * SAME import record is being updated, not a new one created, so every node
 * keeps a consistent lineage FK).
 *
 * Connection handling is deliberately conservative — this function NEVER
 * rebuilds the full connection graph from scratch (unlike creation.ts's
 * `relinkConnections`, which is safe only because a freshly-created value
 * stream has no user-authored connections yet to destroy):
 *   - A removed node's connections are dropped (both directions) — exactly
 *     `vsm-editor.tsx`'s own `deleteNode` behaviour (no auto-bridging of the
 *     gap; the user reconnects manually if desired), so reimport-driven
 *     removal reads identically to a manual deletion.
 *   - Newly adopted steps are APPENDED after the existing chain and linked
 *     only among themselves (+ one edge from the last existing node to the
 *     first new one) — every pre-existing connection is left byte-identical,
 *     including any non-linear/manual rewiring the user made.
 */
export function applySyncAdoption(delta: QvsSyncDelta, liveNodes: readonly VsmNode[], connections: readonly VsmConnection[], plan: QvsSyncAdoptionPlan, importId: string): QvsSyncApplyResult {
  const summary: QvsSyncApplySummary = {
    fieldsAdopted: 0,
    stepsAdded: 0,
    stepsRemoved: 0,
    conflictsResolvedTakeSource: 0,
    conflictsResolvedKeepLocal: 0,
    fieldsKeptLocal: 0,
  }

  const nodeById = new Map<string, VsmNode>(liveNodes.map((n) => [n.id, { ...n }]))
  const removedIds = new Set<string>()

  for (const step of delta.steps) {
    if (step.status === 'REMOVED_FROM_SOURCE') {
      if (step.liveNode && removedStepAction(plan, step.stepMatchId) === 'remove') {
        removedIds.add(step.liveNode.id)
        summary.stepsRemoved++
      }
      continue
    }

    if (step.status === 'NEW_IN_SOURCE') continue // handled after this loop (needs to append, not patch)

    if (!step.liveNode) continue // locallyDeleted matched step — nothing to write into.
    const liveClone = nodeById.get(step.liveNode.id)
    if (!liveClone) continue

    for (const fieldDelta of step.fields) {
      if (fieldDelta.status === 'SOURCE_CHANGED') {
        if (fieldSelected(plan, step.stepMatchId, fieldDelta.field)) {
          applyFieldValue(liveClone, fieldDelta.field, fieldDelta.sourceValue, step.sourceNode)
          summary.fieldsAdopted++
        }
      } else if (fieldDelta.status === 'BOTH_CHANGED') {
        const resolution = conflictResolutionFor(plan, step.stepMatchId, fieldDelta.field)
        if (resolution === 'take_source') {
          applyFieldValue(liveClone, fieldDelta.field, fieldDelta.sourceValue, step.sourceNode)
          summary.conflictsResolvedTakeSource++
        } else if (resolution === 'keep_local') {
          summary.conflictsResolvedKeepLocal++
        }
        // No resolution at all: untouched, exactly like LOCAL_CHANGED below —
        // BOTH_CHANGED is NEVER silently moved either way (Leitplanke).
      } else if (fieldDelta.status === 'LOCAL_CHANGED') {
        summary.fieldsKeptLocal++
      }
    }
  }

  const keptExistingNodes = liveNodes.filter((n) => !removedIds.has(n.id)).map((n) => nodeById.get(n.id) ?? n)
  const keptConnections = connections.filter((c) => !removedIds.has(c.fromNodeId) && !removedIds.has(c.toNodeId))

  const newNodes: VsmNode[] = []
  const newSteps = delta.steps.filter((s) => s.status === 'NEW_IN_SOURCE' && newStepSelected(plan, s.stepMatchId))
  const maxX = keptExistingNodes.reduce((m, n) => Math.max(m, n.x + NODE_W), 100)
  newSteps
    .slice()
    .sort((a, b) => (a.sourceNode?.qafSource?.rowIndex ?? 0) - (b.sourceNode?.qafSource?.rowIndex ?? 0))
    .forEach((step, idx) => {
      const sourceNode = step.sourceNode!
      newNodes.push({
        ...sourceNode,
        x: maxX + 80 + idx * (NODE_W + 40),
        qafSource: { ...sourceNode.qafSource!, importId },
      })
    })
  summary.stepsAdded = newNodes.length

  const finalNodes = [...keptExistingNodes, ...newNodes]
  const bridgeConnections: VsmConnection[] =
    keptExistingNodes.length > 0 && newNodes.length > 0
      ? [{ id: crypto.randomUUID(), fromNodeId: keptExistingNodes[keptExistingNodes.length - 1].id, toNodeId: newNodes[0].id }]
      : []
  const finalConnections = [...keptConnections, ...bridgeConnections, ...linearConnections(newNodes)]

  return { nodes: finalNodes, connections: finalConnections, summary }
}
