import enum
from dataclasses import dataclass
from typing import Iterable, Optional

from asn1crypto import cms
from pyhanko.sign.timestamps import TimeStamper

from ..attributes import CMSAttributeProvider, TSTProvider
from .cades_asn1 import (
    CertifiedAttributeChoices,
    CommitmentTypeIdentifier,
    CommitmentTypeIndication,
    SignaturePolicyIdentifier,
    SignerAttributesV2,
)

__all__ = ['GenericCommitment', 'CAdESSignedAttrSpec', 'SignerAttrSpec']


# TODO add semantics explanations from the standard


@enum.unique
class GenericCommitment(enum.Enum):
    PROOF_OF_ORIGIN = enum.auto()
    PROOF_OF_RECEIPT = enum.auto()
    PROOF_OF_DELIVERY = enum.auto()
    PROOF_OF_SENDER = enum.auto()
    PROOF_OF_APPROVAL = enum.auto()
    PROOF_OF_CREATION = enum.auto()

    @property
    def asn1(self) -> CommitmentTypeIndication:
        return CommitmentTypeIndication(
            {'commitment_type_id': CommitmentTypeIdentifier(self.name.lower())}
        )


@dataclass(frozen=True)
class SignerAttrSpec:
    """
    Class that controls the ``signer-attributes-v2`` signed CAdES attribute.

    These represent attributes of the signing entity, not the signature or
    signed content.

    .. note::
        Out of the box, only basic claimed attributes and certified attributes
        through V2 X.509 attribute certificates are supported.
    """

    claimed_attrs: Iterable[cms.AttCertAttribute]
    """
    Attributes claimed by the signer without further justification.
    """

    certified_attrs: Iterable[cms.AttributeCertificateV2]
    """
    Attribute certificates containing signer attributes.
    """


@dataclass(frozen=True)
class CAdESSignedAttrSpec:
    """
    Class that controls signed CAdES attributes on a PDF signature.
    """

    commitment_type: Optional[CommitmentTypeIndication] = None
    """
    Signature commitment type. Can be one of the standard values, or a custom
    one.
    """

    timestamp_content: bool = False
    """
    Indicate whether the signature should include a signed timestamp.
    
    .. note::
        This should be contrasted with *unsigned* timestamps:
        a signed timestamp proves that the signature was created *after* some
        point in time, while an *unsigned* timestamp computed over the signed
        content proves that the signature existed *before* said point in time.
    """

    signature_policy_identifier: Optional[SignaturePolicyIdentifier] = None
    """
    Signature policy identifier to embed into the signature.
    
    .. warning::
        Right now, pyHanko does not "understand" signature policies, so the
        signature policy identifier will be taken at face value and embedded
        without paying any heed to the actual rules of the signature policy.
        It is the API user's responsibility to make sure that all relevant
        provisions of the signature policy are adhered to.
    """

    signer_attributes: Optional[SignerAttrSpec] = None
    """
    Settings for signer's attributes, to be included in a
    ``signer-attributes-v2`` attribute on the signature.
    """

    def prepare_providers(
        self,
        message_digest,
        md_algorithm,
        timestamper: Optional[TimeStamper] = None,
    ):
        if self.timestamp_content and timestamper is not None:
            yield TSTProvider(
                digest_algorithm=md_algorithm,
                data_to_ts=message_digest,
                timestamper=timestamper,
                attr_type='content_time_stamp',
                prehashed=True,
            )
        if self.signature_policy_identifier is not None:
            yield SigPolicyIDProvider(self.signature_policy_identifier)
        if self.commitment_type is not None:
            yield CommitmentTypeProvider(self.commitment_type)
        if self.signer_attributes is not None:
            yield SignerAttributesProvider(self.signer_attributes)


class CommitmentTypeProvider(CMSAttributeProvider):
    attribute_type = 'commitment_type'

    def __init__(self, commitment_type: CommitmentTypeIndication):
        self.commitment_type = commitment_type

    async def build_attr_value(self, dry_run=False) -> CommitmentTypeIndication:
        return self.commitment_type


class SigPolicyIDProvider(CMSAttributeProvider):
    attribute_type = 'signature_policy_identifier'

    def __init__(self, policy_id: SignaturePolicyIdentifier):
        self.policy_id = policy_id

    async def build_attr_value(
        self, dry_run=False
    ) -> SignaturePolicyIdentifier:
        return self.policy_id


class SignerAttributesProvider(CMSAttributeProvider):
    attribute_type = 'signer_attributes_v2'

    def __init__(self, signer_attr_spec: SignerAttrSpec):
        self.signer_attr_spec = signer_attr_spec

    async def build_attr_value(self, dry_run=False) -> SignerAttributesV2:
        spec = self.signer_attr_spec
        claimed = list(spec.claimed_attrs)
        certified = list(spec.certified_attrs)
        result = {}
        if claimed:
            result['claimed_attributes'] = claimed
        if certified:
            result['certified_attributes_v2'] = [
                CertifiedAttributeChoices(name='attr_cert', value=attr_cert)
                for attr_cert in certified
            ]
        return SignerAttributesV2(result)
