import datetime
import re
from typing import Any, Dict, List, Optional

from moto.core import BaseBackend, BackendDict, BaseModel

from .exceptions import (
    BadRequestException,
    ResourceNotFoundException,
    InvalidRequestException,
    ResourceLimitExceededException,
    ResourceAlreadyExistsException,
)

from .utils import (
    get_network_id,
    get_member_id,
    get_proposal_id,
    get_invitation_id,
    member_name_exist_in_network,
    number_of_members_in_network,
    admin_password_ok,
    get_node_id,
    number_of_nodes_in_member,
    nodes_in_member,
)

FRAMEWORKS = [
    "HYPERLEDGER_FABRIC",
]

FRAMEWORKVERSIONS = [
    "1.2",
]

EDITIONS: Dict[str, Any] = {
    "STARTER": {
        "MaxMembers": 5,
        "MaxNodesPerMember": 2,
        "AllowedNodeInstanceTypes": ["bc.t3.small", "bc.t3.medium"],
    },
    "STANDARD": {
        "MaxMembers": 14,
        "MaxNodesPerMember": 3,
        "AllowedNodeInstanceTypes": ["bc.t3", "bc.m5", "bc.c5"],
    },
}

VOTEVALUES = ["YES", "NO"]


class ManagedBlockchainNetwork(BaseModel):
    def __init__(
        self,
        network_id: str,
        name: str,
        framework: str,
        frameworkversion: str,
        frameworkconfiguration: Dict[str, Any],
        voting_policy: Dict[str, Any],
        member_configuration: Dict[str, Any],
        region: str,
        description: Optional[str] = None,
    ):
        self.creationdate = datetime.datetime.utcnow()
        self.id = network_id
        self.name = name
        self.description = description
        self.framework = framework
        self.frameworkversion = frameworkversion
        self.frameworkconfiguration = frameworkconfiguration
        self.voting_policy = voting_policy
        self.member_configuration = member_configuration
        self.region = region

    @property
    def network_name(self) -> str:
        return self.name

    @property
    def network_framework(self) -> str:
        return self.framework

    @property
    def network_framework_version(self) -> str:
        return self.frameworkversion

    @property
    def network_creationdate(self) -> str:
        return self.creationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z")

    @property
    def network_description(self) -> Optional[str]:
        return self.description

    @property
    def network_edition(self) -> str:
        return self.frameworkconfiguration["Fabric"]["Edition"]

    @property
    def vote_pol_proposal_duration(self) -> float:
        return self.voting_policy["ApprovalThresholdPolicy"]["ProposalDurationInHours"]

    @property
    def vote_pol_threshold_percentage(self) -> float:
        return self.voting_policy["ApprovalThresholdPolicy"]["ThresholdPercentage"]

    @property
    def vote_pol_threshold_comparator(self) -> str:
        return self.voting_policy["ApprovalThresholdPolicy"]["ThresholdComparator"]

    def to_dict(self) -> Dict[str, Any]:
        # Format for list_networks
        d = {
            "Id": self.id,
            "Name": self.name,
            "Framework": self.framework,
            "FrameworkVersion": self.frameworkversion,
            "Status": "AVAILABLE",
            "CreationDate": self.creationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
        }
        if self.description is not None:
            d["Description"] = self.description
        return d

    def get_format(self) -> Dict[str, Any]:
        # Format for get_network
        frameworkattributes = {
            "Fabric": {
                "OrderingServiceEndpoint": f"orderer.{self.id.lower()}.managedblockchain.{self.region}.amazonaws.com:30001",
                "Edition": self.frameworkconfiguration["Fabric"]["Edition"],
            }
        }

        vpcendpointname = (
            f"com.amazonaws.{self.region}.managedblockchain.{self.id.lower()}"
        )

        d = {
            "Id": self.id,
            "Name": self.name,
            "Framework": self.framework,
            "FrameworkVersion": self.frameworkversion,
            "FrameworkAttributes": frameworkattributes,
            "VpcEndpointServiceName": vpcendpointname,
            "VotingPolicy": self.voting_policy,
            "Status": "AVAILABLE",
            "CreationDate": self.creationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
        }
        if self.description is not None:
            d["Description"] = self.description
        return d


