"""Skill signing + verification runtime.

Three signature schemes supported — pick the one that matches your threat model:

    sha256:<hex>                   Content-integrity only. Detects tampering
                                   but provides zero authorship proof. Useful
                                   for vault-local skills the user signs for
                                   themselves.

    hmac:<key_id>:<hex>            Symmetric HMAC-SHA256. The verifier must
                                   share a secret with the signer. Good for
                                   a closed group (team sharing skills via a
                                   private mirror) where you trust the shared
                                   key. Key ids resolve through the publisher
                                   trust store.

    ed25519:<key_id>:<hex>         Asymmetric Ed25519. The signer has a
                                   private key; the verifier only needs the
                                   public key. This is the marketplace path —
                                   the trust store distributes public keys
                                   and anyone can verify without secrets.

The publisher trust store lives at `system/skill_publishers.yaml` in the
vault. Format:

    alice@example.com:
      scheme: ed25519
      public_key: "<base64>"
    team-secrets:
      scheme: hmac
      key: "<raw-secret-or-base64>"

On load, the skill loader reads the skill's `signature:` and `publisher:`
frontmatter, resolves the publisher's key from the trust store, and verifies
the signature against the skill body. Three outcomes:

    valid      — signature recognized and matches
    invalid    — signature present but doesn't match (tampered!)
    unsigned   — no signature set (vault-local content, trusted by default)
    unknown    — scheme/publisher unknown or key missing (quarantined)
"""

from __future__ import annotations

import base64
import hashlib
import hmac as _hmac
import logging
from pathlib import Path
from typing import Any

log = logging.getLogger(__name__)

SIGNATURE_SCHEMES = ("sha256:", "hmac:", "ed25519:")

# Process-wide publisher trust store (loaded from vault YAML at startup).
# Maps publisher/key_id → {scheme, public_key?, key?}.
_TRUST_STORE: dict[str, dict[str, str]] = {}
_TRUST_STORE_PATH: str = ""


def load_trust_store(path: object) -> int:
    """Load the publisher trust store from a YAML file.

    Returns the number of publisher entries loaded. Never raises — a
    missing file is treated as an empty store, and malformed entries are
    silently dropped with a debug log.
    """
    global _TRUST_STORE, _TRUST_STORE_PATH

    if path is None:
        _TRUST_STORE = {}
        _TRUST_STORE_PATH = ""
        return 0

    try:
        p = Path(str(path))
        if not p.is_file():
            _TRUST_STORE = {}
            _TRUST_STORE_PATH = str(p)
            return 0
        import yaml  # type: ignore
        with open(p, encoding="utf-8") as f:
            data = yaml.safe_load(f) or {}
    except Exception:
        log.debug("Failed to load skill trust store", exc_info=True)
        return 0

    if not isinstance(data, dict):
        return 0

    cleaned: dict[str, dict[str, str]] = {}
    for publisher, entry in data.items():
        if not isinstance(publisher, str) or not isinstance(entry, dict):
            continue
        scheme = str(entry.get("scheme", "")).lower()
        if scheme not in {"hmac", "ed25519"}:
            continue
        record: dict[str, str] = {"scheme": scheme}
        if scheme == "hmac":
            key = entry.get("key")
            if not isinstance(key, str) or not key:
                continue
            record["key"] = key
        elif scheme == "ed25519":
            pk = entry.get("public_key")
            if not isinstance(pk, str) or not pk:
                continue
            record["public_key"] = pk
        cleaned[publisher] = record

    _TRUST_STORE = cleaned
    _TRUST_STORE_PATH = str(p)
    return len(cleaned)


def trust_store_info() -> dict:
    """Expose the loaded trust store (keys only, no secrets) for /api/skills."""
    return {
        "path": _TRUST_STORE_PATH,
        "publishers": [
            {"id": pid, "scheme": rec.get("scheme", "")}
            for pid, rec in _TRUST_STORE.items()
        ],
    }


def compute_content_hash(body: str) -> str:
    """Return the sha256 hex digest of the skill body."""
    return hashlib.sha256(body.encode("utf-8")).hexdigest()


