"""NetworkManagerBackend class with methods for supported APIs."""

from datetime import datetime, timezone
from typing import Any, Optional

from moto.core.base_backend import BackendDict, BaseBackend
from moto.core.common_models import BaseModel
from moto.moto_api._internal import mock_random
from moto.utilities.paginator import paginate
from moto.utilities.utils import PARTITION_NAMES

from .exceptions import ResourceNotFound, ValidationError

PAGINATION_MODEL = {
    "describe_global_networks": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "global_network_arn",
    },
    "list_core_networks": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "core_network_arn",
    },
    "get_sites": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "site_arn",
    },
    "get_links": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "link_arn",
    },
    "get_devices": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "device_arn",
    },
}


class GlobalNetwork(BaseModel):
    def __init__(
        self,
        account_id: str,
        partition: str,
        description: Optional[str],
        tags: Optional[list[dict[str, str]]],
    ):
        self.description = description
        self.tags = tags or []
        self.global_network_id = "global-network-" + "".join(
            mock_random.get_random_hex(18)
        )
        self.global_network_arn = f"arn:{partition}:networkmanager:{account_id}:global-network/{self.global_network_id}"
        self.created_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
        self.state = "PENDING"

    def to_dict(self) -> dict[str, Any]:
        return {
            "GlobalNetworkId": self.global_network_id,
            "GlobalNetworkArn": self.global_network_arn,
            "Description": self.description,
            "Tags": self.tags,
            "State": self.state,
            "CreatedAt": self.created_at,
        }


class CoreNetwork(BaseModel):
    def __init__(
        self,
        account_id: str,
        partition: str,
        global_network_id: str,
        description: Optional[str],
        tags: Optional[list[dict[str, str]]],
        policy_document: str,
        client_token: str,
    ):
        self.owner_account_id = account_id
        self.global_network_id = global_network_id
        self.description = description
        self.tags = tags or []
        self.policy_document = policy_document
        self.client_token = client_token
        self.core_network_id = "core-network-" + "".join(mock_random.get_random_hex(18))
        self.core_network_arn = f"arn:{partition}:networkmanager:{account_id}:core-network/{self.core_network_id}"

        self.created_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
        self.state = "PENDING"

    def to_dict(self) -> dict[str, Any]:
        return {
            "CoreNetworkId": self.core_network_id,
            "CoreNetworkArn": self.core_network_arn,
            "GlobalNetworkId": self.global_network_id,
            "OwnerAccountId": self.owner_account_id,
            "Description": self.description,
            "Tags": self.tags,
            "PolicyDocument": self.policy_document,
            "State": self.state,
            "CreatedAt": self.created_at,
        }


class Site(BaseModel):
    def __init__(
        self,
        account_id: str,
        partition: str,
        global_network_id: str,
        description: Optional[str],
        location: Optional[dict[str, Any]],
        tags: Optional[list[dict[str, str]]],
    ):
        self.global_network_id = global_network_id
        self.description = description
        self.location = location
        self.tags = tags or []
        self.site_id = "site-" + "".join(mock_random.get_random_hex(18))
        self.site_arn = (
            f"arn:{partition}:networkmanager:{account_id}:site/{self.site_id}"
        )
        self.created_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
        self.state = "PENDING"

    def to_dict(self) -> dict[str, Any]:
        return {
            "SiteId": self.site_id,
            "SiteArn": self.site_arn,
            "GlobalNetworkId": self.global_network_id,
            "Description": self.description,
            "Location": self.location,
            "Tags": self.tags,
            "State": self.state,
            "CreatedAt": self.created_at,
        }


