import crypto from 'node:crypto';

const DEFAULT_LIMITS = {
  maxTraceNodes: 2000,
  maxTraceDepth: 50,
  maxOwaEntries: 100
};

const LEVEL_ORDER = ['never', 'always', 'requires', 'when', 'unless', 'ordinary'];
const RULE_TYPE_RANK = { strict: 3, defeasible: 2, defeater: 1 };

export class ExplainSerializer {
  constructor(arbiter) {
    this.arbiter = arbiter;
  }

  serialize({ userKey, relation, objectKey, result, options = {}, startedAt, endedAt, config = null }) {
    const redaction = options.redaction || 'none';
    const limits = this._resolveLimits(options);
    const timestamp = new Date().toISOString();
    const includeMeta = options.includeMeta !== undefined ? options.includeMeta : true;
    const collectValues = options.collectValues !== undefined ? options.collectValues : true;

    const decision = this._buildDecision(result);
    const request = this._buildRequest({ userKey, relation, objectKey, options, config, redaction, includeMeta, collectValues });
    const timing = this._buildTiming(result, startedAt, endedAt);
    const trace = this._buildTrace({ userKey, relation, objectKey, result, redaction, limits, includeMeta, collectValues });
    const errors = this._buildErrors(result, trace.nodeIdByIndex, redaction);

    const audit = this._buildAudit({ userKey, relation, objectKey, options, trace: trace.trace, decision, result });

    const output = {
      explainVersion: '1.0',
      timestamp,
      request,
      decision,
      timing,
      trace: trace.trace,
      errors,
      limits: {
        maxTraceNodes: limits.maxTraceNodes,
        maxTraceDepth: limits.maxTraceDepth,
        maxOwaEntries: limits.maxOwaEntries,
        truncated: trace.truncated || (errors && errors.length > 0 && errors.some(err => err.truncated)) || false
      },
      ...(audit ? { audit } : {})
    };

    return output;
  }

  _resolveLimits(options) {
    return {
      maxTraceNodes: options.maxTraceNodes || DEFAULT_LIMITS.maxTraceNodes,
      maxTraceDepth: options.maxTraceDepth || DEFAULT_LIMITS.maxTraceDepth,
      maxOwaEntries: options.maxOwaEntries || DEFAULT_LIMITS.maxOwaEntries
    };
  }

  _buildRequest({ userKey, relation, objectKey, options, config, redaction, includeMeta, collectValues }) {
    const request = {
      userKey: this._redactKeyValue(userKey, redaction),
      relation,
      objectKey: this._redactKeyValue(objectKey, redaction),
      mode: this._resolveMode(options, config),
      threshold: this._resolveThreshold(options),
      options: {
        includeMeta: !!includeMeta,
        collectValues: !!collectValues,
        redaction
      }
    };

    // Temporal context: the explain rerun must reproduce the original
    // decision, so the temporal parameters (now, the TTL/proof clock) are
    // recorded here — the caller replays them verbatim on the rerun.
    if (options && options.now !== undefined && options.now !== null) {
      request.temporal = { now: options.now };
    }

    if (redaction === 'hash') {
      request.userKeyHash = this._hashKey(userKey);
      request.objectKeyHash = this._hashKey(objectKey);
    }

    return request;
  }

  _buildDecision(result) {
    const allowPossibility = result?.possibility || 0;
    const reliability = result?.reliability;
    const denyPossibility = result?.meta?.maxDeny || 0;
    const allowMeta = result?.meta?.allow || null;
    const denyMeta = result?.meta?.deny || null;
    const ruleMeta = allowMeta || denyMeta || {};
    const level = this._mapRuleTypeToLevel(ruleMeta?.ruleType || ruleMeta?.type || null);

    return {
      result: allowPossibility > 0,
      possibility: allowPossibility,
      ...(reliability !== undefined ? { reliability } : {}),
      // The validity block is the full debugging detail on the explain
      // (internal) surface — the public check result carries only the
      // minimal label unless includeMeta is set.
      ...(result?.validity ? { validity: result.validity } : {}),
      determinedBy: ruleMeta?.reason || result?.reason || null,
      level,
      ruleType: ruleMeta?.ruleType || ruleMeta?.type || null,
      ruleId: ruleMeta?.rule?.id || ruleMeta?.rule?.name || null,
      denyPossibility: denyPossibility || undefined
    };
  }

