"""
cert_trust/core.py

Core logic for cert-trust. Handles CA certificate trust for two targets:
  - Python  : via a .pth file that auto-injects system certs at interpreter startup
  - curl    : via the OS system cert store

Supported platforms
-------------------
Linux (Debian/Ubuntu)  Python: .pth + certifi   curl: update-ca-certificates
Linux (RHEL/Fedora)    Python: .pth + certifi   curl: update-ca-trust extract
macOS                  Python: .pth + certifi   curl: security (Keychain)
Windows 10/11          Python: .pth + certifi   curl: certutil → Windows Cert Store
                       (targets CPython from python.org + native curl.exe)

Public API
----------
enable()        Install the .pth file → Python trusts system CAs on next start.
disable()       Remove the .pth file → reverts to Python's default CA bundle.
status()        Return current trust state for Python and curl.
add_cert(path)  Register a custom CA cert for both Python and curl.
verify(url)     Probe a URL with curl and Python ssl to confirm trust works.
"""
import os
import sys
import site
import shutil
import platform
import subprocess
from pathlib import Path

PTH_FILENAME = "cert_trust_bootstrap.pth"
PTH_CONTENT  = "import cert_trust.bootstrap\n"
SYSTEM       = platform.system()  # "Linux" | "Darwin" | "Windows"


# ---------------------------------------------------------------------------
# Helpers — site-packages
# ---------------------------------------------------------------------------

def _site_packages() -> list[Path]:
    """Return all site-packages directories for the running interpreter."""
    dirs: list[str] = []
    try:
        dirs.extend(site.getsitepackages())
    except AttributeError:
        # Not available inside some virtualenvs on older Python builds.
        pass
    user = site.getusersitepackages()
    if user:
        dirs.append(user)
    return [Path(d) for d in dirs if d]


def _writable_site_packages() -> Path | None:
    """Return the first writable site-packages directory.

    Falls back to the user site-packages directory, creating it if necessary.
    """
    for d in _site_packages():
        if d.exists() and os.access(d, os.W_OK):
            return d
    # User site-packages is always writable without elevated privileges.
    p = Path(site.getusersitepackages())
    p.mkdir(parents=True, exist_ok=True)
    return p


def _existing_pth_files() -> list[Path]:
    """Return all cert-trust .pth files currently installed."""
    return [d / PTH_FILENAME for d in _site_packages() if (d / PTH_FILENAME).exists()]


# ---------------------------------------------------------------------------
# Helpers — curl
# ---------------------------------------------------------------------------

def _curl_ca_bundle() -> Path | None:
    """Detect the CA bundle path that curl is currently using.

    Asks curl directly first (verbose output contains 'CAfile: <path>'), then
    falls back to well-known bundle locations for common distros and platforms.
    On Windows, native curl.exe uses the Windows Certificate Store and does not
    report a CAfile path, so None is returned — that is expected and fine.
    """
    if not shutil.which("curl"):
        return None

    try:
        result = subprocess.run(
            ["curl", "-v", "--max-time", "2", "https://example.com"],
            capture_output=True, text=True, timeout=5,
        )
        for line in result.stderr.splitlines():
            if "CAfile:" in line:
                return Path(line.split("CAfile:")[-1].strip())
    except (FileNotFoundError, subprocess.TimeoutExpired):
        pass

    # Fallback: well-known paths (Linux / macOS only; Windows uses the cert store)
    candidates = [
        "/etc/ssl/certs/ca-certificates.crt",                # Debian / Ubuntu
        "/etc/pki/tls/certs/ca-bundle.crt",                  # RHEL / CentOS
        "/etc/ssl/ca-bundle.pem",                            # openSUSE
        "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", # Fedora
        "/usr/local/etc/openssl/cert.pem",                   # macOS (Homebrew OpenSSL)
        "/etc/ssl/cert.pem",                                  # macOS system / Alpine
    ]
    for c in candidates:
        if os.path.exists(c):
            return Path(c)
    return None


