"""Container image signing helpers for local module publishing."""

from __future__ import annotations

import hashlib
import json
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Any

from .config import Config

DEFAULT_GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com"


class ContainerSigningError(RuntimeError):
    """Raised when local container signing or verification fails."""


def ensure_cosign_available() -> None:
    if not shutil.which("cosign"):
        raise ContainerSigningError("cosign is required but was not found in PATH")


def container_signing_dir() -> Path:
    path = Config.get_config_dir() / "cosign"
    path.mkdir(parents=True, exist_ok=True)
    return path


def default_private_key_path() -> Path:
    return container_signing_dir() / "cosign.key"


def default_public_key_path() -> Path:
    return container_signing_dir() / "cosign.pub"


def public_key_fingerprint(public_key_pem: str) -> str:
    digest = hashlib.sha256(public_key_pem.encode("utf-8")).hexdigest()
    return digest


def generate_keypair(*, force: bool = False) -> tuple[Path, Path]:
    ensure_cosign_available()
    private_key = default_private_key_path()
    public_key = default_public_key_path()

    if not force and (private_key.exists() or public_key.exists()):
        raise FileExistsError(f"Cosign keys already exist at {container_signing_dir()}")

    private_key.unlink(missing_ok=True)
    public_key.unlink(missing_ok=True)

    env = os.environ.copy()
    env.setdefault("COSIGN_PASSWORD", "")
    subprocess.run(
        [
            "cosign",
            "generate-key-pair",
            "--output-key-prefix",
            str(container_signing_dir() / "cosign"),
        ],
        check=True,
        env=env,
    )
    return private_key, public_key


def sign_image(image_ref: str, *, key_path: Path | None = None) -> None:
    ensure_cosign_available()
    resolved_key = key_path or default_private_key_path()
    if not resolved_key.exists():
        raise FileNotFoundError(f"Cosign private key not found at {resolved_key}")

    env = os.environ.copy()
    env.setdefault("COSIGN_PASSWORD", "")
    env.setdefault("COSIGN_YES", "true")
    subprocess.run(
        ["cosign", "sign", "--yes", "--key", str(resolved_key), image_ref],
        check=True,
        env=env,
    )


def verify_image_against_signers(image_ref: str, trusted_signers: list[dict[str, Any]]) -> dict[str, Any]:
    ensure_cosign_available()
    if not trusted_signers:
        raise ContainerSigningError("No trusted signers were provided for verification")

    env = os.environ.copy()
    env.setdefault("COSIGN_EXPERIMENTAL", "1")
    errors: list[str] = []

    for signer in trusted_signers:
        signer_type = str(signer.get("type") or signer.get("signer_type") or "").strip().lower()
        source = signer.get("source")

        if signer_type == "github_actions_oidc":
            identity_regexp = str(signer.get("identityRegexp") or signer.get("identity_regexp") or "").strip()
            oidc_issuer = str(signer.get("oidcIssuer") or signer.get("oidc_issuer") or DEFAULT_GITHUB_OIDC_ISSUER)
            if not identity_regexp:
                errors.append("github_actions_oidc: missing identity regexp")
                continue
            cmd = [
                "cosign",
                "verify",
                "--certificate-identity-regexp",
                identity_regexp,
                "--certificate-oidc-issuer",
                oidc_issuer,
                image_ref,
            ]
            cleanup_path = None
        elif signer_type == "cosign_public_key":
            public_key_pem = str(signer.get("publicKeyPem") or signer.get("public_key_pem") or "").strip()
            if not public_key_pem:
                errors.append("cosign_public_key: missing public key")
                continue
            with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False) as handle:
                handle.write(public_key_pem)
                cleanup_path = Path(handle.name)
            cmd = ["cosign", "verify", "--key", str(cleanup_path), image_ref]
        else:
            continue

        try:
            proc = subprocess.run(cmd, capture_output=True, text=True, env=env, check=False, timeout=60)
        finally:
            if cleanup_path:
                cleanup_path.unlink(missing_ok=True)

        if proc.returncode == 0:
            return {
                "verified": True,
                "signerType": signer_type,
                "source": source,
                "identityRegexp": signer.get("identityRegexp") or signer.get("identity_regexp"),
                "publicKeyFingerprint": signer.get("publicKeyFingerprint") or signer.get("public_key_fingerprint"),
            }

        errors.append(f"{signer_type}: {(proc.stderr or proc.stdout or 'verification failed').strip()}")

    return {"verified": False, "error": " | ".join(errors)}


def trivy_available() -> bool:
    return bool(shutil.which("trivy"))


def scan_image_with_trivy(
    image_ref: str,
    *,
    blocking_severities: tuple[str, ...] = ("CRITICAL", "HIGH"),
) -> dict[str, Any]:
    if not trivy_available():
        return {"available": False, "status": "skipped", "message": "trivy not available"}

    cmd = [
        "trivy",
        "image",
        "--quiet",
        "--format",
        "json",
        "--severity",
        ",".join(blocking_severities),
        image_ref,
    ]
    proc = subprocess.run(cmd, capture_output=True, text=True, check=False, timeout=300)
    if proc.returncode not in {0, 1}:
        return {
            "available": True,
            "status": "error",
            "message": (proc.stderr or proc.stdout or "Trivy scan failed").strip(),
        }

    try:
        payload = json.loads(proc.stdout or "{}")
    except json.JSONDecodeError as exc:
        return {"available": True, "status": "error", "message": f"Invalid Trivy output: {exc}"}

    severity_counts = {severity: 0 for severity in blocking_severities}
    for result in payload.get("Results") or []:
        for vulnerability in result.get("Vulnerabilities") or []:
            severity = str(vulnerability.get("Severity") or "").upper()
            if severity in severity_counts:
                severity_counts[severity] += 1

    failed = [severity for severity, count in severity_counts.items() if count > 0]
    return {
        "available": True,
        "status": "failed" if failed else "passed",
        "severityCounts": severity_counts,
        "failedSeverities": failed,
    }
