<?php

namespace Kylin\Modules\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Kylin\Modules\ModuleManager;

/**
 * 模块安全扫描命令
 *
 * 扫描模块中的安全漏洞，帮助开发者确保模块的安全性
 */
class ModuleSecurityCommand extends Command
{
    /**
     * 命令名称和签名
     *
     * @var string
     * 
     * 命令参数说明：
     * 1. {modules?*} - 模块名称列表（可选）
     *    - 数据类型：字符串数组
     *    - 用途：指定要扫描安全漏洞的模块名称列表，不指定则扫描所有模块
     *    - 示例：php artisan module:security game user
     * 
     * 命令选项说明：
     * 1. {--all} - 扫描所有模块
     *    - 完整形式：--all
     *    - 用途：扫描所有模块的安全漏洞
     *    - 示例：php artisan module:security --all
     * 
     * 2. {--detailed} - 显示详细扫描结果
     *    - 完整形式：--detailed
     *    - 用途：显示详细的安全扫描结果
     *    - 示例：php artisan module:security game --detailed
     * 
     * 3. {--fix} - 尝试自动修复安全问题
     *    - 完整形式：--fix
     *    - 用途：尝试自动修复发现的安全问题
     *    - 示例：php artisan module:security game --fix
     * 
     * 4. {--report=} - 生成安全报告文件路径
     *    - 完整形式：--report
     *    - 数据类型：字符串
     *    - 用途：指定生成安全报告的文件路径
     *    - 示例：php artisan module:security game --report=security-report.txt
     */
    protected $signature = 'module:security {modules?* : 模块名称列表} {--all : 扫描所有模块} {--detailed : 显示详细扫描结果} {--fix : 尝试自动修复安全问题} {--report= : 生成安全报告文件路径}';

    /**
     * 命令描述
     *
     * @var string
     */
    protected $description = '扫描模块中的安全漏洞，帮助开发者确保模块的安全性';

    /**
     * 文件系统实例
     *
     * @var Filesystem
     */
    private readonly Filesystem $files;

    /**
     * 创建新命令实例
     *
     * @param  ModuleManager  $moduleManager  模块管理器实例
     * @param  Filesystem  $files  文件系统实例
     */
    public function __construct(
        private readonly ModuleManager $moduleManager,
        Filesystem $files
    ) {
        parent::__construct();
        $this->files = $files;
    }

    /**
     * 执行控制台命令
     */
    public function handle(): int
    {
        $modules = $this->argument('modules') ?: [];
        $all = $this->option('all');
        $detailed = $this->option('detailed');
        $fix = $this->option('fix');
        $report = $this->option('report');

        // 确定要扫描的模块
        $targetModules = [];
        if ($all) {
            $targetModules = $this->moduleManager->all();
        } elseif (!empty($modules)) {
            foreach ($modules as $moduleName) {
                $module = $this->moduleManager->get($moduleName);
                if ($module) {
                    $targetModules[] = $module;
                } else {
                    $this->warn("模块 [{$moduleName}] 未找到，将被跳过。");
                }
            }
        } else {
            $this->error('请指定模块名称或使用 --all 选项');
            return 1;
        }

        if (empty($targetModules)) {
            $this->error('没有找到要扫描的模块');
            return 1;
        }

        $this->info('正在扫描模块安全漏洞...');
        $this->line('');

        $allIssues = [];
        $totalIssues = 0;

        foreach ($targetModules as $module) {
            $modulePath = $module->getPath();
            $moduleName = $module->getName();

            $this->info("扫描模块 [{$moduleName}]:");

            // 扫描代码安全问题
            $codeIssues = $this->scanCodeSecurity($modulePath, $detailed, $fix);

            // 扫描配置安全问题
            $configIssues = $this->scanConfigSecurity($modulePath, $detailed, $fix);

            // 扫描依赖安全问题
            $dependencyIssues = $this->scanDependencySecurity($module, $detailed, $fix);

            // 合并问题
            $moduleIssues = array_merge($codeIssues, $configIssues, $dependencyIssues);
            $issueCount = count($moduleIssues);
            $totalIssues += $issueCount;

            if (!empty($moduleIssues)) {
                $allIssues[$moduleName] = $moduleIssues;
                $this->error("  发现 {$issueCount} 个安全问题");
            } else {
                $this->info("  未发现安全问题");
            }

            $this->line('');
        }

        // 显示总体扫描结果
        $this->info('扫描完成:');
        $this->line("  扫描模块数: " . count($targetModules));
        $this->line("  发现安全问题数: {$totalIssues}");

        if ($totalIssues > 0) {
            $this->error('请检查并修复发现的安全问题');
        } else {
            $this->info('所有模块安全扫描通过');
        }

        // 生成安全报告
        if ($report) {
            $this->generateSecurityReport($report, $allIssues);
            $this->info("安全报告已生成到: {$report}");
        }

        return $totalIssues > 0 ? 1 : 0;
    }

