#!/usr/bin/env php
<?php

declare(strict_types=1);

/**
 * PHP/Laravel Security Linter CLI
 */

$autoloaderPaths = [
    __DIR__ . '/../vendor/autoload.php',
    __DIR__ . '/../../../autoload.php',
];

$autoloaderFound = false;
foreach ($autoloaderPaths as $path) {
    if (file_exists($path)) {
        require $path;
        $autoloaderFound = true;
        break;
    }
}

if (!$autoloaderFound) {
    fwrite(STDERR, "Error: Could not find autoloader. Please run 'composer install'.\n");
    exit(1);
}

use SecurityLinter\SecurityLinter;
use SecurityLinter\Report\Vulnerability;
use SecurityLinter\I18n\Messages;

/**
 * CLI Application
 */
class SecurityLintCLI
{
    private const VERSION = '0.0.1';

    /** @var array Default directories/patterns to exclude */
    private const DEFAULT_EXCLUDES = [
        'vendor/*',
        'node_modules/*',
        'storage/*',
        'storage/framework/*',
        'bootstrap/cache/*',
        'public/vendor/*',
        'public/build/*',
        '.git/*',
        '.svn/*',
        '.idea/*',
        '.vscode/*',
        'cache/*',
        'tmp/*',
        'temp/*',
        'logs/*',
        '*.log',
        '*.cache',
        'tests/*',              // Exclude tests by default (configurable)
        'test-samples/*',       // Exclude test samples
        'test-projects/*',      // Exclude cloned test projects
        'database/migrations/*', // Migration files are usually safe
    ];

    private array $options = [
        'format' => 'text',
        'severity' => 'low',
        'output' => null,
        'exclude' => [],
        'include' => [],      // Patterns to explicitly include (overrides exclude)
        'config' => null,
        'recursive-depth' => 10,
        'no-colors' => false,
        'quiet' => false,
        'verbose' => false,
        'lang' => 'ja',
        'include-vendor' => false,  // Include vendor directory
        'include-tests' => false,   // Include tests directory
        'show-excluded' => false,   // Show what is being excluded
        'no-default-excludes' => false, // Don't apply default excludes
        'context' => 0,             // Number of context lines to show (0 = disabled)
    ];

    private array $colors = [
        'reset' => "\033[0m",
        'red' => "\033[31m",
        'green' => "\033[32m",
        'yellow' => "\033[33m",
        'blue' => "\033[34m",
        'magenta' => "\033[35m",
        'cyan' => "\033[36m",
        'white' => "\033[37m",
        'bold' => "\033[1m",
        'dim' => "\033[2m",
        // Syntax highlighting colors
        'syn_keyword' => "\033[38;5;198m",    // Pink/magenta for keywords
        'syn_string' => "\033[38;5;113m",     // Green for strings
        'syn_variable' => "\033[38;5;208m",   // Orange for variables
        'syn_comment' => "\033[38;5;245m",    // Gray for comments
        'syn_number' => "\033[38;5;141m",     // Purple for numbers
        'syn_function' => "\033[38;5;81m",    // Cyan for functions
        'syn_class' => "\033[38;5;221m",      // Yellow for class names
        'syn_operator' => "\033[38;5;248m",   // Light gray for operators
        'syn_constant' => "\033[38;5;141m",   // Purple for constants
    ];

    public function run(array $argv): int
    {
        $args = $this->parseArguments($argv);

        if (isset($args['help']) || isset($args['h'])) {
            $this->showHelp();
            return 0;
        }

        if (isset($args['version']) || isset($args['v'])) {
            $this->showVersion();
            return 0;
        }

        $target = $args['_'][0] ?? '.';

        if (!file_exists($target)) {
            $this->error("Target not found: {$target}");
            return 1;
        }

        $this->options = array_merge($this->options, $this->extractOptions($args));

        // Set language
        Messages::setLocale($this->options['lang']);

        if ($this->options['no-colors'] || !$this->isInteractive()) {
            $this->colors = array_fill_keys(array_keys($this->colors), '');
        }

        return $this->analyze($target);
    }

