// V2-Envelope des Ergebnis-JSON (Spezifikation Kap. 6, 13, 20).
//
// Das V1-Format war ein Deckel über einer Sektionsliste: `{format, version, meta,
// sections}`. Wer es konsumierte — Oberfläche, Export, später eine Sprachschicht —
// musste alles Übrige wissen: in welche Richtung ein Delta zeigt, ob Prozent ein
// Anteil oder eine Zahl ist, in welcher Einheit ein Betrag steht, ob eine leere
// Sektion nichts gefunden hat oder nie gerechnet wurde. Nichts davon stand im
// Dokument.
//
// Der V2-Envelope beantwortet diese Fragen im Dokument selbst:
//
//   engine          womit erzeugt (Version, Config, Registry)
//   conventions     Delta-Richtung, Toleranzen, Einheiten, Rundung, null-Semantik
//   comparisonType  Zeitvergleich? Objektvergleich? (R-23, Befund F-20)
//   sectionState    je Sektion: befüllt / leer / nicht anwendbar / nicht berechnet
//                   (R-24, Befund F-22)
//
// Pure Daten + pure Funktionen, keine I/O.

/** Engine-Version dieses Moduls; wandert in jedes Ergebnis-JSON. */
export const QAF_COMPARE_ENGINE = {
  name: 'qaf-compare-core',
  version: '2.0.0',
} as const

/**
 * Fachliche Art des Vergleichs (R-23).
 *
 * Befund F-20: V1 presste jeden Vergleich in dasselbe Schema, obwohl die
 * Interpretation grundverschieden ist. Beim zeitlichen Vergleich desselben
 * Projekts sind geänderte Prämissen ein **Störfaktor**, der jede Delta-Aussage
 * entwertet; beim Standort- oder Lieferantenvergleich sind sie der
 * **Analysegegenstand**. Wer das nicht weiß, liest dieselben Zahlen falsch.
 *
 * `other_declared` ist der ehrliche Ausweg, wenn die Identitätsfelder keine
 * Entscheidung hergeben — besser als eine geratene Einordnung.
 */
export type QafComparisonType =
  | 'temporal_same_project'
  | 'cross_site_same_part'
  | 'cross_supplier_same_part'
  | 'other_declared'

/** Woher die Einordnung stammt — abgeleitet oder mangels Merkmalen offen gelassen. */
export type ComparisonTypeBasis = 'derived_from_identity' | 'undetermined'

export interface ComparisonTypeVerdict {
  type: QafComparisonType
  basis: ComparisonTypeBasis
  /** Merkmale, auf denen die Einordnung beruht — nachvollziehbar statt behauptet. */
  evidence: string[]
}

/** Identitätsmerkmale beider Seiten, soweit sie vorliegen. */
export interface ComparisonIdentity {
  partNumberAward: string | null
  partNumberCurrent: string | null
  supplierAward: string | null
  supplierCurrent: string | null
  siteAward: string | null
  siteCurrent: string | null
}

function sameNonEmpty(a: string | null, b: string | null): boolean | null {
  const x = a?.trim()
  const y = b?.trim()
  if (!x || !y) return null // eine Seite fehlt: keine Aussage möglich
  return x.toLowerCase() === y.toLowerCase()
}

/**
 * Leitet den Vergleichstyp deterministisch aus den Identitätsfeldern ab (R-23).
 *
 * Bewusst konservativ: Fehlt ein Merkmal, wird nicht geraten. `undetermined`
 * ist eine Aussage („wir wissen es nicht"), eine erfundene Einordnung wäre
 * keine — und sie würde die nachgelagerte Interpretation in die Irre führen.
 */
