"""
Vulnerability checking service for SBOM dependencies.

Checks software dependencies against vulnerability databases
to identify known security issues.

Supported Databases:
- OSV (Open Source Vulnerabilities) - Primary
- NVD (National Vulnerability Database) - Fallback
- GitHub Advisory Database - For GitHub-hosted packages

Features:
- Batch vulnerability checking
- Caching to reduce API calls
- Severity classification (CVSS-based)
- Package URL (purl) support

Environment Variables:
    SBOM_VULN_CHECK_ENABLED: Enable vulnerability checking (default: true)
    SBOM_VULN_DB_URL: Custom vulnerability database URL
    SBOM_VULN_CACHE_TTL: Cache TTL in seconds (default: 3600)
    SBOM_VULN_TIMEOUT: API timeout in seconds (default: 30)
    SBOM_OSV_API_URL: OSV API URL (default: https://api.osv.dev/v1)
    SBOM_OSV_API_TIMEOUT: OSV API timeout in seconds (default: 30)

Example Usage:
    >>> from arcp.services.vulnerability import check_dependencies_for_vulnerabilities
    >>> deps = [Dependency(name="requests", version="2.28.0")]
    >>> vulns = await check_dependencies_for_vulnerabilities(deps)
    >>> for v in vulns:
    ...     print(f"{v.id}: {v.severity.value} in {v.package_name}")
"""

import asyncio
import hashlib
import json
import logging
from typing import Any, Dict, List, Optional, Tuple

import httpx

from ..core.config import config
from ..models.sbom import Dependency, SeverityLevel, VulnerabilityInfo
from ..services.redis import get_redis_service

logger = logging.getLogger(__name__)

# Cache key prefix for vulnerability lookups
VULN_CACHE_PREFIX = "arcp:vuln:cache:"

# Default OSV API endpoint
DEFAULT_OSV_API_URL = "https://api.osv.dev/v1"

# Map ecosystem names to OSV ecosystem identifiers
ECOSYSTEM_MAP = {
    "pypi": "PyPI",
    "npm": "npm",
    "maven": "Maven",
    "nuget": "NuGet",
    "go": "Go",
    "cargo": "crates.io",
    "packagist": "Packagist",
    "rubygems": "RubyGems",
    "hex": "Hex",
    "pub": "Pub",
}