    private function analyze(string $target): int
    {
        $startTime = microtime(true);

        // Load config file if exists
        $this->loadConfigFile($target);

        if (!$this->options['quiet']) {
            $this->printBanner();
            $this->info(Messages::get('cli.analyzing', ['path' => $target]));
            $this->info("");
        }

        // Build exclude patterns
        $excludePatterns = $this->buildExcludePatterns();

        // Show excluded directories if requested
        if ($this->options['show-excluded']) {
            $this->showExcludedPatterns($excludePatterns, $target);
        }

        $config = [
            'recursive_depth' => (int) $this->options['recursive-depth'],
            'exclude_patterns' => $excludePatterns,
            'include_patterns' => $this->options['include'],
        ];

        $linter = new SecurityLinter($config);

        try {
            if (is_dir($target)) {
                $vulnerabilities = $linter->analyzeDirectory($target);
            } else {
                $vulnerabilities = $linter->analyzeFile($target);
            }
        } catch (\Exception $e) {
            $this->error("Analysis failed: " . $e->getMessage());
            if ($this->options['verbose']) {
                $this->error($e->getTraceAsString());
            }
            return 1;
        }

        $vulnerabilities = $linter->getVulnerabilities($this->options['severity']);
        $report = $linter->generateReport($this->options['format'], $vulnerabilities);
        $stats = $this->calculateStats($vulnerabilities);

        if ($this->options['output']) {
            file_put_contents($this->options['output'], $report);
            if (!$this->options['quiet']) {
                $this->success(Messages::get('cli.report_written', ['path' => $this->options['output']]));
            }
        } else {
            if ($this->options['format'] === 'text') {
                $this->printTextReport($vulnerabilities, $stats);
            } else {
                echo $report;
            }
        }

        $elapsed = round(microtime(true) - $startTime, 2);

        if (!$this->options['quiet']) {
            $this->info("");
            $this->info(Messages::get('cli.completed', ['time' => $elapsed]));
        }

        if ($stats['by_severity']['critical'] > 0 || $stats['by_severity']['high'] > 0) {
            return 2;
        }
        if ($stats['total'] > 0) {
            return 1;
        }
        return 0;
    }

    private function printTextReport(array $vulnerabilities, array $stats): void
    {
        if (empty($vulnerabilities)) {
            $this->success(Messages::get('cli.no_vulnerabilities'));
            return;
        }

        $grouped = [
            'critical' => [],
            'high' => [],
            'medium' => [],
            'low' => [],
        ];

        foreach ($vulnerabilities as $vuln) {
            $grouped[$vuln->getSeverity()][] = $vuln;
        }

        foreach (['critical', 'high', 'medium', 'low'] as $severity) {
            if (empty($grouped[$severity])) {
                continue;
            }

            $color = $this->getSeverityColor($severity);
            $count = count($grouped[$severity]);
            $label = Messages::get("severity.{$severity}");

            echo "\n";
            echo $this->colors['bold'] . $color;
            echo strtoupper($label) . " ({$count})";
            echo $this->colors['reset'] . "\n";
            echo str_repeat("─", 60) . "\n";

            foreach ($grouped[$severity] as $vuln) {
                $this->printVulnerability($vuln);
            }
        }

        echo "\n" . str_repeat("═", 60) . "\n";
        echo $this->colors['bold'] . Messages::get('cli.summary') . $this->colors['reset'] . "\n";
        echo str_repeat("─", 60) . "\n";

        $summaryItems = [
            [Messages::get('severity.critical'), $stats['by_severity']['critical'], 'red'],
            [Messages::get('severity.high'), $stats['by_severity']['high'], 'yellow'],
            [Messages::get('severity.medium'), $stats['by_severity']['medium'], 'cyan'],
            [Messages::get('severity.low'), $stats['by_severity']['low'], 'dim'],
        ];

        foreach ($summaryItems as [$label, $count, $color]) {
            $colorCode = $this->colors[$color] ?? '';
            echo sprintf("  %-12s %s%d%s\n", $label . ":", $colorCode, $count, $this->colors['reset']);
        }

        echo str_repeat("─", 60) . "\n";
        echo sprintf("  %-12s %s%d%s\n", Messages::get('cli.total') . ":", $this->colors['bold'], $stats['total'], $this->colors['reset']);
    }