class ManagedBlockchainProposal(BaseModel):
    def __init__(
        self,
        proposal_id: str,
        networkid: str,
        memberid: str,
        membername: str,
        numofmembers: int,
        actions: Dict[str, Any],
        network_expiration: float,
        network_threshold: float,
        network_threshold_comp: str,
        description: Optional[str] = None,
    ):
        # In general, passing all values instead of creating
        # an apparatus to look them up
        self.id = proposal_id
        self.networkid = networkid
        self.memberid = memberid
        self.membername = membername
        self.numofmembers = numofmembers
        self.actions = actions
        self.network_expiration = network_expiration
        self.network_threshold = network_threshold
        self.network_threshold_comp = network_threshold_comp
        self.description = description

        self.creationdate = datetime.datetime.utcnow()
        self.expirationdate = self.creationdate + datetime.timedelta(
            hours=network_expiration
        )
        self.yes_vote_count = 0
        self.no_vote_count = 0
        self.outstanding_vote_count = self.numofmembers
        self.status = "IN_PROGRESS"
        self.votes: Dict[str, Dict[str, str]] = {}

    @property
    def network_id(self) -> str:
        return self.networkid

    @property
    def proposal_status(self) -> str:
        return self.status

    @property
    def proposal_votes(self) -> Dict[str, Any]:  # type: ignore[misc]
        return self.votes

    def proposal_actions(self, action_type: str) -> List[Dict[str, Any]]:
        if action_type.lower() == "invitations":
            if "Invitations" in self.actions:
                return self.actions["Invitations"]
        elif action_type.lower() == "removals":
            if "Removals" in self.actions:
                return self.actions["Removals"]
        return []

    def check_to_expire_proposal(self) -> None:
        if datetime.datetime.utcnow() > self.expirationdate:
            self.status = "EXPIRED"

    def to_dict(self) -> Dict[str, Any]:
        # Format for list_proposals
        return {
            "ProposalId": self.id,
            "ProposedByMemberId": self.memberid,
            "ProposedByMemberName": self.membername,
            "Status": self.status,
            "CreationDate": self.creationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
            "ExpirationDate": self.expirationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
        }

    def get_format(self) -> Dict[str, Any]:
        # Format for get_proposal
        d = {
            "ProposalId": self.id,
            "NetworkId": self.networkid,
            "Actions": self.actions,
            "ProposedByMemberId": self.memberid,
            "ProposedByMemberName": self.membername,
            "Status": self.status,
            "CreationDate": self.creationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
            "ExpirationDate": self.expirationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
            "YesVoteCount": self.yes_vote_count,
            "NoVoteCount": self.no_vote_count,
            "OutstandingVoteCount": self.outstanding_vote_count,
        }
        if self.description is not None:
            d["Description"] = self.description
        return d

    def set_vote(self, votermemberid: str, votermembername: str, vote: str) -> None:
        if vote.upper() == "YES":
            self.yes_vote_count += 1
        else:
            self.no_vote_count += 1
        self.outstanding_vote_count -= 1

        perct_yes = (self.yes_vote_count / self.numofmembers) * 100
        perct_no = (self.no_vote_count / self.numofmembers) * 100
        self.votes[votermemberid] = {
            "MemberId": votermemberid,
            "MemberName": votermembername,
            "Vote": vote.upper(),
        }

        if self.network_threshold_comp == "GREATER_THAN_OR_EQUAL_TO":
            if perct_yes >= self.network_threshold:
                self.status = "APPROVED"
            elif perct_no >= self.network_threshold:
                self.status = "REJECTED"
        else:
            if perct_yes > self.network_threshold:
                self.status = "APPROVED"
            elif perct_no > self.network_threshold:
                self.status = "REJECTED"

        # It is a tie - reject
        if (
            self.status == "IN_PROGRESS"
            and self.network_threshold_comp == "GREATER_THAN"
            and self.outstanding_vote_count == 0
            and perct_yes == perct_no
        ):
            self.status = "REJECTED"


