from typing import Any, Dict, List, Optional, Tuple

from moto.core.common_models import CloudFormationModel
from moto.ec2.models.instances import Instance
from moto.iam.models import InstanceProfile

from ..exceptions import (
    IncorrectStateIamProfileAssociationError,
    InvalidAssociationIDIamProfileAssociationError,
)
from ..utils import (
    filter_iam_instance_profile_associations,
    filter_iam_instance_profiles,
    random_iam_instance_profile_association_id,
)


class IamInstanceProfileAssociation(CloudFormationModel):
    def __init__(
        self,
        ec2_backend: Any,
        association_id: str,
        instance: Instance,
        iam_instance_profile: InstanceProfile,
    ):
        self.ec2_backend = ec2_backend
        self.id = association_id
        self.instance = instance
        self.iam_instance_profile = iam_instance_profile
        self.state = "associated"
        ec2_backend.modify_instance_attribute(
            instance.id,
            "iam_instance_profile",
            {"Arn": self.iam_instance_profile.arn, "Id": association_id},
        )


class IamInstanceProfileAssociationBackend:
    def __init__(self) -> None:
        self.iam_instance_profile_associations: Dict[
            str, IamInstanceProfileAssociation
        ] = {}

    def associate_iam_instance_profile(
        self,
        instance_id: str,
        iam_instance_profile_name: Optional[str] = None,
        iam_instance_profile_arn: Optional[str] = None,
    ) -> IamInstanceProfileAssociation:
        iam_association_id = random_iam_instance_profile_association_id()

        instance_profile = filter_iam_instance_profiles(
            self.account_id,  # type: ignore[attr-defined]
            partition=self.partition,  # type: ignore[attr-defined]
            iam_instance_profile_arn=iam_instance_profile_arn,
            iam_instance_profile_name=iam_instance_profile_name,
        )

        if instance_id in self.iam_instance_profile_associations.keys():
            raise IncorrectStateIamProfileAssociationError(instance_id)

        iam_instance_profile_association = IamInstanceProfileAssociation(
            self,
            iam_association_id,
            self.get_instance(instance_id) if instance_id else None,  # type: ignore[attr-defined, arg-type]
            instance_profile,
        )
        # Regarding to AWS there can be only one association with ec2.
        self.iam_instance_profile_associations[instance_id] = (
            iam_instance_profile_association
        )
        return iam_instance_profile_association

    def describe_iam_instance_profile_associations(
        self,
        association_ids: List[str],
        filters: Any = None,
        max_results: int = 100,
        next_token: Optional[str] = None,
    ) -> Tuple[List[IamInstanceProfileAssociation], Optional[str]]:
        associations_list: List[IamInstanceProfileAssociation] = []
        if association_ids:
            for association in self.iam_instance_profile_associations.values():
                if association.id in association_ids:
                    associations_list.append(association)
        else:
            # That's mean that no association id were given. Showing all.
            associations_list.extend(self.iam_instance_profile_associations.values())

        associations_list = filter_iam_instance_profile_associations(
            associations_list, filters
        )

        starting_point = int(next_token or 0)
        ending_point = starting_point + int(max_results or 100)
        associations_page = associations_list[starting_point:ending_point]
        new_next_token = (
            str(ending_point) if ending_point < len(associations_list) else None
        )

        return associations_page, new_next_token

    def disassociate_iam_instance_profile(
        self, association_id: str
    ) -> IamInstanceProfileAssociation:
        iam_instance_profile_association = None
        for association_key in self.iam_instance_profile_associations.keys():
            if (
                self.iam_instance_profile_associations[association_key].id
                == association_id
            ):
                iam_instance_profile_association = (
                    self.iam_instance_profile_associations[association_key]
                )
                del self.iam_instance_profile_associations[association_key]
                # Deleting once and avoiding `RuntimeError: dictionary changed size during iteration`

                iam_instance_profile_association.ec2_backend.modify_instance_attribute(
                    association_key, "iam_instance_profile", None
                )

                break

        if not iam_instance_profile_association:
            raise InvalidAssociationIDIamProfileAssociationError(association_id)

        return iam_instance_profile_association

    def replace_iam_instance_profile_association(
        self,
        association_id: str,
        iam_instance_profile_name: Optional[str] = None,
        iam_instance_profile_arn: Optional[str] = None,
    ) -> IamInstanceProfileAssociation:
        instance_profile = filter_iam_instance_profiles(
            self.account_id,  # type: ignore[attr-defined]
            partition=self.partition,  # type: ignore[attr-defined]
            iam_instance_profile_arn=iam_instance_profile_arn,
            iam_instance_profile_name=iam_instance_profile_name,
        )

        iam_instance_profile_association = None
        for association_key in self.iam_instance_profile_associations.keys():
            if (
                self.iam_instance_profile_associations[association_key].id
                == association_id
            ):
                self.iam_instance_profile_associations[
                    association_key
                ].iam_instance_profile = instance_profile
                iam_instance_profile_association = (
                    self.iam_instance_profile_associations[association_key]
                )
                break

        if not iam_instance_profile_association:
            raise InvalidAssociationIDIamProfileAssociationError(association_id)

        return iam_instance_profile_association