class Link(BaseModel):
    def __init__(
        self,
        account_id: str,
        partition: str,
        global_network_id: str,
        description: Optional[str],
        type: Optional[str],
        bandwidth: dict[str, int],
        provider: Optional[str],
        site_id: str,
        tags: Optional[list[dict[str, str]]],
    ):
        self.global_network_id = global_network_id
        self.description = description
        self.type = type
        self.bandwidth = bandwidth
        self.provider = provider
        self.site_id = site_id
        self.tags = tags or []
        self.link_id = "link-" + "".join(mock_random.get_random_hex(18))
        self.link_arn = (
            f"arn:{partition}:networkmanager:{account_id}:link/{self.link_id}"
        )
        self.created_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
        self.state = "PENDING"

    def to_dict(self) -> dict[str, Any]:
        return {
            "LinkId": self.link_id,
            "LinkArn": self.link_arn,
            "GlobalNetworkId": self.global_network_id,
            "Description": self.description,
            "Type": self.type,
            "Bandwidth": self.bandwidth,
            "Provider": self.provider,
            "SiteId": self.site_id,
            "Tags": self.tags,
            "State": self.state,
            "CreatedAt": self.created_at,
        }


class Device(BaseModel):
    def __init__(
        self,
        account_id: str,
        partition: str,
        global_network_id: str,
        aws_location: Optional[dict[str, str]],
        description: Optional[str],
        type: Optional[str],
        vendor: Optional[str],
        model: Optional[str],
        serial_number: Optional[str],
        location: Optional[dict[str, str]],
        site_id: Optional[str],
        tags: Optional[list[dict[str, str]]],
    ):
        self.global_network_id = global_network_id
        self.aws_location = aws_location
        self.description = description
        self.type = type
        self.vendor = vendor
        self.model = model
        self.serial_number = serial_number
        self.location = location
        self.site_id = site_id
        self.tags = tags or []
        self.device_id = "device-" + "".join(mock_random.get_random_hex(18))
        self.device_arn = (
            f"arn:{partition}:networkmanager:{account_id}:device/{self.device_id}"
        )
        self.created_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
        self.state = "PENDING"

    def to_dict(self) -> dict[str, Any]:
        return {
            "DeviceId": self.device_id,
            "DeviceArn": self.device_arn,
            "GlobalNetworkId": self.global_network_id,
            "AWSLocation": self.aws_location,
            "Description": self.description,
            "Type": self.type,
            "Vendor": self.vendor,
            "Model": self.model,
            "SerialNumber": self.serial_number,
            "Location": self.location,
            "SiteId": self.site_id,
            "Tags": self.tags,
            "State": self.state,
            "CreatedAt": self.created_at,
        }