  _buildTiming(result, startedAt, endedAt) {
    const totalMs = startedAt && endedAt ? Math.max(0, endedAt - startedAt) : null;
    const levelTimings = result?.meta?.timingLevels || null;

    const timing = {
      totalMs: totalMs !== null ? totalMs : 0,
      levels: {
        never: levelTimings?.never || 0,
        always: levelTimings?.always || 0,
        requires: levelTimings?.requires || 0,
        when: levelTimings?.when || 0,
        unless: levelTimings?.unless || 0,
        ordinary: levelTimings?.ordinary || 0
      }
    };

    return timing;
  }

  _buildTrace({ userKey, relation, objectKey, result, redaction, limits, includeMeta, collectValues }) {
    const nodes = [];
    const nodeIdByIndex = new Map();
    const evaluation = result?.meta?.evaluation || null;
    const truncated = { value: false };

    if (evaluation?.rules && Array.isArray(evaluation.rules) && evaluation.rules.length > 0) {
      evaluation.rules.forEach((ruleEval, index) => {
        const nodeId = this._makeNodeId(index + 1);
        nodeIdByIndex.set(index, nodeId);
        const node = this._buildNodeFromRuleEval({
          nodeId,
          ruleEval,
          userKey,
          relation,
          objectKey,
          redaction,
          includeMeta
        });
        nodes.push(node);
      });
    } else {
      const node = this._buildNodeFromMeta({
        nodeId: this._makeNodeId(1),
        result,
        userKey,
        relation,
        objectKey,
        redaction,
        includeMeta
      });
      if (node) {
        nodes.push(node);
        nodeIdByIndex.set(0, node.nodeId);
      }
    }

    const filteredNodes = this._applyNodeLimits(nodes, limits.maxTraceNodes, truncated);
    const levels = this._buildLevels(filteredNodes);
    const path = filteredNodes.map(node => this._stripInternalFields(node));

    const rulePaths = this._buildRulePaths(filteredNodes, redaction, limits.maxTraceDepth, truncated);
    const owaEntries = this._buildOwaEntries(filteredNodes, limits.maxOwaEntries, truncated);
    const defeats = this._buildDefeats(result, filteredNodes);
    const values = collectValues ? this._buildValues(result, redaction, limits.maxTraceDepth, truncated) : [];

    return {
      trace: {
        levels,
        path,
        rulePaths,
        owa: owaEntries,
        defeats,
        values
      },
      truncated: truncated.value,
      nodeIdByIndex
    };
  }

  _buildNodeFromRuleEval({ nodeId, ruleEval, userKey, relation, objectKey, redaction, includeMeta }) {
    const rule = ruleEval?.rule || {};
    const result = ruleEval?.result || {};
    const meta = ruleEval?.meta || {};
    const allowMeta = meta.allow || null;
    const denyMeta = meta.deny || null;
    const allowPossibility = result.possibility_allow !== undefined ? result.possibility_allow : (result.possibility || 0);
    const ruleType = rule.ruleType || allowMeta?.ruleType || denyMeta?.ruleType || rule.type || null;

    const node = {
      nodeId,
      type: rule.type || rule.ruleType || 'unknown',
      relation: rule.relation || relation || null,
      userKey: this._redactKeyValue(userKey, redaction),
      objectKey: this._redactKeyValue(objectKey, redaction),
      result: allowPossibility > 0,
      possibility: allowPossibility,
      rulePriority: rule.priority ?? null,
      ruleType,
      aggregator: meta.full?.aggregator || null,
      __meta: allowMeta || denyMeta || null,
      __rawMeta: meta.full || meta || null
    };

    const source = allowMeta?.source || denyMeta?.source || ruleEval?.result?.source || null;
    if (source) {
      node.source = source;
    }
    if (allowMeta?.layer_name || denyMeta?.layer_name) {
      node.layer_name = allowMeta?.layer_name || denyMeta?.layer_name || null;
    }
    if (allowMeta?.source_class || denyMeta?.source_class) {
      node.source_class = allowMeta?.source_class || denyMeta?.source_class || null;
    }
    if (allowMeta?.reducer_applied || denyMeta?.reducer_applied) {
      node.reducer_applied = allowMeta?.reducer_applied || denyMeta?.reducer_applied || null;
    }

    const details = allowMeta?.details || denyMeta?.details || null;
    if (includeMeta && details) {
      node.details = details;
    }

    if (redaction === 'hash') {
      node.userKeyHash = this._hashKey(userKey);
      node.objectKeyHash = this._hashKey(objectKey);
    }

    if (includeMeta && allowMeta?.rule?.id) {
      node.ruleId = allowMeta.rule.id;
    }

    return node;
  }

