import { BaseRule } from './BaseRule.js';
import { Arbiter } from '../../core/Arbiter.js';
import { OWAFusion } from '../../utils/OWAFusion.js';

/**
 * TupleToUsersetRule - Evaluates access through intermediate entities (group membership pattern)
 * 
 * This rule implements the tuple-to-userset pattern where access is granted if:
 * 1. Object has a tupleset relation to an intermediate entity (e.g., doc -> group)
 * 2. User has a computed relation to that same intermediate entity (e.g., user -> group)
 * 
 * Supports sophisticated batch processing and OWA fusion for combining multiple paths.
 * 
 * Configuration:
 * {
 *   type: 'tuple_to_userset',
 *   tuplesetRelation: string,    // Relation from object to intermediate (e.g., 'owner')
 *   computedRelation: string,    // Relation from user to intermediate (e.g., 'member_of')
 *   reverse: boolean,            // Check in reverse direction (default: false)
 *   minPossibility: number,      // Minimum possibility for a path to be considered (0-1, default: 0)
 *   owaWeights: Array<number>,   // OWA weights for fusion (default: 'max' like behavior [1,0,...])
 *   earlyExitThreshold: number,  // Stop when path exceeds this threshold (default: 0.95)
 *   maxIntermediates: number     // Maximum intermediates to check (default: 20)
 * }
 */
export class TupleToUsersetRule extends BaseRule {
  constructor(arbiter) {
    super(arbiter);
    // Performance tracking
    this.performanceStats = {
      totalChecks: 0,
      earlyExits: 0,
      maxIntermediatesHit: 0
    };
  }

