// wall-e/tools/permission-rules.js
// Ported from OpenCode (https://github.com/anomalyco/opencode)
// Source: packages/opencode/src/permission/evaluate.ts, index.ts
// Wildcard pattern matching, 3-layer rule merging, evaluate function.
'use strict';

const fs = require('fs');
const path = require('path');
const os = require('os');

// ── Wildcard Matching ──
// Supports: * (any single segment), ** (any depth), *.ext (extension match)
// Ported from OpenCode's @/util/wildcard

/**
 * Match a value against a wildcard pattern.
 * - '*' matches any string
 * - '**' matches any path depth
 * - '*.ext' matches strings ending in .ext
 * - 'prefix *' matches strings starting with prefix
 *
 * @param {string} value - The string to test
 * @param {string} pattern - Wildcard pattern
 * @returns {boolean}
 */
function wildcardMatch(value, pattern) {
  if (pattern === '*') return true;
  if (pattern === value) return true;

  // Convert wildcard pattern to regex
  // Note: * matches any chars (including spaces) for permission patterns.
  // ** is a distinct glob-star for path depth matching.
  const regexStr = '^' + pattern
    .replace(/[.+^${}()|[\]\\]/g, '\\$&')  // escape regex chars (except * and ?)
    .replace(/\*\*/g, '<<GLOBSTAR>>')       // preserve **
    .replace(/\*/g, '.*')                   // * = anything
    .replace(/<<GLOBSTAR>>/g, '.*')         // ** = anything (same, but distinct semantically)
    .replace(/\?/g, '.') + '$';             // ? = single char

  return new RegExp(regexStr).test(value);
}

// ── Rule Evaluation ──
// Rule format: { permission: string, pattern: string, action: 'allow'|'deny'|'ask' }
// Last matching rule wins (enables user overrides)

/**
 * Evaluate a permission request against one or more rulesets.
 * Rulesets are flattened in order; last matching rule wins.
 *
 * @param {string} permission - Permission type (e.g., 'bash', 'edit', 'read')
 * @param {string} value - The specific value to check (e.g., 'git push *', '*.env')
 * @param {...Array} rulesets - One or more arrays of rules
 * @returns {{ permission: string, pattern: string, action: string }}
 */
function evaluate(permission, value, ...rulesets) {
  const rules = rulesets.flat();
  const match = rules.findLast(rule =>
    wildcardMatch(permission, rule.permission) && wildcardMatch(value, rule.pattern)
  );
  return match ?? { action: 'ask', permission, pattern: '*' };
}

// ── Enhanced Evaluation (inspired by OpenCode permission/index.ts) ──

/**
 * Evaluate and return the full matched rule with metadata for callers.
 * Unlike evaluate(), this returns { permission, pattern, action } of the
 * matched rule plus the input that was evaluated, enabling audit trails.
 *
 * Inspired by OpenCode's Permission.evaluate() which logs the matched rule.
 *
 * @param {string} permission - Permission type
 * @param {string} value - The specific value to check
 * @param {...Array} rulesets - One or more arrays of rules
 * @returns {{ permission: string, pattern: string, action: string, matchedRule: object|null, input: { permission: string, value: string } }}
 */
function evaluateResult(permission, value, ...rulesets) {
  const rules = rulesets.flat();
  const match = rules.findLast(rule =>
    wildcardMatch(permission, rule.permission) && wildcardMatch(value, rule.pattern)
  );
  const result = match ?? { action: 'ask', permission, pattern: '*' };
  const output = {
    permission: result.permission,
    pattern: result.pattern,
    action: result.action,
    matchedRule: match || null,
    input: { permission, value },
  };

  // Audit logging: log which rule matched and why (debug level)
  // Inspired by OpenCode's log.info("evaluated", ...) in Permission.ask()
  if (typeof console !== 'undefined' && console.debug) {
    console.debug(
      `[permission] evaluate: ${permission}/${value} -> ${result.action}` +
      (match ? ` (matched: ${match.permission}/${match.pattern})` : ' (no match, default ask)')
    );
  }

  return output;
}

