#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
风七六项目开发规范自动化检查脚本
检查代码是否符合《后端统一开发规范 v2.0.0》

使用方法:
    python scripts/code_compliance_check.py              # 检查所有文件
    python scripts/code_compliance_check.py --path src   # 检查指定目录
    python scripts/code_compliance_check.py --verbose    # 详细输出

退出码:
    0 - 所有检查通过
    1 - 发现违规
    2 - 检查执行错误
"""

import argparse
import os
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional
from collections import defaultdict

# 【大厂标准 跨平台Unicode兼容】Fix: Windows GBK/PowerShell/cmd终端默认编码≠utf-8时 emoji🔍✅❌等Unicode字符抛UnicodeEncodeError导致husky 5号门禁误判code=1
if sys.platform == 'win32':
    try:
        sys.stdout.reconfigure(encoding='utf-8', errors='backslashreplace')
        sys.stderr.reconfigure(encoding='utf-8', errors='backslashreplace')
    except Exception:
        import io
        if not isinstance(sys.stdout, io.TextIOWrapper) or (getattr(sys.stdout, 'encoding', '') or '').lower() not in ('utf-8','utf8'):
            sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='backslashreplace', newline='')
            sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='backslashreplace', newline='')

# ANSI 颜色代码
class Colors:
    RED = '\033[91m'
    GREEN = '\033[92m'
    YELLOW = '\033[93m'
    BLUE = '\033[94m'
    MAGENTA = '\033[95m'
    CYAN = '\033[96m'
    WHITE = '\033[97m'
    BOLD = '\033[1m'
    END = '\033[0m'

    @staticmethod
    def red(text):
        return f"{Colors.RED}{text}{Colors.END}"

    @staticmethod
    def green(text):
        return f"{Colors.GREEN}{text}{Colors.END}"

    @staticmethod
    def yellow(text):
        return f"{Colors.YELLOW}{text}{Colors.END}"

    @staticmethod
    def blue(text):
        return f"{Colors.BLUE}{text}{Colors.END}"

    @staticmethod
    def cyan(text):
        return f"{Colors.CYAN}{text}{Colors.END}"

    @staticmethod
    def bold(text):
        return f"{Colors.BOLD}{text}{Colors.END}"


@dataclass
class Violation:
    """违规信息"""
    file_path: str
    line_number: int
    rule_id: str
    severity: str  # BLOCKER, CRITICAL, MAJOR, MINOR
    message: str
    code_snippet: str = ""


@dataclass
class CheckResult:
    """检查结果"""
    rule_id: str
    name: str
    description: str
    violations: List[Violation] = field(default_factory=list)

    @property
    def passed(self) -> bool:
        return len(self.violations) == 0


class ComplianceChecker:
    """规范检查器"""

    # 规则定义
    RULES = {
        # ========== 日期时间处理规范 ==========
        "DATETIME001": {
            "name": "禁止使用Date类型",
            "description": "必须使用LocalDateTime，禁止使用java.util.Date",
            "severity": "BLOCKER",
            "pattern": r'\bjava\.util\.Date\b',
            "message": "禁止使用java.util.Date，必须使用java.time.LocalDateTime"
        },
        "DATETIME002": {
            "name": "禁止使用Timestamp类型",
            "description": "必须使用LocalDateTime，禁止使用java.sql.Timestamp",
            "severity": "BLOCKER",
            "pattern": r'\bjava\.sql\.Timestamp\b',
            "message": "禁止使用java.sql.Timestamp，必须使用java.time.LocalDateTime"
        },
        "DATETIME003": {
            "name": "禁止使用getTime()方法",
            "description": "禁止使用Date.getTime()进行时间比较",
            "severity": "MAJOR",
            "pattern": r'\.getTime\(\)',
            "message": "禁止使用getTime()进行时间比较，应使用Duration或isBefore/isAfter"
        },

        # ========== 集合处理规范 ==========
        "COLLECTION001": {
            "name": "禁止未指定容量的集合初始化",
            "description": "集合初始化必须指定初始容量",
            "severity": "MAJOR",
            "pattern": r'new\s+ArrayList\s*\<\s*\>\s*\(\s*\)',
            "message": "集合初始化应指定初始容量，如: new ArrayList<>(预估大小)"
        },
        "COLLECTION002": {
            "name": "禁止使用+拼接日志",
            "description": "日志输出必须使用占位符",
            "severity": "CRITICAL",
            "pattern": r'log\.(info|warn|error|debug)\s*\(\s*["\'].*?["\']\s*\+\s*\w+',
            "message": "日志必须使用占位符，如: log.info(\"id={}\", id)"
        },

        # ========== SQL编写规范 ==========
        "SQL001": {
            "name": "禁止使用SELECT *",
            "description": "必须明确列出查询字段，禁止使用SELECT *",
            "severity": "BLOCKER",
            "pattern": r'SELECT\s+\*\s+FROM',
            "message": "禁止使用SELECT *，必须明确列出需要的字段"
        },
        "SQL002": {
            "name": "禁止使用!=或<>",
            "description": "WHERE条件中禁止使用!=或<>",
            "severity": "CRITICAL",
            "pattern": r'WHERE\s+.*?\s*(!=|<>)\s*',
            "message": "禁止使用!=或<>，应使用OR条件或>、<替代"
        },
        "SQL003": {
            "name": "禁止使用$拼接SQL",
            "description": "必须使用#{}，禁止使用${}拼接SQL",
            "severity": "BLOCKER",
            "pattern": r'#\{[^}]*\}|\$\{[^}]*\}',
            "message": "必须使用#{}，禁止使用${}",
            "check_dollar_only": True
        },
        "SQL004": {
            "name": "禁止ORDER BY RAND()",
            "description": "禁止使用ORDER BY RAND()进行随机排序",
            "severity": "CRITICAL",
            "pattern": r'ORDER\s+BY\s+RAND\s*\(',
            "message": "禁止使用ORDER BY RAND()，性能差，应使用其他方式"
        },

        # ========== 并发编程规范 ==========
        "CONCURRENT001": {
            "name": "禁止使用Executors创建线程池",
            "description": "必须使用ThreadPoolExecutor，禁止使用Executors",
            "severity": "BLOCKER",
            "pattern": r'Executors\.(newFixedThreadPool|newCachedThreadPool|newSingleThreadExecutor)',
            "message": "禁止使用Executors创建线程池，必须使用ThreadPoolExecutor"
        },
        "CONCURRENT002": {
            "name": "禁止在Spring Bean中使用synchronized",
            "description": "Spring Bean中禁止使用synchronized",
            "severity": "CRITICAL",
            "pattern": r'synchronized\s*\(',
            "message": "禁止在Spring Bean中使用synchronized，应使用乐观锁或分布式锁"
        },

        # ========== 异常处理规范 ==========
        "EXCEPTION001": {
            "name": "禁止吞掉异常",
            "description": "catch块中必须记录日志或抛出异常",
            "severity": "CRITICAL",
            "pattern": r'catch\s*\([^)]+\)\s*\{\s*\}',  # 空catch块
            "message": "catch块不能为空，必须处理异常或记录日志"
        },
        "EXCEPTION002": {
            "name": "catch块需处理异常",
            "description": "catch块必须包含日志或异常处理",
            "severity": "MAJOR",
            "pattern": r'catch\s*\([^)]+\)\s*\{\s*(//[^\n]*)?\s*\}',
            "message": "catch块必须包含日志或异常处理"
        },

        # ========== 依赖注入规范 ==========
        "DI001": {
            "name": "禁止字段注入",
            "description": "必须使用构造器注入，禁止@Autowired字段注入",
            "severity": "BLOCKER",
            "pattern": r'@Autowired\s+private\s+\w+',
            "message": "禁止使用@Autowired字段注入，必须使用构造器注入"
        },

        # ========== 日志规范 ==========
        "LOG001": {
            "name": "ERROR日志必须包含堆栈",
            "description": "ERROR级别日志必须记录异常堆栈",
            "severity": "CRITICAL",
            # 检测 log.error 调用只有消息参数，没有异常参数的情况
            # log.error("message") 会匹配，log.error("message", e) 不会匹配
            "pattern": r'log\.error\s*\(\s*["\'][^"\']*["\']\s*\)(?!\s*,)',
            "message": "ERROR日志必须包含异常堆栈信息"
        },

        # ========== 安全规范 ==========
        "SECURITY001": {
            "name": "禁止明文密码日志",
            "description": "禁止在日志中记录明文密码",
            "severity": "BLOCKER",
            "pattern": r'log\.(info|debug).*(password|pwd|secret).*["\']\s*\+',
            "message": "禁止在日志中记录明文密码或密钥"
        },
        "SECURITY002": {
            "name": "禁止硬编码敏感信息",
            "description": "禁止在代码中硬编码密码、密钥等敏感信息",
            "severity": "BLOCKER",
            "pattern": r'(password|secret|apiKey|api_key)\s*=\s*["\'][^"\']{8,}["\']',
            "message": "禁止硬编码敏感信息，应使用环境变量或配置中心"
        },

        # ========== 其他规范 ==========
        "CODE001": {
            "name": "禁止魔法值",
            "description": "禁止在代码中使用未命名的常量",
            "severity": "MAJOR",
            "pattern": r'(if|while)\s*\([^)]*\b(==|!=)\s*\d+\b',
            "message": "禁止使用魔法值，应使用枚举或常量"
        },
        "CODE002": {
            "name": "禁止System.out",
            "description": "禁止使用System.out/err，应使用日志框架",
            "severity": "MAJOR",
            "pattern": r'System\.(out|err)\.(print|println)',
            "message": "禁止使用System.out/err，应使用日志框架"
        },
    }

    def __init__(self, verbose: bool = False):
        self.verbose = verbose
        self.results: List[CheckResult] = []
        self.total_violations = 0

    def check_file(self, file_path: Path) -> List[Violation]:
        """检查单个文件"""
        violations = []

        if not file_path.exists():
            return violations

        # 跳过target目录下的生成文件
        if 'target' in str(file_path):
            return violations

        # 跳过测试文件中的某些检查（降低误报）
        is_test_file = '/test/' in str(file_path) or '\\test\\' in str(file_path)

        # 只检查Java和配置文件
        if file_path.suffix not in ['.java', '.xml', '.yml', '.yaml', '.properties']:
            return violations

        try:
            content = file_path.read_text(encoding='utf-8')
            lines = content.split('\n')
        except Exception as e:
            print(f"{Colors.yellow('⚠️')} 无法读取文件 {file_path}: {e}")
            return violations

        for rule_id, rule in self.RULES.items():
            pattern = rule['pattern']
            check_dollar_only = rule.get('check_dollar_only', False)
            skip_dollar_patterns = ['@Value', '@ConfigurationProperties', '@PropertySource', '@Scheduled']

            for line_no, line in enumerate(lines, 1):
                # 跳过注释
                if self._is_comment(line):
                    continue

                # 跳过字符串中的内容（降低误报）
                if self._is_in_string(line, 'SELECT'):
                    continue

                matches = re.finditer(pattern, line, re.IGNORECASE)
                for match in matches:
                    if check_dollar_only:
                        # 特别检查${}而不是#{}
                        if '#{' in line:
                            continue
                        # 跳过 @Value 等注解中的 ${} 使用
                        if any(skip_pattern in line for skip_pattern in skip_dollar_patterns):
                            continue

                    # 跳过测试文件中的 SQL001 检查（测试代码中可能有故意的 SQL 注入测试）
                    if is_test_file and rule_id == 'SQL001':
                        continue

                    # 跳过测试文件中的 DATETIME003 检查（测试代码中的 Date 使用是允许的）
                    if is_test_file and rule_id == 'DATETIME003':
                        continue

                    # 跳过 DATETIME003 检查：String 类型的 getTime() 方法（不是 Date）
                    # 例如 request.getTime() 在 Map 上下文中通常返回 String
                    # 常见模式：request.getTime(), optimizationFactors.getTime()
                    if rule_id == 'DATETIME003' and '.getTime()' in line:
                        # 跳过 request.getTime(), optimizationFactors.getTime() 等明显是 String 的情况
                        skip_patterns = [
                            'request.getTime()',
                            'optimizationFactors.getTime()',
                            'params.getTime()',
                            'request.get("time")',
                            '.get("time")'
                        ]
                        if any(pattern in line for pattern in skip_patterns):
                            continue

                    # 跳过 JwtUtil.java 和 AuthService.java 中的 Date 使用（第三方JWT库要求）
                    if rule_id == 'DATETIME001' and ('JwtUtil.java' in str(file_path) or 'AuthService.java' in str(file_path)):
                        continue

                    # 跳过测试文件中的 SECURITY002 检查（测试代码中可能有故意的密码测试）
                    if is_test_file and rule_id == 'SECURITY002':
                        continue

                    # 跳过测试文件中的 COLLECTION001 检查（测试代码中集合大小不确定）
                    if is_test_file and rule_id == 'COLLECTION001':
                        continue

                    violation = Violation(
                        file_path=str(file_path),
                        line_number=line_no,
                        rule_id=rule_id,
                        severity=rule['severity'],
                        message=rule['message'],
                        code_snippet=line.strip()
                    )
                    violations.append(violation)

        return violations

    def _is_comment(self, line: str) -> bool:
        """判断是否在注释中"""
        stripped = line.strip()
        return stripped.startswith('//') or stripped.startswith('/*') or stripped.startswith('*')

    def _is_in_string(self, line: str, keyword: str) -> bool:
        """判断关键字是否在字符串中"""
        # 简单判断，实际项目中可能需要更精确的解析
        return f'"{keyword}' in line or f'"{keyword.lower()}' in line

    def check_directory(self, directory: Path, patterns: List[str] = None) -> None:
        """检查目录"""
        if patterns is None:
            patterns = ['**/*.java']

        for pattern in patterns:
            for file_path in directory.glob(pattern):
                if file_path.is_file():
                    violations = self.check_file(file_path)
                    if violations:
                        self.total_violations += len(violations)

                        # 按规则分组
                        for violation in violations:
                            rule_id = violation.rule_id
                            result = self._get_or_create_result(rule_id)
                            result.violations.append(violation)

    def _get_or_create_result(self, rule_id: str) -> CheckResult:
        """获取或创建检查结果"""
        for result in self.results:
            if result.rule_id == rule_id:
                return result

        rule = self.RULES.get(rule_id, {})
        result = CheckResult(
            rule_id=rule_id,
            name=rule.get('name', rule_id),
            description=rule.get('description', '')
        )
        self.results.append(result)
        return result

    def print_report(self) -> None:
        """打印检查报告"""
        print()
        print("=" * 70)
        print(f"{Colors.BOLD}📋 风七六项目开发规范检查报告{Colors.END}")
        print("=" * 70)
        print()

        if not self.results:
            print(f"{Colors.green('✅')} 未发现违规项目，所有检查通过！")
            return

        # 按严重性排序
        severity_order = {'BLOCKER': 0, 'CRITICAL': 1, 'MAJOR': 2, 'MINOR': 3}
        self.results.sort(key=lambda x: (severity_order.get(
            self.RULES.get(x.rule_id, {}).get('severity', 'MINOR'), 3), x.rule_id))

        for result in self.results:
            rule = self.RULES.get(result.rule_id, {})
            severity = rule.get('severity', 'UNKNOWN')
            severity_color = {
                'BLOCKER': Colors.red,
                'CRITICAL': Colors.red,
                'MAJOR': Colors.yellow,
                'MINOR': Colors.blue
            }.get(severity, lambda x: x)

            print(f"{severity_color(f'[{severity}]')} {Colors.BOLD}{result.name}{Colors.END}")
            print(f"  规则ID: {result.rule_id}")
            print(f"  说明: {result.description}")
            print(f"  违规数: {len(result.violations)}")

            if self.verbose:
                print(f"  {Colors.yellow('违规详情:')}")
                for v in result.violations[:5]:  # 最多显示5条
                    print(f"    - {Colors.red('✗')} {v.file_path}:{v.line_number}")
                    print(f"      代码: {Colors.cyan(v.code_snippet[:60])}...")
                    print(f"      建议: {v.message}")
                if len(result.violations) > 5:
                    print(f"    ... 还有 {len(result.violations) - 5} 条违规")
            print()

        # 统计摘要
        print("-" * 70)
        print(f"📊 统计摘要")
        print("-" * 70)

        severity_counts = defaultdict(int)
        for result in self.results:
            severity = self.RULES.get(result.rule_id, {}).get('severity', 'UNKNOWN')
            severity_counts[severity] += len(result.violations)

        print(f"  总违规数: {Colors.red(str(self.total_violations))}")
        for severity in ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR']:
            count = severity_counts.get(severity, 0)
            if count > 0:
                color = Colors.red if severity in ['BLOCKER', 'CRITICAL'] else Colors.yellow
                print(f"  {severity}: {color(str(count))}")

        print()

        # 阻塞性问题检查
        blocker_count = severity_counts.get('BLOCKER', 0)
        if blocker_count > 0:
            print(f"{Colors.red('❌')} 发现 {blocker_count} 个阻塞性问题，必须修复后才能提交！")
        else:
            print(f"{Colors.green('✅')} 未发现阻塞性问题，可以通过代码审查！")

    def has_blockers(self) -> bool:
        """是否有阻塞性问题"""
        for result in self.results:
            if self.RULES.get(result.rule_id, {}).get('severity') == 'BLOCKER':
                return True
        return False


def main():
    parser = argparse.ArgumentParser(
        description='风七六项目开发规范自动化检查工具',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  python code_compliance_check.py                    # 检查所有文件
  python code_compliance_check.py --path services   # 检查指定目录
  python code_compliance_check.py --verbose          # 详细输出
  python code_compliance_check.py --strict          # 严格模式，发现问题直接退出
        """
    )
    parser.add_argument('--path', '-p', default='.',
                        help='检查的目录路径（默认当前目录）')
    parser.add_argument('--verbose', '-v', action='store_true',
                        help='详细输出模式')
    parser.add_argument('--strict', '-s', action='store_true',
                        help='严格模式，发现问题直接退出（退出码非0）')

    args = parser.parse_args()

    print(f"{Colors.blue('🔍')} 开始检查: {args.path}")
    print()

    checker = ComplianceChecker(verbose=args.verbose)

    # 确定检查路径
    if args.path == '.':
        # 检查主要代码目录
        paths_to_check = []
        for dir_name in ['services', 'common', 'gateway']:
            path = Path(dir_name)
            if path.exists():
                paths_to_check.append(path)

        if not paths_to_check:
            print(f"{Colors.yellow('⚠️')} 未找到代码目录，将检查当前目录")
            paths_to_check = [Path('.')]
    else:
        paths_to_check = [Path(args.path)]

    for path in paths_to_check:
        print(f"  检查目录: {path}")
        checker.check_directory(path)

    # 打印报告
    checker.print_report()

    # 根据结果返回退出码
    if checker.has_blockers():
        sys.exit(1) if args.strict else sys.exit(0)
    elif checker.total_violations > 0:
        sys.exit(0)  # 有警告但无阻塞性问题
    else:
        sys.exit(0)


if __name__ == '__main__':
    main()