  /**
   * Evaluate tuple-to-userset relationship with aggressive early exit optimization
   * @protected
   */
  _evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) {
    this.performanceStats.totalChecks++;
    
    const { fastPath, includeMeta = true } = options;
    const collectValues = options.collectValues !== undefined ? options.collectValues : true;
    const minPossibility = rule.minPossibility !== undefined ? rule.minPossibility : (options.minPossibility !== undefined ? options.minPossibility : 0);
    const earlyExitThreshold = rule.earlyExitThreshold !== undefined ? rule.earlyExitThreshold : 0.95;
    const maxIntermediates = rule.maxIntermediates !== undefined ? rule.maxIntermediates : 20;
    const resolvePossibility = (value) => value !== undefined ? value : 1.0;
    const resolveReliability = (value) => value !== undefined ? value : 1.0;
    
    const reverse = rule.reverse || false;
    if (!rule._computedRelationId) {
      rule._computedRelationId = this.arbiter.keyManager._getRelationId(rule.computedRelation);
    }
    if (!rule.computedRelationConfig) {
      rule.computedRelationConfig = this.arbiter.relationConfigs.get(rule.computedRelation);
    }
    if (!rule.tuplesetRelationConfig) {
      rule.tuplesetRelationConfig = this.arbiter.relationConfigs.get(rule.tuplesetRelation);
    }
    
    const ruleMetaBase = includeMeta ? {
      ruleType: 'TupleToUsersetRule',
      userKey,
      objectKey,
      tuplesetRelation: rule.tuplesetRelation,
      computedRelation: rule.computedRelation,
      reverse,
      minPossibilityUsed: minPossibility,
      earlyExitThreshold,
      maxIntermediates,
      evaluationStarted: Date.now()
    } : null;

    let evaluationMeta = options.trackEvaluation && includeMeta ? { ...ruleMetaBase } : null;
    
    // Get tuples (intermediate entities) with early circuit breaker
    // tuplesetDirection: 'out' = object has relation TO intermediates (document → owner → group)
    // tuplesetDirection: 'in' = intermediates have relation TO object (group → belongs_to → org)
    const tuplesetDirection = rule.tuplesetDirection || 'out'; // Default to 'out' for backward compatibility
    
    let tuples;
    const useRelationGraph = !options?.partialGraphContext;
    if (reverse) {
      // reverse honors tuplesetDirection like the forward path: 'in' means
      // intermediates hold the tupleset relation TO the user (edge.src is the
      // intermediate), 'out' means the user holds it to the intermediate.
      const reverseLookup = tuplesetDirection === 'in';
      const useGraphNeighbors = useRelationGraph &&
        this.arbiter.relationManager.shouldUseRelationGraphTraversal(userId, rule.tuplesetRelation, reverseLookup);
      const neighbors = useGraphNeighbors
        ? this.arbiter.relationManager.getRelationGraphNeighbors(userId, rule.tuplesetRelation, reverseLookup)
        : null;
      if (neighbors) {
        tuples = [];
        for (const neighborId of neighbors) {
          const edge = reverseLookup
            ? this.arbiter.relationManager.getDirectRelation(neighborId, rule.tuplesetRelation, userId, options)
            : this.arbiter.relationManager.getDirectRelation(userId, rule.tuplesetRelation, neighborId, options);
          if (edge) tuples.push(edge);
        }
      } else {
        tuples = reverseLookup
          ? this.arbiter.relationManager.getRelationsToDst(userId, rule.tuplesetRelation, options)
          : this.arbiter.relationManager.getRelationsFromSrc(userId, rule.tuplesetRelation, options);
      }
    } else {
      const reverseLookup = tuplesetDirection === 'in';
      const useGraphNeighbors = useRelationGraph &&
        this.arbiter.relationManager.shouldUseRelationGraphTraversal(objectId, rule.tuplesetRelation, reverseLookup);
      const neighbors = useGraphNeighbors
        ? this.arbiter.relationManager.getRelationGraphNeighbors(objectId, rule.tuplesetRelation, reverseLookup)
        : null;
      if (neighbors) {
        tuples = [];
        for (const neighborId of neighbors) {
          const srcId = reverseLookup ? neighborId : objectId;
          const dstId = reverseLookup ? objectId : neighborId;
          const edge = this.arbiter.relationManager.getDirectRelation(srcId, rule.tuplesetRelation, dstId, options);
          if (edge) tuples.push(edge);
        }
      } else if (reverseLookup) {
        tuples = this.arbiter.relationManager.getRelationsToDst(objectId, rule.tuplesetRelation, options);
      } else {
        tuples = this.arbiter.relationManager.getRelationsFromSrc(objectId, rule.tuplesetRelation, options);
      }
    }
    
    
    // Circuit breaker: if too many intermediates, limit and warn
    if (tuples.length > maxIntermediates * 3) {
      // Sort by possibility and take top N
      tuples.sort((a, b) => resolvePossibility(b.possibility) - resolvePossibility(a.possibility));
      tuples = tuples.slice(0, maxIntermediates);
      if (evaluationMeta) evaluationMeta.circuitBreakerTriggered = true;
    }

    const checkingId = reverse ? this.arbiter.resolveNodeId(objectKey, options) : userId;

    const useDirectJoin = rule.computedRelation && rule.computedRelationConfig?.type === 'direct';
    const computedEdges = useDirectJoin
      ? this.arbiter.relationManager.getRelationsFromSrc(checkingId, rule.computedRelation, options)
      : null;
    const joinMode = useDirectJoin && computedEdges && computedEdges.length < tuples.length
      ? 'computed'
      : 'tuples';

    const tuplesWithKeys = [];
    if (includeMeta || joinMode === 'tuples') {
      for (const t of tuples) {
        const srcKey = this.arbiter.resolveKey(t.src, options);
        const dstKey = this.arbiter.resolveKey(t.dst, options);
        const intermediateId = tuplesetDirection === 'in' ? t.src : t.dst;
        const intermediateKey = tuplesetDirection === 'in' ? srcKey : dstKey;
        tuplesWithKeys.push({ tuple: t, srcKey, dstKey, intermediateId, intermediateKey });
      }
    }

    if (evaluationMeta) {
      evaluationMeta.directTuplesFound = tuplesWithKeys.length;
      evaluationMeta.directTuples = tuplesWithKeys.map(({ tuple, srcKey, dstKey }) => ({
        from: srcKey,
        to: dstKey,
        relation: tuple.rel,
        possibility: tuple.possibility,
        reliability: tuple.reliability,
        source: tuple.source || 'persistent'
      }));
    }
    
    const useLightweightPaths = !includeMeta && !collectValues;
    let bestPath = null;
    let allValidPaths = [];
    let reasons = [];
    let intermediateEvaluationDetails = includeMeta ? [] : null;
    let processedCount = 0;

    // Collected values carry the tupleset edge's value when it has one (the
    // intermediate key alone is not enough for downstream value consumers
    // like relational_comparator operands); otherwise the intermediate key
    // is collected for path-based value lookups.
    const buildCollectedValue = (tupleEdge, tupleSrcKey, intermediateKey) => {
      if (tupleEdge && typeof tupleEdge.value === 'number') {
        return this._createCollectedValue(
          tupleEdge.value,
          tupleEdge.possibility,
          [tupleSrcKey, intermediateKey],
          { entityKey: tupleSrcKey, relation: rule.tuplesetRelation, step: 0 },
          {
            timestamp: tupleEdge.changed_last_at || tupleEdge.updated_last_at ||
              (options && options.now !== undefined && options.now !== null ? options.now : Date.now()),
            reliability: tupleEdge.reliability || 1.0,
            source: tupleEdge.source || 'persistent'
          }
        );
      }
      return intermediateKey;
    };
    
    // Process direct tuples with early exit optimization
    const computedRelationCache = new Map();
    const computedByIntermediate = joinMode === 'tuples' && computedEdges
      ? new Map(computedEdges.map(edge => [edge.dst, edge]))
      : null;

    if (joinMode === 'computed' && computedEdges) {
      for (const edge of computedEdges) {
        if (processedCount >= maxIntermediates) {
          if (evaluationMeta) evaluationMeta.maxIntermediatesReached = true;
          this.performanceStats.maxIntermediatesHit++;
          break;
        }

        const intermediateId = edge.dst;
        const intermediateKey = this.arbiter.resolveKey(intermediateId, options);
        if (!intermediateKey) continue;

        processedCount++;

        const tupleEdge = reverse
          ? (tuplesetDirection === 'in'
            ? this.arbiter.relationManager.getDirectRelation(intermediateId, rule.tuplesetRelation, userId, options)
            : this.arbiter.relationManager.getDirectRelation(userId, rule.tuplesetRelation, intermediateId, options))
          : (tuplesetDirection === 'in'
            ? this.arbiter.relationManager.getDirectRelation(intermediateId, rule.tuplesetRelation, objectId, options)
            : this.arbiter.relationManager.getDirectRelation(objectId, rule.tuplesetRelation, intermediateId, options));

        if (!tupleEdge) continue;

        let res;
        if (visited && visited.size) {
          const visitKey = `${checkingId}|${rule._computedRelationId}|${intermediateId}`;
          if (visited.has(visitKey)) {
            res = { possibility: 0, reliability: 1.0, reason: 'cycle' };
          }
        }

        if (!res) {
          res = {
            possibility: edge.possibility,
            reliability: edge.reliability !== undefined ? edge.reliability : 1.0,
            reason: 'direct_match'
          };
        }

        if (includeMeta && intermediateEvaluationDetails) {
          const pathDetail = {
            intermediateKey,
            type: 'direct_tuple',
            tuplesetRelationPossibility: resolvePossibility(tupleEdge.possibility),
            tuplesetRelationReliability: resolveReliability(tupleEdge.reliability),
            computedRelationResult: {
              possibility: res.possibility,
              reliability: res.reliability,
              reason: res.reason
            },
            metaFromCheck: res.meta
          };
          if (evaluationMeta) intermediateEvaluationDetails.push(pathDetail);
        }

        if (res.reason === 'cycle') reasons.push('cycle');

        const combinedPossibility = Math.min(resolvePossibility(tupleEdge.possibility), res.possibility);
        const combinedReliability = resolveReliability(tupleEdge.reliability) * (res.reliability !== undefined ? res.reliability : 1.0);

        // A 0-strength "match" (e.g. the computed leg missing with
        // minPossibility 0) is not a valid path: it would report
        // tuple_to_userset_found and leak the tupleset value into the
        // collected values on a denied decision.
        if (combinedPossibility > 0 && combinedPossibility >= minPossibility) {
          const path = {
            possibility: combinedPossibility,
            reliability: combinedReliability,
            intermediateKey,
            tuplesetPossibility: resolvePossibility(tupleEdge.possibility),
            computedPossibility: res.possibility,
            ...(!useLightweightPaths && includeMeta && {
              meta: {
                intermediateKey,
                pathType: 'direct_tuple',
                tuplesetRelation: { relation: rule.tuplesetRelation, possibility: resolvePossibility(tupleEdge.possibility), reliability: resolveReliability(tupleEdge.reliability) },
                computedRelation: { relation: rule.computedRelation, possibility: res.possibility, reliability: res.reliability, meta: res.meta }
              }
            }),
            ...(!useLightweightPaths && collectValues && { collectedValue: buildCollectedValue(tupleEdge, this.arbiter.resolveKey(tupleEdge.src, options), intermediateKey) })
          };
          allValidPaths.push(path);
          if (!bestPath || combinedPossibility > bestPath.possibility) {
            bestPath = path;
          }
        }
      }
    }

    if (joinMode === 'tuples') for (const entry of tuplesWithKeys) {
      const t = entry.tuple;
      if (processedCount >= maxIntermediates) {
        if (evaluationMeta) evaluationMeta.maxIntermediatesReached = true;
        this.performanceStats.maxIntermediatesHit++;
        break;
      }
      
      // For 'out' direction: intermediate is destination (document → owner → group)  
      // For 'in' direction: intermediate is source (group → belongs_to → org)
      const intermediateKey = entry.intermediateKey;
      if (!intermediateKey) continue;
      
      processedCount++;
      
      // In reverse mode: check if the "object" (3rd param) has computed relation to intermediate
      // In normal mode: check if the "user" (1st param) has computed relation to intermediate  
      const checkingEntityKey = reverse ? objectKey : userKey;
      

      let res = computedRelationCache.get(entry.intermediateId);
      if (!res) {
        if (useDirectJoin) {
          if (visited && visited.size) {
            const visitKey = `${checkingId}|${rule._computedRelationId}|${entry.intermediateId}`;
            if (visited.has(visitKey)) {
              res = {
                possibility: 0,
                reliability: 1.0,
                reason: 'cycle'
              };
            }
          }
          if (!res) {
            const directRel = computedByIntermediate
              ? computedByIntermediate.get(entry.intermediateId)
              : this.arbiter.relationManager.getDirectRelation(checkingId, rule.computedRelation, entry.intermediateId, options);
            if (directRel && (minPossibility === null || directRel.possibility >= minPossibility)) {
              res = {
                possibility: directRel.possibility,
                reliability: directRel.reliability !== undefined ? directRel.reliability : 1.0,
                reason: 'direct_match'
              };
            } else {
              res = {
                possibility: 0,
                reliability: 1.0,
                reason: 'no_direct_match'
              };
            }
          }
        } else {
          res = this.arbiter.authChecker.check(checkingEntityKey, rule.computedRelation, intermediateKey, {
            ...options,
            minPossibility,
            fastPath: true, // Enable fast path for intermediate checks
            _visited: visited,
            _currentRelation: rule.computedRelation
          });
        }
        computedRelationCache.set(entry.intermediateId, res);
      }
      
      
      if (includeMeta && intermediateEvaluationDetails) {
        const pathDetail = {
          intermediateKey,
          type: 'direct_tuple',
          tuplesetRelationPossibility: resolvePossibility(t.possibility),
          tuplesetRelationReliability: resolveReliability(t.reliability),
          computedRelationResult: {
            possibility: res.possibility,
            reliability: res.reliability,
            reason: res.reason
          },
          metaFromCheck: res.meta
        };
        if (evaluationMeta) intermediateEvaluationDetails.push(pathDetail);
      }
      
      if (res.reason === 'cycle') reasons.push('cycle');
      
      const combinedPossibility = Math.min(resolvePossibility(t.possibility), res.possibility);
      const combinedReliability = resolveReliability(t.reliability) * (res.reliability !== undefined ? res.reliability : 1.0);
      
      // 0-strength tuples (missing computed leg, or 0-possibility edges)
      // must not count as found paths (see computed join mode).
      if (combinedPossibility > 0 && combinedPossibility >= minPossibility) {
        const pathData = {
          possibility: combinedPossibility,
          reliability: combinedReliability,
          ...(!useLightweightPaths && includeMeta && {
            meta: {
              intermediateKey,
              pathType: 'direct',
              tuplesetRelation: { relation: rule.tuplesetRelation, possibility: resolvePossibility(t.possibility), reliability: resolveReliability(t.reliability) },
              computedRelation: { relation: rule.computedRelation, possibility: res.possibility, reliability: res.reliability, meta: res.meta }
            }
          }),
          ...(!useLightweightPaths && collectValues && { collectedValue: buildCollectedValue(t, entry.srcKey, intermediateKey) })
        };
        
        allValidPaths.push(pathData);
        
        // Update best path
        if (!bestPath || combinedPossibility > bestPath.possibility) {
          bestPath = pathData;
        }
        
        // AGGRESSIVE EARLY EXIT: Stop if we found a very good path
        if (combinedPossibility >= earlyExitThreshold) {
          if (evaluationMeta) {
            evaluationMeta.earlyExitTriggered = true;
            evaluationMeta.earlyExitReason = 'direct_path_threshold_exceeded';
            evaluationMeta.earlyExitPossibility = combinedPossibility;
            evaluationMeta.intermediatesProcessed = processedCount;
          }
          this.performanceStats.earlyExits++;
          
          // Return immediately with the excellent path
          return this._buildFinalResult(
            [pathData], 
            reasons, 
            ruleMetaBase, 
            evaluationMeta, 
            'early_exit_direct_path',
            includeMeta,
            collectValues,
            options.trackEvaluation
          );
        }
      }
    }
    
    if (evaluationMeta) {
      evaluationMeta.evaluationCompleted = Date.now();
      evaluationMeta.evaluationDuration = evaluationMeta.evaluationCompleted - (evaluationMeta.evaluationStarted || evaluationMeta.evaluationCompleted);
      evaluationMeta.intermediateEvaluationDetails = intermediateEvaluationDetails;
      evaluationMeta.totalValidPathsFound = allValidPaths.length;
      evaluationMeta.intermediatesProcessed = processedCount;
    }
    
    return this._buildFinalResult(allValidPaths, reasons, ruleMetaBase, evaluationMeta, 'complete_evaluation', includeMeta, collectValues, options.trackEvaluation);
  }

  /**
   * Build the final result from valid paths
   * @private
   */
  _buildFinalResult(validPathData, reasons, ruleMetaBase, evaluationMeta, evaluationType, includeMeta = true, collectValues = true, trackEvaluation = false) {
    if (!validPathData.length) {
      const reason = reasons.includes('cycle') ? 'cycle' : 'no_valid_intermediate_paths';
      if (evaluationMeta) {
        evaluationMeta.outcome = reason;
        evaluationMeta.finalPossibility = 0;
        evaluationMeta.finalReliability = 1.0;
        evaluationMeta.evaluationType = evaluationType;
      }
      
      return {
        possibility: 0,
        reliability: 1.0, 
        ...(includeMeta && { meta: { ...ruleMetaBase, outcomeReason: reason, evaluation: evaluationMeta } }),
        ...(collectValues && { collectedValues: [] }),
        reason
      };
    }

    // For single path (common with early exit), skip OWA fusion overhead
    if (validPathData.length === 1) {
      const singlePath = validPathData[0];
      if (evaluationMeta) {
        evaluationMeta.outcome = 'single_path_found';
        evaluationMeta.finalPossibility = singlePath.possibility;
        evaluationMeta.finalReliability = singlePath.reliability;
        evaluationMeta.evaluationType = evaluationType;
        evaluationMeta.fusionSkipped = 'single_path_optimization';
      }
      
      return {
        possibility: singlePath.possibility,
        reliability: singlePath.reliability,
        ...(includeMeta && {
          meta: {
            ...ruleMetaBase,
            outcomeReason: reasons.includes('cycle') ? 'cycle' : 'tuple_to_userset_evaluated',
            pathMeta: singlePath.meta,
            evaluation: evaluationMeta
          }
        }),
        ...(collectValues && { collectedValues: [singlePath.collectedValue] }),
        reason: reasons.includes('cycle') ? 'cycle' : 'tuple_to_userset_found'
      };
    }

    if (!includeMeta && !collectValues) {
      let maxPossibility = 0;
      let maxReliability = 1.0;
      for (const path of validPathData) {
        if (path.possibility > maxPossibility) {
          maxPossibility = path.possibility;
          maxReliability = path.reliability;
        }
      }
      return {
        possibility: maxPossibility,
        reliability: maxReliability,
        validity: this._validity('min', [(ruleMetaBase && ruleMetaBase.tuplesetRelation) || null, (ruleMetaBase && ruleMetaBase.computedRelation) || null].filter(Boolean), [], 2, maxPossibility),
        reason: reasons.includes('cycle') ? 'cycle' : (maxPossibility > 0 ? 'tuple_to_userset_found' : 'no_sufficient_tuple_to_userset_path')
      };
    }

    // Multiple paths - use OWA fusion
    const possibilities = validPathData.map(p => p.possibility);
    const reliabilities = validPathData.map(p => p.reliability);
    const metaObjects = includeMeta ? validPathData.map(p => p.meta) : [];
    const collectedValues = collectValues ? validPathData.map(p => p.collectedValue) : [];

    const owaWeights = [1, ...Array(possibilities.length - 1).fill(0)]; // Default to MAX for performance
    const fusionResult = OWAFusion.fuseWithMeta(possibilities, metaObjects, owaWeights, 'max', true, trackEvaluation ? { includeTrace: true } : null);
    
    let fusedReliability = 1.0;
    if (fusionResult.meta && fusionResult.meta.intermediateKey) {
        const winningPath = validPathData.find(p => p.meta.intermediateKey === fusionResult.meta.intermediateKey && p.meta.pathType === fusionResult.meta.pathType);
        if (winningPath) {
            fusedReliability = winningPath.reliability;
      }
    }
    if (fusedReliability === 1.0 && reliabilities.length > 0) {
      // Fallback: the fusion aggregator is max, so the winner is the
      // max-possibility path — its reliability, not the max reliability
      // (which would pair a possibility from one intermediate with a
      // reliability from another).
      const maxIndex = possibilities.indexOf(Math.max(...possibilities));
      fusedReliability = maxIndex >= 0 ? (reliabilities[maxIndex] ?? 1.0) : 1.0;
    }

    if (evaluationMeta) {
      evaluationMeta.outcome = 'paths_evaluated_and_fused';
      evaluationMeta.fusion = {
        method: 'owa',
        weightsUsed: owaWeights,
        fusedPossibility: fusionResult.value,
        fusedReliability: fusedReliability,
        winningPathMeta: fusionResult.meta,
        ...(trackEvaluation && fusionResult.trace ? {
          owa: {
            level: null,
            aggregator: 'max',
            weights: fusionResult.trace.weights,
            sortedValues: fusionResult.trace.sortedValues,
            contributions: fusionResult.trace.contributions,
            selectedIndex: fusionResult.trace.selectedIndex
          }
        } : {})
      };
      evaluationMeta.finalPossibility = fusionResult.value;
      evaluationMeta.finalReliability = fusedReliability;
      evaluationMeta.evaluationType = evaluationType;
    }
    
    const finalMeta = includeMeta ? {
        ...ruleMetaBase,
        outcomeReason: reasons.includes('cycle') ? 'cycle' : 'tuple_to_userset_evaluated',
        fusionMethod: 'owa',
        owaWeightsUsed: owaWeights,
        contributingPathMeta: fusionResult.meta,
        evaluation: evaluationMeta
    } : null;

    return {
      possibility: fusionResult.value,
      reliability: fusedReliability,
      validity: this._validity('min', [(ruleMetaBase && ruleMetaBase.tuplesetRelation) || null, (ruleMetaBase && ruleMetaBase.computedRelation) || null].filter(Boolean), [], 2, fusionResult.value),
      ...(includeMeta && { meta: finalMeta }),
      ...(collectValues && { collectedValues: fusionResult.value > 0 ? collectedValues : [] }),
      reason: reasons.includes('cycle') ? 'cycle' : (fusionResult.value > 0 ? 'tuple_to_userset_found' : 'no_sufficient_tuple_to_userset_path')
    };
  }

  /**
   * Get performance statistics
   * @returns {Object} Performance statistics
   */
  getPerformanceStats() {
    return { ...this.performanceStats };
  }

  /**
   * Reset performance statistics
   */
  resetPerformanceStats() {
    this.performanceStats = {
      totalChecks: 0,
      earlyExits: 0,
      maxIntermediatesHit: 0
    };
  }
} 