  _buildNodeFromMeta({ nodeId, result, userKey, relation, objectKey, redaction, includeMeta }) {
    if (!result) return null;
    const allowMeta = result?.meta?.allow || result?.meta_allow || null;
    const denyMeta = result?.meta?.deny || result?.meta_deny || null;
    const ruleMeta = allowMeta || denyMeta || result?.meta || {};
    const ruleType = ruleMeta?.ruleType || ruleMeta?.type || null;
    const node = {
      nodeId,
      type: ruleMeta?.ruleType || ruleMeta?.type || 'unknown',
      relation: ruleMeta?.relation || relation || null,
      userKey: this._redactKeyValue(userKey, redaction),
      objectKey: this._redactKeyValue(objectKey, redaction),
      result: (result?.possibility || 0) > 0,
      possibility: result?.possibility || 0,
      rulePriority: ruleMeta?.rule?.priority ?? null,
      ruleType,
      aggregator: ruleMeta?.aggregator || null,
      __meta: ruleMeta || null
    };

    const source = ruleMeta?.source || allowMeta?.source || null;
    if (source) {
      node.source = source;
    }
    if (ruleMeta?.layer_name || allowMeta?.layer_name) {
      node.layer_name = ruleMeta?.layer_name || allowMeta?.layer_name || null;
    }
    if (ruleMeta?.source_class || allowMeta?.source_class) {
      node.source_class = ruleMeta?.source_class || allowMeta?.source_class || null;
    }
    if (ruleMeta?.reducer_applied || allowMeta?.reducer_applied) {
      node.reducer_applied = ruleMeta?.reducer_applied || allowMeta?.reducer_applied || null;
    }

    const details = allowMeta?.details || denyMeta?.details || null;
    if (includeMeta && details) {
      node.details = details;
    }

    if (redaction === 'hash') {
      node.userKeyHash = this._hashKey(userKey);
      node.objectKeyHash = this._hashKey(objectKey);
    }

    if (includeMeta && ruleMeta?.rule?.id) {
      node.ruleId = ruleMeta.rule.id;
    }

    return node;
  }

  _buildLevels(nodes) {
    const levels = {};
    for (const level of LEVEL_ORDER) {
      levels[level] = { nodes: [], aggregator: 'max' };
    }

    for (const node of nodes) {
      const level = this._mapRuleTypeToLevel(node.ruleType);
      levels[level].nodes.push(this._stripInternalFields(node));
      if (node.aggregator && levels[level].aggregator === 'max') {
        levels[level].aggregator = node.aggregator;
      }
    }

    for (const level of LEVEL_ORDER) {
      levels[level].nodes.sort((a, b) => {
        const possDiff = (b.possibility || 0) - (a.possibility || 0);
        if (possDiff !== 0) return possDiff;
        const prioDiff = (b.rulePriority || 0) - (a.rulePriority || 0);
        if (prioDiff !== 0) return prioDiff;
        const aRank = RULE_TYPE_RANK[a.ruleType] || 0;
        const bRank = RULE_TYPE_RANK[b.ruleType] || 0;
        if (bRank !== aRank) return bRank - aRank;
        return a.nodeId.localeCompare(b.nodeId);
      });
    }

    return levels;
  }

  _buildRulePaths(nodes, redaction, maxTraceDepth, truncatedFlag) {
    const rulePaths = [];
    for (const node of nodes) {
      const pathSteps = this._extractPathSteps(node);
      if (!pathSteps || pathSteps.length === 0) continue;
      let steps = pathSteps.map(step => this._redactPathStep(step, redaction));
      if (steps.length > maxTraceDepth) {
        steps = steps.slice(0, maxTraceDepth);
        truncatedFlag.value = true;
      }
      rulePaths.push({
        ruleType: node.type,
        relation: node.relation || null,
        pathSteps: steps
      });
    }
    return rulePaths;
  }

