"""Reproducible, signed, declarative card packages.

Card archives intentionally cannot contain Python, shared libraries, shell
scripts, or arbitrary files. The runtime maps a signed manifest and JSON config
onto audited built-in implementations.
"""

from __future__ import annotations

import base64
import hashlib
import json
import zipfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
    Ed25519PrivateKey,
    Ed25519PublicKey,
)

from .contracts import CardManifest, CardPackage
from .errors import PackageVerificationError
from .receipts import canonical_json

ALLOWED_FILES = frozenset(
    {"manifest.json", "config.json", "checksums.json", "signature.json"}
)


@dataclass(frozen=True)
class PackageTrustPolicy:
    require_signature: bool = True
    trusted_fingerprints: frozenset[str] = frozenset()


def keygen(private_path: str | Path, public_path: str | Path) -> None:
    private = Ed25519PrivateKey.generate()
    private_bytes = private.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption(),
    )
    public_bytes = private.public_key().public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    Path(private_path).write_bytes(private_bytes)
    Path(public_path).write_bytes(public_bytes)


def _zip_write(archive: zipfile.ZipFile, name: str, body: bytes) -> None:
    info = zipfile.ZipInfo(name, date_time=(2026, 1, 1, 0, 0, 0))
    info.compress_type = zipfile.ZIP_DEFLATED
    info.external_attr = 0o644 << 16
    archive.writestr(info, body)


def build_package(
    manifest: CardManifest,
    config: dict[str, Any],
    destination: str | Path,
    private_key_path: str | Path,
) -> CardPackage:
    manifest_bytes = canonical_json(manifest.model_dump(mode="json"))
    config_bytes = canonical_json(config)
    checksums = {
        "manifest.json": hashlib.sha256(manifest_bytes).hexdigest(),
        "config.json": hashlib.sha256(config_bytes).hexdigest(),
    }
    checksums_bytes = canonical_json(checksums)
    private = serialization.load_pem_private_key(
        Path(private_key_path).read_bytes(), password=None
    )
    if not isinstance(private, Ed25519PrivateKey):
        raise PackageVerificationError("card signing key must be Ed25519")
    public_raw = private.public_key().public_bytes(
        encoding=serialization.Encoding.Raw,
        format=serialization.PublicFormat.Raw,
    )
    signature = private.sign(checksums_bytes)
    signature_body = canonical_json(
        {
            "algorithm": "Ed25519",
            "public_key": base64.b64encode(public_raw).decode("ascii"),
            "signature": base64.b64encode(signature).decode("ascii"),
        }
    )
    destination = Path(destination)
    destination.parent.mkdir(parents=True, exist_ok=True)
    with zipfile.ZipFile(destination, "w") as archive:
        _zip_write(archive, "manifest.json", manifest_bytes)
        _zip_write(archive, "config.json", config_bytes)
        _zip_write(archive, "checksums.json", checksums_bytes)
        _zip_write(archive, "signature.json", signature_body)
    return load_package(destination, PackageTrustPolicy())


def load_package(
    path: str | Path,
    trust: PackageTrustPolicy | None = None,
) -> CardPackage:
    trust = trust or PackageTrustPolicy()
    package_path = Path(path)
    try:
        with zipfile.ZipFile(package_path, "r") as archive:
            names = set(archive.namelist())
            if names != ALLOWED_FILES:
                extra = sorted(names - ALLOWED_FILES)
                missing = sorted(ALLOWED_FILES - names)
                raise PackageVerificationError(
                    f"invalid package contents; extra={extra}, missing={missing}"
                )
            if any(
                name.startswith(("/", "\\")) or ".." in Path(name).parts
                for name in names
            ):
                raise PackageVerificationError("unsafe archive path")
            bodies = {name: archive.read(name) for name in names}
    except (OSError, zipfile.BadZipFile) as exc:
        raise PackageVerificationError("card package is not a readable archive") from exc

    try:
        checksums = json.loads(bodies["checksums.json"])
        for name in ("manifest.json", "config.json"):
            actual = hashlib.sha256(bodies[name]).hexdigest()
            if checksums.get(name) != actual:
                raise PackageVerificationError(f"checksum mismatch for {name}")
        signature_data = json.loads(bodies["signature.json"])
        if signature_data.get("algorithm") != "Ed25519":
            raise PackageVerificationError("unsupported signature algorithm")
        public_raw = base64.b64decode(signature_data["public_key"], validate=True)
        signature = base64.b64decode(signature_data["signature"], validate=True)
        public = Ed25519PublicKey.from_public_bytes(public_raw)
        public.verify(signature, canonical_json(checksums))
    except PackageVerificationError:
        raise
    except Exception as exc:
        raise PackageVerificationError("invalid card signature") from exc

    fingerprint = hashlib.sha256(public_raw).hexdigest()
    if trust.trusted_fingerprints and fingerprint not in trust.trusted_fingerprints:
        raise PackageVerificationError("card signer is not trusted")

    manifest = CardManifest.model_validate_json(bodies["manifest.json"])
    config = json.loads(bodies["config.json"])
    digest = hashlib.sha256(package_path.read_bytes()).hexdigest()
    return CardPackage(
        manifest=manifest,
        config=config,
        digest_sha256=digest,
        signer_fingerprint=fingerprint,
        source="signed-package",
    )

