import { getOWAWeightsFromRule } from '../utils/OWAFusion.js';

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

  compile(relationName, config) {
    const errors = [];
    const warnings = [];
    const compiled = this._compileNode(config, relationName, errors, warnings);
    if (compiled) {
      compiled.source = config;
    }
    this._optimizeCompiled(compiled);
    return { compiled, errors, warnings };
  }

  _optimizeCompiled(compiled) {
    if (!compiled || typeof compiled !== 'object') return;
    if (compiled.type === 'logical') {
      compiled._optimized = this._optimizeLogical(compiled);
      compiled._precomputedWeights = this._precomputeLogicalWeights(compiled);
    }
    if (compiled.type === 'tuple_to_userset') {
      compiled._optimized = this._optimizeTupleToUserset(compiled);
    }
    if (compiled.type === 'chain') {
      compiled._optimized = this._optimizeChain(compiled);
    }
    if (compiled.type === 'multi_hop') {
      compiled._optimized = this._optimizeMultiHop(compiled);
    }
    if (compiled.type === 'defeasible') {
      if (compiled.never) this._optimizeCompiled(compiled.never);
      if (compiled.unless) this._optimizeCompiled(compiled.unless);
      if (compiled.always) this._optimizeCompiled(compiled.always);
      if (compiled.when) this._optimizeCompiled(compiled.when);
      if (compiled.requires) this._optimizeCompiled(compiled.requires);
    }
    if (compiled.type === 'relational_comparator') {
      if (compiled.left?.rule) this._optimizeCompiled(compiled.left.rule);
      if (compiled.right?.rule) this._optimizeCompiled(compiled.right.rule);
    }
    if (compiled.type === 'logical') {
      for (const child of compiled.children) {
        this._optimizeCompiled(child);
      }
    }
  }

  _compileNode(config, relationName, errors, warnings) {
    if (!config || typeof config !== 'object') {
      errors.push({ relation: relationName, message: 'Missing or invalid config node' });
      return null;
    }

    if (Array.isArray(config)) {
      if (config.length === 0) {
        errors.push({ relation: relationName, message: 'Empty rule array' });
        return null;
      }
      if (config.length === 1) {
        return this._compileNode(config[0], relationName, errors, warnings);
      }
      return this._compileNode({ type: 'logical', union: { rules: config } }, relationName, errors, warnings);
    }

    if (config.never || config.always || config.when || config.unless || config.requires) {
      const compiled = this._compileDefeasible(config, relationName, errors, warnings);
      if (compiled) {
        compiled.source = config;
        config._compiled = compiled;
      }
      return compiled;
    }

    if (config.union || config.intersection || config.exclusion) {
      const compiled = this._compileLogical(config, relationName, errors, warnings);
      if (compiled) {
        compiled.source = config;
        config._compiled = compiled;
      }
      return compiled;
    }

    const type = config.type || 'direct';
    let compiled;
    switch (type) {
      case 'direct':
        compiled = this._compileDirect(config, relationName, errors);
        break;
      case 'computed':
        compiled = this._compileComputed(config, relationName, errors);
        break;
      case 'parent':
        compiled = this._compileParent(config, relationName, errors);
        break;
      case 'tuple_to_userset':
        compiled = this._compileTupleToUserset(config, relationName, errors);
        break;
      case 'chain':
        compiled = this._compileChain(config, relationName, errors);
        break;
      case 'multi_hop':
        compiled = this._compileMultiHop(config, relationName, errors);
        break;
      case 'relational_comparator':
        compiled = this._compileRelationalComparator(config, relationName, errors, warnings);
        break;
      case 'challenge':
        compiled = this._compileChallenge(config, relationName, errors);
        break;
      default:
        errors.push({ relation: relationName, message: `Unknown rule type: ${type}` });
        return null;
    }
    if (compiled) {
      compiled.source = config;
      config._compiled = compiled;
    }
    return compiled;
  }

  _compileLogical(config, relationName, errors, warnings) {
    const logicalKey = config.union ? 'union' : config.intersection ? 'intersection' : 'exclusion';
    const logicalConfig = config[logicalKey];
    const rawRules = Array.isArray(logicalConfig?.rules)
      ? logicalConfig.rules
      : Array.isArray(config[logicalKey])
        ? config[logicalKey]
        : [];

    if (!rawRules.length) {
      errors.push({ relation: relationName, message: `${logicalKey} has no child rules` });
    }

    const children = rawRules.map(child => this._compileNode(child, relationName, errors, warnings)).filter(Boolean);
    const compiled = {
      type: 'logical',
      op: logicalKey,
      children,
      // The array form ({ union: [a, b], aggregator: 'average' }) carries the
      // operator options on the TOP-level config; the object form carries
      // them on the union object itself. Honor both.
      aggregator: logicalConfig?.aggregator ?? config.aggregator,
      owaWeights: logicalConfig?.owaWeights ?? config.owaWeights,
      _owaSparseCount: logicalConfig?._owaSparseCount ?? config._owaSparseCount,
      reliabilityWeighting: logicalConfig?.reliabilityWeighting ?? config.reliabilityWeighting ?? false,
      useBilattice: logicalConfig?.useBilattice ?? config.useBilattice ?? false,
      capacityType: logicalConfig?.capacityType ?? config.capacityType,
      epistemicMode: logicalConfig?.epistemicMode ?? config.epistemicMode,
      negate: logicalConfig?.negate ?? config.negate ?? false
    };

    return compiled;
  }

  _compileDefeasible(config, relationName, errors, warnings) {
    return {
      type: 'defeasible',
      never: config.never ? this._compileNode(config.never, relationName, errors, warnings) : null,
      unless: config.unless ? this._compileNode(config.unless, relationName, errors, warnings) : null,
      always: config.always ? this._compileNode(config.always, relationName, errors, warnings) : null,
      when: config.when ? this._compileNode(config.when, relationName, errors, warnings) : null,
      requires: config.requires ? this._compileNode(config.requires, relationName, errors, warnings) : null,
      mode: config.mode || 'normal',
      minPossibility: config.minPossibility,
      priority: config.priority || 0
    };
  }

  _compileDirect(config, relationName, errors) {
    const relation = config.relation || config.rel || config.label || config.name || relationName;
    if (!relation) {
      errors.push({ relation: relationName, message: 'Direct rule missing relation name' });
    }
    return {
      type: 'direct',
      relation,
      relationId: relation ? this.arbiter.keyManager._getRelationId(relation) : null,
      reverse: !!config.reverse,
      collectValues: config.collectValues !== false
    };
  }

  _compileComputed(config, relationName, errors) {
    const relation = config.relation || relationName;
    if (!relation) {
      errors.push({ relation: relationName, message: 'Computed rule missing relation name' });
    }
    return {
      type: 'computed',
      relation
    };
  }

  _compileParent(config, relationName, errors) {
    const parentRelation = config.parentRelation || 'parent';
    return {
      type: 'parent',
      parentRelation,
      parentRelationId: this.arbiter.keyManager._getRelationId(parentRelation),
      relation: config.relation || relationName,
      reverse: !!config.reverse,
      aggregator: config.aggregator,
      owaWeights: config.owaWeights,
      _owaSparseCount: config._owaSparseCount,
      reliabilityWeighting: !!config.reliabilityWeighting
    };
  }

  _compileTupleToUserset(config, relationName, errors) {
    if (!config.tuplesetRelation) {
      errors.push({ relation: relationName, message: 'tuple_to_userset missing tuplesetRelation' });
    }
    if (!config.computedRelation) {
      errors.push({ relation: relationName, message: 'tuple_to_userset missing computedRelation' });
    }
    return {
      type: 'tuple_to_userset',
      tuplesetRelation: config.tuplesetRelation,
      tuplesetRelationId: config.tuplesetRelation ? this.arbiter.keyManager._getRelationId(config.tuplesetRelation) : null,
      computedRelation: config.computedRelation,
      tuplesetDirection: config.tuplesetDirection || 'out',
      reverse: !!config.reverse,
      minPossibility: config.minPossibility,
      earlyExitThreshold: config.earlyExitThreshold,
      maxIntermediates: config.maxIntermediates
    };
  }

  _compileChain(config, relationName, errors) {
    const chainConfig = config.chain || config;
    const steps = Array.isArray(chainConfig.steps) ? chainConfig.steps : [];
    if (!steps.length) {
      errors.push({ relation: relationName, message: 'chain missing steps' });
    }
    const reverse = !!config.reverse;
    const orderedSteps = reverse ? steps.slice().reverse() : steps;
    const normalizedSteps = orderedSteps.map(step => ({
      relation: step.relation,
      relationId: step.relation ? this.arbiter.keyManager._getRelationId(step.relation) : null,
      direction: reverse ? (step.direction === 'out' ? 'in' : 'out') : (step.direction || 'out')
    }));
    return {
      type: 'chain',
      steps: normalizedSteps,
      reverse,
      collectValues: config.collectValues !== false,
      valueFilters: config.valueFilters,
      valueAggregation: config.valueAggregation
    };
  }

  _compileMultiHop(config, relationName, errors) {
    const relation = config.relation || relationName;
    if (!relation) {
      errors.push({ relation: relationName, message: 'multi_hop missing relation name' });
    }
    return {
      type: 'multi_hop',
      relation,
      relationId: relation ? this.arbiter.keyManager._getRelationId(relation) : null,
      maxDepth: config.maxDepth || 5,
      reverse: !!config.reverse,
      pathAggregation: config.pathAggregation || 'max',
      owaWeights: config.owaWeights,
      _owaSparseCount: config._owaSparseCount,
      collectValues: config.collectValues !== false,
      valueFilters: config.valueFilters,
      valueAggregation: config.valueAggregation || 'sum',
      fallbackToBasicPaths: config.fallbackToBasicPaths !== false,
      trackPaths: config.trackPaths !== false
    };
  }

  _compileRelationalComparator(config, relationName, errors, warnings) {
    const left = config.left || (config.leftRelation ? { rule: { type: 'direct', relation: config.leftRelation }, extractValue: true } : null);
    const right = config.right || (config.rightRelation ? { rule: { type: 'direct', relation: config.rightRelation }, extractValue: true, evaluateFrom: 'object' } : null);
    if (!left || !right) {
      errors.push({ relation: relationName, message: 'relational_comparator missing operands' });
    }
    // Normalize source config so the non-compiled fallback path can find left/right
    if (!config.left && left) config.left = left;
    if (!config.right && right) config.right = right;
    if (!config.comparator && config.operator) config.comparator = config.operator;
    return {
      type: 'relational_comparator',
      comparator: config.comparator || config.operator,
      marginOfSafety: config.marginOfSafety,
      minRulePossibility: config.minRulePossibility,
      fallbackBehavior: config.fallbackBehavior,
      epsilon: config.epsilon,
      left: this._compileComparatorOperand(left, relationName, errors, warnings),
      right: this._compileComparatorOperand(right, relationName, errors, warnings),
      qualitative: !!config.qualitative
    };
  }

  _compileChallenge(config, relationName, errors) {
    const challenge = config.challenge || config.name || relationName;
    if (!challenge) {
      errors.push({ relation: relationName, message: 'challenge rule missing challenge name' });
    }
    return {
      type: 'challenge',
      challenge,
      subject: config.subject || 'user',
      subjectKey: config.subjectKey,
      withinMs: config.withinMs,
      withinSeconds: config.withinSeconds,
      withinMinutes: config.withinMinutes,
      withinHours: config.withinHours
    };
  }

  _compileComparatorOperand(operand, relationName, errors, warnings) {
    if (!operand) {
      errors.push({ relation: relationName, message: 'relational_comparator operand missing' });
      return null;
    }
    const nestedRule = operand.rule ? this._compileNode(operand.rule, relationName, errors, warnings) : null;
    const resolvedValueRelation = operand.valueRelation || nestedRule?.relation || null;
    const resolvedEvaluateFrom = operand.evaluateFrom || 'auto';
    const valueRelation = operand.valueRelation;
    return {
      rule: nestedRule,
      extractValue: operand.extractValue !== false,
      valueRelation,
      valueRelationResolved: resolvedValueRelation,
      valueRelationId: resolvedValueRelation ? this.arbiter.keyManager._getRelationId(resolvedValueRelation) : null,
      evaluateFrom: resolvedEvaluateFrom,
      aggregator: operand.aggregator,
      owaWeights: operand.owaWeights,
      _owaSparseCount: operand._owaSparseCount,
      minOperandPossibility: operand.minOperandPossibility
    };
  }

  _optimizeLogical(node) {
    if (!node || node.type !== 'logical') return null;
    const allDirect = node.children.length > 0 && node.children.every(child => child?.type === 'direct');
    if (allDirect) {
      return {
        kind: 'direct_list',
        size: node.children.length,
        direct: node.children.map(child => ({
          relation: child.relation,
          relationId: child.relationId,
          reverse: !!child.reverse,
          collectValues: child.collectValues !== false
        }))
      };
    }
    return null;
  }

  _optimizeTupleToUserset(node) {
    if (!node || node.type !== 'tuple_to_userset') return null;
    const tuplesetConfig = this.arbiter.relationConfigs.get(node.tuplesetRelation);
    const computedConfig = this.arbiter.relationConfigs.get(node.computedRelation);
    if (!tuplesetConfig || !computedConfig) return null;
    if (tuplesetConfig.type !== 'direct' || computedConfig.type !== 'direct') return null;
    return {
      kind: 'direct_join',
      tuplesetRelation: node.tuplesetRelation,
      computedRelation: node.computedRelation,
      tuplesetDirection: node.tuplesetDirection || 'out',
      reverse: !!node.reverse,
      minPossibility: node.minPossibility,
      earlyExitThreshold: node.earlyExitThreshold,
      maxIntermediates: node.maxIntermediates
    };
  }

  _optimizeChain(node) {
    if (!node || node.type !== 'chain') return null;
    if (!Array.isArray(node.steps) || node.steps.length !== 1) return null;
    const step = node.steps[0];
    if (!step || !step.relation) return null;
    return {
      kind: 'direct',
      relation: step.relation,
      relationId: step.relationId,
      reverse: step.direction === 'in'
    };
  }

  _optimizeMultiHop(node) {
    if (!node || node.type !== 'multi_hop') return null;
    if (!node.relation) return null;
    if (node.maxDepth && node.maxDepth !== 1) return null;
    return {
      kind: 'direct',
      relation: node.relation,
      relationId: node.relationId,
      reverse: !!node.reverse
    };
  }

  _precomputeLogicalWeights(node) {
    if (!node || node.type !== 'logical' || !Array.isArray(node.children)) return null;
    const length = node.children.length;
    if (!length) return null;
    if (node.aggregator === 'priority') return null;

    if (node.op === 'intersection' && !node.aggregator && !node.owaWeights) {
      return [...Array(Math.max(0, length - 1)).fill(0), 1];
    }

    if (node.op === 'exclusion' && !node.aggregator && !node.owaWeights) {
      return null;
    }

    return getOWAWeightsFromRule(node, length, null);
  }
}