class VulnerabilityChecker:
    """
    Check dependencies for known vulnerabilities.

    Uses the OSV API as the primary source, with caching
    to reduce API calls.
    """

    def __init__(self):
        self.enabled = getattr(config, "SBOM_VULNERABILITY_CHECK", True)
        self.cache_ttl = getattr(config, "SBOM_VULNERABILITY_CACHE_TTL", 3600)
        self.api_url = getattr(config, "SBOM_OSV_API_URL", DEFAULT_OSV_API_URL)
        self.timeout = getattr(config, "SBOM_OSV_API_TIMEOUT", 30)
        self._redis = None

    def _get_redis(self):
        """Get Redis client for caching."""
        if self._redis is None:
            try:
                redis_service = get_redis_service()
                if redis_service and redis_service.is_available():
                    self._redis = redis_service.get_client()
            except Exception:
                pass
        return self._redis

    def _get_cache_key(self, package: str, version: str, ecosystem: str) -> str:
        """Generate cache key for a package lookup."""
        key_data = f"{ecosystem}:{package}:{version}"
        key_hash = hashlib.md5(key_data.encode()).hexdigest()
        return f"{VULN_CACHE_PREFIX}{key_hash}"

    async def _get_cached(self, cache_key: str) -> Optional[List[Dict]]:
        """Get cached vulnerability data."""
        redis = self._get_redis()
        if not redis:
            return None

        try:
            data = redis.get(cache_key)
            if data:
                return json.loads(data)
        except Exception as e:
            logger.debug(f"Cache get error: {e}")

        return None

    async def _set_cached(self, cache_key: str, data: List[Dict]) -> None:
        """Cache vulnerability data."""
        redis = self._get_redis()
        if not redis:
            return

        try:
            redis.setex(cache_key, self.cache_ttl, json.dumps(data))
        except Exception as e:
            logger.debug(f"Cache set error: {e}")

    def _parse_ecosystem_from_purl(self, purl: str) -> Tuple[str, str, str]:
        """
        Parse package URL to extract ecosystem, name, and version.

        purl format: pkg:<ecosystem>/<namespace>/<name>@<version>

        Returns:
            Tuple of (ecosystem, name, version)
        """
        if not purl or not purl.startswith("pkg:"):
            return "", "", ""

        try:
            # Remove pkg: prefix
            rest = purl[4:]

            # Split ecosystem and rest
            if "/" not in rest:
                return "", "", ""

            ecosystem, path = rest.split("/", 1)

            # Extract version
            version = ""
            if "@" in path:
                path, version = path.rsplit("@", 1)
                # Remove qualifiers from version
                if "?" in version:
                    version = version.split("?")[0]

            # Handle namespace/name
            if "/" in path:
                # Has namespace (e.g., @scope/name for npm)
                name = path
            else:
                name = path

            # Map to OSV ecosystem
            osv_ecosystem = ECOSYSTEM_MAP.get(ecosystem.lower(), ecosystem)

            return osv_ecosystem, name, version

        except Exception as e:
            logger.debug(f"Failed to parse purl {purl}: {e}")
            return "", "", ""

    def _convert_severity(
        self, severity_str: str, score: Optional[float] = None
    ) -> SeverityLevel:
        """Convert severity string to SeverityLevel enum."""
        if severity_str:
            severity_upper = severity_str.upper()
            if severity_upper in ("CRITICAL", "CVSS:CRITICAL"):
                return SeverityLevel.CRITICAL
            if severity_upper in ("HIGH", "CVSS:HIGH"):
                return SeverityLevel.HIGH
            if severity_upper in ("MEDIUM", "MODERATE", "CVSS:MEDIUM"):
                return SeverityLevel.MEDIUM
            if severity_upper in ("LOW", "CVSS:LOW"):
                return SeverityLevel.LOW

        # Derive from CVSS score if available
        if score is not None:
            if score >= 9.0:
                return SeverityLevel.CRITICAL
            if score >= 7.0:
                return SeverityLevel.HIGH
            if score >= 4.0:
                return SeverityLevel.MEDIUM
            if score > 0:
                return SeverityLevel.LOW
            return SeverityLevel.NONE

        return SeverityLevel.UNKNOWN

    async def check_package(
        self, name: str, version: str, ecosystem: str = "PyPI"
    ) -> List[VulnerabilityInfo]:
        """
        Check a single package for vulnerabilities.

        Args:
            name: Package name
            version: Package version
            ecosystem: Package ecosystem (PyPI, npm, etc.)

        Returns:
            List of VulnerabilityInfo for found vulnerabilities
        """
        if not self.enabled:
            return []

        # Check cache first
        cache_key = self._get_cache_key(name, version, ecosystem)
        cached = await self._get_cached(cache_key)
        if cached is not None:
            return [self._dict_to_vuln_info(v) for v in cached]

        vulnerabilities = []

        try:
            async with httpx.AsyncClient(timeout=self.timeout) as client:
                # Query OSV API
                response = await client.post(
                    f"{self.api_url}/query",
                    json={
                        "package": {
                            "name": name,
                            "ecosystem": ecosystem,
                        },
                        "version": version,
                    },
                )

                if response.status_code == 200:
                    data = response.json()
                    vulns = data.get("vulns", [])

                    for vuln in vulns:
                        vuln_info = self._parse_osv_vulnerability(vuln, name, version)
                        if vuln_info:
                            vulnerabilities.append(vuln_info)
                else:
                    logger.warning(
                        f"OSV API returned {response.status_code} for {name}@{version}"
                    )

        except httpx.TimeoutException:
            logger.warning(f"Timeout checking vulnerabilities for {name}@{version}")
        except Exception as e:
            logger.error(f"Error checking vulnerabilities for {name}@{version}: {e}")

        # Cache results (even empty ones)
        await self._set_cached(cache_key, [v.to_dict() for v in vulnerabilities])

        return vulnerabilities

    def _parse_osv_vulnerability(
        self, vuln_data: Dict[str, Any], package_name: str, installed_version: str
    ) -> Optional[VulnerabilityInfo]:
        """Parse OSV API vulnerability response."""
        try:
            vuln_id = vuln_data.get("id", "")

            # Get severity
            severity = SeverityLevel.UNKNOWN
            cvss_score = None
            cvss_vector = None

            severities = vuln_data.get("severity", [])
            for sev in severities:
                if isinstance(sev, dict):
                    sev_type = sev.get("type", "")
                    sev_score = sev.get("score", "")

                    if sev_type.startswith("CVSS"):
                        # Parse CVSS score from vector string
                        try:
                            # CVSS:3.1/AV:N/AC:L/... format
                            cvss_vector = sev_score
                            # Extract base score (not always in vector)
                        except Exception:
                            pass

            # Try database-specific severity
            db_specific = vuln_data.get("database_specific", {})
            if "severity" in db_specific:
                severity = self._convert_severity(db_specific["severity"])

            # Get affected versions and fixed version
            fixed_version = None
            affected = vuln_data.get("affected", [])
            for aff in affected:
                if isinstance(aff, dict):
                    ranges = aff.get("ranges", [])
                    for r in ranges:
                        if isinstance(r, dict):
                            events = r.get("events", [])
                            for event in events:
                                if isinstance(event, dict) and "fixed" in event:
                                    fixed_version = event["fixed"]
                                    break

            # Get summary/details
            summary = vuln_data.get("summary", "")
            details = vuln_data.get("details", "")

            # Get references
            references = []
            refs = vuln_data.get("references", [])
            for ref in refs:
                if isinstance(ref, dict) and "url" in ref:
                    references.append(ref["url"])

            return VulnerabilityInfo(
                id=vuln_id,
                severity=severity,
                package_name=package_name,
                installed_version=installed_version,
                fixed_version=fixed_version,
                title=summary[:200] if summary else None,
                description=details[:500] if details else None,
                cvss_score=cvss_score,
                cvss_vector=cvss_vector,
                references=references[:5],  # Limit to 5 refs
            )

        except Exception as e:
            logger.debug(f"Failed to parse vulnerability: {e}")
            return None

    def _dict_to_vuln_info(self, data: Dict[str, Any]) -> VulnerabilityInfo:
        """Convert cached dictionary back to VulnerabilityInfo."""
        return VulnerabilityInfo(
            id=data.get("id", ""),
            severity=SeverityLevel(data.get("severity", "UNKNOWN")),
            package_name=data.get("package_name", ""),
            installed_version=data.get("installed_version", ""),
            fixed_version=data.get("fixed_version"),
            title=data.get("title"),
            description=data.get("description"),
            cvss_score=data.get("cvss_score"),
            cvss_vector=data.get("cvss_vector"),
            references=data.get("references", []),
        )

    async def check_dependencies(
        self, dependencies: List[Dependency], max_concurrent: int = 10
    ) -> List[VulnerabilityInfo]:
        """
        Check multiple dependencies for vulnerabilities.

        Uses concurrent requests with rate limiting.

        Args:
            dependencies: List of dependencies to check
            max_concurrent: Maximum concurrent API requests

        Returns:
            List of all vulnerabilities found
        """
        if not self.enabled or not dependencies:
            return []

        all_vulnerabilities: List[VulnerabilityInfo] = []
        semaphore = asyncio.Semaphore(max_concurrent)

        async def check_with_semaphore(dep: Dependency) -> List[VulnerabilityInfo]:
            async with semaphore:
                # Determine ecosystem
                ecosystem = "PyPI"  # Default
                if dep.purl:
                    eco, name, version = self._parse_ecosystem_from_purl(dep.purl)
                    if eco:
                        ecosystem = eco
                    if name:
                        dep_name = name
                    else:
                        dep_name = dep.name
                    if version:
                        dep_version = version
                    else:
                        dep_version = dep.version
                else:
                    dep_name = dep.name
                    dep_version = dep.version
                    if dep.ecosystem:
                        ecosystem = ECOSYSTEM_MAP.get(
                            dep.ecosystem.lower(), dep.ecosystem
                        )

                return await self.check_package(dep_name, dep_version, ecosystem)

        # Check all dependencies concurrently
        tasks = [check_with_semaphore(dep) for dep in dependencies]
        results = await asyncio.gather(*tasks, return_exceptions=True)

        for result in results:
            if isinstance(result, list):
                all_vulnerabilities.extend(result)
            elif isinstance(result, Exception):
                logger.warning(f"Dependency check failed: {result}")

        logger.info(
            f"Vulnerability check complete: {len(dependencies)} packages, "
            f"{len(all_vulnerabilities)} vulnerabilities found"
        )

        return all_vulnerabilities


# Module-level singleton
_vulnerability_checker: Optional[VulnerabilityChecker] = None


def get_vulnerability_checker() -> VulnerabilityChecker:
    """Get the vulnerability checker singleton."""
    global _vulnerability_checker
    if _vulnerability_checker is None:
        _vulnerability_checker = VulnerabilityChecker()
    return _vulnerability_checker


async def check_dependencies_for_vulnerabilities(
    dependencies: List[Dependency],
) -> List[VulnerabilityInfo]:
    """
    Convenience function to check dependencies for vulnerabilities.

    Args:
        dependencies: List of Dependency objects to check

    Returns:
        List of VulnerabilityInfo for all found vulnerabilities
    """
    checker = get_vulnerability_checker()
    return await checker.check_dependencies(dependencies)


async def check_package_for_vulnerabilities(
    name: str, version: str, ecosystem: str = "PyPI"
) -> List[VulnerabilityInfo]:
    """
    Convenience function to check a single package.

    Args:
        name: Package name
        version: Package version
        ecosystem: Package ecosystem (default: PyPI)

    Returns:
        List of VulnerabilityInfo for found vulnerabilities
    """
    checker = get_vulnerability_checker()
    return await checker.check_package(name, version, ecosystem)