def _linux_add_dir() -> tuple[Path | None, str | None]:
    """Return (cert_drop_directory, update_command) for the current Linux distro."""
    if shutil.which("update-ca-certificates"):
        return Path("/usr/local/share/ca-certificates"), "update-ca-certificates"
    if shutil.which("update-ca-trust"):
        return Path("/etc/pki/ca-trust/source/anchors"), "update-ca-trust extract"
    return None, None


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def enable() -> dict:
    """Enable system CA trust for Python by installing a .pth file.

    The .pth file is placed in the first writable site-packages directory.
    Python automatically imports it at startup, which triggers the bootstrap
    module to inject system certificates into the SSL context.

    Returns a dict with keys:
        success (bool)
        already_enabled (bool)   – True if the .pth file was already present.
        pth_files (list[str])    – Path(s) of the installed .pth file(s).
        message (str)            – Present only on failure.
    """
    existing = _existing_pth_files()
    if existing:
        return {"success": True, "already_enabled": True, "pth_files": [str(p) for p in existing]}

    site_dir = _writable_site_packages()
    if not site_dir:
        return {
            "success": False,
            "message": (
                "No writable site-packages directory found. "
                "Try activating a virtualenv or ensure user site-packages is enabled."
            ),
        }

    pth = site_dir / PTH_FILENAME
    pth.write_text(PTH_CONTENT, encoding='utf-8')
    return {"success": True, "already_enabled": False, "pth_files": [str(pth)]}


def disable() -> dict:
    """Disable system CA trust for Python by removing the .pth file(s).

    Returns a dict with keys:
        success (bool)
        was_enabled (bool)    – False if there was nothing to remove.
        removed (list[str])   – Paths of deleted .pth files.
        errors (list[str])    – Present only on partial failure (permission issues).
    """
    pth_files = _existing_pth_files()
    if not pth_files:
        return {"success": True, "was_enabled": False, "removed": []}

    removed: list[str] = []
    errors:  list[str] = []
    for pth in pth_files:
        try:
            pth.unlink()
            removed.append(str(pth))
        except PermissionError as exc:
            errors.append(str(exc))

    if errors:
        return {"success": False, "removed": removed, "errors": errors}
    return {"success": True, "was_enabled": True, "removed": removed}


def status() -> dict:
    """Return the current trust status for Python and curl.

    Returns a dict with keys:
        python (dict):
            enabled (bool)        – Whether the .pth file is installed.
            env_disabled (bool)   – Whether CERT_TRUST_DISABLED is set.
            effective (bool)      – enabled AND NOT env_disabled.
            pth_files (list[str]) – Installed .pth paths.
            has_truststore (bool) – Whether the truststore package is available.
        curl (dict):
            available (bool)      – Whether curl is on PATH.
            ca_bundle (str|None)  – Detected CA bundle path (None on Windows = uses cert store).
            bundle_exists (bool)  – Whether the bundle file exists on disk.
            uses_windows_store (bool) – True on Windows; native curl.exe reads the cert store.
        platform (str)
        python_version (str)
    """
    pth_files   = _existing_pth_files()
    python_on   = bool(pth_files)
    env_off     = os.environ.get("CERT_TRUST_DISABLED", "").lower() in ("1", "true", "yes")
    curl_bundle = _curl_ca_bundle()

    has_truststore = False
    try:
        import truststore  # noqa: F401
        has_truststore = True
    except ImportError:
        pass

    return {
        "python": {
            "enabled":        python_on,
            "env_disabled":   env_off,
            "effective":      python_on and not env_off,
            "pth_files":      [str(p) for p in pth_files],
            "has_truststore": has_truststore,
        },
        "curl": {
            "available":           shutil.which("curl") is not None,
            "ca_bundle":           str(curl_bundle) if curl_bundle else None,
            "bundle_exists":       curl_bundle is not None and curl_bundle.exists(),
            "uses_windows_store":  SYSTEM == "Windows",
        },
        "platform":       SYSTEM,
        "python_version": sys.version.split()[0],
    }