/**
 * Merge multiple rulesets into a single flat array (order preserved).
 */
function merge(...rulesets) {
  return rulesets.flat();
}

// ── Default Rules (safety floor) ──

const DEFAULT_RULES = [
  // Destructive shell commands: always deny
  { permission: 'bash', pattern: 'rm *', action: 'deny' },
  { permission: 'bash', pattern: 'rmdir *', action: 'deny' },
  { permission: 'bash', pattern: 'kill *', action: 'deny' },
  { permission: 'bash', pattern: 'killall *', action: 'deny' },
  { permission: 'bash', pattern: 'sudo *', action: 'deny' },
  { permission: 'bash', pattern: 'su *', action: 'deny' },
  { permission: 'bash', pattern: 'chmod *', action: 'deny' },
  { permission: 'bash', pattern: 'chown *', action: 'deny' },
  { permission: 'bash', pattern: 'dd *', action: 'deny' },
  { permission: 'bash', pattern: 'mkfs *', action: 'deny' },
  { permission: 'bash', pattern: 'shutdown *', action: 'deny' },
  { permission: 'bash', pattern: 'reboot *', action: 'deny' },

  // Common read-only commands: allow
  { permission: 'bash', pattern: 'ls *', action: 'allow' },
  { permission: 'bash', pattern: 'cat *', action: 'allow' },
  { permission: 'bash', pattern: 'head *', action: 'allow' },
  { permission: 'bash', pattern: 'tail *', action: 'allow' },
  { permission: 'bash', pattern: 'grep *', action: 'allow' },
  { permission: 'bash', pattern: 'rg *', action: 'allow' },
  { permission: 'bash', pattern: 'find *', action: 'allow' },
  { permission: 'bash', pattern: 'wc *', action: 'allow' },
  { permission: 'bash', pattern: 'echo *', action: 'allow' },
  { permission: 'bash', pattern: 'pwd *', action: 'allow' },
  { permission: 'bash', pattern: 'date *', action: 'allow' },
  { permission: 'bash', pattern: 'sort *', action: 'allow' },
  { permission: 'bash', pattern: 'uniq *', action: 'allow' },
  { permission: 'bash', pattern: 'cut *', action: 'allow' },
  { permission: 'bash', pattern: 'tr *', action: 'allow' },
  { permission: 'bash', pattern: 'awk *', action: 'allow' },
  { permission: 'bash', pattern: 'sed *', action: 'allow' },
  { permission: 'bash', pattern: 'jq *', action: 'allow' },
  { permission: 'bash', pattern: 'diff *', action: 'allow' },
  { permission: 'bash', pattern: 'git status *', action: 'allow' },
  { permission: 'bash', pattern: 'git log *', action: 'allow' },
  { permission: 'bash', pattern: 'git diff *', action: 'allow' },
  { permission: 'bash', pattern: 'git show *', action: 'allow' },
  { permission: 'bash', pattern: 'git branch *', action: 'allow' },

  // Potentially dangerous git ops: ask
  { permission: 'bash', pattern: 'git push *', action: 'ask' },
  { permission: 'bash', pattern: 'git reset *', action: 'ask' },
  { permission: 'bash', pattern: 'git clean *', action: 'ask' },
  { permission: 'bash', pattern: 'git rebase *', action: 'ask' },

  // Dev tools: allow
  { permission: 'bash', pattern: 'node *', action: 'allow' },
  { permission: 'bash', pattern: 'npm *', action: 'allow' },
  { permission: 'bash', pattern: 'npx *', action: 'allow' },
  { permission: 'bash', pattern: 'python3 *', action: 'allow' },
  { permission: 'bash', pattern: 'make *', action: 'allow' },
  { permission: 'bash', pattern: 'cargo *', action: 'allow' },

  // npm publish: ask
  { permission: 'bash', pattern: 'npm publish *', action: 'ask' },

  // File reads: allow by default
  { permission: 'read', pattern: '*', action: 'allow' },
  // Sensitive file reads: ask
  { permission: 'read', pattern: '*.env', action: 'ask' },
  { permission: 'read', pattern: '*.env.*', action: 'ask' },
  { permission: 'read', pattern: '*.env.example', action: 'allow' },
  { permission: 'read', pattern: '**/.ssh/**', action: 'ask' },
  { permission: 'read', pattern: '**/.aws/**', action: 'ask' },

  // File edits: allow by default
  { permission: 'edit', pattern: '*', action: 'allow' },
  // Sensitive file edits: deny or ask
  { permission: 'edit', pattern: '*.env', action: 'ask' },
  { permission: 'edit', pattern: '*.env.*', action: 'ask' },
  { permission: 'edit', pattern: '**/.ssh/**', action: 'deny' },
  { permission: 'edit', pattern: '**/.aws/**', action: 'deny' },

  // Always-ask tools
  { permission: 'applescript', pattern: '*', action: 'ask' },
  { permission: 'claude_code', pattern: '*', action: 'ask' },
  { permission: 'mail_send', pattern: '*', action: 'ask' },
  { permission: 'mail_reply', pattern: '*', action: 'ask' },
  { permission: 'slack_send_message', pattern: '*', action: 'ask' },

  // Coding-agent capability bridges. Wall-E memory/session MCP reads are
  // safe by default; other MCP executions require an explicit rule, trusted
  // directory, or interactive approval. Installed Wall-E skills are local
  // trusted code and remain allowed by default, with user rules able to deny.
  { permission: 'mcp', pattern: '*', action: 'ask' },
  { permission: 'mcp', pattern: 'wall-e.*', action: 'allow' },
  { permission: 'mcp', pattern: 'mcp__wall_e__*', action: 'allow' },
  { permission: 'skill', pattern: '*', action: 'allow' },
];