  _buildOwaEntries(nodes, maxOwaEntries, truncatedFlag) {
    const owaEntries = [];
    for (const node of nodes) {
      const meta = node.__meta || node.__rawMeta || {};
      if (meta.owa) {
        owaEntries.push(meta.owa);
      }
      if (meta.owaEntries && Array.isArray(meta.owaEntries)) {
        owaEntries.push(...meta.owaEntries);
      }
      if (meta.evaluation?.aggregation?.owa) {
        owaEntries.push(meta.evaluation.aggregation.owa);
      }
      if (meta.evaluation?.fusion?.owa) {
        owaEntries.push(meta.evaluation.fusion.owa);
      }
      if (meta.similarityRule?.owa) {
        owaEntries.push(meta.similarityRule.owa);
      }
      if (meta.parentRule?.owa) {
        owaEntries.push(meta.parentRule.owa);
      }
    }
    if (owaEntries.length > maxOwaEntries) {
      owaEntries.length = maxOwaEntries;
      truncatedFlag.value = true;
    }
    return owaEntries;
  }

  _buildDefeats(result, nodes) {
    const defeats = [];
    const metaDefeats = result?.meta?.defeats;
    if (Array.isArray(metaDefeats)) {
      for (const defeat of metaDefeats) {
        defeats.push({
          defeater: defeat.defeater,
          target: defeat.target,
          evidence: defeat.evidence || null,
          possibility: defeat.possibility || 0
        });
      }
    }
    if (defeats.length === 0 && nodes.length > 0 && result?.meta?.reason) {
      const reason = result.meta.reason;
      if (reason === 'defeated_by_unless') {
        defeats.push({ defeater: 'unless', target: 'when', evidence: null, possibility: 1.0 });
      }
      if (reason === 'requirements_not_met') {
        defeats.push({ defeater: 'requires', target: 'when', evidence: null, possibility: 1.0 });
      }
    }
    return defeats;
  }

  _buildValues(result, redaction, maxTraceDepth, truncatedFlag) {
    const collectedValues = result?.collectedValues || result?.meta?.collectedValues || [];
    if (!Array.isArray(collectedValues)) return [];

    const values = collectedValues.map(cv => {
      const path = Array.isArray(cv.path) ? cv.path.map(step => this._redactKeyValue(step, redaction)) : [];
      if (path.length > maxTraceDepth) {
        truncatedFlag.value = true;
      }
      const sourceEntityKey = cv.source?.entityKey ? this._redactKeyValue(cv.source.entityKey, redaction) : undefined;
      const valueEntry = {
        value: cv.value,
        possibility: cv.possibility,
        path: path.length > maxTraceDepth ? path.slice(0, maxTraceDepth) : path,
        source: {
          entityKey: sourceEntityKey,
          relation: cv.source?.relation || null,
          step: cv.source?.step ?? null
        },
        metadata: {
          ...this._sanitizeMetadata(cv.metadata || {}, redaction),
          source: cv.metadata?.source || cv.source?.source || cv.metadata?.origin || cv.source?.origin || cv.metadata?.source || cv.source?.source
        }
      };

      if (redaction === 'hash' && cv.source?.entityKey) {
        valueEntry.source.entityKeyHash = this._hashKey(cv.source.entityKey);
      }
      if (redaction === 'hash' && path.length > 0) {
        valueEntry.pathHash = path.map(step => this._hashKey(step));
      }

      return valueEntry;
    });

    return values;
  }

  _buildErrors(result, nodeIdByIndex, redaction = 'none') {
    const evaluation = result?.meta?.evaluation || null;
    const errors = evaluation?.errors || result?.meta?.errors || [];
    if (!Array.isArray(errors) || errors.length === 0) return [];

    return errors.map(err => {
      const nodeId = err.ruleIndex !== undefined && nodeIdByIndex?.has(err.ruleIndex)
        ? nodeIdByIndex.get(err.ruleIndex)
        : err.nodeId || null;
      return {
        level: err.level || null,
        nodeId,
        relation: err.relation || null,
        message: err.message || err.error || 'evaluation_error',
        ...(redaction === 'none' ? { stack: err.stack || null } : {})
      };
    });
  }