    private function printVulnerability(Vulnerability $vuln): void
    {
        $color = $this->getSeverityColor($vuln->getSeverity());

        echo "\n" . $color . $this->colors['bold'];
        echo "[{$vuln->getType()}]" . $this->colors['reset'] . " ";
        echo $vuln->getMessage() . "\n";

        echo $this->colors['dim'];
        echo "  📍 {$vuln->getFile()}:{$vuln->getLine()}\n";
        echo $this->colors['reset'];

        // Show code context if enabled
        if ($this->options['context'] > 0) {
            $this->printCodeContext($vuln->getFile(), $vuln->getLine(), $this->options['context']);
        } elseif ($vuln->getCode()) {
            echo $this->colors['cyan'];
            echo "  💻 " . $this->truncate($vuln->getCode(), 70) . "\n";
            echo $this->colors['reset'];
        }

        $meta = [];
        if ($vuln->getCweId()) {
            $meta[] = $vuln->getCweId();
        }
        if ($vuln->getOwaspCategory()) {
            $meta[] = $vuln->getOwaspCategory();
        }
        if (!empty($meta)) {
            echo $this->colors['dim'];
            echo "  🏷️  " . implode(" | ", $meta) . "\n";
            echo $this->colors['reset'];
        }

        if (!empty($vuln->getCallTrace()) && $this->options['verbose']) {
            echo $this->colors['magenta'];
            echo "  📚 " . Messages::get('cli.call_trace') . ":\n";
            foreach ($vuln->getCallTrace() as $trace) {
                echo "     → {$trace['function']} at {$trace['file']}:{$trace['line']}\n";
            }
            echo $this->colors['reset'];
        }

        if ($vuln->getRecommendation()) {
            echo $this->colors['green'];
            echo "  💡 " . $vuln->getRecommendation() . "\n";
            echo $this->colors['reset'];
        }
    }

    /**
     * Print code context around the vulnerable line
     */
    private function printCodeContext(string $file, int $line, int $contextLines): void
    {
        // Handle Docker path mapping - try both /target/ prefix and without
        $filePath = $file;
        if (!file_exists($filePath) && str_starts_with($file, '/target/')) {
            $filePath = substr($file, 8); // Remove '/target/' prefix
        }

        if (!file_exists($filePath)) {
            return;
        }

        $lines = @file($filePath);
        if ($lines === false) {
            return;
        }

        $startLine = max(1, $line - $contextLines);
        $endLine = min(count($lines), $line + $contextLines);

        echo "\n";
        echo $this->colors['dim'] . "  ┌─ " . basename($file) . "\n";

        for ($i = $startLine; $i <= $endLine; $i++) {
            $lineContent = rtrim($lines[$i - 1] ?? '');
            $lineNum = str_pad((string)$i, 4, ' ', STR_PAD_LEFT);

            // Apply syntax highlighting
            $highlightedContent = $this->highlightPhpSyntax($lineContent);

            if ($i === $line) {
                // Highlight the vulnerable line with red background indicator
                echo $this->colors['red'] . $this->colors['bold'];
                echo "  │ {$lineNum} ▶ ";
                echo $this->colors['reset'];
                echo $highlightedContent . "\n";
            } else {
                echo $this->colors['dim'];
                echo "  │ {$lineNum}   ";
                echo $this->colors['reset'];
                echo $highlightedContent . "\n";
            }
        }

        echo $this->colors['dim'] . "  └─\n" . $this->colors['reset'];
    }

    private function getSeverityColor(string $severity): string
    {
        return match ($severity) {
            'critical' => $this->colors['red'] . $this->colors['bold'],
            'high' => $this->colors['yellow'],
            'medium' => $this->colors['cyan'],
            'low' => $this->colors['dim'],
            default => $this->colors['white'],
        };
    }

    private function printBanner(): void
    {
        echo $this->colors['cyan'] . $this->colors['bold'];
        echo "╔════════════════════════════════════════════════════════════╗\n";
        echo "║       " . Messages::get('cli.banner') . " v" . self::VERSION . "                 ║\n";
        echo "╚════════════════════════════════════════════════════════════╝\n";
        echo $this->colors['reset'];
    }

