import copy
import string
from typing import Any, Dict, List, Optional

from jinja2 import Template

from moto.core.base_backend import BackendDict, BaseBackend
from moto.core.common_models import BaseModel
from moto.core.utils import iso_8601_datetime_with_milliseconds
from moto.moto_api._internal import mock_random as random
from moto.utilities.utils import load_resource

from .exceptions import DBClusterNotFoundError


class GlobalCluster(BaseModel):
    def __init__(
        self,
        account_id: str,
        global_cluster_identifier: str,
        engine: Optional[str],
        engine_version: Optional[str],
        storage_encrypted: Optional[str],
        deletion_protection: Optional[str],
    ):
        self.global_cluster_identifier = global_cluster_identifier
        self.global_cluster_resource_id = "cluster-" + random.get_random_hex(8)
        self.global_cluster_arn = (
            f"arn:aws:rds::{account_id}:global-cluster:{global_cluster_identifier}"
        )
        self.engine = engine or "neptune"
        self.engine_version = engine_version or "1.2.0.0"
        self.storage_encrypted = (
            storage_encrypted and storage_encrypted.lower() == "true"
        )
        self.deletion_protection = (
            deletion_protection and deletion_protection.lower() == "true"
        )

    def to_xml(self) -> str:
        template = Template(
            """
          <GlobalClusterIdentifier>{{ cluster.global_cluster_identifier }}</GlobalClusterIdentifier>
          <GlobalClusterResourceId>{{ cluster.global_cluster_resource_id }}</GlobalClusterResourceId>
          <GlobalClusterArn>{{ cluster.global_cluster_arn }}</GlobalClusterArn>
          <Engine>{{ cluster.engine }}</Engine>
          <Status>available</Status>
          <EngineVersion>{{ cluster.engine_version }}</EngineVersion>
          <StorageEncrypted>{{ 'true' if cluster.storage_encrypted else 'false' }}</StorageEncrypted>
          <DeletionProtection>{{ 'true' if cluster.deletion_protection else 'false' }}</DeletionProtection>"""
        )
        return template.render(cluster=self)