def add_cert(cert_path: str) -> dict:
    """Register a custom CA certificate for both Python and curl.

    Python (all platforms):
      - Ensures the .pth bootstrap is enabled.
      - Appends the cert to the certifi bundle (if installed) so that
        requests / urllib3 pick it up immediately without a restart.

    curl on Linux:
      - Copies the cert to the distro CA drop directory.
      - Runs update-ca-certificates (Debian/Ubuntu) or update-ca-trust (RHEL/Fedora).

    curl on macOS:
      - Adds the cert to the user's login keychain via `security add-trusted-cert`.
      - The system curl reads both user and system keychains automatically.
      - No administrator privileges required (uses user keychain).

    curl on Windows (native curl.exe via python.org CPython):
      - Runs `certutil -addstore -user -f ROOT <cert>` to import into the Current User's
        Certificate Store. Native curl.exe uses WinSSL/Schannel which reads
        both system and user stores automatically — no extra configuration needed.
      - No administrator privileges required (uses current user's store).

    Args:
        cert_path: Filesystem path to a .crt, .pem, or .cer file.

    Returns a dict with keys:
        success (bool)   – True only if ALL steps succeeded.
        message (str)    – Present only on early validation failure.
        results (dict):
            python_pth     (dict) – Result of enable().
            python_certifi (dict) – Result of appending to certifi bundle.
            curl_system    (dict) – Result of the OS-level cert installation.
    """
    cert_file = Path(cert_path)
    if not cert_file.exists():
        return {"success": False, "message": f"File not found: {cert_path}"}
    if cert_file.suffix.lower() not in (".crt", ".pem", ".cer"):
        return {"success": False, "message": "Unsupported format — use .crt, .pem, or .cer"}

    results: dict = {}

    # -- Python: ensure .pth bootstrap is active ------------------------------
    results["python_pth"] = enable()

    # -- Python: append cert to certifi bundle --------------------------------
    try:
        import certifi
        bundle      = Path(certifi.where())
        bundle_text = bundle.read_text(encoding='utf-8')
        new_cert    = cert_file.read_text(encoding='utf-8')
        if new_cert.strip() not in bundle_text:
            with open(bundle, "a") as fh:
                fh.write(f"\n# Added by cert-trust: {cert_file.name}\n")
                fh.write(new_cert)
            results["python_certifi"] = {"success": True, "bundle": str(bundle)}
        else:
            results["python_certifi"] = {"success": True, "already_present": True}
    except ImportError:
        results["python_certifi"] = {
            "success": False,
            "message": "certifi is not installed (fine if using truststore instead)",
        }
    except PermissionError:
        results["python_certifi"] = {
            "success": False,
            "message": "Permission denied writing to certifi bundle — try using a virtualenv or reinstall certifi with 'pip install --user certifi'",
        }

    # -- curl: OS-level cert store --------------------------------------------
    if SYSTEM == "Linux":
        add_dir, update_cmd = _linux_add_dir()
        if add_dir:
            dest = add_dir / cert_file.name
            try:
                add_dir.mkdir(parents=True, exist_ok=True)
                shutil.copy2(cert_file, dest)
                r = subprocess.run(update_cmd.split(), capture_output=True, text=True)
                results["curl_system"] = {
                    "success":     r.returncode == 0,
                    "destination": str(dest),
                    "update_cmd":  update_cmd,
                    "output":      (r.stdout + r.stderr).strip(),
                }
            except PermissionError:
                results["curl_system"] = {
                    "success": False,
                    "message": (
                        f"Permission denied copying to {dest}. "
                        "Re-run with sudo to update the system cert store."
                    ),
                }
        else:
            results["curl_system"] = {
                "success": False,
                "message": (
                    "Could not find update-ca-certificates or update-ca-trust. "
                    "Install ca-certificates and re-run with sudo."
                ),
            }

    elif SYSTEM == "Darwin":
        # Use user's login keychain instead of system keychain to avoid requiring sudo.
        # macOS curl reads from both user and system keychains automatically.
        try:
            r = subprocess.run(
                [
                    "security", "add-trusted-cert",
                    "-d", "-r", "trustRoot",
                    "-k", os.path.expanduser("~/Library/Keychains/login.keychain-db"),
                    str(cert_file),
                ],
                capture_output=True, text=True,
            )
            results["curl_system"] = {
                "success": r.returncode == 0,
                "method":  "macOS Keychain (user login keychain)",
                "message": (r.stdout + r.stderr).strip(),
            }
        except FileNotFoundError:
            results["curl_system"] = {
                "success": False,
                "message": "'security' command not found",
            }

    elif SYSTEM == "Windows":
        # certutil is built into every Windows installation.
        # It imports the cert into the Current User's Certificate Store (ROOT store),
        # which native curl.exe reads automatically via WinSSL/Schannel.
        # Using -user flag to avoid requiring administrator privileges.
        try:
            r = subprocess.run(
                ["certutil", "-addstore", "-user", "-f", "ROOT", str(cert_file)],
                capture_output=True, text=True,
            )
            results["curl_system"] = {
                "success": r.returncode == 0,
                "method":  "Windows Certificate Store (certutil, current user)",
                "output":  (r.stdout + r.stderr).strip(),
            }
            if r.returncode != 0:
                results["curl_system"]["message"] = (
                    f"certutil failed with exit code {r.returncode}. "
                    f"Output: {(r.stdout + r.stderr).strip()}"
                )
        except FileNotFoundError:
            results["curl_system"] = {
                "success": False,
                "message": (
                    "certutil not found. Run from Command Prompt or PowerShell "
                    "(not from a third-party shell)."
                ),
            }

    else:
        results["curl_system"] = {
            "success": False,
            "message": f"Unsupported platform: {SYSTEM}",
        }

    overall = all(v.get("success", False) for v in results.values() if isinstance(v, dict))
    return {"success": overall, "results": results}


