/**
 * Predicate Resolver
 * 
 * Maps predicate names to existing compiled DSL rules across all graph scopes.
 * Does NOT create new rules - only looks up existing ones.
 */

export class PredicateResolver {
  constructor(context) {
    this.context = context;
    this.graphStores = context.graphStores || {};
    this.cache = new Map();
  }

  /**
   * Resolve a predicate name to its DSL rule
   * 
   * @param {string} predicateName - Name of the predicate
   * @returns {Object|null} Rule info or null if not found
   */
  resolve(predicateName) {
    // Check cache first
    if (this.cache.has(predicateName)) {
      return this.cache.get(predicateName);
    }

    // Look up in all graph scopes
    const rule = this.findRule(predicateName);
    
    if (rule) {
      this.cache.set(predicateName, rule);
    }
    
    return rule;
  }

  /**
   * Find a rule across all graph scopes
   * Prefers logical rules over direct rules for evidence predicates
   */
  findRule(predicateName) {
    const scopes = [
      'tenantExternal',
      'tenantInternal',
      'rootExternal',
      'rootInternal',
      'masterExternal',
      'masterInternal'
    ];

    let directRule = null;
    let directScope = null;

    for (const scopeName of scopes) {
      const graphStore = this.graphStores[scopeName];
      if (!graphStore) {
        continue;
      }

      // Handle both: Arbiter directly (has .check()) or wrapper with .arbiter
      const arbiter = graphStore.arbiter || graphStore;
      const config = arbiter.relationConfigs?.get(predicateName);

      if (config) {
        // Prefer logical rules (intersection/union) over direct rules
        // This ensures evidence rules work correctly across all scopes
        if (config.type === 'intersection' || config.type === 'union' || config.type === 'logical') {
          return {
            name: predicateName,
            scope: scopeName,
            config: config,
            arity: this.inferArity(config)
          };
        }

        // Remember the first direct rule as fallback
        if (!directRule && config.type === 'direct') {
          directRule = config;
          directScope = scopeName;
        }
      }
    }

    // Return direct rule if no logical rule found
    if (directRule) {
      return {
        name: predicateName,
        scope: directScope,
        config: directRule,
        arity: this.inferArity(directRule)
      };
    }

    return null;
  }

  /**
   * Infer the arity (parameter count) from rule config
   */
  inferArity(config) {
    // Most DSL evidence rules have 1 or 2 parameters:
    // - 1 param: just the subject (user)
    // - 2 params: subject (user) + object
    
    if (config.arity) {
      return config.arity;
    }

    // Default to checking if it's a relation that typically needs an object
    // This is a heuristic - in practice, the DSL defines this explicitly
    if (config.type === 'tuple_to_userset' || config.type === 'direct') {
      return 2; // Likely needs subject + object
    }
    
    return 1; // Default to 1 param
  }

  /**
   * Check if a predicate exists without full resolution
   */
  exists(predicateName) {
    return this.resolve(predicateName) !== null;
  }

  /**
   * Get all available predicates across all scopes
   */
  getAllPredicates() {
    const predicates = [];
    const scopes = [
      'tenantExternal',
      'tenantInternal', 
      'rootExternal',
      'rootInternal',
      'masterExternal',
      'masterInternal'
    ];

    for (const scopeName of scopes) {
      const graphStore = this.graphStores[scopeName];
      if (!graphStore) {
        continue;
      }

      // Handle both: Arbiter directly (has .check()) or wrapper with .arbiter
      const arbiter = graphStore.arbiter || graphStore;
      if (arbiter.relationConfigs) {
        for (const [name, config] of arbiter.relationConfigs) {
          predicates.push({
            name,
            scope: scopeName,
            arity: this.inferArity(config)
          });
        }
      }
    }

    return predicates;
  }

  /**
   * Clear the cache (useful for testing or when rules change)
   */
  clearCache() {
    this.cache.clear();
  }
}