class ManagedBlockchainInvitation(BaseModel):
    def __init__(
        self,
        invitation_id: str,
        networkid: str,
        networkname: str,
        networkframework: str,
        networkframeworkversion: str,
        networkcreationdate: str,
        region: str,
        networkdescription: Optional[str] = None,
    ):
        self.id = invitation_id
        self.networkid = networkid
        self.networkname = networkname
        self.networkdescription = networkdescription
        self.networkframework = networkframework
        self.networkframeworkversion = networkframeworkversion
        self.networkstatus = "AVAILABLE"
        self.networkcreationdate = networkcreationdate
        self.status = "PENDING"
        self.region = region

        self.creationdate = datetime.datetime.utcnow()
        self.expirationdate = self.creationdate + datetime.timedelta(days=7)

    @property
    def invitation_status(self) -> str:
        return self.status

    @property
    def invitation_networkid(self) -> str:
        return self.networkid

    def to_dict(self) -> Dict[str, Any]:
        d: Dict[str, Any] = {
            "InvitationId": self.id,
            "CreationDate": self.creationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
            "ExpirationDate": self.expirationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
            "Status": self.status,
            "NetworkSummary": {
                "Id": self.networkid,
                "Name": self.networkname,
                "Framework": self.networkframework,
                "FrameworkVersion": self.networkframeworkversion,
                "Status": self.networkstatus,
                "CreationDate": self.networkcreationdate,
            },
        }
        if self.networkdescription is not None:
            d["NetworkSummary"]["Description"] = self.networkdescription
        return d

    def accept_invitation(self) -> None:
        self.status = "ACCEPTED"

    def reject_invitation(self) -> None:
        self.status = "REJECTED"

    def set_network_status(self, network_status: str) -> None:
        self.networkstatus = network_status


class ManagedBlockchainMember(BaseModel):
    def __init__(
        self,
        member_id: str,
        networkid: str,
        member_configuration: Dict[str, Any],
        region: str,
    ):
        self.creationdate = datetime.datetime.utcnow()
        self.id = member_id
        self.networkid = networkid
        self.member_configuration = member_configuration
        self.status = "AVAILABLE"
        self.region = region
        self.description = None

    @property
    def network_id(self) -> str:
        return self.networkid

    @property
    def name(self) -> str:
        return self.member_configuration["Name"]

    @property
    def member_status(self) -> str:
        return self.status

    def to_dict(self) -> Dict[str, Any]:
        # Format for list_members
        d = {
            "Id": self.id,
            "Name": self.member_configuration["Name"],
            "Status": self.status,
            "CreationDate": self.creationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
            "IsOwned": True,
        }
        if "Description" in self.member_configuration:
            self.description = self.member_configuration["Description"]
        return d

    def get_format(self) -> Dict[str, Any]:
        # Format for get_member
        frameworkattributes = {
            "Fabric": {
                "AdminUsername": self.member_configuration["FrameworkConfiguration"][
                    "Fabric"
                ]["AdminUsername"],
                "CaEndpoint": f"ca.{self.id.lower()}.{self.networkid.lower()}.managedblockchain.{self.region}.amazonaws.com:30002",
            }
        }

        d = {
            "NetworkId": self.networkid,
            "Id": self.id,
            "Name": self.name,
            "FrameworkAttributes": frameworkattributes,
            "LogPublishingConfiguration": self.member_configuration[
                "LogPublishingConfiguration"
            ],
            "Status": self.status,
            "CreationDate": self.creationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
        }
        if "Description" in self.member_configuration:
            d["Description"] = self.description
        return d

    def delete(self) -> None:
        self.status = "DELETED"

    def update(self, logpublishingconfiguration: Dict[str, Any]) -> None:
        self.member_configuration[
            "LogPublishingConfiguration"
        ] = logpublishingconfiguration


