"""Credential-gated trust circles for encrypted agent groups.

Provides :class:`TrustCircle` for managing admission to encrypted channels
based on verifiable attestations. Agents must present attestations that
satisfy an :class:`AdmissionPolicy` before being added to the MLS group.

Uses the :mod:`~skytale_sdk.attestation` module for credential verification
and :class:`~skytale_sdk.channels.SkytaleChannelManager` for channel operations.

Usage::

    from skytale_sdk.trust import TrustCircle, AdmissionPolicy
    from skytale_sdk.attestation import create_attestation

    policy = AdmissionPolicy(
        required_claims=["task_score", "certification"],
        min_score=0.8,
        required_issuers=["did:key:z6Mk..."],
    )

    circle = TrustCircle.create(manager, "acme/trusted/agents", policy)

    # Agent presents attestations to request admission
    admitted = circle.request_admission(
        requester_did="did:key:z6Mk...",
        attestations=[att1, att2],
        key_package=kp_bytes,
    )
"""

from __future__ import annotations

import json
import logging
from dataclasses import dataclass, field
from typing import Optional

from skytale_sdk.attestation import Attestation, verify_attestation
from skytale_sdk.channels import SkytaleChannelManager

logger = logging.getLogger(__name__)


@dataclass
class AdmissionPolicy:
    """Policy governing admission to a trust circle.

    Agents requesting admission must present attestations that satisfy
    all configured requirements.

    Attributes:
        required_claims: Claim keys that each attestation must contain.
        min_score: Minimum value for a ``"score"`` or ``"reputation"`` claim.
            If ``None``, no score threshold is enforced.
        required_issuers: DIDs whose attestations are accepted. If empty,
            attestations from any issuer are accepted.
        min_attestations: Minimum number of valid attestations required
            for admission (default 1).

    Example::

        policy = AdmissionPolicy(
            required_claims=["task_score", "certification"],
            min_score=0.8,
            required_issuers=["did:key:z6Mk..."],
        )
    """

    required_claims: list[str] = field(default_factory=list)
    min_score: Optional[float] = None
    required_issuers: list[str] = field(default_factory=list)
    min_attestations: int = 1

    def to_dict(self) -> dict:
        """Serialize the policy to a dict for channel metadata.

        Returns:
            Dict representation of the policy.
        """
        return {
            "required_claims": self.required_claims,
            "min_score": self.min_score,
            "required_issuers": self.required_issuers,
            "min_attestations": self.min_attestations,
        }

    @classmethod
    def from_dict(cls, data: dict) -> AdmissionPolicy:
        """Deserialize a policy from a dict.

        Args:
            data: Dict with policy fields.

        Returns:
            Reconstructed :class:`AdmissionPolicy`.
        """
        return cls(
            required_claims=data.get("required_claims", []),
            min_score=data.get("min_score"),
            required_issuers=data.get("required_issuers", []),
            min_attestations=data.get("min_attestations", 1),
        )


