"""
Capability token issuance and verification.

CapAuth tokens are PGP-signed JSON payloads that grant specific
permissions to agents or services. They are self-contained,
cryptographically verifiable, and don't require a central authority.

Token types:
    - AgentToken: proves identity, grants access to agent runtime
    - CapabilityToken: grants specific permissions (read memory, push sync, etc.)
    - DelegationToken: allows one agent to act on behalf of another

The issuer signs with their CapAuth PGP key. Any holder can verify
with the issuer's public key. No server required.
"""

from __future__ import annotations

import hashlib
import json
import logging
import subprocess
import shutil
from datetime import datetime, timedelta, timezone
from enum import Enum
from pathlib import Path
from typing import Optional

from pydantic import BaseModel, Field

logger = logging.getLogger("skcapstone.tokens")


class TokenType(str, Enum):
    """Types of capability tokens."""

    AGENT = "agent"
    CAPABILITY = "capability"
    DELEGATION = "delegation"


class Capability(str, Enum):
    """Granular permissions that can be granted via token."""

    MEMORY_READ = "memory:read"
    MEMORY_WRITE = "memory:write"
    SYNC_PUSH = "sync:push"
    SYNC_PULL = "sync:pull"
    IDENTITY_VERIFY = "identity:verify"
    IDENTITY_SIGN = "identity:sign"
    TRUST_READ = "trust:read"
    TRUST_WRITE = "trust:write"
    AUDIT_READ = "audit:read"
    AGENT_STATUS = "agent:status"
    AGENT_CONNECT = "agent:connect"
    TOKEN_ISSUE = "token:issue"
    ALL = "*"


class TokenPayload(BaseModel):
    """The signed content of a capability token.

    This is the JSON structure that gets PGP-signed.
    It's self-describing and independently verifiable.
    """

    token_id: str = Field(description="Unique token identifier (SHA-256 hash)")
    token_type: TokenType = Field(description="What kind of token this is")
    issuer: str = Field(description="PGP fingerprint of the issuer")
    subject: str = Field(description="Who/what this token is for (fingerprint or name)")
    capabilities: list[str] = Field(
        default_factory=list, description="List of granted capabilities"
    )
    issued_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
    expires_at: Optional[datetime] = Field(
        default=None, description="When the token expires (None = no expiry)"
    )
    not_before: Optional[datetime] = Field(
        default=None, description="Token not valid before this time"
    )
    metadata: dict = Field(
        default_factory=dict, description="Additional claims (agent name, platform, etc.)"
    )

    @property
    def is_expired(self) -> bool:
        """Check if the token has expired."""
        if self.expires_at is None:
            return False
        return datetime.now(timezone.utc) > self.expires_at

    @property
    def is_active(self) -> bool:
        """Check if the token is currently valid (time-wise)."""
        now = datetime.now(timezone.utc)
        if self.expires_at and now > self.expires_at:
            return False
        if self.not_before and now < self.not_before:
            return False
        return True

    def has_capability(self, cap: str) -> bool:
        """Check if this token grants a specific capability.

        Args:
            cap: The capability string to check (e.g., 'memory:read').

        Returns:
            True if the capability is granted (or ALL is granted).
        """
        return Capability.ALL.value in self.capabilities or cap in self.capabilities


class SignedToken(BaseModel):
    """A complete token with its PGP signature."""

    payload: TokenPayload
    signature: Optional[str] = Field(
        default=None, description="PGP detached signature (ASCII-armored)"
    )
    verified: bool = Field(default=False, description="Whether signature has been verified")


