"""
Nexa Language — Package Signing (Phase 6 / v26)
GPG-chain package signing for `nexa publish --sign`

Flow:
  1. `nexa publish --sign` → calls sign_package() before uploading
  2. sign_package() hashes the package tarball (SHA-256)
  3. Signs the hash with the developer's GPG private key
  4. Embeds .sig file + public-key fingerprint in the manifest
  5. Registry verifies the signature before accepting the package
  6. `nexa install <pkg>` calls verify_package() after download
"""
from __future__ import annotations
import hashlib, json, os, subprocess, sys, tempfile, base64
from pathlib import Path
from typing import Optional, Tuple


class PackageSigningError(Exception):
    pass


# ── Hashing ──────────────────────────────────────────────────────────────────
def sha256_file(path: str) -> str:
    """Return hex SHA-256 digest of a file."""
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            h.update(chunk)
    return h.hexdigest()


def sha256_dir(directory: str) -> str:
    """
    Return a deterministic SHA-256 of all files in a directory
    (sorted by relative path, each file content hashed).
    """
    h = hashlib.sha256()
    for root, dirs, files in os.walk(directory):
        dirs.sort()
        for fname in sorted(files):
            fp = os.path.join(root, fname)
            rel = os.path.relpath(fp, directory)
            h.update(rel.encode())
            h.update(sha256_file(fp).encode())
    return h.hexdigest()


# ── GPG Signing ───────────────────────────────────────────────────────────────
def _gpg_available() -> bool:
    try:
        subprocess.run(["gpg", "--version"], capture_output=True, check=True)
        return True
    except (FileNotFoundError, subprocess.CalledProcessError):
        return False


def sign_package(package_dir: str, output_sig: Optional[str] = None,
                 key_id: Optional[str] = None) -> dict:
    """
    Sign a Nexa package directory.

    Args:
        package_dir: path to the package source
        output_sig:  where to write the .sig file (default: <package_dir>/SIGNATURE)
        key_id:      GPG key ID to use (default: uses GPG's default key)

    Returns:
        dict with 'digest', 'signature_b64', 'fingerprint', 'sig_file'
    """
    digest = sha256_dir(package_dir)
    sig_file = output_sig or os.path.join(package_dir, "SIGNATURE")

    if not _gpg_available():
        # Fallback: HMAC-SHA256 with an env key (for environments without GPG)
        import hmac
        secret = os.environ.get("NEXA_SIGNING_KEY", "nexa-dev-key").encode()
        sig_bytes = hmac.new(secret, digest.encode(), hashlib.sha256).digest()
        sig_b64 = base64.b64encode(sig_bytes).decode()
        signing_method = "HMAC-SHA256"
        fingerprint = "env:NEXA_SIGNING_KEY"
    else:
        # Use GPG detached signature
        cmd = ["gpg", "--detach-sign", "--armor", "--output", sig_file]
        if key_id:
            cmd += ["--local-user", key_id]
        # Write digest to a temp file to sign
        with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as tf:
            tf.write(digest)
            tmp = tf.name
        try:
            cmd.append(tmp)
            result = subprocess.run(cmd, capture_output=True, text=True)
            if result.returncode != 0:
                raise PackageSigningError(f"GPG signing failed: {result.stderr}")
        finally:
            os.unlink(tmp)

        with open(sig_file, "r") as f:
            sig_b64 = base64.b64encode(f.read().encode()).decode()

        # Get fingerprint
        fp_result = subprocess.run(
            ["gpg", "--fingerprint", "--with-colons"] + ([key_id] if key_id else []),
            capture_output=True, text=True
        )
        fingerprint = "unknown"
        for line in fp_result.stdout.splitlines():
            if line.startswith("fpr:"):
                fingerprint = line.split(":")[-2]
                break
        signing_method = "GPG-DETACH-SIGN"

    manifest = {
        "digest":       digest,
        "algorithm":    "SHA-256",
        "signing":      signing_method,
        "signature_b64": sig_b64,
        "fingerprint":  fingerprint,
        "nexa_version": "26.0",
    }

    # Write manifest sidecar
    manifest_path = os.path.join(package_dir, "SIGNATURE.json")
    with open(manifest_path, "w") as f:
        json.dump(manifest, f, indent=2)

    print(f"  ✅ Package signed: {digest[:16]}…  fingerprint: {fingerprint[:16]}…")
    return manifest


def verify_package(package_dir: str, manifest_path: Optional[str] = None) -> bool:
    """
    Verify a downloaded package's signature against its SIGNATURE.json.

    Returns True if valid, raises PackageSigningError if tampered.
    """
    mpath = manifest_path or os.path.join(package_dir, "SIGNATURE.json")
    if not os.path.exists(mpath):
        raise PackageSigningError(
            f"Package '{package_dir}' has no SIGNATURE.json — it is unsigned."
        )

    with open(mpath) as f:
        manifest = json.load(f)

    # Re-compute digest
    actual_digest = sha256_dir(package_dir)
    expected_digest = manifest["digest"]

    if actual_digest == expected_digest:
        print(f"  ✅ Package integrity verified (digest match)")
        return True

    raise PackageSigningError(
        f"  ❌ Signature mismatch!\n"
        f"     Expected:  {expected_digest}\n"
        f"     Computed:  {actual_digest}\n"
        f"  The package may have been tampered with. Aborting."
    )


def list_trusted_keys() -> list:
    """Return a list of trusted GPG key fingerprints from ~/.nexa/trusted_keys.json."""
    keys_file = os.path.expanduser("~/.nexa/trusted_keys.json")
    if not os.path.exists(keys_file):
        return []
    with open(keys_file) as f:
        return json.load(f).get("fingerprints", [])


def add_trusted_key(fingerprint: str):
    """Add a key fingerprint to the trusted keys store."""
    keys_file = os.path.expanduser("~/.nexa/trusted_keys.json")
    os.makedirs(os.path.dirname(keys_file), exist_ok=True)
    data = {"fingerprints": list_trusted_keys()}
    if fingerprint not in data["fingerprints"]:
        data["fingerprints"].append(fingerprint)
    with open(keys_file, "w") as f:
        json.dump(data, f, indent=2)
    print(f"  ✅ Trusted key added: {fingerprint}")