  _buildAudit({ userKey, relation, objectKey, options, trace, decision, result }) {
    const partialContext = options?.partialGraphContext || null;
    if (!partialContext || !this.arbiter?.nodeIdByKey || !this.arbiter?.relationManager) return null;

    const srcId = this.arbiter.nodeIdByKey.get(userKey);
    const dstId = this.arbiter.nodeIdByKey.get(objectKey);
    if (srcId === undefined || dstId === undefined) return null;

    const partialRel = partialContext.getDirectRelation(srcId, relation, dstId);
    const effectiveSource = trace?.path?.[0]?.source || null;
    const conflicts = [];
    const seenConflictKeys = {};

    if (partialRel) {
      const persistentRel = this.arbiter.relationManager.getDirectRelation(srcId, relation, dstId, null);
      if (persistentRel && this._isMaterialConflict(partialRel, persistentRel)) {
      this._pushProvenanceConflict({
        conflicts,
        seenConflictKeys,
        options,
        relation,
        srcKey: userKey,
        dstKey: objectKey,
        effectiveSource,
        partialRel,
        persistentRel,
        ruleType: result?.meta?.allow?.ruleType || null,
        decision
      });
      }
    }

    this._appendPathConflicts({
      conflicts,
      seenConflictKeys,
      options,
      trace,
      result,
      decision,
      partialContext
    });

    const usedFacts = this._collectUsedFacts({
      userKey,
      relation,
      objectKey,
      trace,
      partialContext,
      options
    });
    const policyConflicts = this._collectPolicyConflicts({ partialContext, options });

    return {
      provenance: {
        has_partial_inputs: true,
        partial_fact_used: effectiveSource === 'partial',
        effective_source: effectiveSource,
        provenance_conflicts: conflicts,
        used_facts: usedFacts,
        policy_conflicts: policyConflicts
      }
    };
  }

  _collectPolicyConflicts({ partialContext, options }) {
    const auditEntries = typeof partialContext?.getReducerAudit === 'function'
      ? (partialContext.getReducerAudit() || [])
      : [];
    const conflicts = [];
    for (let i = 0; i < auditEntries.length; i++) {
      const entry = auditEntries[i];
      if (!entry) continue;
      if (entry.type === 'invalid_reducer_config') {
        const invalid = Array.isArray(entry.invalid) ? entry.invalid : [];
        for (let j = 0; j < invalid.length; j++) {
          conflicts.push({
            kind: 'invalid_reducer_config',
            relation: invalid[j].relation || null,
            reducer: invalid[j].reducer || null,
            reason_code: 'reducer_not_allowed_for_relation',
            allowed_reducers: invalid[j].allowed || []
          });
        }
        continue;
      }
      if (entry.type === 'relation_conflict_absent') {
        conflicts.push({
          kind: 'reducer_conflict_absent',
          relation: entry.relation || null,
          src: this._redactKeyValue(entry.src || null, options.redaction || 'none'),
          object: this._redactKeyValue(entry.dst || null, options.redaction || 'none'),
          reducer: entry.reducer || null,
          reason_code: entry.audit?.reason_code || 'conflict_absent'
        });
      }
    }
    return conflicts;
  }

