"""Dependencies Analyzer — converts OSV vulnerability results into typed findings."""

from __future__ import annotations

import re
from pathlib import Path
from typing import Any

from sentinel.analyzers.base import BaseAnalyzer
from sentinel.models.config import DepsConfig
from sentinel.models.findings import (
    ComplianceMapping,
    Evidence,
    Finding,
    ScanResult,
    ScanStats,
    Severity,
)


class DepsAnalyzer(BaseAnalyzer):
    module_name = "deps"

    def __init__(self, repo_path: Path, config: DepsConfig) -> None:
        super().__init__(repo_path)
        self.config = config

    def analyze(self, collected: dict[str, Any]) -> ScanResult:
        findings: list[Finding] = []
        ignore_cves = set(self.config.ignore_cves)

        for eco_result in collected.get("ecosystems_found", []):
            findings += self._check_lockfile(eco_result)
            findings += self._check_floating_versions(eco_result)

        findings += self._check_vulnerabilities(collected.get("vulnerabilities", []), ignore_cves)

        stats = ScanStats(
            files_scanned=len(collected.get("ecosystems_found", [])),
            rules_evaluated=len(collected.get("vulnerabilities", [])),
            findings_count=len(findings),
        )
        return self._make_result(findings, stats)

    def _check_lockfile(self, eco: dict[str, Any]) -> list[Finding]:
        if eco.get("has_lockfile"):
            return []
        lockfile_help = {
            "npm": "npm install  →  package-lock.json",
            "pip": "pipenv install  →  Pipfile.lock  (or pin with pip freeze)",
            "Go": "go mod tidy  →  go.sum",
            "crates.io": "cargo build  →  Cargo.lock",
            "RubyGems": "bundle install  →  Gemfile.lock",
        }
        return [Finding(
            id=f"deps.no_lockfile.{eco['ecosystem'].lower().replace('.', '_')}",
            title=f"No lockfile committed for {eco['ecosystem']}",
            description=(
                f"The {eco['ecosystem']} manifest ({eco.get('manifest_file', '')}) has no "
                "committed lockfile. Builds are non-reproducible — a new install could "
                "resolve a vulnerable version without any config change."
            ),
            severity=Severity.HIGH,
            module=self.module_name,
            evidence=[Evidence(
                file=eco.get("manifest_file", ""),
                context=f"Expected: {eco.get('lockfile', 'lockfile')}",
            )],
            remediation=(
                "Generate and commit the lockfile:\n  "
                + lockfile_help.get(eco["ecosystem"], "run your package manager's install command")
            ),
            compliance=ComplianceMapping(
                soc2=["CC8.1"],
                cis=["CIS-SSCS-2"],
                iso27001=["A.8.8"],
            ),
        )]

    def _check_floating_versions(self, eco: dict[str, Any]) -> list[Finding]:
        findings: list[Finding] = []
        seen: set[str] = set()
        for pkg in eco.get("packages", []):
            name = pkg["name"]
            if name in seen:
                continue
            ver_spec = str(pkg.get("version_spec", ""))
            is_floating = (
                any(c in ver_spec for c in ("^", "~", "*"))
                or not pkg.get("resolved_version")
            )
            if is_floating:
                seen.add(name)
                findings.append(Finding(
                    id=(
                        f"deps.floating.{eco['ecosystem'].lower()}"
                        f".{re.sub(r'[^a-z0-9]', '_', name.lower()[:30])}"
                    ),
                    title=f"Floating version range: {name} ({ver_spec or 'unpinned'})",
                    description=(
                        f"Package `{name}` uses a floating version specifier (`{ver_spec}`). "
                        "This allows the package manager to upgrade to any compatible version, "
                        "including versions that introduce new vulnerabilities."
                    ),
                    severity=Severity.MEDIUM,
                    module=self.module_name,
                    evidence=[Evidence(
                        file=eco.get("manifest_file", ""),
                        context=f"{name}: {ver_spec}",
                    )],
                    remediation=f"Pin `{name}` to an exact version and commit a lockfile.",
                    compliance=ComplianceMapping(
                        soc2=["CC8.1"],
                        cis=["CIS-SSCS-2"],
                        iso27001=["A.8.8"],
                    ),
                ))
        return findings

    def _check_vulnerabilities(
        self,
        vuln_entries: list[dict[str, Any]],
        ignore_cves: set[str],
    ) -> list[Finding]:
        findings: list[Finding] = []
        seen: set[str] = set()

        for entry in vuln_entries:
            pkg = entry["package"]
            for vuln in entry["vulns"]:
                cve_id = vuln.get("id", "UNKNOWN")
                # Check primary ID and aliases
                all_ids = {cve_id} | set(vuln.get("aliases", []))
                if all_ids & ignore_cves:
                    continue

                dedup_key = f"{cve_id}:{pkg['name']}:{pkg['version']}"
                if dedup_key in seen:
                    continue
                seen.add(dedup_key)

                severity = self._osv_severity(vuln)

                findings.append(Finding(
                    id=f"deps.cve.{re.sub(r'[^a-z0-9]', '_', cve_id.lower())}.{pkg['name'][:20]}",
                    title=f"{cve_id} in {pkg['name']} {pkg['version']}",
                    description=(
                        f"{vuln.get('summary', 'Known vulnerability in dependency')}. "
                        f"Affects `{pkg['name']}` version `{pkg['version']}` "
                        f"({pkg['source_ecosystem']})."
                    ),
                    severity=severity,
                    module=self.module_name,
                    evidence=[Evidence(
                        file=f"({pkg['source_ecosystem']} manifest)",
                        context=f"{pkg['name']}@{pkg['version']}",
                    )],
                    remediation=(
                        f"Upgrade `{pkg['name']}` to a patched version. "
                        f"See: https://osv.dev/vulnerability/{cve_id}"
                    ),
                    compliance=ComplianceMapping(
                        soc2=["CC7.1"],
                        cis=["CIS-GitLab-3.3"],
                        owasp_cicd=["CICD-SEC-3"],
                        iso27001=["A.8.8"],
                    ),
                    tags=["cve", cve_id],
                ))

        return findings

    @staticmethod
    def _osv_severity(vuln: dict[str, Any]) -> Severity:
        """Map CVSS score or OSV severity rating to Sentinel Severity."""
        highest = 0.0
        for sev_entry in vuln.get("severity", []):
            if sev_entry.get("type") == "CVSS_V3":
                score_str = str(sev_entry.get("score", ""))
                # CVSS vector string ends with the base score: /7.5 or similar
                m = re.search(r"(\d+\.\d+)$", score_str)
                if m:
                    try:
                        highest = max(highest, float(m.group(1)))
                    except ValueError:
                        pass

        if highest >= 9.0:
            return Severity.CRITICAL
        if highest >= 7.0:
            return Severity.HIGH
        if highest >= 4.0:
            return Severity.MEDIUM
        if highest > 0:
            return Severity.LOW

        # Fall back to database_specific severity string
        db_sev = str(vuln.get("database_specific", {}).get("severity", "")).upper()
        mapping = {"CRITICAL": Severity.CRITICAL, "HIGH": Severity.HIGH,
                   "MEDIUM": Severity.MEDIUM, "LOW": Severity.LOW}
        return mapping.get(db_sev, Severity.MEDIUM)