def issue_token(
    home: Path,
    subject: str,
    capabilities: list[str],
    token_type: TokenType = TokenType.CAPABILITY,
    ttl_hours: Optional[int] = 24,
    metadata: Optional[dict] = None,
    sign: bool = True,
) -> SignedToken:
    """Issue a new capability token signed by the agent's CapAuth key.

    Args:
        home: Agent home directory (~/.skcapstone).
        subject: Who the token is for (fingerprint, name, or email).
        capabilities: List of capability strings to grant.
        token_type: Type of token to issue.
        ttl_hours: Hours until expiry (None = no expiry).
        metadata: Additional claims to embed.
        sign: Whether to PGP-sign the token.

    Returns:
        SignedToken with the payload and optional signature.
    """
    issuer_fp = _get_issuer_fingerprint(home)
    now = datetime.now(timezone.utc)

    payload = TokenPayload(
        token_id="",
        token_type=token_type,
        issuer=issuer_fp,
        subject=subject,
        capabilities=capabilities,
        issued_at=now,
        expires_at=now + timedelta(hours=ttl_hours) if ttl_hours else None,
        metadata=metadata or {},
    )

    payload.token_id = _compute_token_id(payload)

    token = SignedToken(payload=payload)

    if sign:
        signature = _pgp_sign_payload(payload, home)
        if signature:
            token.signature = signature
            token.verified = True

    _store_token(home, token)
    logger.info("Issued token %s for %s (%s)", payload.token_id[:12], subject, token_type.value)
    return token


def verify_token(token: SignedToken, home: Optional[Path] = None) -> bool:
    """Verify a token's signature and validity.

    Args:
        token: The signed token to verify.
        home: Agent home for accessing the keyring.

    Returns:
        True if the token is valid and signature checks out.
    """
    if not token.payload.is_active:
        logger.warning("Token %s is not active (expired or not yet valid)", token.payload.token_id[:12])
        return False

    if token.signature:
        verified = _pgp_verify_signature(token.payload, token.signature, home)
        token.verified = verified
        return verified

    logger.warning("Token %s has no signature", token.payload.token_id[:12])
    return False


def revoke_token(home: Path, token_id: str) -> bool:
    """Revoke a previously issued token.

    Adds the token ID to the revocation list. Revoked tokens
    fail verification even if their signature is valid.

    Args:
        home: Agent home directory.
        token_id: The token ID to revoke.

    Returns:
        True if the token was found and revoked.
    """
    revocation_file = home / "security" / "revoked-tokens.json"
    revoked = _load_revocation_list(revocation_file)

    if token_id in revoked:
        return True

    revoked[token_id] = {
        "revoked_at": datetime.now(timezone.utc).isoformat(),
        "reason": "manual_revocation",
    }

    revocation_file.parent.mkdir(parents=True, exist_ok=True)
    revocation_file.write_text(json.dumps(revoked, indent=2), encoding="utf-8")
    logger.info("Revoked token %s", token_id[:12])
    return True


def is_revoked(home: Path, token_id: str) -> bool:
    """Check if a token has been revoked.

    Args:
        home: Agent home directory.
        token_id: The token ID to check.

    Returns:
        True if the token is on the revocation list.
    """
    revocation_file = home / "security" / "revoked-tokens.json"
    revoked = _load_revocation_list(revocation_file)
    return token_id in revoked


def list_tokens(home: Path) -> list[SignedToken]:
    """List all issued tokens.

    Args:
        home: Agent home directory.

    Returns:
        List of all stored tokens.
    """
    token_dir = home / "security" / "tokens"
    if not token_dir.exists():
        return []

    tokens = []
    for f in sorted(token_dir.iterdir()):
        if f.suffix == ".json":
            try:
                data = json.loads(f.read_text(encoding="utf-8"))
                token = SignedToken(
                    payload=TokenPayload(**data["payload"]),
                    signature=data.get("signature"),
                    verified=data.get("verified", False),
                )
                tokens.append(token)
            except (json.JSONDecodeError, KeyError, ValueError) as exc:
                logger.warning("Failed to load token %s: %s", f.name, exc)
    return tokens


def export_token(token: SignedToken) -> str:
    """Export a token as a portable JSON string.

    Args:
        token: The token to export.

    Returns:
        JSON string suitable for sharing.
    """
    return json.dumps(
        {
            "skcapstone_token": "1.0",
            "payload": token.payload.model_dump(mode="json"),
            "signature": token.signature,
        },
        indent=2,
        default=str,
    )


