"""AP016: Insecure Dependencies Detection."""

import re
from collections.abc import Iterator
from pathlib import Path

from apiposture.core.models.endpoint import Endpoint
from apiposture.core.models.enums import Severity
from apiposture.core.models.finding import Finding

from apiposture_pro.rules.base import ProSecurityRule


def _parse_version(version_str: str) -> tuple[int, ...]:
    """Parse a version string into a tuple of integers for proper comparison."""
    try:
        return tuple(int(x) for x in version_str.split("."))
    except (ValueError, AttributeError):
        return (0,)


# Known vulnerable package patterns (simplified - in production would use a CVE database)
VULNERABLE_PATTERNS = {
    "django": {
        "<2.2.28": "Multiple security vulnerabilities including SQL injection",
        "<3.2.18": "Security vulnerabilities in older versions",
    },
    "flask": {
        "<2.3.0": "Security vulnerabilities in older versions",
    },
    "requests": {
        "<2.31.0": "Proxy authentication header leak vulnerability",
    },
    "pyyaml": {
        "<6.0": "Arbitrary code execution via unsafe yaml.load()",
    },
    "pillow": {
        "<9.3.0": "Multiple image processing vulnerabilities",
    },
    "cryptography": {
        "<41.0.0": "Security vulnerabilities in older versions",
    },
    "urllib3": {
        "<1.26.18": "Cookie leakage and request smuggling vulnerabilities",
    },
    "jinja2": {
        "<3.1.0": "Template injection vulnerabilities",
    },
    "sqlalchemy": {
        "<1.4.46": "SQL injection vulnerabilities in specific use cases",
    },
    "werkzeug": {
        "<2.3.0": "Path traversal and debugger PIN bypass vulnerabilities",
    },
    "starlette": {
        "<0.27.0": "Directory traversal and multipart parsing vulnerabilities",
    },
    "fastapi": {
        "<0.100.0": "Security fixes for dependency injection edge cases",
    },
    "gunicorn": {
        "<21.2.0": "HTTP request smuggling vulnerabilities",
    },
    "celery": {
        "<5.3.0": "Deserialization and command injection vulnerabilities",
    },
    "paramiko": {
        "<3.4.0": "Authentication bypass and prefix truncation vulnerabilities",
    },
    "lxml": {
        "<4.9.0": "XML external entity and buffer overflow vulnerabilities",
    },
    "setuptools": {
        "<65.5.1": "Remote code execution via crafted package URLs",
    },
}

# Regex to parse version specifiers from requirements lines
_VERSION_RE = re.compile(r"^([a-zA-Z0-9_.-]+)\s*(==|>=|~=|<=|>|<|!=)\s*([^\s;,#]+)")


