"""
BoundWarrant - Warrant bound to a signing key.

This module provides the BoundWarrant class, which wraps a Warrant with a SigningKey
for convenience in repeated operations. BoundWarrant is intentionally NOT a subclass
of Warrant to prevent accidental serialization of private keys.

Security:
    - BoundWarrant cannot be serialized (raises TypeError)
    - BoundWarrant should never be stored in state/cache
    - Use for short-lived operations only (e.g., loops, API calls)

Context Manager:
    BoundWarrant can be used as a context manager to set both warrant and key scope:

        with bound:
            @guard(tool="search")
            def my_func(): ...
"""

from datetime import timedelta
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union

from tenuo_core import PublicKey, SigningKey, Warrant  # type: ignore[import-untyped]

from .validation import ValidationResult

try:
    from tenuo_core import WARRANT_HEADER as _WARRANT_HEADER  # type: ignore[attr-defined]
except ImportError:
    _WARRANT_HEADER = "X-Tenuo-Warrant"

if TYPE_CHECKING:
    pass


class BoundWarrant:
    """
    Warrant bound to a signing key.

    This is NOT a Warrant subclass to prevent accidental serialization.
    Use for convenience in repeated operations, but never store in state.

    Security:
        - Uses __slots__ to prevent __dict__ access (key not exposed via vars())
        - __getstate__/__reduce__ raise TypeError (blocks pickle)
        - Not JSON serializable

    Example:
        # Bind once, use multiple times
        bound = warrant.bind(key)
        for item in items:
            headers = bound.headers("process", {"item": item})
            # ... make API call

        # As context manager (sets both warrant and key scope)
        with bound:
            @guard(tool="search")
            def my_func(): ...

        # DON'T do this:
        state.warrant = warrant.bind(key)  # Type error! (good)
    """

    # Use __slots__ to prevent __dict__ access (security: key not exposed via vars())
    __slots__ = ("_warrant", "_key", "_warrant_token", "_key_token", "_trusted_roots")

    def __init__(self, warrant: Warrant, key: SigningKey, trusted_roots: Optional[List[Any]] = None) -> None:
        """
        Create a BoundWarrant.

        Args:
            warrant: The warrant to bind
            key: The signing key to bind to
            trusted_roots: Optional list of trusted issuer PublicKeys.  When provided,
                ``enforce_tool_call`` uses these as the trust anchor instead of
                requiring callers to pass ``trusted_roots`` explicitly.  If neither
                this attribute nor the ``trusted_roots`` parameter to
                ``enforce_tool_call`` is set, enforcement fails closed.
        """
        self._warrant = warrant
        self._key = key
        self._trusted_roots = trusted_roots
        self._warrant_token = None
        self._key_token = None

    @staticmethod
    def bind_warrant(warrant: Warrant, key: SigningKey, trusted_roots: Optional[List[Any]] = None) -> "BoundWarrant":
        """
        Bind a warrant to a signing key.

        Usage:
            bound = warrant.bind(key)
        """
        return BoundWarrant(warrant, key, trusted_roots=trusted_roots)

    # ========================================================================
    # Context Manager (sets both warrant and key scope)
    # ========================================================================

    def __enter__(self):
        """
        Enter context: set both warrant and key scope.

        Allows using BoundWarrant as a context manager:
            with bound:
                @guard(tool="search")
                def my_func(): ...
        """
        from .decorators import _keypair_context, _warrant_context

        self._warrant_token = _warrant_context.set(self._warrant)
        self._key_token = _keypair_context.set(self._key)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Exit context: reset warrant and key scope."""
        from .decorators import _keypair_context, _warrant_context

        if self._warrant_token is not None:
            _warrant_context.reset(self._warrant_token)
        if self._key_token is not None:
            _keypair_context.reset(self._key_token)
        return False

    # ========================================================================
    # Forward ReadableWarrant properties
    # ========================================================================

    @property
    def id(self) -> str:
        """Warrant ID."""
        return self._warrant.id

    @property
    def tools(self) -> list:
        """List of authorized tools."""
        return self._warrant.tools

    @property
    def ttl_remaining(self) -> timedelta:
        """Time remaining until expiration."""
        return self._warrant.ttl_remaining

    @property
    def ttl(self) -> timedelta:
        """Alias for ttl_remaining."""
        return self._warrant.ttl_remaining

    @property
    def expires_at(self) -> str:
        """Absolute expiration time (RFC3339 string)."""
        return self._warrant.expires_at()

    @property
    def is_terminal(self) -> bool:
        """Cannot delegate further."""
        return self._warrant.is_terminal()

    @property
    def is_expired(self) -> bool:
        """TTL has elapsed."""
        return self._warrant.is_expired()

    @property
    def clearance(self):
        """Clearance level."""
        return self._warrant.clearance

    @property
    def depth(self) -> int:
        """Current delegation depth."""
        return self._warrant.depth

    @property
    def max_depth(self) -> int:
        """Maximum delegation depth."""
        # Handle both max_depth and max_issue_depth for compatibility
        max_d = getattr(self._warrant, "max_depth", None)
        if max_d is None:
            max_d = getattr(self._warrant, "max_issue_depth", None)
        return max_d if max_d is not None else 0

    @property
    def warrant_type(self):
        """Warrant type (Execution or Issuer)."""
        return self._warrant.warrant_type

    # ========================================================================
    # Access to inner warrant and key metadata
    # ========================================================================

    @property
    def holder_key(self) -> PublicKey:
        """The holder's public key (derived from the bound signing key)."""
        return self._key.public_key

    @property
    def warrant(self) -> Warrant:
        """Get the inner warrant (read-only access)."""
        return self._warrant

    def unbind(self) -> Warrant:
        """Return the inner warrant without the key."""
        return self._warrant

    def bind(self, key: SigningKey, trusted_roots: Optional[List[Any]] = None) -> "BoundWarrant":
        """Return a new BoundWarrant with a different key."""
        return BoundWarrant(self._warrant, key, trusted_roots=trusted_roots)

    # ========================================================================
    # Convenience methods (use bound key)
    # ========================================================================

    def grant(
        self,
        *,
        to: PublicKey,
        allow: "Union[str, List[str]]",
        ttl: int,
        **constraints,
    ) -> Warrant:
        """Grant using the bound key.

        Args:
            to: Public key of new holder
            allow: Tool(s) to grant
            ttl: Time-to-live in seconds
            **constraints: Additional constraints

        Returns:
            New child warrant (plain Warrant, not BoundWarrant)
        """
        return self._warrant.grant(to=to, allow=allow, ttl=ttl, key=self._key, **constraints)

    def headers(self, tool: str, args: dict, approvals=None) -> Dict[str, str]:
        """
        Generate HTTP authorization headers using the bound key.

        Args:
            tool: Tool name
            args: Tool arguments
            approvals: Optional list of SignedApproval objects. Required when the
                warrant has a guard on this tool — pass the verified approvals so
                the guard is satisfied during PoP validation on the server side.

        Returns:
            Dictionary with X-Tenuo-Warrant and X-Tenuo-PoP headers
        """
        import base64
        import time

        # Validate before signing for better error messages.
        # Pass approvals so guarded tools don't fail here.
        validation = self.validate(tool, args, approvals=approvals)
        if not validation:
            raise RuntimeError(f"Authorization failed: {validation.reason}")

        pop_sig = self._warrant.sign(self._key, tool, args, int(time.time()))
        # sign returns bytes, encode to base64
        pop_b64 = base64.b64encode(pop_sig).decode("ascii")
        return {
            _WARRANT_HEADER: self._warrant.to_base64(),
            "X-Tenuo-PoP": pop_b64,
        }

    def validate(self, tool: str, args: dict, approvals=None) -> ValidationResult:
        """
        Pre-check if this action would be authorized.

        Use before making the actual API call to verify locally that
        the warrant allows the action and the PoP signature is valid.

        Args:
            tool: Tool name
            args: Tool arguments (constraints)
            approvals: Optional list of SignedApproval objects. Pass when the warrant
                has guards that require approval — the Rust core verifies these and
                satisfies the guard check atomically with PoP verification.

        Returns:
            ValidationResult (True if authorized and PoP is valid, with feedback on failure)

        Example:
            result = bound.validate("search", {"query": "test"})
            if result:
                headers = bound.headers("search", {"query": "test"})
                # ...
            else:
                print(f"Validation failed: {result.reason}")
        """
        import time

        from tenuo_core import Authorizer

        # 1. Sign PoP
        pop_signature = self._warrant.sign(self._key, tool, args, int(time.time()))

        # 2. Verify via Authorizer.authorize_one() — full check: issuer trust,
        #    expiry, revocation, clearance, PoP, constraints, guard satisfaction.
        issuer_pub = getattr(self._warrant, "issuer_public_key", None) or getattr(self._warrant, "issuer", None)
        roots = [issuer_pub] if issuer_pub is not None else []
        auth = Authorizer(trusted_roots=roots)
        try:
            auth.authorize_one(self._warrant, tool, args, signature=bytes(pop_signature), approvals=approvals or [])
            return ValidationResult.ok()
        except Exception as exc:
            # Re-raise structural failures — callers should handle these explicitly.
            # ExpiredError, SignatureInvalid, MissingSignature are NOT policy denials;
            # they indicate the warrant itself is invalid, not just a constraint miss.
            from tenuo.exceptions import ExpiredError, MissingSignature, SignatureInvalid
            if isinstance(exc, (ExpiredError, SignatureInvalid, MissingSignature)):
                raise
            # 3. Policy-level denial: return rich feedback via why_denied
            why = self.why_denied(tool, args)
            return ValidationResult.fail(
                reason=why.suggestion or f"Authorization failed ({why.deny_code})",
                suggestions=[why.suggestion] if why.suggestion else [],
            )

    # ========================================================================
    # Forward preview/debugging methods
    # ========================================================================

    def allows(self, tool: str, args: Optional[dict] = None) -> bool:
        """
        Check if the warrant allows the given tool and arguments.

        This is a pure logic check (math only) and does not perform
        cryptographic verification or Proof-of-Possession.

        Args:
            tool: The tool name to check.
            args: Optional arguments to check against constraints.
                  If None, only checks if tool is in the allowlist.

        Returns:
            True if allowed, False otherwise.
        """
        # Use the newly injected allowed method on warrant
        return self._warrant.allows(tool, args)

    def why_denied(self, tool: str, args: Optional[dict] = None):
        """Get structured explanation for why a request would be denied."""
        return self._warrant.why_denied(tool, args)

    def explain(self, include_chain: bool = False) -> str:
        """Human-readable warrant explanation."""
        return self._warrant.explain(include_chain=include_chain)

    def inspect(self) -> str:
        """Alias for explain() with chain information."""
        return self._warrant.inspect()

    @property
    def capabilities(self) -> dict:
        """Human-readable constraints for each tool."""
        return self._warrant.capabilities

    def constraints_dict(self) -> dict:
        """Get flattened constraints dict for authorization checks.

        This is used by guard() to verify critical tools have constraints.
        Extracts constraints from capabilities and flattens into a single dict.
        """
        caps = self._warrant.capabilities
        if caps:
            all_constraints = {}
            for tool_name, constraints in caps.items():
                if constraints:
                    all_constraints.update(constraints)
            return all_constraints
        return {}

    @property
    def expired(self) -> bool:
        """Whether the warrant has expired."""
        return self._warrant.expired

    @property
    def terminal(self) -> bool:
        """Whether the warrant is terminal (cannot delegate)."""
        return self._warrant.terminal

    # ========================================================================
    # Serialization guards (SECURITY-CRITICAL)
    # ========================================================================

    def __getstate__(self):
        """Prevent pickle serialization."""
        raise TypeError(
            "BoundWarrant cannot be serialized (contains private key). "
            "Store the Warrant separately and rebind at runtime."
        )

    def __reduce__(self):
        """Prevent pickle serialization."""
        raise TypeError("BoundWarrant cannot be pickled")

    def __repr__(self):
        """String representation (hides key)."""
        return f"<BoundWarrant id={self._warrant.id[:12]}... KEY_BOUND=True>"


# ============================================================================
# Add bind method to Warrant class
# ============================================================================


def _warrant_bind(self: Warrant, key: SigningKey) -> BoundWarrant:
    """
    Bind this warrant to a signing key for convenience.

    Returns a BoundWarrant which wraps the warrant and key together.
    Use for repeated operations, but never store in state/cache.

    Args:
        key: The signing key to bind

    Returns:
        BoundWarrant instance

    Example:
        # Bind once, use multiple times
        bound = warrant.bind(key)

        # Make multiple API calls
        headers1 = bound.headers("search", {"query": "test"})
        headers2 = bound.headers("read", {"path": "/data/file.txt"})

        worker1 = bound.grant(to=w1_key, allow="search", ttl=300)
        worker2 = bound.grant(to=w2_key, allow="read", ttl=300)

    Security Note:
        This method performs **lazy validation**. It does not check if the key matches
        the warrant's authorized holder immediately. Validation happens strictly at
        usage time (when `authorize()` is called), which verifies the Proof-of-Possession
        signature. If the wrong key is bound, `authorize()` will fail securely.
    """
    return BoundWarrant(self, key)


# Attach to Warrant class
if not hasattr(Warrant, "bind"):
    Warrant.bind = _warrant_bind  # type: ignore[attr-defined]