export function deriveComparisonType(identity: ComparisonIdentity): ComparisonTypeVerdict {
  const samePart = sameNonEmpty(identity.partNumberAward, identity.partNumberCurrent)
  const sameSupplier = sameNonEmpty(identity.supplierAward, identity.supplierCurrent)
  const sameSite = sameNonEmpty(identity.siteAward, identity.siteCurrent)
  const evidence: string[] = []

  if (samePart === false) {
    evidence.push('Sachnummern unterscheiden sich')
    return { type: 'other_declared', basis: 'derived_from_identity', evidence }
  }
  if (samePart === null) {
    evidence.push('Sachnummer auf mindestens einer Seite unbekannt')
    return { type: 'other_declared', basis: 'undetermined', evidence }
  }
  evidence.push('gleiche Sachnummer')

  if (sameSupplier === false) {
    evidence.push('unterschiedliche Lieferanten')
    return { type: 'cross_supplier_same_part', basis: 'derived_from_identity', evidence }
  }
  if (sameSite === false) {
    evidence.push('gleicher Lieferant, unterschiedliche Standorte')
    return { type: 'cross_site_same_part', basis: 'derived_from_identity', evidence }
  }
  if (sameSupplier === null || sameSite === null) {
    evidence.push('Lieferant oder Standort auf mindestens einer Seite unbekannt')
    return { type: 'temporal_same_project', basis: 'undetermined', evidence }
  }

  evidence.push('gleicher Lieferant', 'gleicher Standort')
  return { type: 'temporal_same_project', basis: 'derived_from_identity', evidence }
}

/**
 * Zustand einer Sektion (R-24).
 *
 * Befund F-22: Eine leere Sektion war nicht interpretierbar — „keine Hebel
 * nötig", „Daten fehlen" und „für diesen Typ nicht implementiert" sahen
 * identisch aus. Ausgerechnet der Lauf mit +441 % Fertigungskosten hatte eine
 * leere Hebel-Liste.
 *
 * `not_computed` ist die einzige Ausprägung, die einen Mangel beschreibt — sie
 * darf im Regelbetrieb nicht vorkommen und erzeugt deshalb einen Hinweis.
 */
export type SectionStatus = 'populated' | 'empty_no_findings' | 'not_applicable' | 'not_computed'

export interface SectionState {
  status: SectionStatus
  reasonDe: string
}

const SECTION_REASON: Record<SectionStatus, string> = {
  populated: 'Enthält Ergebnisse.',
  empty_no_findings: 'Geprüft, nichts gefunden.',
  not_applicable: 'Für diesen Vergleich nicht anwendbar.',
  not_computed: 'Nicht berechnet.',
}

/**
 * Zustand aus dem Inhalt ableiten. `applicable: false` schlägt die Zählung —
 * eine Sektion, die für diesen Vergleichstyp gar nicht gilt, ist nicht „leer".
 */
export function sectionStateOf(
  content: unknown,
  opts: { applicable?: boolean; computed?: boolean } = {},
): SectionState {
  const { applicable = true, computed = true } = opts
  if (!computed) return { status: 'not_computed', reasonDe: SECTION_REASON.not_computed }
  if (!applicable) return { status: 'not_applicable', reasonDe: SECTION_REASON.not_applicable }

  const empty =
    content === null ||
    content === undefined ||
    (Array.isArray(content) && content.length === 0) ||
    (typeof content === 'object' && !Array.isArray(content) && Object.keys(content as object).length === 0)

  return empty
    ? { status: 'empty_no_findings', reasonDe: SECTION_REASON.empty_no_findings }
    : { status: 'populated', reasonDe: SECTION_REASON.populated }
}

/**
 * Die Konventionen, unter denen die Zahlen dieses Dokuments zu lesen sind
 * (Kap. 6.3, R-17/R-18). Stehen im JSON, nicht nur in einer Dokumentation —
 * ein Konsument soll nichts raten müssen, auch nicht die Delta-Richtung.
 */
export const QAF_CONVENTIONS = {
  deltaDefinition: 'current_minus_award',
  signSemantics: 'positive_means_more_expensive',
  tolerances: {
    bridgeEur: 0.0005,
    reconciliationEur: 0.005,
    sameFlagEur: 0.00005,
  },
  unitsVocabulary: ['EUR_per_piece', 'EUR', 'pct_decimal', 'seconds', 'pieces'] as const,
  rounding: {
    EUR_per_piece: 4,
    EUR: 2,
    pct_decimal: 6,
    seconds: 2,
  },
  nullSemantics: 'null = nicht vorhanden oder nicht berechenbar; 0 = ausdrücklicher Wert',
} as const

export type QafConventions = typeof QAF_CONVENTIONS