class ManagedBlockchainNode(BaseModel):
    def __init__(
        self,
        node_id: str,
        networkid: str,
        memberid: str,
        availabilityzone: str,
        instancetype: str,
        logpublishingconfiguration: Dict[str, Any],
        region: str,
    ):
        self.creationdate = datetime.datetime.utcnow()
        self.id = node_id
        self.instancetype = instancetype
        self.networkid = networkid
        self.memberid = memberid
        self.logpublishingconfiguration = logpublishingconfiguration
        self.region = region
        self.status = "AVAILABLE"
        self.availabilityzone = availabilityzone

    @property
    def member_id(self) -> str:
        return self.memberid

    @property
    def node_status(self) -> str:
        return self.status

    def to_dict(self) -> Dict[str, Any]:
        # Format for list_nodes
        return {
            "Id": self.id,
            "Status": self.status,
            "CreationDate": self.creationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
            "AvailabilityZone": self.availabilityzone,
            "InstanceType": self.instancetype,
        }

    def get_format(self) -> Dict[str, Any]:
        # Format for get_node
        frameworkattributes = {
            "Fabric": {
                "PeerEndpoint": f"{self.id.lower()}.{self.networkid.lower()}.{self.memberid.lower()}.managedblockchain.{self.region}.amazonaws.com:30003",
                "PeerEventEndpoint": f"{self.id.lower()}.{self.networkid.lower()}.{self.memberid.lower()}.managedblockchain.{self.region}.amazonaws.com:30004",
            }
        }

        return {
            "NetworkId": self.networkid,
            "MemberId": self.memberid,
            "Id": self.id,
            "InstanceType": self.instancetype,
            "AvailabilityZone": self.availabilityzone,
            "FrameworkAttributes": frameworkattributes,
            "LogPublishingConfiguration": self.logpublishingconfiguration,
            "Status": self.status,
            "CreationDate": self.creationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
        }

    def delete(self) -> None:
        self.status = "DELETED"

    def update(self, logpublishingconfiguration: Dict[str, Any]) -> None:
        self.logpublishingconfiguration = logpublishingconfiguration