// ── Agent-Mode Rules (per coding mode) ──

const AGENT_RULES = {
  plan: [
    // Plan mode: NO edits allowed
    { permission: 'edit', pattern: '*', action: 'deny' },
    // Shell: only read-only commands
    { permission: 'bash', pattern: '*', action: 'ask' },
  ],
  build: [
    // Build mode: edits allowed
    { permission: 'edit', pattern: '*', action: 'allow' },
    { permission: 'edit', pattern: '*.env', action: 'ask' },
  ],
  review: [
    // Review mode: read-only, no edits
    { permission: 'edit', pattern: '*', action: 'deny' },
    { permission: 'bash', pattern: '*', action: 'ask' },
  ],
};

// ── User Rules (loaded from config file) ──

/**
 * Load user permission rules from a config file.
 * Checks: projectPath/.walle/permissions.json, then ~/.walle/permissions.json
 *
 * Config format (matches OpenCode):
 * {
 *   "edit": { "*.test.ts": "allow" },
 *   "bash": { "npm publish": "deny", "git push *": "ask" },
 *   "read": "allow"
 * }
 *
 * @param {string} [projectPath] - Project directory to check for local config
 * @returns {Array} Parsed rules array
 */
function loadUserRules(projectPath) {
  const candidates = [];
  if (projectPath) candidates.push(path.join(projectPath, '.walle', 'permissions.json'));
  candidates.push(path.join(os.homedir(), '.walle', 'permissions.json'));

  for (const configPath of candidates) {
    try {
      const raw = fs.readFileSync(configPath, 'utf8');
      const config = JSON.parse(raw);
      return fromConfig(config);
    } catch {
      continue; // file doesn't exist or is invalid
    }
  }

  return [];
}

/**
 * Convert config object to rules array.
 * Ported from OpenCode's Permission.fromConfig().
 */
