#!/usr/bin/env python3
"""Protocol設計ガイドライン検証スクリプト v2

File: scripts/quality_gates/protocol_validator_v2.py
Purpose: より正確な Protocol/非Protocol の判定ロジック
Context: v1 の false positive を削減
"""

import argparse
import ast
import sys
from collections import defaultdict
from pathlib import Path
from typing import NamedTuple


class ValidationIssue(NamedTuple):
    """検証結果の問題"""
    severity: str  # "CRITICAL", "WARNING", "INFO"
    file_path: str
    line_number: int
    message: str
    fix_suggestion: str


class ProtocolValidatorV2:
    """より正確な Protocol 検証器 (AST ベース)"""

    def __init__(self, root_path: Path) -> None:
        """初期化"""
        self.root_path = root_path
        self.issues: list[ValidationIssue] = []

    def validate(self) -> None:
        """全Pythonファイルを検証"""
        python_files = self.root_path.rglob("*.py")

        for file_path in python_files:
            self._validate_file(file_path)

    def _validate_file(self, file_path: Path) -> None:
        """単一ファイルを検証 (AST ベース)"""
        try:
            content = file_path.read_text(encoding="utf-8")
            tree = ast.parse(content)
        except (SyntaxError, UnicodeDecodeError, OSError):
            return

        # Protocol クラスを検出
        for node in ast.walk(tree):
            if isinstance(node, ast.ClassDef):
                # この クラスが Protocol か判定
                if self._is_protocol_class(node):
                    self._check_protocol_class(file_path, node, content.split("\n"))

    def _is_protocol_class(self, node: ast.ClassDef) -> bool:
        """クラスが Protocol か判定"""
        # 親クラスに Protocol があるか確認
        for base in node.bases:
            if isinstance(base, ast.Name) and base.id == "Protocol":
                return True
            if isinstance(base, ast.Subscript) and isinstance(base.value, ast.Name):
                if base.value.id == "Protocol":
                    return True
        return False

    def _check_protocol_class(self, file_path: Path, node: ast.ClassDef, lines: list[str]) -> None:
        """Protocol クラス内を検査"""
        # Protocol メソッド内に制御フロー がないか確認
        for item in node.body:
            if isinstance(item, ast.FunctionDef):
                # メソッドが単なる ... または pass のみか確認
                if not self._is_protocol_method_stub(item):
                    # 制御フロー がある
                    line_num = item.lineno
                    self.issues.append(
                        ValidationIssue(
                            severity="WARNING",
                            file_path=str(file_path),
                            line_number=line_num,
                            message=f"Protocol メソッド '{item.name}' 内に実装が存在",
                            fix_suggestion="Protocol は契約のみ。実装クラスに移動してください",
                        )
                    )

            # @abstractmethod デコレータ確認
            if isinstance(item, ast.FunctionDef):
                for decorator in item.decorator_list:
                    dec_name = self._get_decorator_name(decorator)
                    if dec_name == "abstractmethod":
                        self.issues.append(
                            ValidationIssue(
                                severity="CRITICAL",
                                file_path=str(file_path),
                                line_number=item.lineno,
                                message="Protocol内に@abstractmethodが存在（アンチパターン）",
                                fix_suggestion="@abstractmethodを削除。Protocolは構造的部分型なので不要",
                            )
                        )

    def _is_protocol_method_stub(self, node: ast.FunctionDef) -> bool:
        """メソッドがシンプルなスタブか判定

        True の場合: ... または pass のみ（Docstring 可）
        False の場合: 実装ロジックあり
        """
        # Body が空
        if not node.body:
            return True

        # Body が Docstring のみ
        if len(node.body) == 1:
            return self._is_single_stmt_stub(node.body[0])

        # Body が Docstring + ... または Docstring + pass パターン
        if len(node.body) == 2:
            return self._is_docstring_plus_stub(node.body[0], node.body[1])

        # その他は実装あり
        return False

    def _is_single_stmt_stub(self, stmt: ast.stmt) -> bool:
        """単一ステートメントがスタブか判定"""
        # Docstring (文字列)
        if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant):
            if isinstance(stmt.value.value, str):
                return True
            # ... (Ellipsis)
            if stmt.value.value is ...:
                return True
        # pass
        if isinstance(stmt, ast.Pass):
            return True
        return False

    def _is_docstring_plus_stub(self, first: ast.stmt, second: ast.stmt) -> bool:
        """Docstring + stub パターンか判定"""
        # 最初が Docstring か確認
        is_docstring = (
            isinstance(first, ast.Expr)
            and isinstance(first.value, ast.Constant)
            and isinstance(first.value.value, str)
        )

        if not is_docstring:
            return False

        # 2番目が ... か pass
        if isinstance(second, ast.Expr) and isinstance(second.value, ast.Constant):
            if second.value.value is ...:
                return True
        if isinstance(second, ast.Pass):
            return True
        return False

    def _get_decorator_name(self, node: ast.expr) -> str:
        """デコレータ名を取得"""
        if isinstance(node, ast.Name):
            return node.id
        if isinstance(node, ast.Attribute):
            return node.attr
        return ""

    def report(self, export_csv: str | None = None) -> None:
        """検証結果をレポート出力"""
        # 重要度別に集計
        by_severity = defaultdict(list)
        for issue in self.issues:
            by_severity[issue.severity].append(issue)

        print("=" * 70)
        print("Protocol設計ガイドライン検証レポート (v2 - AST ベース)")
        print("=" * 70)
        print()

        for severity in ["CRITICAL", "WARNING", "INFO"]:
            issues = by_severity.get(severity, [])
            if not issues:
                continue

            emoji = {"CRITICAL": "🔴", "WARNING": "🟡", "INFO": "🔵"}[severity]
            print(f"{emoji} {severity} ({len(issues)}件)")
            print("-" * 70)

            for issue in issues:
                print(f"  ファイル: {issue.file_path}:{issue.line_number}")
                print(f"  問題: {issue.message}")
                print(f"  対策: {issue.fix_suggestion}")
                print()

        print("=" * 70)
        total = len(self.issues)
        critical = len(by_severity.get("CRITICAL", []))
        warning = len(by_severity.get("WARNING", []))
        info = len(by_severity.get("INFO", []))
        print(f"合計: {total}件 (🔴 {critical} 🟡 {warning} 🔵 {info})")
        print("=" * 70)

        # CSV エクスポート
        if export_csv:
            self._export_csv(export_csv)
            print(f"\n✅ CSV出力: {export_csv}")

    def _export_csv(self, output_path: str) -> None:
        """CSV にエクスポート"""
        import csv

        with open(output_path, "w", newline="", encoding="utf-8") as f:
            writer = csv.writer(f)
            writer.writerow(["severity", "file_path", "line_number", "message", "fix_suggestion"])
            for issue in self.issues:
                writer.writerow(
                    [issue.severity, issue.file_path, issue.line_number, issue.message, issue.fix_suggestion]
                )


def main():
    """メインエントリーポイント"""
    parser = argparse.ArgumentParser(
        description="Protocol 設計ガイドライン検証スクリプト v2 (AST ベース)"
    )
    parser.add_argument(
        "path", nargs="?", default="src/noveler/domain/",
        help="検証対象ディレクトリ"
    )
    parser.add_argument(
        "--export", help="CSV エクスポート先"
    )
    args = parser.parse_args()

    root = Path(args.path)
    if not root.exists():
        print(f"❌ ディレクトリが見つかりません: {root}")
        return 1

    validator = ProtocolValidatorV2(root)
    validator.validate()
    validator.report(export_csv=args.export)

    return 0 if not validator.issues else 1


if __name__ == "__main__":
    sys.exit(main())
