'use strict';
const path = require('path');
const { evaluate, merge, loadUserRules, DEFAULT_RULES, AGENT_RULES } = require('./permission-rules');
const { bashArityPrefix } = require('./shell-analyzer');

// Layer 1: NEVER allowed — safety floor (kept for fast-path rejection)
const DENYLIST = new Set([
  'rm', 'rmdir', 'kill', 'killall', 'shutdown', 'reboot',
  'sudo', 'su', 'chmod', 'chown', 'chgrp',
  'dd', 'mkfs', 'fdisk', 'diskutil',
]);

// Tools that always require user approval (kept for backwards compat)
const ALWAYS_ASK_TOOLS = new Set([
  'applescript', 'claude_code', 'mail_send', 'mail_reply', 'slack_send_message',
]);

// Shell builtins that only navigate / change shell state — harmless on their
// own. Treated as allow ONLY inside per-clause evaluation, where every sibling
// clause of a compound command is independently checked, so a safe `cd` can
// never whitewash a dangerous neighbour (e.g. `cd /x && rm -rf /`).
const NAVIGATION_BUILTINS = new Set(['cd', 'pushd', 'popd', 'dirs']);

const _ACTION_RANK = { allow: 0, ask: 1, deny: 2 };

/**
 * Evaluate every clause of a (possibly compound) shell command and return the
 * MOST-RESTRICTIVE decision (deny > ask > allow).
 *
 * `clauses` is an array of raw token arrays — one per clause — exactly as
 * produced by analyzeShellCommand().commandTokens (split on &&, ||, ;, |, &).
 * Judging the whole command by its first clause alone is both a usability bug
 * (a leading `cd` forces an ask and parks the agent) and a security hole (an
 * allow-listed first clause masks a destructive trailing clause).
 *
 * @param {string[][]} clauses
 * @param {Array} allRules - merged ruleset (DEFAULT_RULES + agent + user)
 * @returns {{ action: 'allow'|'ask'|'deny', pattern: string, reason: string }}
 */
function evaluateBashClauses(clauses, allRules) {
  let worst = null;
  for (const tokens of clauses) {
    if (!Array.isArray(tokens) || tokens.length === 0) continue;
    const cmd = tokens[0];
    let action, pattern, reason;
    if (DENYLIST.has(cmd)) {
      action = 'deny';
      pattern = `${cmd} *`;
      reason = `${cmd} is a destructive command`;
    } else if (NAVIGATION_BUILTINS.has(cmd)) {
      action = 'allow';
      pattern = `${cmd} *`;
      reason = `Rule: bash/${cmd} * -> allow (navigation)`;
    } else {
      const prefix = bashArityPrefix(tokens);
      pattern = prefix.join(' ') + ' *';
      action = evaluate('bash', pattern, allRules).action;
      reason = `Rule: bash/${pattern} -> ${action}`;
    }
    if (!worst || _ACTION_RANK[action] > _ACTION_RANK[worst.action]) {
      worst = { action, pattern, reason };
      if (action === 'deny') break; // deny is the floor — nothing is more restrictive
    }
  }
  return worst || { action: 'ask', pattern: '*', reason: 'Rule: bash/* -> ask' };
}

let _ctmBaseUrl = null;
let _ctmToken = null;
let _userRulesCache = null;

function configure({ ctmUrl, ctmToken } = {}) {
  _ctmBaseUrl = ctmUrl || null;
  _ctmToken = ctmToken || null;
  _userRulesCache = null; // invalidate cache on reconfigure
}

// ── Per-directory trust (Phase 6, inspired by cmux CmuxDirectoryTrust.swift) ──

/**
 * Check if a project directory is trusted for auto-approval.
 * Trust is scoped to git repo root if inside a repo.
 */
function isDirectoryTrusted(projectPath) {
  if (!projectPath) return false;
  try {
    const brain = require('../brain');
    const raw = brain.getKv('trusted_directories');
    if (!raw) return false;
    const trusted = JSON.parse(raw);
    if (!Array.isArray(trusted)) return false;
    const resolved = path.resolve(projectPath);
    return trusted.some(dir => resolved === dir || resolved.startsWith(dir + path.sep));
  } catch { return false; }
}

