"""Custom certificate file scanner for quantum vulnerability detection.

Parses the actual certificate structure (X.509 or PKCS#7, PEM or DER) using the
`cryptography` library to determine the real signature algorithm and public key
type, rather than regex-matching the raw file text. Real certificate files are
base64-encoded (PEM) or raw binary (DER) ASN.1 structures — algorithm identifiers
are encoded as ASN.1 OID bytes, not literal strings like "sha256WithRSAEncryption"
or dotted-decimal OID text, so the previous regex-only approach essentially never
matched a genuine certificate file (only a human-readable `openssl x509 -text`
dump, which is not what customers actually have on disk). The regex scan is kept
as a fallback for inputs that don't parse as a valid certificate at all.
"""

import re
import logging
from pathlib import Path
from typing import List, Dict, Any, Optional

from cryptography import x509
from cryptography.hazmat.primitives.serialization import pkcs7
from cryptography.hazmat.primitives.asymmetric import rsa, ec, dsa
from cryptography.exceptions import UnsupportedAlgorithm

logger = logging.getLogger(__name__)

CERT_RULES = [
    {
        "id": "cert-weak-rsa-signature",
        "pattern": re.compile(r"sha(?:1|256|384|512)WithRSAEncryption|md5WithRSAEncryption", re.IGNORECASE),
        "message": "Certificate uses RSA signature algorithm which is quantum-vulnerable. Migrate to a post-quantum algorithm.",
        "severity": "ERROR",
        "metadata": {"quantum_vulnerable": True, "quantum_risk_score": 90},
    },
    {
        "id": "cert-weak-ecc-signature",
        "pattern": re.compile(r"ecdsa-with-SHA(?:256|384|512)|ecPublicKey", re.IGNORECASE),
        "message": "Certificate uses ECDSA/ECC which is quantum-vulnerable. Migrate to a post-quantum algorithm.",
        "severity": "ERROR",
        "metadata": {"quantum_vulnerable": True, "quantum_risk_score": 85},
    },
    {
        "id": "cert-weak-dsa-signature",
        "pattern": re.compile(r"dsaWithSHA|id-dsa", re.IGNORECASE),
        "message": "Certificate uses DSA which is quantum-vulnerable. Migrate to a post-quantum algorithm.",
        "severity": "ERROR",
        "metadata": {"quantum_vulnerable": True, "quantum_risk_score": 90},
    },
    {
        "id": "cert-rsa-oid",
        "pattern": re.compile(r"1\.2\.840\.113549\.1\.1\.(?:1|4|5|11|12|13|14)"),
        "message": "Certificate OID indicates RSA key or RSA-based signature — quantum-vulnerable.",
        "severity": "ERROR",
        "metadata": {"quantum_vulnerable": True, "quantum_risk_score": 90},
    },
    {
        "id": "cert-ecc-oid",
        "pattern": re.compile(r"1\.2\.840\.10045\.(?:2\.1|4\.3\.[12347])"),
        "message": "Certificate OID indicates ECC key or ECDSA signature — quantum-vulnerable.",
        "severity": "ERROR",
        "metadata": {"quantum_vulnerable": True, "quantum_risk_score": 85},
    },
]

# Prefixes of known weak/quantum-vulnerable signature algorithm OIDs (dotted string).
# 1.2.840.113549.1.1.*  = RSA (PKCS#1: md5/sha1/sha256/sha384/sha512WithRSAEncryption)
# 1.2.840.10040.4.*     = DSA (dsaWithSha1 etc.)
# 2.16.840.1.101.3.4.3.* = DSA with SHA-2 (dsaWithSha256 etc., NIST arc)
# 1.2.840.10045.4.*     = ECDSA (ecdsaWithSHA256 etc.)
_RSA_SIG_PREFIX = "1.2.840.113549.1.1."
_DSA_SIG_PREFIXES = ("1.2.840.10040.4.", "2.16.840.1.101.3.4.3.")
_ECDSA_SIG_PREFIX = "1.2.840.10045.4."