def import_token(token_json: str) -> SignedToken:
    """Import a token from a JSON string.

    Args:
        token_json: JSON string from export_token().

    Returns:
        The reconstructed SignedToken.

    Raises:
        ValueError: If the JSON is not a valid token.
    """
    try:
        data = json.loads(token_json)
        if "skcapstone_token" not in data:
            raise ValueError("Not an SKCapstone token")
        return SignedToken(
            payload=TokenPayload(**data["payload"]),
            signature=data.get("signature"),
            verified=False,
        )
    except (json.JSONDecodeError, KeyError) as exc:
        raise ValueError(f"Invalid token format: {exc}") from exc


# --- Private helpers ---


def _get_issuer_fingerprint(home: Path) -> str:
    """Get the agent's PGP fingerprint for signing tokens."""
    identity_file = home / "identity" / "identity.json"
    if identity_file.exists():
        try:
            data = json.loads(identity_file.read_text(encoding="utf-8"))
            fp = data.get("fingerprint")
            if fp:
                return fp
        except (json.JSONDecodeError, OSError):
            pass
    return "unknown"


def _compute_token_id(payload: TokenPayload) -> str:
    """Compute a deterministic token ID from the payload content."""
    content = json.dumps(
        {
            "issuer": payload.issuer,
            "subject": payload.subject,
            "capabilities": sorted(payload.capabilities),
            "issued_at": payload.issued_at.isoformat(),
            "type": payload.token_type.value,
        },
        sort_keys=True,
    )
    return hashlib.sha256(content.encode()).hexdigest()


def _pgp_sign_payload(payload: TokenPayload, home: Path) -> Optional[str]:
    """PGP-sign a token payload using the agent's CapAuth key."""
    if not shutil.which("gpg"):
        logger.warning("gpg not found — token will be unsigned")
        return None

    issuer_fp = _get_issuer_fingerprint(home)
    payload_json = payload.model_dump_json()
    try:
        cmd = [
            "gpg", "--batch", "--yes", "--armor", "--detach-sign",
            "--local-user", issuer_fp,
            "--passphrase", "",
            "--pinentry-mode", "loopback",
        ]
        result = subprocess.run(
            cmd,
            input=payload_json,
            capture_output=True,
            text=True,
            timeout=15,
        )
        if result.returncode == 0:
            return result.stdout
        logger.warning("GPG signing failed: %s", result.stderr.strip())
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
        logger.warning("GPG signing error: %s", exc)
    return None


def _pgp_verify_signature(
    payload: TokenPayload,
    signature: str,
    home: Optional[Path] = None,
) -> bool:
    """Verify a PGP detached signature against a token payload."""
    if not shutil.which("gpg"):
        return False

    import tempfile

    payload_json = payload.model_dump_json()

    try:
        with tempfile.NamedTemporaryFile(mode="w", suffix=".sig", delete=False) as sig_file:
            sig_file.write(signature)
            sig_path = sig_file.name

        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as data_file:
            data_file.write(payload_json)
            data_path = data_file.name

        result = subprocess.run(
            ["gpg", "--batch", "--verify", sig_path, data_path],
            capture_output=True,
            text=True,
            timeout=15,
        )

        Path(sig_path).unlink(missing_ok=True)
        Path(data_path).unlink(missing_ok=True)

        return result.returncode == 0
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError) as exc:
        logger.warning("GPG verify error: %s", exc)
    return False


def _store_token(home: Path, token: SignedToken) -> None:
    """Persist a token to disk."""
    token_dir = home / "security" / "tokens"
    token_dir.mkdir(parents=True, exist_ok=True)

    token_file = token_dir / f"{token.payload.token_id[:16]}.json"
    data = {
        "payload": token.payload.model_dump(mode="json"),
        "signature": token.signature,
        "verified": token.verified,
    }
    token_file.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")


def _load_revocation_list(revocation_file: Path) -> dict:
    """Load the token revocation list."""
    if not revocation_file.exists():
        return {}
    try:
        return json.loads(revocation_file.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}