    /**
     * 扫描代码安全问题
     *
     * @param  string  $modulePath  模块路径
     * @param  bool  $detailed  是否显示详细信息
     * @param  bool  $fix  是否尝试修复
     * @return array  安全问题列表
     */
    protected function scanCodeSecurity(string $modulePath, bool $detailed, bool $fix): array
    {
        $issues = [];
        $patterns = [
            // SQL 注入
            '/\b(select|insert|update|delete|drop|alter)\b.*\$[a-zA-Z_]\w*/i' => '可能的 SQL 注入漏洞',
            // XSS 漏洞
            '/\becho\s+\$[a-zA-Z_]\w*/i' => '可能的 XSS 漏洞',
            // 未过滤的用户输入
            '/\$_(GET|POST|REQUEST|COOKIE|SESSION)\[.*\]/i' => '未过滤的用户输入',
            // 硬编码的密码
            '/\bpassword\s*=\s*[\'"].*[\'"]/i' => '硬编码的密码',
            // 硬编码的密钥
            '/\b(key|secret)\s*=\s*[\'"].*[\'"]/i' => '硬编码的密钥',
            // eval() 函数
            '/\beval\s*\(/i' => '使用了 eval() 函数',
            // exec() 等命令执行函数
            '/\b(exec|system|shell_exec|passthru|proc_open|popen)\s*\(/i' => '使用了命令执行函数',
            // 文件包含漏洞
            '/\b(include|require)(_once)?\s*\(.*\$[a-zA-Z_]\w*.*\)/i' => '可能的文件包含漏洞',
        ];

        // 遍历模块文件
        if ($this->files->exists($modulePath)) {
            $files = $this->files->allFiles($modulePath);
            foreach ($files as $file) {
                if (pathinfo($file, PATHINFO_EXTENSION) !== 'php') {
                    continue;
                }

                $filePath = $file->getPathname();
                $content = $this->files->get($filePath);

                foreach ($patterns as $pattern => $description) {
                    if (preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) {
                        foreach ($matches[0] as $match) {
                            $line = substr_count(substr($content, 0, $match[1]), "\n") + 1;
                            $issue = [
                                'file' => str_replace(base_path(), '', $filePath),
                                'line' => $line,
                                'description' => $description,
                                'code' => $match[0],
                                'severity' => 'medium',
                            ];
                            $issues[] = $issue;

                            if ($detailed) {
                                $this->line("    [{$issue['severity']}] {$issue['file']}:{$issue['line']} - {$issue['description']}");
                                $this->line("      代码: {$issue['code']}");
                            }
                        }
                    }
                }
            }
        }

        return $issues;
    }

    /**
     * 扫描配置安全问题
     *
     * @param  string  $modulePath  模块路径
     * @param  bool  $detailed  是否显示详细信息
     * @param  bool  $fix  是否尝试修复
     * @return array  安全问题列表
     */
    protected function scanConfigSecurity(string $modulePath, bool $detailed, bool $fix): array
    {
        $issues = [];
        $configPath = "{$modulePath}/config";

        if (!$this->files->exists($configPath)) {
            return $issues;
        }

        // 遍历配置文件
        if ($this->files->exists($configPath)) {
            $files = $this->files->allFiles($configPath);
            foreach ($files as $file) {
                if (!in_array(pathinfo($file, PATHINFO_EXTENSION), ['php', 'json', 'env'])) {
                    continue;
                }

                $filePath = $file->getPathname();
                $content = $this->files->get($filePath);

                // 检查硬编码的敏感信息
                $patterns = [
                    '/\bpassword\s*=\s*[\'"].*[\'"]/i' => '硬编码的密码',
                    '/\b(key|secret)\s*=\s*[\'"].*[\'"]/i' => '硬编码的密钥',
                    '/\bapi[_-]?key\s*=\s*[\'"].*[\'"]/i' => '硬编码的 API 密钥',
                    '/\btoken\s*=\s*[\'"].*[\'"]/i' => '硬编码的令牌',
                ];

                foreach ($patterns as $pattern => $description) {
                    if (preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) {
                        foreach ($matches[0] as $match) {
                            $line = substr_count(substr($content, 0, $match[1]), "\n") + 1;
                            $issue = [
                                'file' => str_replace(base_path(), '', $filePath),
                                'line' => $line,
                                'description' => $description,
                                'code' => $match[0],
                                'severity' => 'high',
                            ];
                            $issues[] = $issue;

                            if ($detailed) {
                                $this->line("    [{$issue['severity']}] {$issue['file']}:{$issue['line']} - {$issue['description']}");
                                $this->line("      代码: {$issue['code']}");
                            }
                        }
                    }
                }
            }
        }

        return $issues;
    }

