#!/usr/bin/env python3
"""Quality gate checker for CleverAgents CI pipeline.

Aggregates results from multiple quality tools and produces a summary
report. Returns non-zero exit code if any critical gate fails.

Usage:
    python scripts/check-quality-gates.py [--coverage-min 85] [--complexity-max E]

Gates checked:
    1. Coverage >= threshold (default 85%)
    2. No type errors (pyright clean)
    3. No high-severity security issues (bandit)
    4. No dead code (vulture clean)
    5. No extreme complexity (radon grade F)
"""

import argparse
import json
import re
import subprocess
import sys
from pathlib import Path

_SAFE_PATH_RE = re.compile(r"^[A-Za-z0-9_./-]+$")


def validate_path(path_str: str, project_root: Path | None = None) -> Path:
    """Validate and sanitize a path argument to prevent command injection.

    Checks for safe path characters, dangerous shell metacharacters, path
    traversal patterns, project-root containment, and on-disk existence.

    Args:
        path_str: The path string to validate
        project_root: Optional project root for relative path validation

    Returns:
        A validated, resolved Path object

    Raises:
        ValueError: If the path is invalid or contains dangerous patterns
    """
    if not path_str:
        raise ValueError("Path cannot be empty")

    # Check for dangerous patterns that could be used for injection
    dangerous_patterns = [
        "..",  # Path traversal
        ";",  # Command separator
        "|",  # Pipe operator
        "&",  # Background/AND operator
        "$",  # Variable/command substitution
        "`",  # Backtick command substitution (deprecated)
        "(",  # Subshell grouping start
        ")",  # Subshell grouping end
        "<",  # Input redirection
        ">",  # Output redirection
        "\n",  # Newline injection
        "\r",  # Carriage return injection
        "\\n",  # Escaped newline injection in feature/CLI input
        "\\r",  # Escaped carriage return injection in feature/CLI input
    ]
    for pattern in dangerous_patterns:
        if pattern in path_str:
            raise ValueError(f"Path contains dangerous pattern: {pattern}")

    if _SAFE_PATH_RE.fullmatch(path_str) is None:
        raise ValueError("Path contains characters outside the safe allowlist")

    # Convert to Path object
    path_obj = Path(path_str)

    # Resolve to absolute path to prevent traversal
    try:
        resolved = path_obj.resolve()
    except (OSError, RuntimeError) as e:
        raise ValueError(f"Cannot resolve path: {e}") from e

    if not resolved.is_file() and not resolved.is_dir():
        raise ValueError(f"Path does not exist: {resolved}")

    # If project_root is provided, ensure path is within it
    if project_root is not None:
        project_root_resolved = project_root.resolve()
        try:
            resolved.relative_to(project_root_resolved)
        except ValueError as e:
            msg = f"Path {resolved} is outside project root {project_root_resolved}"
            raise ValueError(msg) from e

    return resolved


def check_coverage(min_coverage: float) -> tuple[bool, str]:
    """Check test coverage meets minimum threshold."""
    result = subprocess.run(
        ["coverage", "report", "--format=total"],
        capture_output=True,
        text=True,
        check=False,
    )
    if result.returncode != 0:
        return False, f"Coverage command failed: {result.stderr.strip()}"

    try:
        total = float(result.stdout.strip())
    except ValueError:
        return False, f"Could not parse coverage output: {result.stdout.strip()}"

    passed = total >= min_coverage
    msg = f"Coverage: {total:.1f}% (minimum: {min_coverage}%)"
    return passed, msg


def check_typecheck() -> tuple[bool, str]:
    """Check pyright type checking passes."""
    result = subprocess.run(
        ["pyright", "--outputjson"],
        capture_output=True,
        text=True,
        check=False,
    )
    try:
        data = json.loads(result.stdout)
        error_count = data.get("summary", {}).get("errorCount", -1)
        if error_count == 0:
            return True, "Type checking: 0 errors"
        return False, f"Type checking: {error_count} errors"
    except (json.JSONDecodeError, KeyError):
        # Fallback to exit code
        if result.returncode == 0:
            return True, "Type checking: passed"
        return False, f"Type checking: failed (exit code {result.returncode})"


