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

  registerDependencyIndex(index) {
    if (!index) return;
    this.arbiter.dependencyIndex = index;
  }

  setRelationConfig(relation, config) {
    if (config === null || typeof config !== 'object') {
      throw new Error(
        `Invalid relation config for ${String(relation)}: expected a config object, got ${config === null ? 'null' : typeof config}`
      );
    }
    const normalized = this._normalizeOwaConfig(config);
    this.arbiter.relationConfigs.set(relation, normalized);
    this.arbiter.graphManager.setRelationConfig(relation, normalized);

    const compiledResult = this.arbiter.ruleCompiler.compile(relation, normalized);
    normalized._compiled = compiledResult.compiled;
    normalized._compileErrors = compiledResult.errors;
    normalized._compileWarnings = compiledResult.warnings;

    if (normalized && this.arbiter.authChecker && this.arbiter.authChecker.ruleEvaluator) {
      normalized._needsValues = this.arbiter.authChecker.ruleEvaluator._ruleRequiresValues(normalized, new Set());
    }
    
    // Update dependency index for rule result cache invalidation (programmatic path)
    this.arbiter._collectRelationUsages(normalized, this.arbiter.dependencyIndex);
    
    // The direct-check cache is keyed by the CHECKED relation name; a
    // redefinition changes that relation's semantics, so every cached
    // decision for it must be dropped (e.g. direct r1 -> direct r2 would
    // otherwise keep serving the r1 result until TTL expiry).
    this.arbiter._invalidateDirectCheckCache(null, relation, null);
    this.arbiter.invalidateRuleResultCacheByRelation(relation);
    if (this.arbiter.authChecker) {
      this.arbiter.authChecker.invalidateRuleCaches(relation);
    }
    
    // Track ChainRule dependencies (no materialization needed - PLTC handles reachability)
    if (normalized && normalized.type === 'chain' && this.arbiter.reachabilityChecker) {
      if (this.arbiter.reachabilityChecker.pltcIndex && this.arbiter.reachabilityChecker.pltcIndex.initialized) {
        this.arbiter.reachabilityChecker._trackChainRuleDependencies(relation, normalized);
      }
    }

    this._applyPartialGraphPolicyFromRelationConfig(relation, normalized);
  }

  setPartialGraphPolicy(policy = {}) {
    const current = this.arbiter.partialGraphPolicy || { conflict_mode: 'deterministic', reducers: {} };
    const next = policy && typeof policy === 'object' ? policy : {};

    const conflictMode = next.conflict_mode || next.conflictMode || current.conflict_mode || 'deterministic';
    const reducers = {
      ...(current.reducers || {}),
      ...((next.reducers && typeof next.reducers === 'object') ? next.reducers : {})
    };

    this.arbiter.partialGraphPolicy = {
      conflict_mode: conflictMode,
      reducers,
      maxNodes: next.maxNodes !== undefined ? next.maxNodes : current.maxNodes,
      maxRelations: next.maxRelations !== undefined ? next.maxRelations : current.maxRelations,
      reservedRelations: next.reservedRelations !== undefined ? next.reservedRelations : current.reservedRelations
    };
    return this.arbiter.partialGraphPolicy;
  }

  getPartialGraphPolicy() {
    const current = this.arbiter.partialGraphPolicy || { conflict_mode: 'deterministic', reducers: {} };
    return {
      conflict_mode: current.conflict_mode || 'deterministic',
      reducers: { ...(current.reducers || {}) }
    };
  }

  _applyPartialGraphPolicyFromRelationConfig(relation, config) {
    if (!config || typeof config !== 'object') return;
    const pg = config.partial_graph || config.partialGraph || null;
    if (!pg || typeof pg !== 'object') return;

    const patch = {};
    if (pg.conflict_mode || pg.conflictMode) {
      patch.conflict_mode = pg.conflict_mode || pg.conflictMode;
    }
    if (pg.reducer) {
      patch.reducers = { [relation]: pg.reducer };
    }
    if (Object.keys(patch).length > 0) {
      this.setPartialGraphPolicy(patch);
    }
  }

  _normalizeOwaConfig(config) {
    if (!config || typeof config !== 'object') return config;

    const normalizeNode = (node) => {
      if (!node || typeof node !== 'object') return;

      if (node.union && typeof node.union === 'object') {
        this._normalizeOwaWeights(node.union);
        if (Array.isArray(node.union.rules)) {
          node.union.rules.forEach(normalizeNode);
        }
      }
      if (node.intersection && typeof node.intersection === 'object') {
        this._normalizeOwaWeights(node.intersection);
        if (Array.isArray(node.intersection.rules)) {
          node.intersection.rules.forEach(normalizeNode);
        }
      }
      if (node.exclusion && typeof node.exclusion === 'object') {
        this._normalizeOwaWeights(node.exclusion);
        if (Array.isArray(node.exclusion.rules)) {
          node.exclusion.rules.forEach(normalizeNode);
        }
      }

      if (node.type === 'relational_comparator') {
        if (node.left) {
          this._normalizeOwaWeights(node.left);
          if (node.left.rule) normalizeNode(node.left.rule);
        }
        if (node.right) {
          this._normalizeOwaWeights(node.right);
          if (node.right.rule) normalizeNode(node.right.rule);
        }
      }
    };

    normalizeNode(config);
    return config;
  }

  _normalizeOwaWeights(node) {
    if (!node || typeof node !== 'object' || !Array.isArray(node.owaWeights)) return;
    if (node.owaWeights.length === 0) return;

    const aggregator = node.aggregator;
    if (aggregator && aggregator !== 'owa') return;

    const detected = this._detectOwaMode(node.owaWeights);
    if (detected) {
      node.aggregator = detected.mode;
      delete node.owaWeights;
      return;
    }

    const sparseCount = this._detectOwaSparseCount(node.owaWeights);
    if (sparseCount) {
      node._owaSparseCount = sparseCount;
      node.owaWeights._owaSparseCount = sparseCount;
    }
  }

  _detectOwaMode(weights) {
    const eps = 1e-9;
    const n = weights.length;
    if (n === 0) return null;

    let sum = 0;
    for (let i = 0; i < n; i++) {
      sum += weights[i];
    }
    if (Math.abs(sum - 1) > 1e-6) return null;

    const isZero = (v) => Math.abs(v) <= eps;
    const isOne = (v) => Math.abs(v - 1) <= eps;
    const isEqual = (a, b) => Math.abs(a - b) <= eps;

    if (isOne(weights[0]) && weights.slice(1).every(isZero)) {
      return { mode: 'max' };
    }
    if (isOne(weights[n - 1]) && weights.slice(0, n - 1).every(isZero)) {
      return { mode: 'min' };
    }

    const uniform = weights.every((v) => isEqual(v, weights[0]));
    if (uniform && isEqual(weights[0] * n, 1)) {
      return { mode: 'average' };
    }

    if (n >= 2) {
      const top2Weight = weights[0];
      if (!isZero(top2Weight) && isEqual(weights[1], top2Weight)) {
        const restZero = weights.slice(2).every(isZero);
        if (restZero && isEqual(top2Weight * 2, 1)) {
          return { mode: 'top2' };
        }
      }
    }

    if (n >= 3) {
      const top3Weight = weights[0];
      if (!isZero(top3Weight) && isEqual(weights[1], top3Weight) && isEqual(weights[2], top3Weight)) {
        const restZero = weights.slice(3).every(isZero);
        if (restZero && isEqual(top3Weight * 3, 1)) {
          return { mode: 'top3' };
        }
      }
    }

    return null;
  }

  _detectOwaSparseCount(weights) {
    const eps = 1e-9;
    let lastNonZero = -1;
    for (let i = 0; i < weights.length; i++) {
      if (Math.abs(weights[i]) > eps) lastNonZero = i;
    }
    const sparseCount = lastNonZero + 1;
    if (sparseCount <= 0) return null;
    if (sparseCount > 3) return null;
    for (let i = sparseCount; i < weights.length; i++) {
      if (Math.abs(weights[i]) > eps) return null;
    }
    return sparseCount;
  }
}