/**
 * Add a directory to the trusted list (scoped to git repo root if available).
 */
function trustDirectory(projectPath) {
  if (!projectPath) return;
  const { execFileSync } = require('child_process');
  let trustRoot = path.resolve(projectPath);
  try {
    trustRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
      cwd: projectPath, timeout: 3000, encoding: 'utf8',
    }).trim();
  } catch { /* not a git repo — trust the exact path */ }

  const brain = require('../brain');
  const raw = brain.getKv('trusted_directories');
  const trusted = raw ? JSON.parse(raw) : [];
  if (!trusted.includes(trustRoot)) {
    trusted.push(trustRoot);
    brain.setKv('trusted_directories', JSON.stringify(trusted));
  }
}

/**
 * Remove a directory from the trusted list.
 */
function untrustDirectory(projectPath) {
  if (!projectPath) return;
  const { execFileSync } = require('child_process');
  let resolved = path.resolve(projectPath);
  try {
    resolved = execFileSync('git', ['rev-parse', '--show-toplevel'], {
      cwd: projectPath, timeout: 3000, encoding: 'utf8',
    }).trim();
  } catch { /* not a git repo — use exact path */ }
  const brain = require('../brain');
  const raw = brain.getKv('trusted_directories');
  if (!raw) return;
  const trusted = JSON.parse(raw).filter(d => d !== resolved);
  brain.setKv('trusted_directories', JSON.stringify(trusted));
}

/**
 * Get list of all trusted directories.
 */
function listTrustedDirectories() {
  try {
    const brain = require('../brain');
    const raw = brain.getKv('trusted_directories');
    return raw ? JSON.parse(raw) : [];
  } catch { return []; }
}

/**
 * Check if a tool call is allowed.
 *
 * Accepts extended format with commandTokens and mode:
 * - { tool, command, commandTokens, args, projectPath, sessionId, mode }
 *
 * Returns: { decision: 'allow'|'deny'|'ask', source, reason, ruleId? }
 */