class NetworkManagerBackend(BaseBackend):
    """Implementation of NetworkManager APIs."""

    def __init__(self, region_name: str, account_id: str) -> None:
        super().__init__(region_name, account_id)
        self.global_networks: dict[str, GlobalNetwork] = {}
        self.core_networks: dict[str, CoreNetwork] = {}
        self.sites: dict[str, dict[str, Site]] = {}
        self.links: dict[str, dict[str, Link]] = {}
        self.devices: dict[str, dict[str, Device]] = {}

    def _get_resource_from_arn(self, arn: str) -> Any:
        resources_types: dict[str, dict[str, Any]] = {
            "core-network": self.core_networks,
            "global-network": self.global_networks,
            "site": self.sites,
            "link": self.links,
            "device": self.devices,
        }
        try:
            target_resource, target_name = arn.split(":")[-1].split("/")
            resources = resources_types.get(target_resource, {})
            # Flatten the nested dictionary stores
            if target_resource not in ["core-network", "global-network"]:
                resources = {
                    k: v
                    for inner_dict in resources.values()
                    for k, v in inner_dict.items()
                }
            return resources[target_name]
        except (KeyError, ValueError, AttributeError):
            raise ResourceNotFound(arn)

    def update_resource_state(self, resource_arn: str, state: str) -> None:
        # Acceptable states: PENDING, AVAILABLE, DELETING, UPDATING
        resource = self._get_resource_from_arn(resource_arn)
        resource.state = state

    def create_global_network(
        self,
        description: Optional[str],
        tags: Optional[list[dict[str, str]]],
    ) -> GlobalNetwork:
        global_network = GlobalNetwork(
            description=description,
            tags=tags,
            account_id=self.account_id,
            partition=self.partition,
        )
        gnw_id = global_network.global_network_id
        self.global_networks[gnw_id] = global_network
        # Create empty dict for resources
        self.sites[gnw_id] = {}
        self.links[gnw_id] = {}
        self.devices[gnw_id] = {}
        return global_network

    def create_core_network(
        self,
        global_network_id: str,
        description: Optional[str],
        tags: Optional[list[dict[str, str]]],
        policy_document: str,
        client_token: str,
    ) -> CoreNetwork:
        # check if global network exists
        if global_network_id not in self.global_networks:
            raise ResourceNotFound(global_network_id)

        core_network = CoreNetwork(
            global_network_id=global_network_id,
            description=description,
            tags=tags,
            policy_document=policy_document,
            client_token=client_token,
            account_id=self.account_id,
            partition=self.partition,
        )
        cnw_id = core_network.core_network_id
        self.core_networks[cnw_id] = core_network
        return core_network

    def delete_core_network(self, core_network_id: str) -> CoreNetwork:
        # Check if core network exists
        if core_network_id not in self.core_networks:
            raise ResourceNotFound(core_network_id)
        core_network = self.core_networks.pop(core_network_id)
        core_network.state = "DELETING"
        return core_network

    def tag_resource(self, resource_arn: str, tags: list[dict[str, Any]]) -> None:
        resource = self._get_resource_from_arn(resource_arn)
        resource.tags.extend(tags)

    def untag_resource(self, resource_arn: str, tag_keys: Optional[list[str]]) -> None:
        resource = self._get_resource_from_arn(resource_arn)
        if tag_keys:
            resource.tags = [tag for tag in resource.tags if tag["Key"] not in tag_keys]

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_core_networks(self) -> list[CoreNetwork]:
        return list(self.core_networks.values())

    def get_core_network(self, core_network_id: str) -> CoreNetwork:
        if core_network_id not in self.core_networks:
            raise ResourceNotFound(core_network_id)
        core_network = self.core_networks[core_network_id]
        return core_network

    @paginate(pagination_model=PAGINATION_MODEL)
    def describe_global_networks(
        self, global_network_ids: list[str]
    ) -> list[GlobalNetwork]:
        queried_global_networks = []
        if not global_network_ids:
            queried_global_networks = list(self.global_networks.values())
        elif isinstance(global_network_ids, str):
            if global_network_ids not in self.global_networks:
                raise ResourceNotFound(global_network_ids)
            queried_global_networks.append(self.global_networks[global_network_ids])
        else:
            for id in global_network_ids:
                if id in self.global_networks:
                    global_network = self.global_networks[id]
                    queried_global_networks.append(global_network)
        return queried_global_networks

    def create_site(
        self,
        global_network_id: str,
        description: Optional[str],
        location: Optional[dict[str, str]],
        tags: Optional[list[dict[str, str]]],
    ) -> Site:
        # check if global network exists
        if global_network_id not in self.global_networks:
            raise ResourceNotFound(global_network_id)
        site = Site(
            global_network_id=global_network_id,
            description=description,
            location=location,
            tags=tags,
            account_id=self.account_id,
            partition=self.partition,
        )
        site_id = site.site_id
        self.sites[global_network_id][site_id] = site
        return site

    def delete_site(self, global_network_id: str, site_id: str) -> Site:
        if global_network_id not in self.global_networks:
            raise ResourceNotFound(global_network_id)
        if global_network_id not in self.sites:
            raise ResourceNotFound(site_id)
        gn_sites = self.sites[global_network_id]
        site = gn_sites.pop(site_id)
        site.state = "DELETING"
        return site

    @paginate(pagination_model=PAGINATION_MODEL)
    def get_sites(self, global_network_id: str, site_ids: list[str]) -> list[Site]:
        if global_network_id not in self.global_networks:
            raise ValidationError("Incorrect input.")
        gn_sites = self.sites.get(global_network_id) or {}
        queried = []
        if not site_ids:
            queried = list(gn_sites.values())
        else:
            for id in site_ids:
                if id in gn_sites:
                    q = gn_sites[id]
                    queried.append(q)

        return queried

    def create_link(
        self,
        global_network_id: str,
        description: Optional[str],
        type: Optional[str],
        bandwidth: dict[str, Any],
        provider: Optional[str],
        site_id: str,
        tags: Optional[list[dict[str, str]]],
    ) -> Link:
        # check if global network exists
        if global_network_id not in self.global_networks:
            raise ResourceNotFound(global_network_id)
        link = Link(
            global_network_id=global_network_id,
            description=description,
            type=type,
            bandwidth=bandwidth,
            provider=provider,
            site_id=site_id,
            tags=tags,
            account_id=self.account_id,
            partition=self.partition,
        )
        self.links[global_network_id][link.link_id] = link
        return link

    @paginate(pagination_model=PAGINATION_MODEL)
    def get_links(
        self,
        global_network_id: str,
        link_ids: list[str],
        site_id: str,
        type: str,
        provider: str,
    ) -> list[Link]:
        if global_network_id not in self.global_networks:
            raise ValidationError("Incorrect input.")
        # TODO: Implement filtering by site_id, type, provider
        gn_links = self.links.get(global_network_id) or {}
        queried = []
        if not link_ids:
            queried = list(gn_links.values())
        else:
            for id in link_ids:
                if id in gn_links:
                    q = gn_links[id]
                    queried.append(q)
        return queried

    def delete_link(self, global_network_id: str, link_id: str) -> Link:
        try:
            link = self.links[global_network_id].pop(link_id)
        except KeyError:
            raise ResourceNotFound(link_id)
        link.state = "DELETING"
        return link

    def create_device(
        self,
        global_network_id: str,
        aws_location: Optional[dict[str, str]],
        description: Optional[str],
        type: Optional[str],
        vendor: Optional[str],
        model: Optional[str],
        serial_number: Optional[str],
        location: Optional[dict[str, str]],
        site_id: Optional[str],
        tags: Optional[list[dict[str, str]]],
    ) -> Device:
        # check if global network exists
        if global_network_id not in self.global_networks:
            raise ResourceNotFound(global_network_id)
        device = Device(
            global_network_id=global_network_id,
            aws_location=aws_location,
            description=description,
            type=type,
            vendor=vendor,
            model=model,
            serial_number=serial_number,
            location=location,
            site_id=site_id,
            tags=tags,
            account_id=self.account_id,
            partition=self.partition,
        )
        self.devices[global_network_id][device.device_id] = device
        return device

    @paginate(pagination_model=PAGINATION_MODEL)
    def get_devices(
        self, global_network_id: str, device_ids: list[str], site_id: Optional[str]
    ) -> list[Device]:
        if global_network_id not in self.global_networks:
            raise ValidationError("Incorrect input.")
        # TODO: Implement filtering by site_id
        gn_devices = self.devices.get(global_network_id) or {}
        queried = []
        if not device_ids:
            queried = list(gn_devices.values())
        else:
            for id in device_ids:
                if id in gn_devices:
                    q = gn_devices[id]
                    queried.append(q)

        return queried

    def delete_device(self, global_network_id: str, device_id: str) -> Device:
        try:
            device = self.devices[global_network_id].pop(device_id)
        except KeyError:
            raise ResourceNotFound(device_id)
        device.state = "DELETING"
        return device

    def list_tags_for_resource(self, resource_arn: str) -> list[dict[str, str]]:
        resource = self._get_resource_from_arn(resource_arn)
        tag_list = resource.tags
        return tag_list


networkmanager_backends = BackendDict(
    NetworkManagerBackend,
    "networkmanager",
    use_boto3_regions=False,
    additional_regions=PARTITION_NAMES,
)
