import datetime
import re
from typing import Any

from moto.core.base_backend import BackendDict, BaseBackend
from moto.core.common_models import BaseModel
from moto.core.utils import utcnow

from .exceptions import (
    BadRequestException,
    InvalidRequestException,
    ResourceAlreadyExistsException,
    ResourceLimitExceededException,
    ResourceNotFoundException,
)
from .utils import (
    admin_password_ok,
    get_invitation_id,
    get_member_id,
    get_network_id,
    get_node_id,
    get_proposal_id,
    member_name_exist_in_network,
    nodes_in_member,
    number_of_members_in_network,
    number_of_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: str | None = None,
    ):
        self.creationdate = utcnow()
        self.id = network_id
        self.name = name
        self.description = description
        self.framework = framework
        self.framework_version = frameworkversion
        self.framework_configuration = frameworkconfiguration
        self.voting_policy = voting_policy
        self.member_configuration = member_configuration
        self.region = region

        self.framework_attributes = {
            "Fabric": {
                "OrderingServiceEndpoint": f"orderer.{self.id.lower()}.managedblockchain.{self.region}.amazonaws.com:30001",
                "Edition": self.framework_configuration["Fabric"]["Edition"],
            }
        }

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

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

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

    @property
    def network_description(self) -> str | None:
        return self.description

    @property
    def network_edition(self) -> str:
        return self.framework_configuration["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"]


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: str | None = None,
    ):
        # In general, passing all values instead of creating
        # an apparatus to look them up
        self.proposal_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 = 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 utcnow() > self.expirationdate:
            self.status = "EXPIRED"

    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 ManagedBlockchainNetworkSummary(BaseModel):
    def __init__(
        self,
        networkid: str,
        networkname: str,
        networkdescription: str | None,
        networkframework: str,
        networkframeworkversion: str,
        networkcreationdate: str,
    ):
        self.id = networkid
        self.name = networkname
        self.networkdescription = networkdescription
        self.framework = networkframework
        self.framework_workversion = networkframeworkversion
        self.status = "AVAILABLE"
        self.creation_date = networkcreationdate


class ManagedBlockchainInvitation(BaseModel):
    def __init__(
        self,
        invitation_id: str,
        networkid: str,
        networkname: str,
        networkframework: str,
        networkframeworkversion: str,
        networkcreationdate: str,
        region: str,
        networkdescription: str | None = None,
    ):
        self.id = invitation_id
        self.network_summary = ManagedBlockchainNetworkSummary(
            networkid=networkid,
            networkname=networkname,
            networkdescription=networkdescription,
            networkframework=networkframework,
            networkframeworkversion=networkframeworkversion,
            networkcreationdate=networkcreationdate,
        )

        self.status = "PENDING"
        self.region = region

        self.creationdate = 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.network_summary.id

    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.network_summary.status = network_status


class ManagedBlockchainMember(BaseModel):
    def __init__(
        self,
        member_id: str,
        networkid: str,
        member_configuration: dict[str, Any],
        region: str,
    ):
        self.creationdate = utcnow()
        self.id = member_id
        self.network_id = networkid
        self.member_configuration = member_configuration
        self.log_publishing_configuration = member_configuration[
            "LogPublishingConfiguration"
        ]
        self.description = member_configuration.get("Description")
        self.status = "AVAILABLE"
        self.region = region

        self.framework_attributes = {
            "Fabric": {
                "AdminUsername": self.member_configuration["FrameworkConfiguration"][
                    "Fabric"
                ]["AdminUsername"],
                "CaEndpoint": f"ca.{self.id.lower()}.{self.network_id.lower()}.managedblockchain.{self.region}.amazonaws.com:30002",
            }
        }

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

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

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

    def update(self, logpublishingconfiguration: dict[str, Any]) -> None:
        self.log_publishing_configuration = 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.creation_date = utcnow()
        self.id = node_id
        self.instance_type = instancetype
        self.network_id = networkid
        self.member_id = memberid
        self.log_publishing_configuration = logpublishingconfiguration
        self.region = region
        self.status = "AVAILABLE"
        self.availability_zone = availabilityzone
        self.framework_attributes = {
            "Fabric": {
                "PeerEndpoint": f"{self.id.lower()}.{self.network_id.lower()}.{self.member_id.lower()}.managedblockchain.{self.region}.amazonaws.com:30003",
                "PeerEventEndpoint": f"{self.id.lower()}.{self.network_id.lower()}.{self.member_id.lower()}.managedblockchain.{self.region}.amazonaws.com:30004",
            }
        }

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

    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: str | None = 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: str | None = 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].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: str | None = 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")