def _decode_key(raw: str) -> bytes:
    """Decode a key string. Accepts hex, base64, or raw ASCII."""
    raw = raw.strip()
    # Try base64 first (standard for keys)
    try:
        return base64.b64decode(raw, validate=True)
    except Exception:
        pass
    # Try hex
    try:
        return bytes.fromhex(raw)
    except Exception:
        pass
    # Fall back to raw UTF-8
    return raw.encode("utf-8")


def sign_content(
    body: str,
    scheme: str = "sha256",
    *,
    key_id: str = "",
    key: bytes | None = None,
) -> str:
    """Produce a signature for the given body.

    scheme="sha256" → integrity-only, no key needed
    scheme="hmac"   → HMAC-SHA256, requires `key_id` + `key` (raw bytes)
    scheme="ed25519"→ Ed25519, requires `key_id` + `key` (raw private key bytes)
    """
    body_bytes = body.encode("utf-8")

    if scheme == "sha256":
        return f"sha256:{compute_content_hash(body)}"

    if scheme == "hmac":
        if not key_id or key is None:
            raise ValueError("hmac signing requires key_id and key")
        digest = _hmac.new(key, body_bytes, hashlib.sha256).hexdigest()
        return f"hmac:{key_id}:{digest}"

    if scheme == "ed25519":
        if not key_id or key is None:
            raise ValueError("ed25519 signing requires key_id and private key")
        try:
            from cryptography.hazmat.primitives.asymmetric import ed25519
        except ImportError as e:
            raise RuntimeError("cryptography library not installed") from e
        priv = ed25519.Ed25519PrivateKey.from_private_bytes(key)
        sig = priv.sign(body_bytes)
        return f"ed25519:{key_id}:{base64.b64encode(sig).decode('ascii')}"

    raise ValueError(f"Unknown signing scheme: {scheme}")


def verify_signature(
    body: str,
    signature: str,
    *,
    publisher: str = "",
) -> tuple[str, str]:
    """Verify a signature against a skill body.

    Returns a (status, reason) tuple:
        status   — "valid" | "invalid" | "unsigned" | "unknown_scheme" | "untrusted_publisher"
        reason   — human-readable explanation

    For hmac and ed25519 schemes, the verifier resolves the publisher's
    key from the loaded trust store. `publisher` is the frontmatter field;
    it's used as the key_id lookup when the signature doesn't carry one.
    """
    if not signature:
        return "unsigned", "no signature set"

    sig = signature.strip()
    if not any(sig.startswith(prefix) for prefix in SIGNATURE_SCHEMES):
        return "unknown_scheme", f"unrecognized scheme in {sig[:20]!r}"

    body_bytes = body.encode("utf-8")

    if sig.startswith("sha256:"):
        expected_hash = sig[len("sha256:"):].strip().lower()
        actual = compute_content_hash(body).lower()
        if _hmac.compare_digest(expected_hash, actual):
            return "valid", "sha256 content hash matches"
        return "invalid", "sha256 hash mismatch — body has been tampered with"

    if sig.startswith("hmac:"):
        # Format: hmac:<key_id>:<hex>
        parts = sig.split(":", 2)
        if len(parts) != 3:
            return "unknown_scheme", "malformed hmac signature (expected hmac:key_id:hex)"
        _, key_id, expected_hex = parts
        record = _TRUST_STORE.get(key_id) or _TRUST_STORE.get(publisher)
        if not record or record.get("scheme") != "hmac" or not record.get("key"):
            return "untrusted_publisher", f"no trusted hmac key for '{key_id}'"
        key_bytes = _decode_key(record["key"])
        actual = _hmac.new(key_bytes, body_bytes, hashlib.sha256).hexdigest()
        if _hmac.compare_digest(expected_hex.lower(), actual.lower()):
            return "valid", f"hmac verified against key '{key_id}'"
        return "invalid", "hmac signature mismatch"

    if sig.startswith("ed25519:"):
        parts = sig.split(":", 2)
        if len(parts) != 3:
            return "unknown_scheme", "malformed ed25519 signature (expected ed25519:key_id:base64)"
        _, key_id, b64_sig = parts
        record = _TRUST_STORE.get(key_id) or _TRUST_STORE.get(publisher)
        if not record or record.get("scheme") != "ed25519" or not record.get("public_key"):
            return "untrusted_publisher", f"no trusted ed25519 public key for '{key_id}'"
        try:
            from cryptography.exceptions import InvalidSignature
            from cryptography.hazmat.primitives.asymmetric import ed25519 as _ed
        except ImportError:
            return "unknown_scheme", "cryptography library not installed"
        try:
            pk_bytes = _decode_key(record["public_key"])
            pk = _ed.Ed25519PublicKey.from_public_bytes(pk_bytes)
            sig_bytes = base64.b64decode(b64_sig)
            pk.verify(sig_bytes, body_bytes)
            return "valid", f"ed25519 verified against public key '{key_id}'"
        except InvalidSignature:
            return "invalid", "ed25519 signature does not verify"
        except Exception as e:
            return "unknown_scheme", f"ed25519 verification error: {e}"

    return "unknown_scheme", f"unhandled scheme in {sig[:20]!r}"