  _collectUsedFacts({ userKey, relation, objectKey, trace, partialContext, options }) {
    const entries = [];
    const seen = {};
    const append = (srcKey, rel, dstKey, preferredSource = null, ruleType = null) => {
      if (!srcKey || !rel || !dstKey) return;
      const key = `${srcKey}|${rel}|${dstKey}|${preferredSource || 'auto'}`;
      if (seen[key]) return;
      seen[key] = true;

      const srcId = this.arbiter.nodeIdByKey.get(srcKey);
      const dstId = this.arbiter.nodeIdByKey.get(dstKey);
      if (srcId === undefined || dstId === undefined) return;

      const chosen = this._resolveEffectiveFact({ srcId, relation: rel, dstId, preferredSource, partialContext });
      if (!chosen.fact) return;
      entries.push(this._toUsedFactEntry({ srcKey, relation: rel, dstKey, chosen, options, ruleType }));
    };

    // Resolve the config-level relation override (rule.relation) so the
    // direct fact is looked up under the relation it actually checks.
    const directConfig = this.arbiter.relationConfigs.get(relation);
    const effectiveRelation = (directConfig && (directConfig.relation || directConfig.rel)) || relation;
    append(userKey, effectiveRelation, objectKey, trace?.path?.[0]?.source || null, trace?.path?.[0]?.type || null);

    const rulePaths = Array.isArray(trace?.rulePaths) ? trace.rulePaths : [];
    for (let rpIndex = 0; rpIndex < rulePaths.length; rpIndex++) {
      const rulePath = rulePaths[rpIndex];
      const steps = Array.isArray(rulePath?.pathSteps) ? rulePath.pathSteps : [];
      if (steps.length < 2) continue;

      const relationConfig = this.arbiter.relationConfigs.get(rulePath.relation);
      const chainSteps = relationConfig?.type === 'chain' && Array.isArray(relationConfig.steps)
        ? relationConfig.steps
        : null;

      for (let i = 0; i < steps.length - 1; i++) {
        const edge = this._resolvePathEdge({
          current: steps[i],
          next: steps[i + 1],
          i,
          rulePath,
          chainSteps,
          relationConfig
        });
        if (!edge) continue;
        append(edge.srcKey, edge.relation, edge.dstKey, steps[i]?.source || null, rulePath.ruleType || null);
      }
    }

    return entries;
  }

  _resolveEffectiveFact({ srcId, relation, dstId, preferredSource, partialContext }) {
    const partialRel = partialContext ? partialContext.getDirectRelation(srcId, relation, dstId) : null;
    const persistentRel = this.arbiter.relationManager.getDirectRelation(srcId, relation, dstId, null);

    if (preferredSource === 'partial' && partialRel) {
      return { source: 'partial', fact: partialRel };
    }
    if (preferredSource === 'persistent' && persistentRel) {
      return { source: 'persistent', fact: persistentRel };
    }
    if (persistentRel) return { source: 'persistent', fact: persistentRel };
    if (partialRel) return { source: 'partial', fact: partialRel };
    return { source: null, fact: null };
  }

  _toUsedFactEntry({ srcKey, relation, dstKey, chosen, options, ruleType }) {
    const fact = chosen.fact;
    return {
      src: this._redactKeyValue(srcKey, options.redaction || 'none'),
      relation,
      object: this._redactKeyValue(dstKey, options.redaction || 'none'),
      source: chosen.source,
      layer_name: fact.layer_name || (chosen.source === 'persistent' ? 'persistent_graph' : 'partial'),
      source_class: fact.source_class || fact.layer_name || chosen.source,
      reducer_applied: fact.reducer_applied || null,
      rule_type: ruleType,
      used: true
    };
  }

  _appendPathConflicts({ conflicts, seenConflictKeys, options, trace, result, decision, partialContext }) {
    const rulePaths = Array.isArray(trace?.rulePaths) ? trace.rulePaths : [];
    for (let rpIndex = 0; rpIndex < rulePaths.length; rpIndex++) {
      const rulePath = rulePaths[rpIndex];
      const steps = Array.isArray(rulePath?.pathSteps) ? rulePath.pathSteps : [];
      if (steps.length < 2) continue;

      const relationConfig = this.arbiter.relationConfigs.get(rulePath.relation);
      const chainSteps = relationConfig?.type === 'chain' && Array.isArray(relationConfig.steps)
        ? relationConfig.steps
        : null;

      for (let i = 0; i < steps.length - 1; i++) {
        const current = steps[i];
        const next = steps[i + 1];
        const edge = this._resolvePathEdge({ current, next, i, rulePath, chainSteps, relationConfig });
        if (!edge?.relation || !edge.srcKey || !edge.dstKey) continue;

        const srcId = this.arbiter.nodeIdByKey.get(edge.srcKey);
        const dstId = this.arbiter.nodeIdByKey.get(edge.dstKey);
        if (srcId === undefined || dstId === undefined) continue;

        const partialRel = partialContext.getDirectRelation(srcId, edge.relation, dstId);
        if (!partialRel) continue;
        const persistentRel = this.arbiter.relationManager.getDirectRelation(srcId, edge.relation, dstId, null);
        if (!persistentRel) continue;
        if (!this._isMaterialConflict(partialRel, persistentRel)) continue;

        this._pushProvenanceConflict({
          conflicts,
          seenConflictKeys,
          options,
          relation: edge.relation,
          srcKey: edge.srcKey,
          dstKey: edge.dstKey,
          effectiveSource: current.source || trace?.path?.[0]?.source || 'persistent',
          partialRel,
          persistentRel,
          ruleType: rulePath.ruleType || relationConfig?.type || null,
          decision
        });
      }
    }
  }