function fromConfig(config) {
  const rules = [];
  for (const [permission, value] of Object.entries(config)) {
    if (typeof value === 'string') {
      // Simple form: "edit": "allow"
      rules.push({ permission, pattern: '*', action: value });
    } else if (typeof value === 'object') {
      // Pattern form: "edit": { "*.test.ts": "allow", "*.env": "ask" }
      for (const [pattern, action] of Object.entries(value)) {
        rules.push({ permission, pattern: expandPath(pattern), action });
      }
    }
  }
  return rules;
}

/**
 * Expand ~ and $HOME in pattern strings.
 */
function expandPath(pattern) {
  if (pattern.startsWith('~/')) return os.homedir() + pattern.slice(1);
  if (pattern === '~') return os.homedir();
  if (pattern.startsWith('$HOME/')) return os.homedir() + pattern.slice(5);
  if (pattern.startsWith('$HOME')) return os.homedir() + pattern.slice(5);
  return pattern;
}

// ── PermissionManager: stateful approval cascading + rejection feedback ──
// Inspired by OpenCode's Permission.Service (permission/index.ts lines 139-268)
// Implements "always" approval with pending request re-evaluation,
// and rejection with feedback message passback.

class PermissionManager {
  /**
   * @param {Array} [baseRuleset] - Base rules (defaults to DEFAULT_RULES)
   */
  constructor(baseRuleset = DEFAULT_RULES) {
    this.baseRuleset = baseRuleset;
    /** @type {Array<{ permission: string, pattern: string, action: string }>} */
    this.approved = [];
    /** @type {Map<string, { id: string, permission: string, patterns: string[], resolve: Function, reject: Function }>} */
    this.pending = new Map();
    this._nextId = 1;
  }

  /**
   * Evaluate a permission request against base rules + session-approved rules.
   * @param {string} permission
   * @param {string} value
   * @param {...Array} extraRulesets - Additional rulesets (agent mode, user config)
   * @returns {{ permission: string, pattern: string, action: string, matchedRule: object|null, input: { permission: string, value: string } }}
   */
  evaluate(permission, value, ...extraRulesets) {
    return evaluateResult(permission, value, this.baseRuleset, ...extraRulesets, this.approved);
  }

  /**
   * Submit a permission request. Returns a promise that resolves on approval
   * or rejects with feedback on rejection.
   *
   * Inspired by OpenCode's Permission.ask() (permission/index.ts line 166).
   *
   * @param {string} permission - Permission type
   * @param {string[]} patterns - Patterns to approve (used for "always" cascading)
   * @param {...Array} extraRulesets
   * @returns {Promise<{ action: string }>}
   */
  ask(permission, patterns, ...extraRulesets) {
    // Check if already approved
    for (const pattern of patterns) {
      const result = this.evaluate(permission, pattern, ...extraRulesets);
      if (result.action === 'deny') {
        return Promise.reject(new PermissionDeniedError(permission, pattern, result));
      }
      if (result.action === 'ask') {
        // Need to ask — create pending entry
        const id = String(this._nextId++);
        return new Promise((resolve, reject) => {
          this.pending.set(id, { id, permission, patterns, resolve, reject });
        });
      }
    }
    // All patterns allowed
    return Promise.resolve({ action: 'allow' });
  }