class DBCluster(BaseModel):
    def __init__(
        self,
        account_id: str,
        region_name: str,
        db_cluster_identifier: str,
        database_name: Optional[str],
        tags: List[Dict[str, str]],
        storage_encrypted: str,
        parameter_group_name: str,
        engine: str,
        engine_version: str,
        kms_key_id: Optional[str],
        preferred_maintenance_window: Optional[str],
        preferred_backup_window: Optional[str],
        backup_retention_period: Optional[int],
        port: Optional[int],
        serverless_v2_scaling_configuration: Optional[Dict[str, int]],
    ):
        self.account_id = account_id
        self.region_name = region_name
        self.db_cluster_identifier = db_cluster_identifier
        self.resource_id = "cluster-" + random.get_random_hex(8)
        self.tags = tags
        self.storage_encrypted = storage_encrypted.lower() != "false"
        self.db_cluster_parameter_group_name = parameter_group_name
        self.engine = engine
        self.engine_version = engine_version
        self.database_name = database_name
        self.db_subnet_group = "default"
        self.status = "available"
        self.backup_retention_period = backup_retention_period
        self.cluster_create_time = iso_8601_datetime_with_milliseconds()
        self.url_identifier = "".join(
            random.choice(string.ascii_lowercase + string.digits) for _ in range(12)
        )
        self.endpoint = f"{self.db_cluster_identifier}.cluster-{self.url_identifier}.{self.region_name}.neptune.amazonaws.com"
        self.reader_endpoint = f"{self.db_cluster_identifier}.cluster-ro-{self.url_identifier}.{self.region_name}.neptune.amazonaws.com"
        self.resource_id = "cluster-" + "".join(
            random.choice(string.ascii_uppercase + string.digits) for _ in range(26)
        )
        self.hosted_zone_id = "".join(
            random.choice(string.ascii_uppercase + string.digits) for _ in range(14)
        )
        self.kms_key_id = kms_key_id or (
            "default_kms_key_id" if self.storage_encrypted else None
        )
        self.preferred_maintenance_window = preferred_maintenance_window
        self.preferred_backup_window = preferred_backup_window
        self.port = port
        self.availability_zones = [
            f"{self.region_name}a",
            f"{self.region_name}b",
            f"{self.region_name}c",
        ]
        self.serverless_v2_scaling_configuration = serverless_v2_scaling_configuration

    @property
    def db_cluster_arn(self) -> str:
        return f"arn:aws:rds:{self.region_name}:{self.account_id}:cluster:{self.db_cluster_identifier}"

    def get_tags(self) -> List[Dict[str, str]]:
        return self.tags

    def add_tags(self, tags: List[Dict[str, str]]) -> List[Dict[str, str]]:
        new_keys = [tag_set["Key"] for tag_set in tags]
        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in new_keys]
        self.tags.extend(tags)
        return self.tags

    def remove_tags(self, tag_keys: List[str]) -> None:
        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in tag_keys]

    def to_xml(self) -> str:
        template = Template(
            """<DBCluster>
            {% if cluster.allocated_storage %}
        <AllocatedStorage>{{ cluster.allocated_storage }}</AllocatedStorage>
        {% endif %}
        <AvailabilityZones>
          {% for zone in cluster.availability_zones %}
          <AvailabilityZone>{{ zone }}</AvailabilityZone>
          {% endfor %}
        </AvailabilityZones>
        {% if cluster.backup_retention_period %}
        <BackupRetentionPeriod>{{ cluster.backup_retention_period }}</BackupRetentionPeriod>
        {% endif %}
        {% if cluster.character_set_name %}
        <CharacterSetName>{{ cluster.character_set_name }}</CharacterSetName>
        {% endif %}
        {% if cluster.database_name %}
        <DatabaseName>{{ cluster.database_name }}</DatabaseName>
        {% endif %}
        <DBClusterIdentifier>{{ cluster.db_cluster_identifier }}</DBClusterIdentifier>
        <DBClusterParameterGroup>{{ cluster.db_cluster_parameter_group_name }}</DBClusterParameterGroup>
        <DBSubnetGroup>{{ cluster.db_subnet_group }}</DBSubnetGroup>
        <Status>{{ cluster.status }}</Status>
        <PercentProgress>{{ cluster.percent_progress }}</PercentProgress>
        {% if cluster.earliest_restorable_time %}
        <EarliestRestorableTime>{{ cluster.earliest_restorable_time }}</EarliestRestorableTime>
        {% endif %}
        <Endpoint>{{ cluster.endpoint }}</Endpoint>
        <ReaderEndpoint>{{ cluster.reader_endpoint }}</ReaderEndpoint>
        <MultiAZ>false</MultiAZ>
        <Engine>{{ cluster.engine }}</Engine>
        <EngineVersion>{{ cluster.engine_version }}</EngineVersion>
        {% if cluster.latest_restorable_time %}
        <LatestRestorableTime>{{ cluster.latest_restorable_time }}</LatestRestorableTime>
        {% endif %}
        {% if cluster.port %}
        <Port>{{ cluster.port }}</Port>
        {% endif %}
        <MasterUsername>{{ cluster.master_username }}</MasterUsername>
        <DBClusterOptionGroupMemberships>
{% for dbclusteroptiongroupmembership in cluster.dbclusteroptiongroupmemberships %}
          <member>
            <DBClusterOptionGroupName>{{ dbclusteroptiongroupmembership.db_cluster_option_group_name }}</DBClusterOptionGroupName>
            <Status>{{ dbclusteroptiongroupmembership.status }}</Status>
          </member>
{% endfor %}
        </DBClusterOptionGroupMemberships>
        <PreferredBackupWindow>{{ cluster.preferred_backup_window }}</PreferredBackupWindow>
        <PreferredMaintenanceWindow>{{ cluster.preferred_maintenance_window }}</PreferredMaintenanceWindow>
        <ReplicationSourceIdentifier>{{ cluster.replication_source_identifier }}</ReplicationSourceIdentifier>
        <ReadReplicaIdentifiers>
{% for readreplicaidentifier in cluster.readreplicaidentifiers %}
          <member/>
{% endfor %}
        </ReadReplicaIdentifiers>
        <DBClusterMembers>
{% for dbclustermember in cluster.dbclustermembers %}
          <member>
            <DBInstanceIdentifier>{{ dbclustermember.db_instance_identifier }}</DBInstanceIdentifier>
            <IsClusterWriter>{{ dbclustermember.is_cluster_writer }}</IsClusterWriter>
            <DBClusterParameterGroupStatus>{{ dbclustermember.db_cluster_parameter_group_status }}</DBClusterParameterGroupStatus>
            <PromotionTier>{{ dbclustermember.promotion_tier }}</PromotionTier>
          </member>
{% endfor %}
        </DBClusterMembers>
        <VpcSecurityGroups>
{% for vpcsecuritygroup in cluster.vpcsecuritygroups %}
          <member>
            <VpcSecurityGroupId>{{ vpcsecuritygroup.vpc_security_group_id }}</VpcSecurityGroupId>
            <Status>{{ vpcsecuritygroup.status }}</Status>
          </member>
{% endfor %}
        </VpcSecurityGroups>
        <HostedZoneId>{{ cluster.hosted_zone_id }}</HostedZoneId>
        <StorageEncrypted>{{ 'true' if cluster.storage_encrypted else 'false'}}</StorageEncrypted>
        <KmsKeyId>{{ cluster.kms_key_id }}</KmsKeyId>
        <DbClusterResourceId>{{ cluster.resource_id }}</DbClusterResourceId>
        <DBClusterArn>{{ cluster.db_cluster_arn }}</DBClusterArn>
        <AssociatedRoles>
{% for associatedrole in cluster.associatedroles %}
          <member>
            <RoleArn>{{ associatedrole.role_arn }}</RoleArn>
            <Status>{{ associatedrole.status }}</Status>
            <FeatureName>{{ associatedrole.feature_name }}</FeatureName>
          </member>
{% endfor %}
        </AssociatedRoles>
        <IAMDatabaseAuthenticationEnabled>false</IAMDatabaseAuthenticationEnabled>
        <CloneGroupId>{{ cluster.clone_group_id }}</CloneGroupId>
        <ClusterCreateTime>{{ cluster.cluster_create_time }}</ClusterCreateTime>
        <CopyTagsToSnapshot>false</CopyTagsToSnapshot>
        <EnabledCloudwatchLogsExports>
{% for enabledcloudwatchlogsexport in cluster.enabledcloudwatchlogsexports %}
          <member/>db_cluster_arn
{% endfor %}
        </EnabledCloudwatchLogsExports>
        <DeletionProtection>false</DeletionProtection>
        <CrossAccountClone>false</CrossAccountClone>
        {% if cluster.automatic_restart_time %}
        <AutomaticRestartTime>{{ cluster.automatic_restart_time }}</AutomaticRestartTime>
        {% endif %}
        {% if cluster.serverless_v2_scaling_configuration %}
        <ServerlessV2ScalingConfiguration>
            <MinCapacity>{{ cluster.serverless_v2_scaling_configuration["MinCapacity"] }}</MinCapacity>
            <MaxCapacity>{{ cluster.serverless_v2_scaling_configuration["MaxCapacity"] }}</MaxCapacity>
        </ServerlessV2ScalingConfiguration>
        {% endif %}
        </DBCluster>"""
        )
        return template.render(cluster=self)