class TrustCircle:
    """A credential-gated group of agents on an encrypted channel.

    Members are admitted only if they present attestations satisfying the
    configured :class:`AdmissionPolicy`. The circle wraps a
    :class:`~skytale_sdk.channels.SkytaleChannelManager` channel and
    maintains a local membership set.

    Use :meth:`create` to create a new trust circle with a fresh channel.

    Args:
        manager: Channel manager for sending/receiving messages.
        channel: Channel name (must already exist in the manager).
        policy: Admission policy for evaluating new members.

    Example::

        circle = TrustCircle.create(manager, "acme/trusted/agents", policy)
        admitted = circle.request_admission(
            "did:key:z6Mk...", [attestation], key_package=kp,
        )
    """

    def __init__(
        self,
        manager: SkytaleChannelManager,
        channel: str,
        policy: AdmissionPolicy,
    ):
        self._manager = manager
        self._channel = channel
        self._policy = policy
        self._members: set[str] = set()

    @classmethod
    def create(
        cls,
        manager: SkytaleChannelManager,
        channel: str,
        policy: AdmissionPolicy,
    ) -> TrustCircle:
        """Create a new trust circle with a fresh encrypted channel.

        Creates the channel via the manager and broadcasts the admission
        policy as channel metadata so other participants can discover it.

        Args:
            manager: Channel manager for channel operations.
            channel: Channel name in ``org/namespace/service`` format.
            policy: Admission policy for the circle.

        Returns:
            A new :class:`TrustCircle` bound to the created channel.

        Example::

            circle = TrustCircle.create(mgr, "acme/trusted/agents", policy)
        """
        manager.create(channel)

        # Broadcast the policy as channel metadata so joiners can discover it.
        metadata = json.dumps({
            "type": "trust_circle_policy",
            "policy": policy.to_dict(),
        })
        manager.send(channel, metadata)

        logger.info(
            "created trust circle on channel %s with policy: "
            "required_claims=%s, min_score=%s, min_attestations=%d",
            channel,
            policy.required_claims,
            policy.min_score,
            policy.min_attestations,
        )

        return cls(manager, channel, policy)

    def evaluate_policy(
        self, attestations: list[Attestation],
    ) -> tuple[bool, list[str]]:
        """Evaluate attestations against the admission policy.

        Checks each attestation for required claims, score thresholds,
        and issuer requirements. This is a local-only check -- no API
        calls are made.

        Args:
            attestations: List of attestations to evaluate.

        Returns:
            Tuple of ``(passed, reasons)`` where *passed* is ``True`` if
            the attestations satisfy the policy, and *reasons* lists any
            failure reasons (empty when passed).

        Example::

            passed, reasons = circle.evaluate_policy([att1, att2])
            if not passed:
                print("Denied:", reasons)
        """
        reasons: list[str] = []
        valid_count = 0

        for i, att in enumerate(attestations):
            att_reasons: list[str] = []

            # Check required claims are present.
            for claim_key in self._policy.required_claims:
                if claim_key not in att.claims:
                    att_reasons.append(
                        f"attestation {i}: missing required claim '{claim_key}'"
                    )

            # Check score/reputation threshold.
            if self._policy.min_score is not None:
                score = att.claims.get("score", att.claims.get("reputation"))
                if score is None:
                    att_reasons.append(
                        f"attestation {i}: no 'score' or 'reputation' claim "
                        f"for min_score check"
                    )
                elif not isinstance(score, (int, float)):
                    att_reasons.append(
                        f"attestation {i}: 'score'/'reputation' claim is not "
                        f"numeric (got {type(score).__name__})"
                    )
                elif score < self._policy.min_score:
                    att_reasons.append(
                        f"attestation {i}: score {score} below minimum "
                        f"{self._policy.min_score}"
                    )

            # Check issuer is in the allowed list.
            if self._policy.required_issuers:
                if att.issuer not in self._policy.required_issuers:
                    att_reasons.append(
                        f"attestation {i}: issuer '{att.issuer}' not in "
                        f"required_issuers"
                    )

            if not att_reasons:
                valid_count += 1
            else:
                reasons.extend(att_reasons)

        # Check minimum attestation count.
        if valid_count < self._policy.min_attestations:
            reasons.append(
                f"only {valid_count} valid attestation(s), "
                f"need {self._policy.min_attestations}"
            )
            return False, reasons

        return True, []

    def request_admission(
        self,
        requester_did: str,
        attestations: list[Attestation],
        key_package: bytes,
    ) -> bool:
        """Request admission to the trust circle.

        Evaluates the provided attestations against the admission policy.
        If they pass, the requester is added to the MLS group via the
        channel manager and recorded as a member.

        Args:
            requester_did: DID of the agent requesting admission.
            attestations: Attestations proving the requester's credentials.
            key_package: MLS key package bytes for the joining agent.

        Returns:
            ``True`` if the agent was admitted, ``False`` if denied.

        Example::

            admitted = circle.request_admission(
                "did:key:z6Mk...", [att], key_package=kp_bytes,
            )
        """
        passed, reasons = self.evaluate_policy(attestations)

        if not passed:
            logger.info(
                "admission denied for %s: %s", requester_did, "; ".join(reasons)
            )
            return False

        self._manager.add_member(self._channel, key_package)
        self._members.add(requester_did)

        logger.info("admitted %s to trust circle on %s", requester_did, self._channel)
        return True

    def members(self) -> list[str]:
        """Return the current member list.

        Returns:
            Sorted list of member DIDs.

        Example::

            for did in circle.members():
                print(did)
        """
        return sorted(self._members)

    def revoke(self, did: str) -> None:
        """Revoke a member's membership in the trust circle.

        Removes the DID from the local member set. Note that MLS group
        removal requires a separate commit operation on the channel.

        Args:
            did: DID of the member to revoke.

        Raises:
            KeyError: If the DID is not a current member.

        Example::

            circle.revoke("did:key:z6Mk...")
        """
        if did not in self._members:
            raise KeyError(f"'{did}' is not a member of this trust circle")
        self._members.discard(did)
        logger.info("revoked %s from trust circle on %s", did, self._channel)

    @property
    def policy(self) -> AdmissionPolicy:
        """The admission policy for this trust circle."""
        return self._policy