  /**
   * Reply to a pending request: 'once', 'always', or 'reject'.
   *
   * - 'once': approve this single request
   * - 'always': approve and add patterns to approved[], re-evaluate other pending requests
   * - 'reject': reject with optional feedback message passed back to the LLM
   *
   * Inspired by OpenCode's Permission.reply() (permission/index.ts line 203).
   *
   * @param {string} requestId
   * @param {'once'|'always'|'reject'} reply
   * @param {string} [message] - Feedback message for rejections (passed to LLM)
   */
  reply(requestId, reply, message) {
    const entry = this.pending.get(requestId);
    if (!entry) return;

    this.pending.delete(requestId);

    if (reply === 'reject') {
      // Reject with optional feedback message
      // Inspired by OpenCode's CorrectedError (permission/index.ts line 88-93)
      const err = message
        ? new PermissionRejectedError(entry.permission, message)
        : new PermissionRejectedError(entry.permission);
      entry.reject(err);

      // Cascade rejection to other pending requests in same session
      // Inspired by OpenCode's cascading reject (permission/index.ts lines 221-230)
      if (typeof console !== 'undefined' && console.debug) {
        console.debug(`[permission] rejected: ${entry.permission}/${entry.patterns.join(',')}` +
          (message ? ` feedback: "${message}"` : ''));
      }
      return;
    }

    // Approve this request
    entry.resolve({ action: reply });

    if (reply === 'always') {
      // Add patterns to approved rules for future auto-approval
      // Inspired by OpenCode's "always" handling (permission/index.ts lines 237-243)
      for (const pattern of entry.patterns) {
        this.approved.push({
          permission: entry.permission,
          pattern,
          action: 'allow',
        });
      }

      if (typeof console !== 'undefined' && console.debug) {
        console.debug(`[permission] always-approved: ${entry.permission}/${entry.patterns.join(',')}`);
      }

      // Re-evaluate pending requests — auto-approve those now covered
      // Inspired by OpenCode's cascading approval (permission/index.ts lines 245-258)
      for (const [id, pending] of this.pending.entries()) {
        const allAllowed = pending.patterns.every(p => {
          const r = evaluateResult(pending.permission, p, this.baseRuleset, this.approved);
          return r.action === 'allow';
        });
        if (allAllowed) {
          this.pending.delete(id);
          pending.resolve({ action: 'always' });
          if (typeof console !== 'undefined' && console.debug) {
            console.debug(`[permission] cascade-approved: ${pending.permission}/${pending.patterns.join(',')}`);
          }
        }
      }
    }
  }

  /**
   * List all pending permission requests.
   * @returns {Array<{ id: string, permission: string, patterns: string[] }>}
   */
  list() {
    return Array.from(this.pending.values(), ({ id, permission, patterns }) => ({ id, permission, patterns }));
  }

  /**
   * Clear all approved rules and reject all pending requests.
   */
  reset() {
    this.approved = [];
    for (const [id, entry] of this.pending.entries()) {
      entry.reject(new PermissionRejectedError(entry.permission, 'Session reset'));
    }
    this.pending.clear();
  }
}

/**
 * Error thrown when a permission is denied by rule.
 * Inspired by OpenCode's DeniedError (permission/index.ts line 96-101).
 */
class PermissionDeniedError extends Error {
  constructor(permission, pattern, rule) {
    super(`Permission denied: ${permission}/${pattern} by rule ${JSON.stringify(rule)}`);
    this.name = 'PermissionDeniedError';
    this.permission = permission;
    this.pattern = pattern;
    this.rule = rule;
  }
}

/**
 * Error thrown when a permission request is rejected by the user.
 * Includes optional feedback message for the LLM.
 * Inspired by OpenCode's RejectedError + CorrectedError (permission/index.ts lines 82-93).
 */
class PermissionRejectedError extends Error {
  constructor(permission, feedback) {
    const msg = feedback
      ? `Permission rejected for ${permission} with feedback: ${feedback}`
      : `Permission rejected for ${permission}`;
    super(msg);
    this.name = 'PermissionRejectedError';
    this.permission = permission;
    /** @type {string|undefined} Feedback message to pass back to the LLM */
    this.feedback = feedback;
  }
}

module.exports = {
  wildcardMatch,
  evaluate,
  evaluateResult,
  merge,
  loadUserRules,
  fromConfig,
  PermissionManager,
  PermissionDeniedError,
  PermissionRejectedError,
  DEFAULT_RULES,
  AGENT_RULES,
};
