"""
CI/CD Enforcer: Scientific Integrity Reports for Pull Requests

Generates comprehensive reports for CI/CD pipelines that check:
    1. Computational mirages in code
    2. Data leakage patterns
    3. Statistical validity
    4. Dimensional consistency
    5. Deep learning integrity
"""

import json
import logging
import os
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional

from demyst.config.manager import ConfigManager

logger = logging.getLogger(__name__)  # Add this line


@dataclass
class IntegrityCheck:
    """Result of a single integrity check."""

    name: str
    passed: bool
    severity: str  # 'critical', 'warning', 'info'
    issues: List[Dict[str, Any]]
    recommendations: List[str]


@dataclass
class ScientificIntegrityReport:
    """Complete scientific integrity report for a codebase."""

    timestamp: str
    repository: str
    branch: str
    commit: str
    files_analyzed: int
    total_issues: int
    blocking_issues: int
    critical_issues: int
    warning_issues: int
    checks: List[IntegrityCheck]
    verdict: str
    badge_status: str  # 'passing', 'failing', 'warning'

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for JSON serialization."""
        return {
            "timestamp": self.timestamp,
            "repository": self.repository,
            "branch": self.branch,
            "commit": self.commit,
            "files_analyzed": self.files_analyzed,
            "total_issues": self.total_issues,
            "blocking_issues": self.blocking_issues,
            "critical_issues": self.critical_issues,
            "warning_issues": self.warning_issues,
            "checks": [
                {
                    "name": c.name,
                    "passed": c.passed,
                    "severity": c.severity,
                    "issues": c.issues,
                    "recommendations": c.recommendations,
                }
                for c in self.checks
            ],
            "verdict": self.verdict,
            "badge_status": self.badge_status,
        }

    def to_markdown(self) -> str:
        """Generate markdown report for PR comments."""
        lines = [
            "# Scientific Integrity Report",
            "",
            f"**Repository:** {self.repository}",
            f"**Branch:** {self.branch}",
            f"**Commit:** `{self.commit[:8] if self.commit else 'unknown'}`",
            f"**Timestamp:** {self.timestamp}",
            "",
            "---",
            "",
            "## Summary",
            "",
            f"| Metric | Value |",
            f"|--------|-------|",
            f"| Files Analyzed | {self.files_analyzed} |",
            f"| Total Issues | {self.total_issues} |",
            f"| Blocking Issues | {self.blocking_issues} |",
            f"| Critical Issues | {self.critical_issues} |",
            f"| Warnings | {self.warning_issues} |",
            f"| **Verdict** | **{self.verdict}** |",
            "",
        ]

        # Add status badge
        badge_color = {
            "passing": "brightgreen",
            "warning": "yellow",
            "failing": "red",
        }.get(self.badge_status, "gray")

        lines.extend(
            [
                f"![Status](https://img.shields.io/badge/demyst-{self.badge_status}-{badge_color})",
                "",
                "---",
                "",
                "## Detailed Results",
                "",
            ]
        )

        # Add each check
        for check in self.checks:
            icon = (
                ":white_check_mark:"
                if check.passed
                else (":x:" if check.severity == "critical" else ":warning:")
            )
            lines.append(f"### {icon} {check.name}")
            lines.append("")

            if check.issues:
                lines.append("**Issues Found:**")
                lines.append("")
                for issue in check.issues[:10]:  # Limit to 10 issues per check
                    desc = issue.get("description", str(issue))
                    line = issue.get("line", "?")
                    lines.append(f"- Line {line}: {desc}")
                if len(check.issues) > 10:
                    lines.append(f"- ... and {len(check.issues) - 10} more")
                lines.append("")

            if check.recommendations:
                lines.append("**Recommendations:**")
                lines.append("")
                for rec in check.recommendations[:5]:
                    lines.append(f"- {rec}")
                lines.append("")

        # Footer
        lines.extend(
            [
                "---",
                "",
                "*Report generated by [Demyst](https://github.com/demyst/demyst) - "
                "The Scientific Integrity Platform*",
            ]
        )

        return "\n".join(lines)


class CIEnforcer:
    """
    Main class for CI/CD enforcement of scientific integrity.

    Usage:
        enforcer = CIEnforcer()
        report = enforcer.analyze_directory("./src")
        report.to_markdown()  # For PR comment
        sys.exit(0 if report.badge_status == 'passing' else 1)  # For CI
    """

    def __init__(self, config_path: Optional[str] = None):
        """
        Initialize the CI enforcer.

        Args:
            config_path: Optional path to a configuration file
        """
        self.config_manager = ConfigManager(config_path=config_path)
        self.config = self.config_manager.config
        self._import_guards()

    def _import_guards(self) -> None:
        """Import guard classes (lazy to avoid circular imports)."""
        try:
            from ..engine.mirage_detector import MirageDetector
            from ..guards.hypothesis_guard import HypothesisGuard
            from ..guards.leakage_hunter import LeakageHunter
            from ..guards.tensor_guard import TensorGuard
            from ..guards.unit_guard import UnitGuard

            self.TensorGuard = TensorGuard
            self.LeakageHunter = LeakageHunter
            self.HypothesisGuard = HypothesisGuard
            self.UnitGuard = UnitGuard
            self.MirageDetector = MirageDetector
            self._guards_available = True
        except ImportError as e:
            self._guards_available = False
            self._import_error = str(e)

    def analyze_file(self, filepath: str) -> Dict[str, Any]:
        """
        Analyze a single Python file.

        Args:
            filepath: Path to Python file

        Returns:
            Analysis results for the file
        """
        if not self._guards_available:
            return {"error": f"Guards not available: {self._import_error}"}

        try:
            with open(filepath, "r", encoding="utf-8") as f:
                source = f.read()
        except Exception as e:
            return {"error": f"Failed to read file: {e}"}

        results: Dict[str, Any] = {
            "filepath": filepath,
            "mirage": None,
            "tensor": None,
            "leakage": None,
            "hypothesis": None,
            "unit": None,
        }

        # Run mirage detection
        if self.config_manager.is_rule_enabled("mirage"):
            try:
                import ast

                tree = ast.parse(source)
                detector = self.MirageDetector(config=self.config_manager.get_rule_config("mirage"))
                # Use analyze() for variance context-aware detection
                mirages = detector.analyze(tree)
                issues = []
                for m in mirages:
                    issues.append(
                        {
                            "type": m.get("type"),
                            "line": m.get("line"),
                            "function": m.get("function"),
                            "description": m.get(
                                "reason",
                                f"Computational mirage: {m.get('type')} operation destroys variance information.",
                            ),
                            "recommendation": (
                                self._generate_recommendations("Computational Mirages", [m])[0]
                                if self._generate_recommendations("Computational Mirages", [m])
                                else None
                            ),
                            "confidence": m.get("confidence", "medium"),
                            "blocking": m.get("blocking", True),
                        }
                    )
                results["mirage"] = {"issues": issues}
            except Exception as e:
                results["mirage"] = {"error": str(e)}
        else:
            results["mirage"] = {"skipped": True}

        # Run tensor guard (deep learning checks)
        if self.config_manager.is_rule_enabled("tensor"):
            try:
                tensor_guard = self.TensorGuard(
                    config=self.config_manager.get_rule_config("tensor")
                )
                results["tensor"] = tensor_guard.analyze(source)
            except Exception as e:
                results["tensor"] = {"error": str(e)}
        else:
            results["tensor"] = {"skipped": True}

        # Run leakage hunter
        if self.config_manager.is_rule_enabled("leakage"):
            try:
                leakage_hunter = self.LeakageHunter(
                    config=self.config_manager.get_rule_config("leakage")
                )
                results["leakage"] = leakage_hunter.analyze(source)
            except Exception as e:
                results["leakage"] = {"error": str(e)}
        else:
            results["leakage"] = {"skipped": True}

        # Run hypothesis guard
        if self.config_manager.is_rule_enabled("hypothesis"):
            try:
                hypothesis_guard = self.HypothesisGuard(
                    config=self.config_manager.get_rule_config("hypothesis")
                )
                results["hypothesis"] = hypothesis_guard.analyze_code(source)
            except Exception as e:
                results["hypothesis"] = {"error": str(e)}
        else:
            results["hypothesis"] = {"skipped": True}

        # Run unit guard
        if self.config_manager.is_rule_enabled("unit"):
            try:
                unit_guard = self.UnitGuard(config=self.config_manager.get_rule_config("unit"))
                results["unit"] = unit_guard.analyze(source)
            except Exception as e:
                results["unit"] = {"error": str(e)}
        else:
            results["unit"] = {"skipped": True}

        return results

    def analyze_directory(
        self,
        directory: str,
        exclude_patterns: Optional[List[str]] = None,
        include_patterns: Optional[List[str]] = None,
    ) -> ScientificIntegrityReport:
        """
        Analyze all Python files in a directory.

        Args:
            directory: Path to directory
            exclude_patterns: Glob patterns to exclude
            include_patterns: Glob patterns to include (default: **/*.py)

        Returns:
            Complete integrity report
        """
        exclude_patterns = exclude_patterns or self.config_manager.get_ignore_patterns()
        include_patterns = include_patterns or ["**/*.py"]

        # Collect files
        import fnmatch
        from pathlib import Path

        base_path = Path(directory)
        all_files = []

        for pattern in include_patterns:
            for filepath in base_path.glob(pattern):
                if filepath.is_file():
                    rel_path = str(filepath.relative_to(base_path))
                    excluded = any(
                        fnmatch.fnmatch(rel_path, exc) or fnmatch.fnmatch(str(filepath), exc)
                        for exc in exclude_patterns
                    )
                    if not excluded:
                        all_files.append(str(filepath))

        # Analyze each file
        all_checks = []
        total_issues = 0
        critical_issues = 0
        warning_issues = 0
        blocking_issues = 0

        mirage_issues = []
        tensor_issues = []
        leakage_issues = []
        hypothesis_issues = []
        unit_issues = []

        for file_path_str in all_files:
            result = self.analyze_file(file_path_str)

            # Collect mirage issues
            if result.get("mirage") and not result["mirage"].get("error"):
                for issue in result["mirage"].get("issues", []):
                    issue["file"] = file_path_str
                    mirage_issues.append(issue)

            # Collect tensor issues
            if result.get("tensor") and not result["tensor"].get("error"):
                for issue in result["tensor"].get("gradient_issues", []):
                    issue["file"] = file_path_str
                    tensor_issues.append(issue)
                for issue in result["tensor"].get("normalization_issues", []):
                    issue["file"] = file_path_str
                    tensor_issues.append(issue)
                for issue in result["tensor"].get("reward_issues", []):
                    issue["file"] = file_path_str
                    tensor_issues.append(issue)

            # Collect leakage issues
            if result.get("leakage") and not result["leakage"].get("error"):
                for issue in result["leakage"].get("violations", []):
                    issue["file"] = file_path_str
                    leakage_issues.append(issue)

            # Collect hypothesis issues
            if result.get("hypothesis") and not result["hypothesis"].get("error"):
                for issue in result["hypothesis"].get("violations", []):
                    issue["file"] = file_path_str
                    hypothesis_issues.append(issue)

            # Collect unit issues
            if result.get("unit") and not result["unit"].get("error"):
                for issue in result["unit"].get("violations", []):
                    issue["file"] = file_path_str
                    unit_issues.append(issue)

        # Build check results
        def make_check(name: str, issues: List, critical_types: List[str]) -> IntegrityCheck:
            has_blocking = any(i.get("blocking", True) for i in issues)
            has_critical = any(
                i.get("severity") == "critical" or i.get("type") in critical_types for i in issues
            )
            severity = (
                "critical" if has_blocking or has_critical else ("warning" if issues else "info")
            )
            return IntegrityCheck(
                name=name,
                passed=len(issues) == 0,
                severity=severity,
                issues=issues,
                recommendations=self._generate_recommendations(name, issues),
            )

        all_checks = [
            make_check("Computational Mirages", mirage_issues, ["mean", "sum", "argmax"]),
            make_check("Deep Learning Integrity", tensor_issues, ["gradient_death_chain"]),
            make_check("Data Leakage", leakage_issues, ["test_in_training", "test_in_tuning"]),
            make_check("Statistical Validity", hypothesis_issues, ["uncorrected_multiple_tests"]),
            make_check("Dimensional Consistency", unit_issues, ["incompatible_addition"]),
        ]

        # Count issues with blocking awareness
        for check in all_checks:
            total_issues += len(check.issues)
            blocking_in_check = sum(1 for i in check.issues if i.get("blocking", True))
            nonblocking_in_check = len(check.issues) - blocking_in_check
            blocking_issues += blocking_in_check
            if check.severity == "critical":
                critical_issues += blocking_in_check
            warning_issues += nonblocking_in_check

        # Determine verdict (default: block only on blocking issues)
        if blocking_issues > 0:
            verdict = "FAIL: Blocking scientific integrity issues detected"
            badge_status = "failing"
        elif warning_issues > 0:
            verdict = "WARNING: Potential issues detected, review recommended"
            badge_status = "warning"
        else:
            verdict = "PASS: No scientific integrity issues detected"
            badge_status = "passing"

        # Get git info
        commit = self._get_git_commit()
        branch = self._get_git_branch()
        repo = self._get_repo_name(directory)

        return ScientificIntegrityReport(
            timestamp=datetime.now().isoformat(),
            repository=repo,
            branch=branch,
            commit=commit,
            files_analyzed=len(all_files),
            total_issues=total_issues,
            blocking_issues=blocking_issues,
            critical_issues=critical_issues,
            warning_issues=warning_issues,
            checks=all_checks,
            verdict=verdict,
            badge_status=badge_status,
        )

    def _generate_recommendations(self, check_name: str, issues: List[Dict[str, Any]]) -> List[str]:
        """Generate recommendations based on issues found."""
        recommendations = []

        if check_name == "Computational Mirages" and issues:
            recommendations.append(
                "Use VariationTensor to preserve statistical metadata during aggregations"
            )
            recommendations.append(
                "Track variance and distribution information alongside mean values"
            )

        if check_name == "Deep Learning Integrity" and issues:
            recommendations.append("Add residual connections to prevent gradient death")
            recommendations.append("Monitor gradient flow during training with TorchModuleWrapper")

        if check_name == "Data Leakage" and issues:
            recommendations.append("Split data BEFORE any preprocessing or feature engineering")
            recommendations.append("Use sklearn Pipeline to ensure proper cross-validation")

        if check_name == "Statistical Validity" and issues:
            recommendations.append("Apply Bonferroni correction for multiple comparisons")
            recommendations.append("Report mean and std across multiple seeds, not best result")

        if check_name == "Dimensional Consistency" and issues:
            recommendations.append("Add explicit unit annotations to variable names")
            recommendations.append("Use dimensional analysis to catch physics errors")

        return recommendations

    def _get_git_commit(self) -> str:
        """Get current git commit hash."""
        try:
            import subprocess

            result = subprocess.run(
                ["git", "rev-parse", "HEAD"], capture_output=True, text=True, timeout=5
            )
            return result.stdout.strip() if result.returncode == 0 else "unknown"
        except Exception:
            return "unknown"

    def _get_git_branch(self) -> str:
        """Get current git branch."""
        try:
            import subprocess

            result = subprocess.run(
                ["git", "rev-parse", "--abbrev-ref", "HEAD"],
                capture_output=True,
                text=True,
                timeout=5,
            )
            return result.stdout.strip() if result.returncode == 0 else "unknown"
        except Exception:
            return "unknown"

    def _get_repo_name(self, directory: str) -> str:
        """Get repository name from directory or git remote."""
        try:
            import subprocess

            result = subprocess.run(
                ["git", "config", "--get", "remote.origin.url"],
                capture_output=True,
                text=True,
                timeout=5,
                cwd=directory,
            )
            if result.returncode == 0:
                url = result.stdout.strip()
                # Extract repo name from URL
                if "/" in url:
                    return url.split("/")[-1].replace(".git", "")
        except Exception:
            pass
        return os.path.basename(os.path.abspath(directory))

    def enforce(self, directory: str = ".", fail_on_warning: bool = False) -> int:
        """
        Run enforcement and return exit code for CI/CD.

        Args:
            directory: Directory to analyze
            fail_on_warning: Whether to fail on warnings (not just critical)

        Returns:
            Exit code (0 for pass, 1 for fail)
        """
        report = self.analyze_directory(directory)

        # Print markdown report to stdout
        print(report.to_markdown())

        # Save JSON report
        report_path = Path(directory) / ".demyst_report.json"
        with open(report_path, "w") as f:
            json.dump(report.to_dict(), f, indent=2)

        # Determine exit code
        if report.badge_status == "failing":
            return 1
        elif report.badge_status == "warning" and fail_on_warning:
            return 1
        else:
            return 0
