#!/usr/bin/env node
/**
 * scout-checker.cjs - Facade for scout-block modules
 *
 * Provides unified interface to scout-block/* modules for reuse in both
 * Claude hooks and OpenCode plugins.
 *
 * @module scout-checker
 */

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

// Import scout-block modules
const { loadPatterns, createMatcher, matchPath } = require('../scout-block/pattern-matcher.cjs');
const { extractFromToolInput } = require('../scout-block/path-extractor.cjs');
const { detectBroadPatternIssue } = require('../scout-block/broad-pattern-detector.cjs');

// ═══════════════════════════════════════════════════════════════════════════
// COMMAND PATTERNS
// ═══════════════════════════════════════════════════════════════════════════

// Build command allowlist - these are allowed even if they contain blocked paths
// Handles flags and filters: npm build, pnpm --filter web run build, yarn workspace app build
const BUILD_COMMAND_PATTERN = /^(npm|pnpm|yarn|bun)\s+([^\s]+\s+)*(run\s+)?(build|test|lint|dev|start|install|ci|add|remove|update|publish|pack|init|create|exec)/;

// Tool commands - JS/TS, Go, Rust, Java, .NET, containers, IaC, Python, Ruby, PHP, Deno, Elixir
const TOOL_COMMAND_PATTERN = /^(\.\/)?(npx|pnpx|bunx|tsc|esbuild|vite|webpack|rollup|turbo|nx|jest|vitest|mocha|eslint|prettier|go|cargo|make|mvn|mvnw|gradle|gradlew|dotnet|docker|podman|kubectl|helm|terraform|ansible|bazel|cmake|sbt|flutter|swift|ant|ninja|meson|python3?|pip|uv|deno|bundle|rake|gem|php|composer|ruby|mix|elixir)/;

// Allow execution from .venv/bin/ or venv/bin/ (Unix) and .venv/Scripts/ or venv/Scripts/ (Windows)
const VENV_EXECUTABLE_PATTERN = /(^|[\/\\])\.?venv[\/\\](bin|Scripts)[\/\\]/;

// Allow Python venv creation commands (cross-platform):
// - python/python3 -m venv (Unix/macOS/Windows)
// - py -m venv (Windows py launcher, supports -3, -3.11, etc.)
// - uv venv (fast Rust-based Python package manager)
// - virtualenv (legacy but still widely used)
const VENV_CREATION_PATTERN = /^(python3?|py)\s+(-[\w.]+\s+)*-m\s+venv\s+|^uv\s+venv(\s|$)|^virtualenv\s+/;

// ═══════════════════════════════════════════════════════════════════════════
// HELPER FUNCTIONS
// ═══════════════════════════════════════════════════════════════════════════

/**
 * Strip leading ENV variable assignments and command wrappers (sudo, env, etc.)
 * e.g., "NODE_ENV=production npm run build" → "npm run build"
 * @param {string} command - The command to strip
 * @returns {string}
 */
function stripCommandPrefix(command) {
  if (!command || typeof command !== 'string') return command;
  let stripped = command.trim();
  // Strip env var assignments (KEY=VALUE KEY2=VALUE2 ...)
  stripped = stripped.replace(/^(\w+=\S+\s+)+/, '');
  // Strip common command wrappers (one level)
  stripped = stripped.replace(/^(sudo|env|nice|nohup|time|timeout)\s+/, '');
  // Strip env vars again (sudo env VAR=x cmd)
  stripped = stripped.replace(/^(\w+=\S+\s+)+/, '');
  return stripped.trim();
}

/**
 * Check if a command is a build/tooling command (should be allowed)
 * @param {string} command - The command to check
 * @returns {boolean}
 */
function isBuildCommand(command) {
  if (!command || typeof command !== 'string') return false;
  const trimmed = command.trim();
  return BUILD_COMMAND_PATTERN.test(trimmed) || TOOL_COMMAND_PATTERN.test(trimmed);
}

/**
 * Split a compound command into sub-commands on &&, ||, and ;.
 * Does NOT split on newlines — newlines in command strings are typically
 * heredoc bodies or multiline strings, not compound operators.
 * Does not handle operators inside quoted strings (extremely rare in practice).
 *
 * @param {string} command - The compound command string
 * @returns {string[]} Array of sub-commands (trimmed, non-empty)
 */