def verify(url: str) -> dict:
    """Probe a URL with both curl and Python ssl to confirm TLS trust works.

    Useful after add_cert() to confirm the certificate was installed correctly.

    Args:
        url: The HTTPS URL to test (e.g. 'https://internal.corp.example.com').

    Returns a dict with keys:
        url (str)
        curl (dict):
            success (bool)
            http_code (str)   – HTTP status code string on success, e.g. '200'.
            error (str|None)  – curl stderr on failure.
        python (dict):
            success (bool)
            http_code (int)   – HTTP status code on success.
            error (str|None)  – Exception message on failure.
    """
    import ssl
    import urllib.request

    out: dict = {"url": url}

    # curl probe
    if shutil.which("curl"):
        try:
            r = subprocess.run(
                ["curl", "-sS", "--max-time", "5", "-o", os.devnull, "-w", "%{http_code}", url],
                capture_output=True, text=True, timeout=8,
            )
            out["curl"] = {
                "success":   r.returncode == 0,
                "http_code": r.stdout.strip(),
                "error":     r.stderr.strip() if r.returncode != 0 else None,
            }
        except subprocess.TimeoutExpired:
            out["curl"] = {"success": False, "error": "Request timed out"}
    else:
        out["curl"] = {"success": False, "error": "curl is not installed"}

    # Python ssl probe
    try:
        ctx = ssl.create_default_context()
        with urllib.request.urlopen(url, context=ctx, timeout=5) as resp:
            out["python"] = {"success": True, "http_code": resp.status}
    except ssl.SSLCertVerificationError as exc:
        out["python"] = {"success": False, "error": f"SSL verification failed: {exc}"}
    except Exception as exc:
        out["python"] = {"success": False, "error": str(exc)}

    return out