def sign_skill_file(
    path: Path,
    scheme: str = "sha256",
    *,
    key_id: str = "",
    key: bytes | None = None,
    publisher: str = "",
) -> str:
    """Rewrite a SKILL.md file in place, adding/updating its signature.

    Returns the new signature. Used by the CLI helper `agntspace sign-skill`.
    Safe to call on an already-signed skill — it replaces the signature.

    Pass `key_id` + `key` for hmac/ed25519. `publisher` is stored in the
    frontmatter so the verifier knows whose trust-store entry to use.
    """
    import frontmatter  # type: ignore

    post = frontmatter.load(str(path))
    new_sig = sign_content(post.content, scheme=scheme, key_id=key_id, key=key)
    post.metadata["signature"] = new_sig  # type: ignore[index]
    if publisher:
        post.metadata["publisher"] = publisher  # type: ignore[index]
    # Bump a `signed_at` timestamp for audit
    from datetime import UTC, datetime
    post.metadata["signed_at"] = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")  # type: ignore[index]
    path.write_text(frontmatter.dumps(post), encoding="utf-8")
    return new_sig


def generate_ed25519_keypair() -> tuple[bytes, bytes]:
    """Generate a new Ed25519 private/public keypair for a publisher.

    Returns (private_key_bytes, public_key_bytes). Used by the CLI helper
    to bootstrap a new publisher identity. Callers should store the
    private key safely and distribute ONLY the public key via the trust
    store.
    """
    from cryptography.hazmat.primitives import serialization
    from cryptography.hazmat.primitives.asymmetric import ed25519

    priv = ed25519.Ed25519PrivateKey.generate()
    priv_bytes = priv.private_bytes(
        encoding=serialization.Encoding.Raw,
        format=serialization.PrivateFormat.Raw,
        encryption_algorithm=serialization.NoEncryption(),
    )
    pub_bytes = priv.public_key().public_bytes(
        encoding=serialization.Encoding.Raw,
        format=serialization.PublicFormat.Raw,
    )
    return priv_bytes, pub_bytes


def skill_verification_result(skill: Any) -> dict:
    """Produce a structured verification report for an already-loaded skill.

    Expected fields on `skill`: `body`, `signature`, `publisher`,
    `content_hash`. Returns a dict the API can surface directly.
    """
    body = getattr(skill, "body", "") or ""
    signature = getattr(skill, "signature", "") or ""
    publisher = getattr(skill, "publisher", "") or "user"
    status, reason = verify_signature(body, signature, publisher=publisher)
    return {
        "status": status,
        "reason": reason,
        "scheme": signature.split(":", 1)[0] if ":" in signature else "",
        "publisher": publisher,
        "content_hash": getattr(skill, "content_hash", "") or "",
    }