class ManagedBlockchainBackend(BaseBackend):
    def __init__(self, region_name: str, account_id: str):
        super().__init__(region_name, account_id)
        self.networks: Dict[str, ManagedBlockchainNetwork] = {}
        self.members: Dict[str, ManagedBlockchainMember] = {}
        self.proposals: Dict[str, ManagedBlockchainProposal] = {}
        self.invitations: Dict[str, ManagedBlockchainInvitation] = {}
        self.nodes: Dict[str, ManagedBlockchainNode] = {}

    def create_network(
        self,
        name: str,
        framework: str,
        frameworkversion: str,
        frameworkconfiguration: Dict[str, Any],
        voting_policy: Dict[str, Any],
        member_configuration: Dict[str, Any],
        description: Optional[str] = None,
    ) -> Dict[str, str]:
        # Check framework
        if framework not in FRAMEWORKS:
            raise BadRequestException("CreateNetwork", "Invalid request body")

        # Check framework version
        if frameworkversion not in FRAMEWORKVERSIONS:
            raise BadRequestException(
                "CreateNetwork",
                f"Invalid version {frameworkversion} requested for framework HYPERLEDGER_FABRIC",
            )

        # Check edition
        if frameworkconfiguration["Fabric"]["Edition"] not in EDITIONS:
            raise BadRequestException("CreateNetwork", "Invalid request body")

        # Generate network ID
        network_id = get_network_id()

        # Generate memberid ID and initial member
        member_id = get_member_id()
        self.members[member_id] = ManagedBlockchainMember(
            member_id=member_id,
            networkid=network_id,
            member_configuration=member_configuration,
            region=self.region_name,
        )

        self.networks[network_id] = ManagedBlockchainNetwork(
            network_id=network_id,
            name=name,
            framework=framework,
            frameworkversion=frameworkversion,
            frameworkconfiguration=frameworkconfiguration,
            voting_policy=voting_policy,
            member_configuration=member_configuration,
            region=self.region_name,
            description=description,
        )

        # Return the network and member ID
        return {"NetworkId": network_id, "MemberId": member_id}

    def list_networks(self) -> List[ManagedBlockchainNetwork]:
        return list(self.networks.values())

    def get_network(self, network_id: str) -> ManagedBlockchainNetwork:
        if network_id not in self.networks:
            raise ResourceNotFoundException(
                "GetNetwork", f"Network {network_id} not found."
            )
        return self.networks[network_id]

    def create_proposal(
        self,
        networkid: str,
        memberid: str,
        actions: Dict[str, Any],
        description: Optional[str] = None,
    ) -> Dict[str, str]:
        # Check if network exists
        if networkid not in self.networks:
            raise ResourceNotFoundException(
                "CreateProposal", f"Network {networkid} not found."
            )

        # Check if member exists
        if memberid not in self.members:
            raise ResourceNotFoundException(
                "CreateProposal", f"Member {memberid} not found."
            )

        # CLI docs say that Invitations and Removals cannot both be passed - but it does
        # not throw an error and can be performed
        if "Invitations" in actions:
            for propinvitation in actions["Invitations"]:
                if re.match("[0-9]{12}", propinvitation["Principal"]) is None:
                    raise InvalidRequestException(
                        "CreateProposal",
                        "Account ID format specified in proposal is not valid.",
                    )

        if "Removals" in actions:
            for propmember in actions["Removals"]:
                if propmember["MemberId"] not in self.members:
                    raise InvalidRequestException(
                        "CreateProposal",
                        "Member ID format specified in proposal is not valid.",
                    )

        # Generate proposal ID
        proposal_id = get_proposal_id()

        self.proposals[proposal_id] = ManagedBlockchainProposal(
            proposal_id=proposal_id,
            networkid=networkid,
            memberid=memberid,
            membername=self.members[memberid].name,
            numofmembers=number_of_members_in_network(self.members, networkid),
            actions=actions,
            network_expiration=self.networks[networkid].vote_pol_proposal_duration,
            network_threshold=self.networks[networkid].vote_pol_threshold_percentage,
            network_threshold_comp=self.networks[
                networkid
            ].vote_pol_threshold_comparator,
            description=description,
        )

        # Return the proposal ID
        return {"ProposalId": proposal_id}

    def list_proposals(self, networkid: str) -> List[ManagedBlockchainProposal]:
        # Check if network exists
        if networkid not in self.networks:
            raise ResourceNotFoundException(
                "ListProposals", f"Network {networkid} not found."
            )

        proposalsfornetwork = []
        for proposal_id in self.proposals:
            if self.proposals[proposal_id].network_id == networkid:
                # See if any are expired
                self.proposals[proposal_id].check_to_expire_proposal()
                proposalsfornetwork.append(self.proposals[proposal_id])
        return proposalsfornetwork

    def get_proposal(
        self, networkid: str, proposalid: str
    ) -> ManagedBlockchainProposal:
        # Check if network exists
        if networkid not in self.networks:
            raise ResourceNotFoundException(
                "GetProposal", f"Network {networkid} not found."
            )

        if proposalid not in self.proposals:
            raise ResourceNotFoundException(
                "GetProposal", f"Proposal {proposalid} not found."
            )

        # See if it needs to be set to expipred
        self.proposals[proposalid].check_to_expire_proposal()
        return self.proposals[proposalid]

    def vote_on_proposal(
        self, networkid: str, proposalid: str, votermemberid: str, vote: str
    ) -> None:
        # Check if network exists
        if networkid not in self.networks:
            raise ResourceNotFoundException(
                "VoteOnProposal", f"Network {networkid} not found."
            )

        if proposalid not in self.proposals:
            raise ResourceNotFoundException(
                "VoteOnProposal", f"Proposal {proposalid} not found."
            )

        if votermemberid not in self.members:
            raise ResourceNotFoundException(
                "VoteOnProposal", f"Member {votermemberid} not found."
            )

        if vote.upper() not in VOTEVALUES:
            raise BadRequestException("VoteOnProposal", "Invalid request body")

        # See if it needs to be set to expipred
        self.proposals[proposalid].check_to_expire_proposal()

        # Exception if EXPIRED
        if self.proposals[proposalid].proposal_status == "EXPIRED":
            raise InvalidRequestException(
                "VoteOnProposal",
                f"Proposal {proposalid} is expired and you cannot vote on it.",
            )

        # Check if IN_PROGRESS
        if self.proposals[proposalid].proposal_status != "IN_PROGRESS":
            raise InvalidRequestException(
                "VoteOnProposal",
                f"Proposal {proposalid} has status {self.proposals[proposalid].proposal_status} and you cannot vote on it.",
            )

        # Check to see if this member already voted
        if votermemberid in self.proposals[proposalid].proposal_votes:
            raise ResourceAlreadyExistsException(
                "VoteOnProposal",
                f"Member {votermemberid} has already voted on proposal {proposalid}.",
            )

        # Cast vote
        self.proposals[proposalid].set_vote(
            votermemberid, self.members[votermemberid].name, vote.upper()
        )

        if self.proposals[proposalid].proposal_status == "APPROVED":
            # Generate invitations
            for _ in self.proposals[proposalid].proposal_actions("Invitations"):
                invitation_id = get_invitation_id()
                self.invitations[invitation_id] = ManagedBlockchainInvitation(
                    invitation_id=invitation_id,
                    networkid=networkid,
                    networkname=self.networks[networkid].network_name,
                    networkframework=self.networks[networkid].network_framework,
                    networkframeworkversion=self.networks[
                        networkid
                    ].network_framework_version,
                    networkcreationdate=self.networks[networkid].network_creationdate,
                    region=self.region_name,
                    networkdescription=self.networks[networkid].network_description,
                )

            # Delete members
            for propmember in self.proposals[proposalid].proposal_actions("Removals"):
                self.delete_member(networkid, propmember["MemberId"])

    def list_proposal_votes(self, networkid: str, proposalid: str) -> List[str]:
        # Check if network exists
        if networkid not in self.networks:
            raise ResourceNotFoundException(
                "ListProposalVotes", f"Network {networkid} not found."
            )

        if proposalid not in self.proposals:
            raise ResourceNotFoundException(
                "ListProposalVotes", f"Proposal {proposalid} not found."
            )

        # Output the vote summaries
        proposalvotesfornetwork = []
        for proposal in self.proposals.values():
            if proposal.network_id == networkid:
                for proposal_vote in proposal.proposal_votes.values():
                    proposalvotesfornetwork.append(proposal_vote)
        return proposalvotesfornetwork

    def list_invitations(self) -> List[ManagedBlockchainInvitation]:
        return list(self.invitations.values())

    def reject_invitation(self, invitationid: str) -> None:
        if invitationid not in self.invitations:
            raise ResourceNotFoundException(
                "RejectInvitation", f"InvitationId {invitationid} not found."
            )
        self.invitations[invitationid].reject_invitation()

    def create_member(
        self, invitationid: str, networkid: str, member_configuration: Dict[str, Any]
    ) -> Dict[str, str]:
        # Check if network exists
        if networkid not in self.networks:
            raise ResourceNotFoundException(
                "CreateMember", f"Network {networkid} not found."
            )

        if invitationid not in self.invitations:
            raise InvalidRequestException(
                "CreateMember", f"Invitation {invitationid} not valid"
            )

        if self.invitations[invitationid].invitation_status != "PENDING":
            raise InvalidRequestException(
                "CreateMember", f"Invitation {invitationid} not valid"
            )

        if member_name_exist_in_network(
            self.members, networkid, member_configuration["Name"]
        ):
            raise InvalidRequestException(
                "CreateMember",
                f"Member name {member_configuration['Name']} already exists in network {networkid}.",
            )

        networkedition = self.networks[networkid].network_edition
        if (
            number_of_members_in_network(self.members, networkid)
            >= EDITIONS[networkedition]["MaxMembers"]
        ):
            raise ResourceLimitExceededException(
                "CreateMember",
                f"You cannot create a member in network {networkid}.{EDITIONS[networkedition]['MaxMembers']} is the maximum number of members allowed in a {networkedition} Edition network.",
            )

        memberadminpassword = member_configuration["FrameworkConfiguration"]["Fabric"][
            "AdminPassword"
        ]
        if admin_password_ok(memberadminpassword) is False:
            raise BadRequestException("CreateMember", "Invalid request body")

        member_id = get_member_id()
        self.members[member_id] = ManagedBlockchainMember(
            member_id=member_id,
            networkid=networkid,
            member_configuration=member_configuration,
            region=self.region_name,
        )

        # Accept the invitaiton
        self.invitations[invitationid].accept_invitation()

        # Return the member ID
        return {"MemberId": member_id}

    def list_members(self, networkid: str) -> List[ManagedBlockchainMember]:
        # Check if network exists
        if networkid not in self.networks:
            raise ResourceNotFoundException(
                "ListMembers", f"Network {networkid} not found."
            )

        membersfornetwork = []
        for member in self.members.values():
            if member.network_id == networkid:
                membersfornetwork.append(member)
        return membersfornetwork

    def get_member(self, networkid: str, memberid: str) -> ManagedBlockchainMember:
        # Check if network exists
        if networkid not in self.networks:
            raise ResourceNotFoundException(
                "GetMember", f"Network {networkid} not found."
            )

        if memberid not in self.members:
            raise ResourceNotFoundException(
                "GetMember", f"Member {memberid} not found."
            )

        # Cannot get a member than has been deleted (it does show up in the list)
        if self.members[memberid].member_status == "DELETED":
            raise ResourceNotFoundException(
                "GetMember", f"Member {memberid} not found."
            )

        return self.members[memberid]

    def delete_member(self, networkid: str, memberid: str) -> None:
        # Check if network exists
        if networkid not in self.networks:
            raise ResourceNotFoundException(
                "DeleteMember", f"Network {networkid} not found."
            )

        if memberid not in self.members:
            raise ResourceNotFoundException(
                "DeleteMember", f"Member {memberid} not found."
            )

        self.members[memberid].delete()

        # Is this the last member in the network? (all set to DELETED)
        if number_of_members_in_network(
            self.members, networkid, member_status="DELETED"
        ) == len(self.members):
            # Set network status to DELETED for all invitations
            for invitation in self.invitations.values():
                if invitation.invitation_networkid == networkid:
                    invitation.set_network_status("DELETED")

            # Remove network
            del self.networks[networkid]

        # Remove any nodes associated
        for nodeid in nodes_in_member(self.nodes, memberid):
            del self.nodes[nodeid]

    def update_member(
        self, networkid: str, memberid: str, logpublishingconfiguration: Dict[str, Any]
    ) -> None:
        # Check if network exists
        if networkid not in self.networks:
            raise ResourceNotFoundException(
                "UpdateMember", f"Network {networkid} not found."
            )

        if memberid not in self.members:
            raise ResourceNotFoundException(
                "UpdateMember", f"Member {memberid} not found."
            )

        self.members[memberid].update(logpublishingconfiguration)

    def create_node(
        self,
        networkid: str,
        memberid: str,
        availabilityzone: str,
        instancetype: str,
        logpublishingconfiguration: Dict[str, Any],
    ) -> Dict[str, str]:
        # Check if network exists
        if networkid not in self.networks:
            raise ResourceNotFoundException(
                "CreateNode", f"Network {networkid} not found."
            )

        if memberid not in self.members:
            raise ResourceNotFoundException(
                "CreateNode", f"Member {memberid} not found."
            )

        networkedition = self.networks[networkid].network_edition
        if (
            number_of_nodes_in_member(self.nodes, memberid)
            >= EDITIONS[networkedition]["MaxNodesPerMember"]
        ):
            raise ResourceLimitExceededException(
                "CreateNode",
                f"Maximum number of nodes exceeded in member {memberid}. The maximum number of nodes you can have in a member in a {networkedition} Edition network is {EDITIONS[networkedition]['MaxNodesPerMember']}",
            )

        # See if the instance family is correct
        correctinstancefamily = False
        for chkinsttypepre in EDITIONS["STANDARD"]["AllowedNodeInstanceTypes"]:
            chkinsttypepreregex = chkinsttypepre + ".*"
            if re.match(chkinsttypepreregex, instancetype, re.IGNORECASE):
                correctinstancefamily = True
                break

        if correctinstancefamily is False:
            raise InvalidRequestException(
                "CreateNode",
                f"Requested instance {instancetype} isn't supported.",
            )

        # Check for specific types for starter
        if networkedition == "STARTER":
            if instancetype not in EDITIONS["STARTER"]["AllowedNodeInstanceTypes"]:
                raise InvalidRequestException(
                    "CreateNode",
                    f"Instance type {instancetype} is not supported with STARTER Edition networks.",
                )

        # Simple availability zone check
        chkregionpreregex = self.region_name + "[a-z]"
        if re.match(chkregionpreregex, availabilityzone, re.IGNORECASE) is None:
            raise InvalidRequestException(
                "CreateNode", "Availability Zone is not valid"
            )

        node_id = get_node_id()
        self.nodes[node_id] = ManagedBlockchainNode(
            node_id=node_id,
            networkid=networkid,
            memberid=memberid,
            availabilityzone=availabilityzone,
            instancetype=instancetype,
            logpublishingconfiguration=logpublishingconfiguration,
            region=self.region_name,
        )

        # Return the node ID
        return {"NodeId": node_id}

    def list_nodes(
        self, networkid: str, memberid: str, status: Optional[str] = None
    ) -> List[ManagedBlockchainNode]:
        if networkid not in self.networks:
            raise ResourceNotFoundException(
                "ListNodes", f"Network {networkid} not found."
            )

        if memberid not in self.members:
            raise ResourceNotFoundException(
                "ListNodes", f"Member {memberid} not found."
            )

        # If member is deleted, cannot list nodes
        if self.members[memberid].member_status == "DELETED":
            raise ResourceNotFoundException(
                "ListNodes", f"Member {memberid} not found."
            )

        nodesformember = []
        for node in self.nodes.values():
            if node.member_id == memberid and (
                status is None or node.node_status == status
            ):
                nodesformember.append(node)
        return nodesformember

    def get_node(
        self, networkid: str, memberid: str, nodeid: str
    ) -> ManagedBlockchainNode:
        # Check if network exists
        if networkid not in self.networks:
            raise ResourceNotFoundException(
                "GetNode", f"Network {networkid} not found."
            )

        if memberid not in self.members:
            raise ResourceNotFoundException("GetNode", f"Member {memberid} not found.")

        if nodeid not in self.nodes:
            raise ResourceNotFoundException("GetNode", f"Node {nodeid} not found.")

        # Cannot get a node than has been deleted (it does show up in the list)
        if self.nodes[nodeid].node_status == "DELETED":
            raise ResourceNotFoundException("GetNode", f"Node {nodeid} not found.")

        return self.nodes[nodeid]

    def delete_node(self, networkid: str, memberid: str, nodeid: str) -> None:
        # Check if network exists
        if networkid not in self.networks:
            raise ResourceNotFoundException(
                "DeleteNode", f"Network {networkid} not found."
            )

        if memberid not in self.members:
            raise ResourceNotFoundException(
                "DeleteNode", f"Member {memberid} not found."
            )

        if nodeid not in self.nodes:
            raise ResourceNotFoundException("DeleteNode", f"Node {nodeid} not found.")

        self.nodes[nodeid].delete()

    def update_node(
        self,
        networkid: str,
        memberid: str,
        nodeid: str,
        logpublishingconfiguration: Dict[str, Any],
    ) -> None:
        # Check if network exists
        if networkid not in self.networks:
            raise ResourceNotFoundException(
                "UpdateNode", f"Network {networkid} not found."
            )

        if memberid not in self.members:
            raise ResourceNotFoundException(
                "UpdateNode", f"Member {memberid} not found."
            )

        if nodeid not in self.nodes:
            raise ResourceNotFoundException("UpdateNode", f"Node {nodeid} not found.")

        self.nodes[nodeid].update(logpublishingconfiguration)


managedblockchain_backends = BackendDict(ManagedBlockchainBackend, "managedblockchain")