  _resolvePathEdge({ current, next, i, rulePath, chainSteps, relationConfig }) {
    if (!current || !next) return null;
    const srcKey = current.key || null;
    const dstKey = next.key || null;
    if (!srcKey || !dstKey) return null;

    if (current.relation) {
      return { relation: current.relation, srcKey, dstKey };
    }

    if (chainSteps && chainSteps[i]) {
      const step = chainSteps[i];
      if (!step?.relation) return null;
      if (step.direction === 'in') {
        return { relation: step.relation, srcKey: dstKey, dstKey: srcKey };
      }
      return { relation: step.relation, srcKey, dstKey };
    }

    if (relationConfig?.type === 'multi_hop' && relationConfig.relation) {
      return { relation: relationConfig.relation, srcKey, dstKey };
    }

    if (rulePath?.ruleType === 'multi_hop' && rulePath?.relation) {
      return { relation: rulePath.relation, srcKey, dstKey };
    }

    return null;
  }

  _pushProvenanceConflict({
    conflicts,
    seenConflictKeys,
    options,
    relation,
    srcKey,
    dstKey,
    effectiveSource,
    partialRel,
    persistentRel,
    ruleType,
    decision
  }) {
    const dedupeKey = `${srcKey}|${relation}|${dstKey}`;
    if (seenConflictKeys[dedupeKey]) return;
    seenConflictKeys[dedupeKey] = true;

    const winner = effectiveSource || 'persistent';
    conflicts.push({
      relation,
      src: this._redactKeyValue(srcKey, options.redaction || 'none'),
      object: this._redactKeyValue(dstKey, options.redaction || 'none'),
      winner_source: winner,
      loser_source: winner === 'partial' ? 'persistent' : 'partial',
      resolution: 'trust_precedence',
      reason_code: 'higher_trust_source_preferred',
      partial_possibility: partialRel.possibility !== undefined ? partialRel.possibility : 1,
      persistent_possibility: persistentRel.possibility !== undefined ? persistentRel.possibility : 1,
      decision_possibility: decision?.possibility ?? null,
      rule_type: ruleType,
      partial_layer_name: partialRel.layer_name || 'partial',
      partial_source_class: partialRel.source_class || partialRel.layer_name || 'partial',
      partial_reducer_applied: partialRel.reducer_applied || null
    });
  }

  _isMaterialConflict(partialRel, persistentRel) {
    if (!partialRel || !persistentRel) return false;
    const partialPossibility = partialRel.possibility !== undefined ? partialRel.possibility : 1;
    const persistentPossibility = persistentRel.possibility !== undefined ? persistentRel.possibility : 1;
    if (partialPossibility !== persistentPossibility) return true;

    const partialValue = partialRel.value;
    const persistentValue = persistentRel.value;
    if (partialValue !== persistentValue) return true;

    const partialReliability = partialRel.reliability !== undefined ? partialRel.reliability : 1;
    const persistentReliability = persistentRel.reliability !== undefined ? persistentRel.reliability : 1;
    if (partialReliability !== persistentReliability) return true;

    return JSON.stringify(partialRel.attributes || null) !== JSON.stringify(persistentRel.attributes || null);
  }

  _applyNodeLimits(nodes, maxTraceNodes, truncatedFlag) {
    if (nodes.length <= maxTraceNodes) return nodes;

    const ranked = nodes
      .map(node => ({ node }))
      .sort((a, b) => {
        const possDiff = (b.node.possibility || 0) - (a.node.possibility || 0);
        if (possDiff !== 0) return possDiff;
        const prioDiff = (b.node.rulePriority || 0) - (a.node.rulePriority || 0);
        if (prioDiff !== 0) return prioDiff;
        const aRank = RULE_TYPE_RANK[a.node.ruleType] || 0;
        const bRank = RULE_TYPE_RANK[b.node.ruleType] || 0;
        if (bRank !== aRank) return bRank - aRank;
        return a.node.nodeId.localeCompare(b.node.nodeId);
      })
      .slice(0, maxTraceNodes)
      .map(entry => entry.node);

    const keepIds = new Set(ranked.map(node => node.nodeId));
    const filtered = nodes.filter(node => keepIds.has(node.nodeId));
    truncatedFlag.value = true;
    return filtered;
  }

