/**
 * DeclarativePolicyEngine — JSON rule matcher with zero eval.
 *
 * ADR-025: Minimal attack surface. No parser, no evaluator, no AST.
 * Rules are plain objects with string matching. First match wins.
 *
 * Rule format:
 *   { match: { op?: string | string[] }, action: 'allow' | 'deny', reason?: string }
 *
 * The `default` field determines what happens when no rule matches.
 */
import { POLICY_ALLOW, type IPolicyEngine, type IPolicyRequest, type IPolicyDecision } from './IPolicyEngine.js';

// ─── Rule Types ─────────────────────────────────────────────────────────────

export interface IDeclarativeRule {
  /** Match criteria — all specified fields must match (AND logic) */
  match: {
    /** Op name(s) to match. String for exact match, array for any-of. */
    op?: string | string[];
    /** Sender mount path(s) to match. */
    sender?: string | string[];
  };
  /** Action to take when this rule matches */
  action: 'allow' | 'deny';
  /** Human-readable reason (included in deny responses) */
  reason?: string;
}

export interface IDeclarativePolicy {
  /** Ordered rules — first match wins */
  rules: IDeclarativeRule[];
  /** Default action when no rule matches */
  default: 'allow' | 'deny';
  /** Default reason for implicit denials */
  defaultReason?: string;
}

// ─── Engine ─────────────────────────────────────────────────────────────────

export class DeclarativePolicyEngine implements IPolicyEngine {
  readonly name = 'declarative';
  private readonly _policy: IDeclarativePolicy;

  constructor(policy: IDeclarativePolicy) {
    this._policy = policy;
  }

  evaluate(request: IPolicyRequest): IPolicyDecision {
    // Linear scan — first match wins. O(n) where n = number of rules.
    // Acceptable for <100 rules. Not suitable for per-message ABAC at scale.
    for (const rule of this._policy.rules) {
      if (this._matches(rule.match, request)) {
        if (rule.action === 'allow') return POLICY_ALLOW;
        return { action: 'deny', error: rule.reason ?? 'Denied by policy rule' };
      }
    }

    // No rule matched — use default
    if (this._policy.default === 'allow') return POLICY_ALLOW;
    return {
      action: 'deny',
      error: this._policy.defaultReason ?? 'Denied by default policy (no matching rule)',
    };
  }

  /** Check if all specified match criteria are satisfied (AND logic) */
  private _matches(match: IDeclarativeRule['match'], request: IPolicyRequest): boolean {
    if (match.op !== undefined) {
      if (!this._matchesField(match.op, request.op)) return false;
    }
    if (match.sender !== undefined) {
      if (!this._matchesField(match.sender, request.sender)) return false;
    }
    return true;
  }

  /** String equality or array-includes check. No regex, no glob. */
  private _matchesField(pattern: string | string[], value: string): boolean {
    if (typeof pattern === 'string') return pattern === value;
    return pattern.includes(value);
  }
}