def _findings_from_parsed_cert(cert: "x509.Certificate") -> List[Dict[str, Any]]:
    """Build findings from an actually-parsed certificate's real algorithm data."""
    findings: List[Dict[str, Any]] = []

    sig_oid = cert.signature_algorithm_oid.dotted_string
    if sig_oid.startswith(_RSA_SIG_PREFIX):
        rule = CERT_RULES[0]  # cert-weak-rsa-signature
        findings.append(_finding(rule, f"Signature algorithm OID {sig_oid} (RSA)"))
    elif sig_oid.startswith(_ECDSA_SIG_PREFIX):
        rule = CERT_RULES[1]  # cert-weak-ecc-signature
        findings.append(_finding(rule, f"Signature algorithm OID {sig_oid} (ECDSA)"))
    elif any(sig_oid.startswith(p) for p in _DSA_SIG_PREFIXES):
        rule = CERT_RULES[2]  # cert-weak-dsa-signature
        findings.append(_finding(rule, f"Signature algorithm OID {sig_oid} (DSA)"))

    try:
        public_key = cert.public_key()
    except (UnsupportedAlgorithm, ValueError) as e:
        logger.debug(f"Could not read public key from certificate: {e}")
        public_key = None

    if isinstance(public_key, rsa.RSAPublicKey):
        rule = CERT_RULES[3]  # cert-rsa-oid
        findings.append(_finding(rule, f"Public key type: RSA ({public_key.key_size}-bit)"))
    elif isinstance(public_key, ec.EllipticCurvePublicKey):
        rule = CERT_RULES[4]  # cert-ecc-oid
        findings.append(_finding(rule, f"Public key type: EC ({public_key.curve.name})"))
    elif isinstance(public_key, dsa.DSAPublicKey):
        rule = CERT_RULES[2]  # cert-weak-dsa-signature (no dedicated DSA-key rule_id exists)
        findings.append(_finding(rule, f"Public key type: DSA ({public_key.key_size}-bit)"))

    return findings


def _finding(rule: Dict[str, Any], detail: str) -> Dict[str, Any]:
    return {
        "rule_id": rule["id"],
        "message": rule["message"],
        "severity": rule["severity"],
        "line": 1,
        "line_content": detail,
        "metadata": rule["metadata"],
    }


def _dedupe(findings: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    seen: set = set()
    deduped = []
    for f in findings:
        key = (f["rule_id"], f["line"])
        if key in seen:
            continue
        seen.add(key)
        deduped.append(f)
    return deduped


def _try_parse_certificates(data: bytes) -> Optional[List["x509.Certificate"]]:
    """Try every supported container/encoding combination. Returns None if none parse."""
    # Single X.509 certificate, PEM then DER
    for loader in (x509.load_pem_x509_certificate, x509.load_der_x509_certificate):
        try:
            return [loader(data)]
        except Exception:
            pass

    # PKCS#7 bundle (.p7b/.p7c, or a .cer exported as a PKCS#7 blob), PEM then DER
    for loader in (pkcs7.load_pem_pkcs7_certificates, pkcs7.load_der_pkcs7_certificates):
        try:
            certs = loader(data)
            if certs:
                return certs
        except Exception:
            pass

    return None


class CertificateCryptoScanner:
    def scan_file(self, file_path: str) -> List[Dict[str, Any]]:
        try:
            data = Path(file_path).read_bytes()
        except Exception as e:
            logger.warning(f"Could not read certificate file {file_path}: {e}")
            return []

        certs = _try_parse_certificates(data)
        if certs:
            findings: List[Dict[str, Any]] = []
            for cert in certs:
                findings.extend(_findings_from_parsed_cert(cert))
            return _dedupe(findings)

        # Fall back to the legacy regex scan for inputs that aren't a valid
        # certificate at all (e.g. a malformed file, or a text file that merely
        # mentions certificate-related terms) — better than reporting nothing,
        # and keeps prior behavior for edge cases outside real cert parsing.
        logger.debug(f"Could not parse {file_path} as X.509/PKCS#7; falling back to text scan")
        return self._scan_file_text_fallback(data)

    def _scan_file_text_fallback(self, data: bytes) -> List[Dict[str, Any]]:
        content = data.decode("utf-8", errors="replace")
        findings = []
        lines = content.splitlines()
        seen: set = set()

        for rule in CERT_RULES:
            for line_no, line in enumerate(lines, start=1):
                if rule["pattern"].search(line):
                    key = (rule["id"], line_no)
                    if key in seen:
                        continue
                    seen.add(key)
                    findings.append({
                        "rule_id": rule["id"],
                        "message": rule["message"],
                        "severity": rule["severity"],
                        "line": line_no,
                        "line_content": line.strip(),
                        "metadata": rule["metadata"],
                    })

        return findings

    def format_findings(self, findings: List[Dict[str, Any]], file_path: str = "") -> Dict[str, Any]:
        results = []
        for f in findings:
            results.append({
                "check_id": f"custom-rules.certificate.security.quantum-crypto-vulnerabilities.{f['rule_id']}",
                "path": file_path,
                "start": {"line": f["line"], "col": 1},
                "end": {"line": f["line"], "col": 1},
                "extra": {
                    "message": f["message"],
                    "severity": f["severity"],
                    "metadata": f["metadata"],
                    "lines": f["line_content"],
                },
            })
        return {"results": results}