  _extractPathSteps(node) {
    const meta = node.__meta || null;
    const rawMeta = node.__rawMeta || null;
    const allow = rawMeta?.allow || null;
    const bestPath = rawMeta?.bestPath || meta?.bestPath || null;
    const evaluation = rawMeta?.evaluation || meta?.evaluation || null;
    const candidates = [
      node.__meta?.pathSteps,
      node.__rawMeta?.pathSteps,
      allow?.pathSteps,
      meta?.pathSteps,
      meta?.bestPath?.pathSteps,
      meta?.bestPathEntities,
      evaluation?.bestPath?.pathSteps,
      evaluation?.aggregation?.selectedPath?.pathSteps
    ];
    for (const steps of candidates) {
      if (Array.isArray(steps) && steps.length > 0) return steps;
    }
    return null;
  }

  _redactPathStep(step, redaction) {
    if (!step || typeof step !== 'object') return step;
    const key = step.key || step.nodeKey || null;
    const id = step.id !== undefined ? step.id : (step.nodeId !== undefined ? step.nodeId : null);
    const source = step.source || step.edge?.source || null;
    const result = {
      id: id,
      key: key ? this._redactKeyValue(key, redaction) : null,
      source: source
    };
    if (redaction === 'hash' && key) {
      result.keyHash = this._hashKey(key);
    }
    if (step.relation) {
      result.relation = step.relation;
    }
    return result;
  }

  _stripInternalFields(node) {
    const { __meta, __rawMeta, ...clean } = node;
    return clean;
  }

  _resolveMode(options, config) {
    if (options.mode) return options.mode;
    if (options.binary) return 'binary';
    if (options.fastPath) return 'threshold';
    if (config && config.mode) return config.mode;
    return 'normal';
  }

  _resolveThreshold(options) {
    if (options.threshold !== undefined) return options.threshold;
    if (options.minAllowPossibility !== undefined && options.minAllowPossibility !== null) return options.minAllowPossibility;
    return null;
  }

  _mapRuleTypeToLevel(ruleType) {
    if (!ruleType) return 'ordinary';
    switch (ruleType) {
      case 'never':
        return 'never';
      case 'always':
      case 'strict':
        return 'always';
      case 'requires':
        return 'requires';
      case 'defeater':
      case 'unless':
        return 'unless';
      case 'defeasible':
      case 'when':
        return 'when';
      case 'ordinary':
        return 'ordinary';
      default:
        return 'ordinary';
    }
  }

  _makeNodeId(index) {
    return `n-${String(index).padStart(3, '0')}`;
  }

  _redactKeyValue(key, redaction) {
    if (key === null || key === undefined) return key;
    if (redaction === 'none' || !redaction) return key;
    if (redaction === 'keys') return this._hashKey(key);
    if (redaction === 'hash') return key;
    return key;
  }

  _hashKey(value) {
    return crypto.createHash('sha256').update(String(value)).digest('hex');
  }

  _sanitizeMetadata(metadata, redaction) {
    if (!metadata || typeof metadata !== 'object') return metadata;
    if (Array.isArray(metadata)) {
      return metadata.map(item => this._sanitizeMetadata(item, redaction));
    }
    const sanitized = {};
    for (const [key, value] of Object.entries(metadata)) {
      if (Array.isArray(value) && value.every(item => typeof item === 'string')) {
        sanitized[key] = value.map(item => this._redactKeyValue(item, redaction));
      } else if (typeof value === 'string' && (key.toLowerCase().includes('key') || key.toLowerCase().includes('path'))) {
        sanitized[key] = this._redactKeyValue(value, redaction);
      } else if (typeof value === 'object' && value !== null) {
        sanitized[key] = this._sanitizeMetadata(value, redaction);
      } else {
        sanitized[key] = value;
      }
    }
    return sanitized;
  }
}