async function checkPermission({ tool, command, commandTokens, commandClauses, args, projectPath, sessionId, mode }) {
  // Layer 1: Hardcoded denylist (safety floor — never overridable)
  if (tool === 'run_shell') {
    // Extract first command token for denylist check
    const cmd = commandTokens ? commandTokens[0] : path.basename(command || '').split(/\s+/)[0];
    if (DENYLIST.has(cmd)) {
      return { decision: 'deny', source: 'denylist', reason: `${cmd} is a destructive command` };
    }
  }

  // Layer 1.5: Per-directory trust — auto-allow file/shell tools if project is trusted
  // Does NOT bypass ALWAYS_ASK_TOOLS (mail_send, slack_send_message, etc.)
  if (projectPath && !ALWAYS_ASK_TOOLS.has(tool) && isDirectoryTrusted(projectPath)) {
    return { decision: 'allow', source: 'trust', reason: `Directory ${projectPath} is trusted` };
  }

  // Layer 2: CTM unified auto-approver (when connected).
  // Shell commands are decided by the SAME auto-approver Claude/Codex use —
  // editable blocklist -> per-clause heuristic risk -> goal-aligned verifier —
  // so policy is a single source of truth. Only `run_shell` is routed: file
  // edits keep the local path-aware rules below (which protect .env/.ssh/.aws —
  // protections the shell approver does not have), and read/mcp/skill keep their
  // local defaults. If CTM is unreachable we fall through to the local rules,
  // so coding never hard-breaks offline.
  if (_ctmBaseUrl && tool === 'run_shell') {
    try {
      const body = { tool, command: command || '', args: args || [], project_path: projectPath || '', session_id: sessionId || '', agent: 'walle' };
      const url = `${_ctmBaseUrl}/api/permissions/walle-check${_ctmToken ? '?token=' + encodeURIComponent(_ctmToken) : ''}`;
      const resp = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(2000),
      });
      if (resp.ok) {
        const result = await resp.json();
        if (result.decision) return result;
        // null decision = no matching rule, fall through
      }
    } catch {
      // CTM unavailable — fall through
    }
  }

  // Layer 2.5: Wildcard rule evaluation (NEW)
  const agentRules = (mode && AGENT_RULES[mode]) || [];
  if (!_userRulesCache) {
    _userRulesCache = loadUserRules(projectPath);
  }
  const allRules = merge(DEFAULT_RULES, agentRules, _userRulesCache);

  if (tool === 'run_shell') {
    // Per-clause evaluation when the full clause list is available. A compound
    // command like `cd ~/x && wc -l f && cat f` must be judged by ALL clauses
    // (most-restrictive wins), not just the first — otherwise a leading `cd`
    // forces an ask (parking the agent) and an allow-listed first clause masks
    // a destructive trailing clause (the `&&`-bypass class).
    if (Array.isArray(commandClauses) && commandClauses.length > 0) {
      const r = evaluateBashClauses(commandClauses, allRules);
      return { decision: r.action, source: 'rules', reason: r.reason };
    }
    // Legacy fallback: only the first clause's tokens (or the raw string) are
    // available. Stay conservative — derive the pattern from BashArity.
    let pattern;
    if (commandTokens && commandTokens.length > 0) {
      const prefix = bashArityPrefix(commandTokens);
      pattern = prefix.join(' ') + ' *';
    } else {
      // Legacy format: use first word of command string
      const cmd = path.basename(command || '').split(/\s+/)[0];
      pattern = cmd + ' *';
    }
    const rule = evaluate('bash', pattern, allRules);
    return { decision: rule.action, source: 'rules', reason: `Rule: bash/${pattern} -> ${rule.action}` };
  }

  if (tool === 'write_file' || tool === 'edit_file') {
    const filePath = command || '';
    const rule = evaluate('edit', filePath, allRules);
    return { decision: rule.action, source: 'rules', reason: `Rule: edit/${path.basename(filePath)} -> ${rule.action}` };
  }

  if (tool === 'read_file') {
    const filePath = command || '';
    const rule = evaluate('read', filePath, allRules);
    return { decision: rule.action, source: 'rules', reason: `Rule: read/${path.basename(filePath)} -> ${rule.action}` };
  }

  if (tool === 'mcp_call') {
    const pattern = command || '*';
    const rule = evaluate('mcp', pattern, allRules);
    return { decision: rule.action, source: 'rules', reason: `Rule: mcp/${pattern} -> ${rule.action}` };
  }

  if (tool === 'skill') {
    const pattern = command || '*';
    const rule = evaluate('skill', pattern, allRules);
    return { decision: rule.action, source: 'rules', reason: `Rule: skill/${pattern} -> ${rule.action}` };
  }

  // Per-tool permission check (always-ask tools and others)
  const rule = evaluate(tool, '*', allRules);
  if (rule.action !== 'ask') {
    return { decision: rule.action, source: 'rules', reason: `Rule: ${tool}/* -> ${rule.action}` };
  }

  // Check ALWAYS_ASK_TOOLS for backwards compat
  if (ALWAYS_ASK_TOOLS.has(tool)) {
    return { decision: 'ask', source: 'rules', reason: `${tool} requires user approval` };
  }

  // Default: allow (internal tools like think, search_memories, etc.)
  return { decision: 'allow', source: 'default', reason: 'No restrictions' };
}

/**
 * Record a permission decision (for learning)
 */
async function recordDecision({ tool, command, args, decision, sessionId, projectPath }) {
  if (!_ctmBaseUrl) return;
  try {
    const url = `${_ctmBaseUrl}/api/permissions/walle-record${_ctmToken ? '?token=' + encodeURIComponent(_ctmToken) : ''}`;
    await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ tool, command: command || '', args: args || [], decision, session_id: sessionId || '', project_path: projectPath || '', agent: 'walle' }),
      signal: AbortSignal.timeout(2000),
    });
  } catch {}
}

module.exports = {
  checkPermission, recordDecision, configure,
  evaluateBashClauses,
  DENYLIST, ALWAYS_ASK_TOOLS, NAVIGATION_BUILTINS,
  isDirectoryTrusted, trustDirectory, untrustDirectory, listTrustedDirectories,
};