def check_security() -> tuple[bool, str]:
    """Check bandit finds no high-severity issues."""
    # Validate the path argument to prevent command injection
    try:
        project_root = Path(".").resolve()
        config_path = validate_path("pyproject.toml", project_root)
        src_path = validate_path("src/cleveragents", project_root)
    except ValueError as e:
        return False, f"Security: invalid path - {e}"

    result = subprocess.run(
        [
            "bandit",
            "-c",
            str(config_path),
            "-r",
            str(src_path),
            "--severity-level",
            "high",
            "--format",
            "json",
        ],
        capture_output=True,
        text=True,
        check=False,
    )
    try:
        data = json.loads(result.stdout)
        high_issues = len(data.get("results", []))
        if high_issues == 0:
            return True, "Security: 0 high-severity issues"
        return False, f"Security: {high_issues} high-severity issues"
    except json.JSONDecodeError:
        if result.returncode == 0:
            return True, "Security: passed"
        return False, f"Security: failed (exit code {result.returncode})"


def check_dead_code() -> tuple[bool, str]:
    """Check vulture finds no dead code."""
    # Validate path arguments to prevent command injection
    try:
        project_root = Path(".").resolve()
        src_path = validate_path("src/cleveragents", project_root)
        whitelist_path = validate_path("vulture_whitelist.py", project_root)
    except ValueError as e:
        return False, f"Dead code: invalid path - {e}"

    result = subprocess.run(
        [
            "vulture",
            str(src_path),
            str(whitelist_path),
            "--min-confidence",
            "80",
        ],
        capture_output=True,
        text=True,
        check=False,
    )
    if result.returncode == 0:
        return True, "Dead code: none detected"
    lines = result.stdout.strip().split("\n")
    return False, f"Dead code: {len(lines)} issue(s) found"


def check_complexity(max_grade: str = "E") -> tuple[bool, str]:
    """Check no functions exceed maximum complexity grade."""
    # Validate path argument to prevent command injection
    try:
        project_root = Path(".").resolve()
        src_path = validate_path("src/cleveragents", project_root)
    except ValueError as e:
        return False, f"Complexity: invalid path - {e}"

    result = subprocess.run(
        [
            "radon",
            "cc",
            str(src_path),
            "--min",
            max_grade,
            "--show-complexity",
            "--json",
        ],
        capture_output=True,
        text=True,
        check=False,
    )
    try:
        data = json.loads(result.stdout)
        total_blocks = sum(len(blocks) for blocks in data.values())
        if total_blocks == 0:
            return True, f"Complexity: no blocks with grade >= {max_grade}"
        return False, f"Complexity: {total_blocks} block(s) with grade >= {max_grade}"
    except json.JSONDecodeError:
        return True, "Complexity: passed (no extreme complexity)"


def main() -> int:
    """Run all quality gates and report results."""
    parser = argparse.ArgumentParser(description="Check quality gates")
    parser.add_argument(
        "--coverage-min", type=int, default=85, help="Minimum coverage percentage"
    )
    parser.add_argument(
        "--complexity-max",
        type=str,
        default="F",
        help="Maximum allowed complexity grade (F = fail only on worst)",
    )
    args = parser.parse_args()

    # Ensure build directory exists
    Path("build").mkdir(exist_ok=True)

    gates: list[tuple[str, tuple[bool, str]]] = [
        ("Coverage", check_coverage(args.coverage_min)),
        ("Type Check", check_typecheck()),
        ("Security", check_security()),
        ("Dead Code", check_dead_code()),
        ("Complexity", check_complexity(args.complexity_max)),
    ]

    print("=" * 60)
    print("QUALITY GATE REPORT")
    print("=" * 60)

    all_passed = True
    for name, (passed, message) in gates:
        icon = "[+]" if passed else "[X]"
        print(f"  {icon} {name}: {message}")
        if not passed:
            all_passed = False

    print("=" * 60)
    if all_passed:
        print("All quality gates PASSED")
    else:
        print("Some quality gates FAILED")
    print("=" * 60)

    return 0 if all_passed else 1


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