function splitCompoundCommand(command) {
  if (!command || typeof command !== 'string') return [];
  return command.split(/\s*(?:&&|\|\||;)\s*/).filter(cmd => cmd && cmd.trim().length > 0);
}

/**
 * Unwrap shell executor wrappers (bash -c "...", sh -c '...', eval "...").
 * Returns the inner command string for re-processing.
 * @param {string} command - The command to unwrap
 * @returns {string} Inner command, or original if not a shell executor
 */
function unwrapShellExecutor(command) {
  if (!command || typeof command !== 'string') return command;
  const match = command.trim().match(
    /^(?:(?:bash|sh|zsh)\s+-c|eval)\s+["'](.+)["']\s*$/
  );
  return match ? match[1] : command;
}

/**
 * Check if command executes from a .venv bin directory
 * @param {string} command - The command to check
 * @returns {boolean}
 */
function isVenvExecutable(command) {
  if (!command || typeof command !== 'string') return false;
  return VENV_EXECUTABLE_PATTERN.test(command);
}

/**
 * Check if command creates a Python virtual environment
 * @param {string} command - The command to check
 * @returns {boolean}
 */
function isVenvCreationCommand(command) {
  if (!command || typeof command !== 'string') return false;
  return VENV_CREATION_PATTERN.test(command.trim());
}

/**
 * Check if command should be allowed (build, venv executable, or venv creation)
 * Strips ENV prefixes and command wrappers before checking.
 * @param {string} command - The command to check
 * @returns {boolean}
 */
function isAllowedCommand(command) {
  const stripped = stripCommandPrefix(command);
  return isBuildCommand(stripped) || isVenvExecutable(stripped) || isVenvCreationCommand(stripped);
}

function findGitRoot(startDir) {
  if (!startDir || typeof startDir !== 'string') return null;

  let dir = path.resolve(startDir);
  const root = path.parse(dir).root;

  while (true) {
    if (fs.existsSync(path.join(dir, '.git')) || dir === root) {
      return fs.existsSync(path.join(dir, '.git')) ? dir : null;
    }

    dir = path.dirname(dir);
  }
}

/**
 * Find an optional project-local .ckignore at the git root config directory.
 * This keeps overrides stable regardless of the caller cwd inside the repo.
 *
 * @param {string} startDir - Directory to start searching from
 * @param {string} [configDirName] - Config directory at git root (.claude, .opencode)
 * @returns {string|null}
 */
function findProjectCkignore(startDir, configDirName) {
  if (!configDirName || typeof configDirName !== 'string') return null;
  const gitRoot = findGitRoot(startDir);
  if (!gitRoot) return null;
  const candidate = path.join(gitRoot, configDirName, '.ckignore');
  return fs.existsSync(candidate) ? candidate : null;
}

// ═══════════════════════════════════════════════════════════════════════════
// MAIN ENTRY POINT
// ═══════════════════════════════════════════════════════════════════════════

/**
 * Check if a tool call accesses blocked directories or uses overly broad patterns
 *
 * @param {Object} params
 * @param {string} params.toolName - Name of tool (Bash, Glob, Read, etc.)
 * @param {Object} params.toolInput - Tool input with file_path, path, pattern, command
 * @param {Object} [params.options]
 * @param {string} [params.options.ckignorePath] - Path to .ckignore file
 * @param {string} [params.options.projectCkignorePath] - Explicit project-local .ckignore path
 * @param {string} [params.options.claudeDir] - Path to .claude or .opencode directory
 * @param {string} [params.options.cwd] - Working directory used to discover a project .ckignore
 * @param {string} [params.options.projectConfigDirName] - Git-root config dir for project-local overrides
 * @param {boolean} [params.options.checkBroadPatterns] - Check for overly broad glob patterns (default: true)
 * @returns {{
 *   blocked: boolean,
  *   path?: string,
  *   pattern?: string,
  *   reason?: string,
 *   configPath?: string,
  *   isBroadPattern?: boolean,
  *   suggestions?: string[],
  *   isAllowedCommand?: boolean
 * }}
 */
function checkScoutBlock({ toolName, toolInput, options = {} }) {
  const {
    ckignorePath,
    projectCkignorePath,
    claudeDir = path.join(process.cwd(), '.claude'),
    cwd = process.cwd(),
    projectConfigDirName,
    checkBroadPatterns = true
  } = options;

  // Unwrap shell executor wrappers (bash -c "...", eval "...")
  // so the inner command gets properly analyzed
  if (toolInput.command) {
    const unwrapped = unwrapShellExecutor(toolInput.command);
    if (unwrapped !== toolInput.command) {
      toolInput = { ...toolInput, command: unwrapped };
    }
  }

  // For Bash commands, split compound commands (&&, ||, ;) and check
  // each sub-command independently. This prevents "echo msg && npm run build"
  // from being blocked due to "build" token in the allowed build sub-command.
  // Must split BEFORE isAllowedCommand because BUILD_COMMAND_PATTERN has no end
  // anchor and would match the prefix of "npm run build && cat dist/file.js".
  if (toolInput.command) {
    const subCommands = splitCompoundCommand(toolInput.command);
    const nonAllowed = subCommands.filter(cmd => !isAllowedCommand(cmd.trim()));
    if (nonAllowed.length === 0) {
      return { blocked: false, isAllowedCommand: true };
    }
    // Only extract paths from non-allowed sub-commands
    if (nonAllowed.length < subCommands.length) {
      toolInput = { ...toolInput, command: nonAllowed.join(' ; ') };
    }
  }

  // Check for overly broad glob patterns (Glob tool)
  if (checkBroadPatterns && (toolName === 'Glob' || toolInput.pattern)) {
    const broadResult = detectBroadPatternIssue(toolInput);
    if (broadResult.blocked) {
      return {
        blocked: true,
        isBroadPattern: true,
        pattern: toolInput.pattern,
        reason: broadResult.reason || 'Pattern too broad - may fill context with too many files',
        suggestions: broadResult.suggestions || []
      };
    }
  }

  // Resolve .ckignore path
  const resolvedCkignorePath = ckignorePath || path.join(claudeDir, '.ckignore');
  const discoveredProjectCkignorePath = projectCkignorePath || findProjectCkignore(cwd, projectConfigDirName);
  const resolvedProjectCkignorePath = discoveredProjectCkignorePath
    && path.resolve(discoveredProjectCkignorePath) !== path.resolve(resolvedCkignorePath)
      ? discoveredProjectCkignorePath
      : null;
  const configPath = resolvedProjectCkignorePath || resolvedCkignorePath;

  // Load patterns and create matcher
  const patterns = loadPatterns(resolvedCkignorePath, resolvedProjectCkignorePath);
  const matcher = createMatcher(patterns);

  // Extract paths from tool input
  const extractedPaths = extractFromToolInput(toolInput);

  // If no paths extracted, allow operation
  if (extractedPaths.length === 0) {
    return { blocked: false };
  }

  // Check each path against patterns
  for (const extractedPath of extractedPaths) {
    const result = matchPath(matcher, extractedPath);
    if (result.blocked) {
      return {
        blocked: true,
        path: extractedPath,
        pattern: result.pattern,
        configPath,
        reason: `Path matches blocked pattern: ${result.pattern}`
      };
    }
  }

  // All paths allowed
  return { blocked: false };
}

// ═══════════════════════════════════════════════════════════════════════════
// EXPORTS
// ═══════════════════════════════════════════════════════════════════════════

module.exports = {
  // Main entry point
  checkScoutBlock,

  // Command checkers
  isBuildCommand,
  isVenvExecutable,
  isVenvCreationCommand,
  isAllowedCommand,
  splitCompoundCommand,
  stripCommandPrefix,
  unwrapShellExecutor,
  findGitRoot,
  findProjectCkignore,

  // Re-export scout-block modules for direct access
  loadPatterns,
  createMatcher,
  matchPath,
  extractFromToolInput,
  detectBroadPatternIssue,

  // Patterns (for testing)
  BUILD_COMMAND_PATTERN,
  TOOL_COMMAND_PATTERN,
  VENV_EXECUTABLE_PATTERN,
  VENV_CREATION_PATTERN
};
