import json
import os
from packaging import version
from packaging.specifiers import SpecifierSet, InvalidSpecifier

_DB_PATH = os.path.join(os.path.dirname(__file__), "database", "db.json")


class NaalScanner:
    """
    Core engine for the NaalScan library.
    Reads the PyUp.io db.json database to find vulnerabilities.
    """

    def __init__(self, custom_db_path=None):
        self.db_path = custom_db_path or _DB_PATH
        self._db = self._load_database()

    def _load_database(self):
        if not os.path.exists(self.db_path):
            return {}
        with open(self.db_path, "r", encoding="utf-8") as f:
            raw_data = json.load(f)
            # Filter out metadata keys like "$meta"
            return {k: v for k, v in raw_data.items() if not k.startswith("$")}

    def check_package(self, name: str, current_version: str) -> list:
        """Checks a single package against the database."""
        name = name.lower().replace("_", "-")
        entries = self._db.get(name, [])

        try:
            pkg_ver = version.parse(current_version)
        except Exception:
            return []

        results = []
        for entry in entries:
            # db.json uses "v" field like ">0,<0" OR a "specs" list
            # We support both formats
            specs = self._get_specs(entry)

            if self._matches(pkg_ver, specs):
                results.append({
                    "id":            entry.get("id", entry.get("cve", "UNKNOWN")),
                    "cve":           entry.get("cve", "N/A"),
                    "package":       name,
                    "version":       current_version,
                    "severity":      entry.get("severity", "HIGH"),       # db.json often omits this
                    "cwe":           entry.get("cwe", "CWE-SupplyChain"), # db.json often omits this
                    "advisory":      entry.get("advisory", "No description available."),
                    "fixed_version": entry.get("fixed_in", "Latest"),     # db.json often omits this
                })
        return results

    def scan_requirements(self, file_content: str) -> dict:
        """Parses a requirements.txt string and runs a full scan."""
        packages = self._parse_requirements(file_content)
        all_findings = []

        for pkg in packages:
            findings = self.check_package(pkg["name"], pkg["version"])
            all_findings.extend(findings)

        vulnerable_packages = list(set(f["package"] for f in all_findings))

        return {
            "total_scanned":      len(packages),
            "critical_count":     sum(1 for f in all_findings if f["severity"] == "CRITICAL"),
            "high_count":         sum(1 for f in all_findings if f["severity"] == "HIGH"),
            "findings":           all_findings,
            "vulnerable_packages": vulnerable_packages,
        }

    # --- Internal Helpers ---

    def _get_specs(self, entry: dict) -> list:
        """
        db.json stores version constraints in two ways:
          1. "specs": [">0", "<0"]  — list of individual specifiers (OR logic)
          2. "v": ">0,<0"           — comma-separated string (AND logic)
        We prefer "v" for AND logic, fall back to "specs" list for OR logic.
        """
        # "v" field: comma-separated AND specifiers e.g. ">=1.0,<2.0"
        v_field = entry.get("v", "")
        if v_field and v_field.strip():
            return [v_field]  # treat as a single SpecifierSet (AND logic)

        # fallback: "specs" list treated as OR logic
        return entry.get("specs", [])

    def _matches(self, pkg_ver, specs: list) -> bool:
        """
        For each spec string, check if the package version matches.
        Returns True if ANY spec matches.
        """
        for spec_str in specs:
            spec_str = spec_str.strip()
            if not spec_str:
                continue
            try:
                if pkg_ver in SpecifierSet(spec_str, prereleases=True):
                    return True
            except InvalidSpecifier:
                continue
        return False

    def _parse_requirements(self, text: str) -> list:
        pkgs = []
        for line in text.splitlines():
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            if "==" in line:
                name, ver = line.split("==", 1)
                pkgs.append({"name": name.strip(), "version": ver.strip()})
        return pkgs