class NeptuneBackend(BaseBackend):
    """Implementation of Neptune APIs."""

    def __init__(self, region_name: str, account_id: str):
        super().__init__(region_name, account_id)
        self.clusters: Dict[str, DBCluster] = dict()
        self.global_clusters: Dict[str, GlobalCluster] = dict()
        self._db_cluster_options: Optional[List[Dict[str, Any]]] = None

    @property
    def global_backend(self) -> "NeptuneBackend":
        return neptune_backends[self.account_id]["us-east-1"]

    @property
    def db_cluster_options(self) -> List[Dict[str, Any]]:  # type: ignore[misc]
        if self._db_cluster_options is None:
            from moto.rds.utils import decode_orderable_db_instance

            decoded_options: List[Dict[str, Any]] = load_resource(
                __name__, "../rds/resources/cluster_options/neptune.json"
            )
            self._db_cluster_options = [
                decode_orderable_db_instance(option) for option in decoded_options
            ]
        return self._db_cluster_options

    def create_db_cluster(self, **kwargs: Any) -> DBCluster:
        cluster = DBCluster(
            account_id=self.account_id,
            region_name=self.region_name,
            db_cluster_identifier=kwargs["db_cluster_identifier"],
            database_name=kwargs.get("database_name"),
            storage_encrypted=kwargs.get("storage_encrypted", True),
            parameter_group_name=kwargs.get("db_cluster_parameter_group_name") or "",
            tags=kwargs.get("tags", []),
            engine=kwargs.get("engine", "neptune"),
            engine_version=kwargs.get("engine_version") or "1.2.0.2",
            kms_key_id=kwargs.get("kms_key_id"),
            preferred_maintenance_window=kwargs.get("preferred_maintenance_window")
            or "none",
            preferred_backup_window=kwargs.get("preferred_backup_window"),
            backup_retention_period=kwargs.get("backup_retention_period") or 1,
            port=kwargs.get("port") or 8192,
            serverless_v2_scaling_configuration=kwargs.get(
                "serverless_v2_scaling_configuration"
            ),
        )
        self.clusters[cluster.db_cluster_identifier] = cluster
        return cluster

    def create_global_cluster(
        self,
        global_cluster_identifier: str,
        engine: Optional[str],
        engine_version: Optional[str],
        storage_encrypted: Optional[str],
        deletion_protection: Optional[str],
    ) -> GlobalCluster:
        cluster = GlobalCluster(
            account_id=self.account_id,
            global_cluster_identifier=global_cluster_identifier,
            engine=engine,
            engine_version=engine_version,
            storage_encrypted=storage_encrypted,
            deletion_protection=deletion_protection,
        )
        self.global_backend.global_clusters[global_cluster_identifier] = cluster
        return cluster

    def delete_global_cluster(self, global_cluster_identifier: str) -> GlobalCluster:
        return self.global_backend.global_clusters.pop(global_cluster_identifier)

    def describe_global_clusters(self) -> List[GlobalCluster]:
        return list(self.global_backend.global_clusters.values())

    def describe_db_clusters(self, db_cluster_identifier: str) -> List[DBCluster]:
        """
        Pagination and the Filters-argument is not yet implemented
        """
        if db_cluster_identifier:
            if db_cluster_identifier not in self.clusters:
                raise DBClusterNotFoundError(db_cluster_identifier)
            return [self.clusters[db_cluster_identifier]]
        return list(self.clusters.values())

    def delete_db_cluster(self, cluster_identifier: str) -> DBCluster:
        """
        The parameters SkipFinalSnapshot and FinalDBSnapshotIdentifier are not yet implemented.
        The DeletionProtection-attribute is not yet enforced
        """
        if cluster_identifier in self.clusters:
            return self.clusters.pop(cluster_identifier)
        raise DBClusterNotFoundError(cluster_identifier)

    def modify_db_cluster(self, kwargs: Any) -> DBCluster:
        cluster_id = kwargs["db_cluster_identifier"]

        cluster = self.clusters[cluster_id]
        del self.clusters[cluster_id]

        kwargs["db_cluster_identifier"] = kwargs.pop("new_db_cluster_identifier")
        for k, v in kwargs.items():
            if v is not None:
                setattr(cluster, k, v)

        cluster_id = kwargs.get("new_db_cluster_identifier", cluster_id)
        self.clusters[cluster_id] = cluster

        initial_state = copy.deepcopy(cluster)  # Return status=creating
        cluster.status = "available"  # Already set the final status in the background
        return initial_state

    def start_db_cluster(self, cluster_identifier: str) -> DBCluster:
        if cluster_identifier not in self.clusters:
            raise DBClusterNotFoundError(cluster_identifier)
        cluster = self.clusters[cluster_identifier]
        temp_state = copy.deepcopy(cluster)
        temp_state.status = "started"
        cluster.status = "available"  # This is the final status - already setting it in the background
        return temp_state

    def describe_orderable_db_instance_options(
        self, engine_version: Optional[str]
    ) -> List[Dict[str, Any]]:
        """
        Only the EngineVersion-parameter is currently implemented.
        """
        if engine_version:
            return [
                option
                for option in self.db_cluster_options
                if option["EngineVersion"] == engine_version
            ]
        return self.db_cluster_options


neptune_backends = BackendDict(NeptuneBackend, "neptune")