class AP016InsecureDependencies(ProSecurityRule):
    """
    AP016: Insecure Dependencies Detection.

    Scans dependency files for:
    - Known vulnerable package versions
    - Deprecated/insecure packages
    - Missing security updates
    """

    def __init__(self) -> None:
        super().__init__()
        self._scanned_projects: set[Path] = set()

    @property
    def rule_id(self) -> str:
        return "AP016"

    @property
    def name(self) -> str:
        return "Insecure Dependencies"

    @property
    def severity(self) -> Severity:
        return Severity.HIGH

    @property
    def description(self) -> str:
        return (
            "Detects vulnerable or insecure dependencies in project requirements"
        )

    def evaluate(self, endpoint: Endpoint) -> Iterator[Finding]:
        """Scan project dependencies for known vulnerabilities."""
        # Get project root (scan once per project)
        project_root = endpoint.file_path.parent
        while project_root.parent != project_root:
            if (project_root / "requirements.txt").exists():
                break
            if (project_root / "pyproject.toml").exists():
                break
            if (project_root / "setup.py").exists():
                break
            project_root = project_root.parent

        # Only scan once per project
        if project_root in self._scanned_projects:
            return

        self._scanned_projects.add(project_root)

        # Check requirements.txt
        requirements_file = project_root / "requirements.txt"
        if requirements_file.exists():
            yield from self._scan_requirements(requirements_file, endpoint)

        # Check Pipfile
        pipfile = project_root / "Pipfile"
        if pipfile.exists():
            yield from self._scan_pipfile(pipfile, endpoint)

        # Check pyproject.toml
        pyproject = project_root / "pyproject.toml"
        if pyproject.exists():
            yield from self._scan_pyproject(pyproject, endpoint)

    def _scan_requirements(self, req_file: Path, endpoint: Endpoint) -> Iterator[Finding]:
        """Scan requirements.txt for vulnerabilities."""
        try:
            content = req_file.read_text(encoding="utf-8", errors="ignore")
        except Exception:
            return

        for line_num, line in enumerate(content.split("\n"), start=1):
            line = line.strip()

            # Skip comments and empty lines
            if not line or line.startswith("#") or line.startswith("-"):
                continue

            # Parse package and version using regex
            match = _VERSION_RE.match(line)
            if match:
                package = match.group(1).strip().lower()
                operator = match.group(2)
                version = match.group(3).strip().split(";")[0].strip()

                # Determine the effective minimum version to check
                # ==, >= and ~= all specify a minimum version we can check
                if operator in ("==", ">=", "~="):
                    effective_version = version
                elif operator in (">",):
                    # > means strictly greater, the installed version is at least this
                    effective_version = version
                else:
                    # <=, <, != — can't determine a minimum version to check
                    effective_version = None

                if effective_version and package in VULNERABLE_PATTERNS:
                    parsed_version = _parse_version(effective_version)
                    for vuln_version, description in VULNERABLE_PATTERNS[package].items():
                        if vuln_version.startswith("<"):
                            max_version_str = vuln_version[1:]
                            parsed_max = _parse_version(max_version_str)
                            if parsed_version < parsed_max:
                                yield self.create_finding(
                                    endpoint,
                                    message=(
                                        f"Vulnerable dependency: {package}{operator}{version} in "
                                        f"{req_file}:{line_num}. {description}"
                                    ),
                                    recommendation=(
                                        f"Update {package} to {max_version_str} or later. "
                                        f"Run 'pip install --upgrade {package}' and test thoroughly. "
                                        "Check release notes for breaking changes."
                                    ),
                                    severity=Severity.HIGH,
                                )

    def _scan_pipfile(self, pipfile: Path, endpoint: Endpoint) -> Iterator[Finding]:
        """Scan Pipfile for vulnerabilities."""
        # Basic Pipfile scanning (would need toml parser for full support)
        try:
            content = pipfile.read_text(encoding="utf-8", errors="ignore")
        except Exception:
            return

        # Simple pattern matching for packages
        for line_num, line in enumerate(content.split("\n"), start=1):
            line = line.strip()
            if not line or line.startswith("#"):
                continue

            # Look for package definitions: package = "==version"
            if "=" in line and '"' in line:
                parts = line.split("=")
                if len(parts) >= 2:
                    package = parts[0].strip().lower()
                    if package in VULNERABLE_PATTERNS:
                        # Try to extract version — only flag if a specific version is found
                        version_str = "".join(parts[1:]).strip().strip('"').strip("'")
                        version_match = _VERSION_RE.match(f"{package}{version_str}")
                        if not version_match:
                            # Try direct version parse (e.g., "==1.2.3")
                            version_match = re.match(r"[=~<>!]*(\d+\.\d+[\d.]*)", version_str)
                        if version_match:
                            version = version_match.group(1) if version_match.lastindex else version_str.lstrip("=~<>!")
                            parsed_version = _parse_version(version)
                            is_vulnerable = False
                            for vuln_version in VULNERABLE_PATTERNS[package]:
                                if vuln_version.startswith("<"):
                                    parsed_max = _parse_version(vuln_version[1:])
                                    if parsed_version < parsed_max:
                                        is_vulnerable = True
                                        break
                            if is_vulnerable:
                                yield self.create_finding(
                                    endpoint,
                                    message=(
                                        f"Vulnerable dependency {package} ({version}) in "
                                        f"{pipfile}:{line_num}"
                                    ),
                                    recommendation=(
                                        f"Review {package} version for security updates. "
                                        "Run 'pipenv update' to upgrade dependencies."
                                    ),
                                    severity=Severity.HIGH,
                                )

    def _scan_pyproject(self, pyproject_file: Path, endpoint: Endpoint) -> Iterator[Finding]:
        """Scan pyproject.toml for vulnerabilities."""
        # Basic pyproject.toml scanning
        try:
            content = pyproject_file.read_text(encoding="utf-8", errors="ignore")
        except Exception:
            return

        in_dependencies = False
        in_pep621_deps = False
        for line_num, line in enumerate(content.split("\n"), start=1):
            stripped = line.strip()

            # Track if we're in dependencies section
            if stripped.startswith("[tool.poetry.dependencies]"):
                in_dependencies = True
                in_pep621_deps = False
                continue
            elif stripped.startswith("[project.dependencies]") or stripped == "dependencies = [":
                in_pep621_deps = True
                in_dependencies = False
                continue
            elif stripped.startswith("["):
                in_dependencies = False
                in_pep621_deps = False

            # Poetry-style: package = "^version" or package = {version = "^1.2"}
            if in_dependencies and "=" in stripped:
                parts = stripped.split("=")
                if len(parts) >= 2:
                    package = parts[0].strip().lower().strip('"').strip("'")
                    if package in VULNERABLE_PATTERNS:
                        # Extract version — only flag if a specific vulnerable version is found
                        version_str = "=".join(parts[1:]).strip().strip('"').strip("'")
                        version_match = re.match(r"[\^~=<>!]*(\d+\.\d+[\d.]*)", version_str)
                        if version_match:
                            version = version_match.group(1)
                            parsed_version = _parse_version(version)
                            for vuln_version, description in VULNERABLE_PATTERNS[package].items():
                                if vuln_version.startswith("<"):
                                    parsed_max = _parse_version(vuln_version[1:])
                                    if parsed_version < parsed_max:
                                        yield self.create_finding(
                                            endpoint,
                                            message=(
                                                f"Vulnerable dependency: {package}=={version} in "
                                                f"{pyproject_file}:{line_num}. {description}"
                                            ),
                                            recommendation=(
                                                f"Update {package} to {vuln_version[1:]} or later. "
                                                "Run 'poetry update' or update version constraints in pyproject.toml."
                                            ),
                                            severity=Severity.HIGH,
                                        )
                                        break

            # PEP 621-style: "package>=version" in a list
            if in_pep621_deps and stripped.startswith('"'):
                dep_str = stripped.strip('",').strip("',")
                dep_match = _VERSION_RE.match(dep_str)
                if dep_match:
                    package = dep_match.group(1).strip().lower()
                    operator = dep_match.group(2)
                    version = dep_match.group(3).strip().split(";")[0].strip()
                    if package in VULNERABLE_PATTERNS and operator in ("==", ">=", "~="):
                        parsed_version = _parse_version(version)
                        for vuln_version, description in VULNERABLE_PATTERNS[package].items():
                            if vuln_version.startswith("<"):
                                parsed_max = _parse_version(vuln_version[1:])
                                if parsed_version < parsed_max:
                                    yield self.create_finding(
                                        endpoint,
                                        message=(
                                            f"Vulnerable dependency: {package}{operator}{version} in "
                                            f"{pyproject_file}:{line_num}. {description}"
                                        ),
                                        recommendation=(
                                            f"Update {package} to {vuln_version[1:]} or later. "
                                            "Update version constraints in pyproject.toml."
                                        ),
                                        severity=Severity.HIGH,
                                    )
                                    break