    /**
     * 扫描依赖安全问题
     *
     * @param  mixed  $module  模块实例
     * @param  bool  $detailed  是否显示详细信息
     * @param  bool  $fix  是否尝试修复
     * @return array  安全问题列表
     */
    protected function scanDependencySecurity($module, bool $detailed, bool $fix): array
    {
        $issues = [];
        $composerPath = "{$module->getPath()}/composer.json";

        if (!file_exists($composerPath)) {
            return $issues;
        }

        try {
            $composer = json_decode($this->files->get($composerPath), true);
            $dependencies = array_merge($composer['require'] ?? [], $composer['require-dev'] ?? []);

            // 检查已知的有漏洞的依赖版本
            $vulnerablePackages = [
                'laravel/framework' => [
                    '5.0.0' => '6.0.0',
                    '6.0.0' => '6.18.31',
                    '7.0.0' => '7.30.4',
                    '8.0.0' => '8.40.0',
                ],
                'symfony/http-foundation' => [
                    '4.0.0' => '4.4.13',
                    '5.0.0' => '5.1.5',
                ],
                'guzzlehttp/guzzle' => [
                    '6.0.0' => '6.5.5',
                    '7.0.0' => '7.2.0',
                ],
            ];

            foreach ($dependencies as $package => $version) {
                if (isset($vulnerablePackages[$package])) {
                    foreach ($vulnerablePackages[$package] as $vulnerableFrom => $vulnerableTo) {
                        if ($this->versionInRange($version, $vulnerableFrom, $vulnerableTo)) {
                            $issue = [
                                'package' => $package,
                                'version' => $version,
                                'description' => "依赖包 {$package} 的版本 {$version} 存在安全漏洞",
                                'severity' => 'high',
                            ];
                            $issues[] = $issue;

                            if ($detailed) {
                                $this->line("    [{$issue['severity']}] 依赖 - {$issue['description']}");
                                $this->line("      建议升级到版本 {$vulnerableTo} 或更高");
                            }
                        }
                    }
                }
            }
        } catch (\Exception $e) {
            // 忽略解析错误
        }

        return $issues;
    }

    /**
     * 检查版本是否在指定范围内
     *
     * @param  string  $version  版本号
     * @param  string  $from  开始版本
     * @param  string  $to  结束版本
     * @return bool  是否在范围内
     */
    protected function versionInRange(string $version, string $from, string $to): bool
    {
        // 移除版本约束符
        $cleanVersion = preg_replace('/[^0-9.]/', '', $version);
        return version_compare($cleanVersion, $from, '>=') && version_compare($cleanVersion, $to, '<');
    }

    /**
     * 生成安全报告
     *
     * @param  string  $reportPath  报告文件路径
     * @param  array  $allIssues  所有安全问题
     */
    protected function generateSecurityReport(string $reportPath, array $allIssues): void
    {
        $report = "# 模块安全扫描报告\n\n";
        $report .= "生成时间: " . date('Y-m-d H:i:s') . "\n\n";

        if (empty($allIssues)) {
            $report .= "## 扫描结果\n\n";
            $report .= "未发现安全问题\n";
        } else {
            $report .= "## 扫描结果\n\n";
            $report .= "发现安全问题的模块: " . count($allIssues) . "\n\n";

            foreach ($allIssues as $moduleName => $issues) {
                $report .= "### 模块: {$moduleName}\n\n";
                $report .= "发现问题数: " . count($issues) . "\n\n";

                foreach ($issues as $issue) {
                    $report .= "- **严重程度**: {$issue['severity']}\n";
                    if (isset($issue['file'])) {
                        $report .= "- **文件**: {$issue['file']}\n";
                        $report .= "- **行号**: {$issue['line']}\n";
                    } elseif (isset($issue['package'])) {
                        $report .= "- **包**: {$issue['package']}\n";
                        $report .= "- **版本**: {$issue['version']}\n";
                    }
                    $report .= "- **描述**: {$issue['description']}\n";
                    if (isset($issue['code'])) {
                        $report .= "- **代码**: {$issue['code']}\n";
                    }
                    $report .= "\n";
                }
            }
        }

        $this->files->put($reportPath, $report);
    }
}