    private function showHelp(): void
    {
        $isJapanese = ($this->options['lang'] ?? 'ja') === 'ja';

        $this->printBanner();

        if ($isJapanese) {
            echo <<<HELP

{$this->colors['bold']}使用方法:{$this->colors['reset']}
    security-lint [オプション] <パス>

{$this->colors['bold']}引数:{$this->colors['reset']}
    <パス>              解析するファイルまたはディレクトリ (デフォルト: カレントディレクトリ)

{$this->colors['bold']}出力オプション:{$this->colors['reset']}
    -h, --help          このヘルプメッセージを表示
    -v, --version       バージョン情報を表示
    -f, --format        出力形式: text, json, html, sarif, markdown (デフォルト: text)
    -s, --severity      報告する最小重大度: low, medium, high, critical (デフォルト: low)
    -o, --output        標準出力の代わりにファイルにレポートを書き込み
    -l, --lang          出力言語: ja, en (デフォルト: ja)
    -c, --context [N]   問題行の前後N行のコードスニペットを表示 (デフォルト: 3)
    --no-colors         カラー出力を無効化
    -q, --quiet         進捗出力を抑制
    --verbose           コールトレースを含む詳細情報を表示

{$this->colors['bold']}除外/包含オプション:{$this->colors['reset']}
    -e, --exclude       除外パターン (複数回使用可能)
    -i, --include       包含パターン (除外を上書き、複数回使用可能)
    --include-vendor    vendor ディレクトリも解析 (依存関係のセキュリティ監査用)
    --include-tests     tests ディレクトリも解析
    --no-default-excludes  デフォルト除外パターンを使用しない
    --show-excluded     除外されるパターンとディレクトリを表示

{$this->colors['bold']}解析オプション:{$this->colors['reset']}
    -d, --recursive-depth  コール追跡の最大深度 (デフォルト: 10)

{$this->colors['bold']}デフォルト除外パターン:{$this->colors['reset']}
    vendor/*, node_modules/*, storage/*, .git/*, tests/*,
    bootstrap/cache/*, public/vendor/*, cache/*, tmp/*

{$this->colors['bold']}設定ファイル:{$this->colors['reset']}
    .security-lint.json をプロジェクトルートに配置することで設定を永続化できます。
    例: {"exclude": ["custom/*"], "severity": "medium", "includeTests": true}

{$this->colors['bold']}使用例:{$this->colors['reset']}
    security-lint app/                    # app ディレクトリをスキャン
    security-lint -f json -o report.json  # JSONレポートを生成
    security-lint -s high                 # 高/クリティカルの問題のみ表示
    security-lint -c                      # コードスニペット表示 (前後3行)
    security-lint -c 5                    # コードスニペット表示 (前後5行)
    security-lint -e "tests/*"            # tests ディレクトリを除外
    security-lint --include-vendor        # vendor も含めて解析
    security-lint --show-excluded         # 除外パターンを確認
    security-lint -l en                   # 英語で出力

{$this->colors['bold']}終了コード:{$this->colors['reset']}
    0  問題なし
    1  問題あり (中/低重大度)
    2  クリティカル/高重大度の問題あり

{$this->colors['bold']}検出可能な脆弱性:{$this->colors['reset']}
    • XSS (クロスサイトスクリプティング)
    • SQLインジェクション
    • コマンドインジェクション
    • パストラバーサル
    • CSRF脆弱性
    • 認証セキュリティの問題
    • セッションセキュリティの問題
    • 設定セキュリティの問題

HELP;
        } else {
            echo <<<HELP

{$this->colors['bold']}USAGE:{$this->colors['reset']}
    security-lint [options] <path>

{$this->colors['bold']}ARGUMENTS:{$this->colors['reset']}
    <path>              File or directory to analyze (default: current directory)

{$this->colors['bold']}OUTPUT OPTIONS:{$this->colors['reset']}
    -h, --help          Show this help message
    -v, --version       Show version information
    -f, --format        Output format: text, json, html, sarif, markdown (default: text)
    -s, --severity      Minimum severity to report: low, medium, high, critical (default: low)
    -o, --output        Write report to file instead of stdout
    -l, --lang          Output language: ja, en (default: ja)
    -c, --context [N]   Show N lines of code context around issues (default: 3)
    --no-colors         Disable colored output
    -q, --quiet         Suppress progress output
    --verbose           Show detailed information including call traces

{$this->colors['bold']}EXCLUDE/INCLUDE OPTIONS:{$this->colors['reset']}
    -e, --exclude       Exclude patterns (can be used multiple times)
    -i, --include       Include patterns (overrides excludes, can be used multiple times)
    --include-vendor    Also analyze vendor directory (for dependency security audit)
    --include-tests     Also analyze tests directory
    --no-default-excludes  Do not apply default exclude patterns
    --show-excluded     Show excluded patterns and directories

{$this->colors['bold']}ANALYSIS OPTIONS:{$this->colors['reset']}
    -d, --recursive-depth  Maximum depth for call tracing (default: 10)

{$this->colors['bold']}DEFAULT EXCLUDE PATTERNS:{$this->colors['reset']}
    vendor/*, node_modules/*, storage/*, .git/*, tests/*,
    bootstrap/cache/*, public/vendor/*, cache/*, tmp/*

{$this->colors['bold']}CONFIGURATION FILE:{$this->colors['reset']}
    Place .security-lint.json in your project root to persist settings.
    Example: {"exclude": ["custom/*"], "severity": "medium", "includeTests": true}

{$this->colors['bold']}EXAMPLES:{$this->colors['reset']}
    security-lint app/                    # Scan app directory
    security-lint -f json -o report.json  # Generate JSON report
    security-lint -s high                 # Only show high/critical issues
    security-lint -c                      # Show code snippets (3 lines context)
    security-lint -c 5                    # Show code snippets (5 lines context)
    security-lint -e "tests/*"            # Exclude tests directory
    security-lint --include-vendor        # Include vendor in analysis
    security-lint --show-excluded         # Show what is being excluded
    security-lint -l en                   # Output in English

{$this->colors['bold']}EXIT CODES:{$this->colors['reset']}
    0  No issues found
    1  Issues found (medium/low severity)
    2  Critical/high severity issues found

{$this->colors['bold']}DETECTED VULNERABILITIES:{$this->colors['reset']}
    • XSS (Cross-Site Scripting)
    • SQL Injection
    • Command Injection
    • Path Traversal
    • CSRF vulnerabilities
    • Insecure Authentication
    • Session Security issues
    • Insecure Configuration

HELP;
        }
    }

    private function showVersion(): void
    {
        echo Messages::get('cli.banner') . " v" . self::VERSION . "\n";
    }

    private function parseArguments(array $argv): array
    {
        $args = ['_' => []];
        $i = 1;

        // Options that take optional numeric values only
        $numericOnlyOptions = ['c', 'context'];

        while ($i < count($argv)) {
            $arg = $argv[$i];

            if (str_starts_with($arg, '--')) {
                $option = substr($arg, 2);
                if (str_contains($option, '=')) {
                    [$key, $value] = explode('=', $option, 2);
                    $args[$key] = $value;
                } else {
                    $nextArg = $argv[$i + 1] ?? null;
                    $isNumericOnly = in_array($option, $numericOnlyOptions, true);

                    if ($nextArg !== null && !str_starts_with($nextArg, '-')) {
                        // For numeric-only options, only consume if next arg is numeric
                        if ($isNumericOnly) {
                            if (ctype_digit($nextArg)) {
                                $args[$option] = $argv[++$i];
                            } else {
                                $args[$option] = true;
                            }
                        } else {
                            $args[$option] = $argv[++$i];
                        }
                    } else {
                        $args[$option] = true;
                    }
                }
            } elseif (str_starts_with($arg, '-')) {
                $chars = substr($arg, 1);
                for ($j = 0; $j < strlen($chars); $j++) {
                    $char = $chars[$j];
                    $isNumericOnly = in_array($char, $numericOnlyOptions, true);
                    $nextArg = $argv[$i + 1] ?? null;

                    if ($j === strlen($chars) - 1 && $nextArg !== null && !str_starts_with($nextArg, '-')) {
                        // For numeric-only options, only consume if next arg is numeric
                        if ($isNumericOnly) {
                            if (ctype_digit($nextArg)) {
                                $args[$char] = $argv[++$i];
                            } else {
                                $args[$char] = true;
                            }
                        } else {
                            $args[$char] = $argv[++$i];
                        }
                    } else {
                        $args[$char] = true;
                    }
                }
            } else {
                $args['_'][] = $arg;
            }

            $i++;
        }

        return $args;
    }

    private function extractOptions(array $args): array
    {
        $options = [];

        $format = $args['format'] ?? $args['f'] ?? null;
        if ($format !== null && $format !== true) {
            $options['format'] = $format;
        }

        $severity = $args['severity'] ?? $args['s'] ?? null;
        if ($severity !== null && $severity !== true) {
            $options['severity'] = $severity;
        }

        $output = $args['output'] ?? $args['o'] ?? null;
        if ($output !== null && $output !== true) {
            $options['output'] = $output;
        }

        // Exclude patterns (can be specified multiple times)
        $exclude = $args['exclude'] ?? $args['e'] ?? [];
        if (!is_array($exclude)) {
            $exclude = [$exclude];
        }
        $options['exclude'] = $exclude;

        // Include patterns (override excludes)
        $include = $args['include'] ?? $args['i'] ?? [];
        if (!is_array($include)) {
            $include = [$include];
        }
        $options['include'] = $include;

        $depth = $args['recursive-depth'] ?? $args['d'] ?? null;
        if ($depth !== null && $depth !== true) {
            $options['recursive-depth'] = $depth;
        }

        $lang = $args['lang'] ?? $args['l'] ?? null;
        if ($lang !== null && $lang !== true) {
            $options['lang'] = $lang;
        }

        $options['no-colors'] = isset($args['no-colors']);
        $options['quiet'] = isset($args['quiet']) || isset($args['q']);
        $options['verbose'] = isset($args['verbose']);

        // New options
        $options['include-vendor'] = isset($args['include-vendor']);
        $options['include-tests'] = isset($args['include-tests']);
        $options['show-excluded'] = isset($args['show-excluded']);
        $options['no-default-excludes'] = isset($args['no-default-excludes']);

        // Context lines option
        $context = $args['context'] ?? $args['c'] ?? null;
        if ($context !== null) {
            $options['context'] = $context === true ? 3 : (int) $context;
        }

        return $options;
    }

    private function info(string $message): void
    {
        if (!$this->options['quiet']) {
            echo $message . "\n";
        }
    }

    private function success(string $message): void
    {
        echo $this->colors['green'] . "✓ " . $message . $this->colors['reset'] . "\n";
    }

    private function error(string $message): void
    {
        fwrite(STDERR, $this->colors['red'] . "✗ " . $message . $this->colors['reset'] . "\n");
    }

    private function truncate(string $text, int $length): string
    {
        $text = str_replace(["\n", "\r", "\t"], ' ', $text);
        $text = preg_replace('/\s+/', ' ', $text);

        if (strlen($text) <= $length) {
            return $text;
        }

        return substr($text, 0, $length - 3) . '...';
    }

    private function isInteractive(): bool
    {
        return function_exists('posix_isatty') && posix_isatty(STDOUT);
    }

    /**
     * Build the list of exclude patterns based on options
     */
    private function buildExcludePatterns(): array
    {
        $patterns = [];

        // Add default excludes unless disabled
        if (!$this->options['no-default-excludes']) {
            $patterns = self::DEFAULT_EXCLUDES;

            // Remove vendor from excludes if --include-vendor is set
            if ($this->options['include-vendor']) {
                $patterns = array_filter($patterns, fn($p) => !str_starts_with($p, 'vendor'));
            }

            // Remove tests from excludes if --include-tests is set
            if ($this->options['include-tests']) {
                $patterns = array_filter($patterns, fn($p) => !str_starts_with($p, 'tests'));
            }
        }

        // Add user-specified excludes
        $patterns = array_merge($patterns, $this->options['exclude']);

        // Remove duplicates
        return array_unique(array_values($patterns));
    }

    /**
     * Load configuration from .security-lint.json if it exists
     */
    private function loadConfigFile(string $target): void
    {
        // Look for config file in target directory or current directory
        $configPaths = [];

        if (is_dir($target)) {
            $configPaths[] = rtrim($target, '/') . '/.security-lint.json';
        }
        $configPaths[] = getcwd() . '/.security-lint.json';
        $configPaths[] = getenv('HOME') . '/.security-lint.json';

        foreach ($configPaths as $configPath) {
            if (file_exists($configPath)) {
                $this->loadConfig($configPath);
                if ($this->options['verbose']) {
                    $this->info("Loaded config from: {$configPath}");
                }
                break;
            }
        }
    }

    /**
     * Load and merge configuration from a JSON file
     */
    private function loadConfig(string $path): void
    {
        $content = file_get_contents($path);
        $config = json_decode($content, true);

        if (!is_array($config)) {
            return;
        }

        // Merge config options (CLI options take precedence)
        $configMapping = [
            'format' => 'format',
            'severity' => 'severity',
            'exclude' => 'exclude',
            'include' => 'include',
            'recursiveDepth' => 'recursive-depth',
            'recursive-depth' => 'recursive-depth',
            'lang' => 'lang',
            'includeVendor' => 'include-vendor',
            'include-vendor' => 'include-vendor',
            'includeTests' => 'include-tests',
            'include-tests' => 'include-tests',
            'noDefaultExcludes' => 'no-default-excludes',
            'no-default-excludes' => 'no-default-excludes',
        ];

        foreach ($configMapping as $jsonKey => $optionKey) {
            if (isset($config[$jsonKey])) {
                // Don't override if already set via CLI
                if ($optionKey === 'exclude' || $optionKey === 'include') {
                    // Merge arrays
                    $this->options[$optionKey] = array_merge(
                        (array)$config[$jsonKey],
                        $this->options[$optionKey]
                    );
                } elseif (!$this->wasSetViaCli($optionKey)) {
                    $this->options[$optionKey] = $config[$jsonKey];
                }
            }
        }
    }

    /**
     * Check if an option was explicitly set via CLI
     */
    private function wasSetViaCli(string $option): bool
    {
        // Track which options were set via CLI
        static $defaults = [
            'format' => 'text',
            'severity' => 'low',
            'recursive-depth' => 10,
            'lang' => 'ja',
            'include-vendor' => false,
            'include-tests' => false,
            'no-default-excludes' => false,
        ];

        return $this->options[$option] !== ($defaults[$option] ?? null);
    }

    /**
     * Show what directories/patterns are being excluded
     */
    private function showExcludedPatterns(array $patterns, string $target): void
    {
        $isJapanese = $this->options['lang'] === 'ja';

        echo $this->colors['cyan'] . $this->colors['bold'];
        echo $isJapanese ? "除外パターン:\n" : "Excluded patterns:\n";
        echo $this->colors['reset'];

        foreach ($patterns as $pattern) {
            echo $this->colors['dim'] . "  - {$pattern}" . $this->colors['reset'] . "\n";
        }

        // Show actual excluded directories if target is a directory
        if (is_dir($target)) {
            $excluded = $this->findExcludedDirs($target, $patterns);
            if (!empty($excluded)) {
                echo "\n";
                echo $this->colors['yellow'];
                echo $isJapanese ? "除外されるディレクトリ:\n" : "Excluded directories:\n";
                echo $this->colors['reset'];

                $count = 0;
                foreach ($excluded as $dir) {
                    if ($count >= 20) {
                        $remaining = count($excluded) - 20;
                        echo $this->colors['dim'] . "  ... " .
                            ($isJapanese ? "他 {$remaining} 件" : "and {$remaining} more") .
                            $this->colors['reset'] . "\n";
                        break;
                    }
                    echo $this->colors['dim'] . "  ✗ {$dir}" . $this->colors['reset'] . "\n";
                    $count++;
                }
            }
        }

        echo "\n";
    }

    /**
     * Calculate statistics from vulnerabilities
     */
    private function calculateStats(array $vulnerabilities): array
    {
        $stats = [
            'total' => count($vulnerabilities),
            'by_severity' => ['critical' => 0, 'high' => 0, 'medium' => 0, 'low' => 0],
            'by_type' => [],
        ];

        foreach ($vulnerabilities as $vuln) {
            $severity = $vuln->getSeverity();
            if (isset($stats['by_severity'][$severity])) {
                $stats['by_severity'][$severity]++;
            }
            $type = $vuln->getType();
            $stats['by_type'][$type] = ($stats['by_type'][$type] ?? 0) + 1;
        }

        return $stats;
    }

    /**
     * Apply PHP syntax highlighting to a line of code
     */
    private function highlightPhpSyntax(string $code): string
    {
        if (empty($this->colors['syn_keyword'])) {
            // Colors disabled
            return $code;
        }

        $reset = $this->colors['reset'];

        // PHP keywords
        $keywords = [
            'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch',
            'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do',
            'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach',
            'endif', 'endswitch', 'endwhile', 'enum', 'eval', 'exit', 'extends',
            'final', 'finally', 'fn', 'for', 'foreach', 'function', 'global', 'goto',
            'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof',
            'interface', 'isset', 'list', 'match', 'namespace', 'new', 'or', 'print',
            'private', 'protected', 'public', 'readonly', 'require', 'require_once',
            'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use',
            'var', 'while', 'xor', 'yield', 'true', 'false', 'null', 'self', 'parent',
        ];

        $result = '';
        $length = strlen($code);
        $i = 0;

        while ($i < $length) {
            // Check for single-line comment
            if ($i < $length - 1 && $code[$i] === '/' && $code[$i + 1] === '/') {
                $comment = substr($code, $i);
                $result .= $this->colors['syn_comment'] . $comment . $reset;
                break;
            }

            // Check for multi-line comment start
            if ($i < $length - 1 && $code[$i] === '/' && $code[$i + 1] === '*') {
                $endPos = strpos($code, '*/', $i + 2);
                if ($endPos !== false) {
                    $comment = substr($code, $i, $endPos - $i + 2);
                    $result .= $this->colors['syn_comment'] . $comment . $reset;
                    $i = $endPos + 2;
                    continue;
                } else {
                    $comment = substr($code, $i);
                    $result .= $this->colors['syn_comment'] . $comment . $reset;
                    break;
                }
            }

            // Check for strings (single quote)
            if ($code[$i] === "'") {
                $j = $i + 1;
                while ($j < $length) {
                    if ($code[$j] === "'" && ($j === $i + 1 || $code[$j - 1] !== '\\')) {
                        break;
                    }
                    $j++;
                }
                $str = substr($code, $i, $j - $i + 1);
                $result .= $this->colors['syn_string'] . $str . $reset;
                $i = $j + 1;
                continue;
            }

            // Check for strings (double quote)
            if ($code[$i] === '"') {
                $j = $i + 1;
                while ($j < $length) {
                    if ($code[$j] === '"' && ($j === $i + 1 || $code[$j - 1] !== '\\')) {
                        break;
                    }
                    $j++;
                }
                $str = substr($code, $i, $j - $i + 1);
                $result .= $this->colors['syn_string'] . $str . $reset;
                $i = $j + 1;
                continue;
            }

            // Check for variables
            if ($code[$i] === '$') {
                $j = $i + 1;
                while ($j < $length && (ctype_alnum($code[$j]) || $code[$j] === '_')) {
                    $j++;
                }
                $var = substr($code, $i, $j - $i);
                $result .= $this->colors['syn_variable'] . $var . $reset;
                $i = $j;
                continue;
            }

            // Check for numbers
            if (ctype_digit($code[$i]) || ($code[$i] === '.' && $i + 1 < $length && ctype_digit($code[$i + 1]))) {
                $j = $i;
                while ($j < $length && (ctype_digit($code[$j]) || $code[$j] === '.' || $code[$j] === 'e' || $code[$j] === 'E')) {
                    $j++;
                }
                $num = substr($code, $i, $j - $i);
                $result .= $this->colors['syn_number'] . $num . $reset;
                $i = $j;
                continue;
            }

            // Check for words (keywords, functions, class names)
            if (ctype_alpha($code[$i]) || $code[$i] === '_') {
                $j = $i;
                while ($j < $length && (ctype_alnum($code[$j]) || $code[$j] === '_')) {
                    $j++;
                }
                $word = substr($code, $i, $j - $i);
                $wordLower = strtolower($word);

                // Check if it's a keyword
                if (in_array($wordLower, $keywords, true)) {
                    $result .= $this->colors['syn_keyword'] . $word . $reset;
                }
                // Check if it's a function call (followed by parenthesis)
                elseif ($j < $length && $code[$j] === '(') {
                    $result .= $this->colors['syn_function'] . $word . $reset;
                }
                // Check if it's a constant (all uppercase)
                elseif ($word === strtoupper($word) && strlen($word) > 1) {
                    $result .= $this->colors['syn_constant'] . $word . $reset;
                }
                // Check if it looks like a class name (PascalCase after new/extends/implements)
                elseif (preg_match('/^[A-Z][a-zA-Z0-9_]*$/', $word)) {
                    $result .= $this->colors['syn_class'] . $word . $reset;
                }
                else {
                    $result .= $word;
                }
                $i = $j;
                continue;
            }

            // Check for operators
            if (in_array($code[$i], ['=>', '->', '::', '=', '+', '-', '*', '/', '%', '.', '<', '>', '!', '&', '|', '^', '~', '?', ':'], true)) {
                $result .= $this->colors['syn_operator'] . $code[$i] . $reset;
                $i++;
                continue;
            }

            // Default: output character as-is
            $result .= $code[$i];
            $i++;
        }

        return $result;
    }

    /**
     * Find directories that would be excluded
     */
    private function findExcludedDirs(string $target, array $patterns): array
    {
        $excluded = [];
        $basePath = realpath($target);

        if (!$basePath) {
            return [];
        }

        try {
            $iterator = new \RecursiveIteratorIterator(
                new \RecursiveDirectoryIterator($basePath, \RecursiveDirectoryIterator::SKIP_DOTS),
                \RecursiveIteratorIterator::SELF_FIRST
            );
            $iterator->setMaxDepth(3); // Only check 3 levels deep for performance

            foreach ($iterator as $file) {
                if (!$file->isDir()) {
                    continue;
                }

                $path = $file->getPathname();
                $relativePath = str_replace($basePath . '/', '', $path);

                foreach ($patterns as $pattern) {
                    if (fnmatch($pattern, $relativePath) || fnmatch($pattern, $relativePath . '/')) {
                        $excluded[] = $relativePath;
                        break;
                    }
                }
            }
        } catch (\Exception $e) {
            // Ignore errors when scanning directories
        }

        return array_unique($excluded);
    }
}

$cli = new SecurityLintCLI();
exit($cli->run($argv));
