import kubernetes
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from encrypted_fields.fields import EncryptedJSONField
from kubernetes import config
from kubernetes.client.rest import ApiException


class ServiceCategory(models.Model):
    """
    Categories for services, e.g. "Databases", "Storage", "Compute".
    """

    name = models.CharField(max_length=100, verbose_name=_("Name"))
    description = models.TextField(blank=True, verbose_name=_("Description"))
    logo = models.ImageField(
        upload_to="public/service_categories",
        blank=True,
        null=True,
        verbose_name=_("Logo"),
    )

    parent = models.ForeignKey(
        to="self",
        on_delete=models.CASCADE,
        related_name="children",
        blank=True,
        null=True,
        verbose_name=_("Parent"),
    )

    class Meta:
        verbose_name = _("Service category")
        verbose_name_plural = _("Service categories")

    def __str__(self):
        return self.name


class Service(models.Model):
    """
    A service that can be offered, e.g. "PostgreSQL", "MinIO", "Kubernetes".
    """

    name = models.CharField(max_length=100, verbose_name=_("Name"))
    slug = models.SlugField(max_length=100, verbose_name=_("URL slug"), unique=True)
    category = models.ForeignKey(
        to="ServiceCategory",
        on_delete=models.PROTECT,
        related_name="services",
        verbose_name=_("Category"),
    )
    description = models.TextField(blank=True, verbose_name=_("Description"))
    logo = models.ImageField(
        upload_to="public/services", blank=True, null=True, verbose_name=_("Logo")
    )
    # TODO schema
    external_links = models.JSONField(
        null=True, blank=True, verbose_name=_("External links")
    )

    class Meta:
        verbose_name = _("Service")
        verbose_name_plural = _("Services")

    def __str__(self):
        return self.name


def validate_dict(data, required_fields=None, allow_empty=True):
    if not data:
        if allow_empty:
            return
        raise ValidationError(_("Data may not be empty!"))

    missing_fields = required_fields - set(data)
    if missing_fields:
        raise ValidationError(
            _("Missing required fields in API credentials: %(fields)s"),
            params={"fields": ", ".join(missing_fields)},
        )


def validate_api_credentials(value):
    required_fields = ("certificate-authority-data", "server", "token")
    return validate_dict(value, required_fields)


class ControlPlane(models.Model):
    name = models.CharField(max_length=100, verbose_name=_("Name"))
    description = models.TextField(blank=True, verbose_name=_("Description"))
    # Either contains the fields "certificate_authority_data", "server" and "token", or is empty
    api_credentials = EncryptedJSONField(
        verbose_name=_("API credentials"),
        help_text="Required fields: certificate-authority-data, server (URL), token",
        validators=[validate_api_credentials],
    )

    cloud_provider = models.ForeignKey(
        to="CloudProvider",
        on_delete=models.PROTECT,
        related_name="control_planes",
        verbose_name=_("Cloud provider"),
    )

    class Meta:
        verbose_name = _("Control plane")
        verbose_name_plural = _("Control planes")

    def __str__(self):
        return self.name

    @property
    def kubernetes_config(self):
        conf = kubernetes.client.Configuration()
        user_name = "servala-user"

        config_dict = {
            "apiVersion": "v1",
            "clusters": [
                {
                    "cluster": {
                        "certificate-authority-data": self.api_credentials[
                            "certificate-authority-data"
                        ],
                        "server": self.api_credentials["server"],
                    },
                    "name": self.name,
                },
            ],
            "contexts": [
                {
                    "context": {
                        "cluster": self.name,
                        "namespace": "default",
                        "user": user_name,
                    },
                    "name": self.name,
                }
            ],
            "current-context": self.name,
            "kind": "Config",
            "preferences": {},
            "users": [
                {
                    "name": user_name,
                    "user": {"token": self.api_credentials["token"]},
                }
            ],
        }
        config.load_kube_config_from_dict(
            config_dict=config_dict,
            client_configuration=conf,
        )
        return conf

    def get_kubernetes_client(self):
        return kubernetes.client.ApiClient(self.kubernetes_config)

    def test_connection(self):
        if not self.api_credentials:
            return False, _("No API credentials provided")

        try:
            v1 = kubernetes.client.CoreV1Api(self.get_kubernetes_client())
            namespace_count = len(v1.list_namespace().items)

            return True, _(
                "Successfully connected to Kubernetes API. Found {} namespaces."
            ).format(namespace_count)
        except ApiException as e:
            return False, _("API error: {}").format(str(e))
        except Exception as e:
            return False, _("Connection error: {}").format(str(e))


class CloudProvider(models.Model):
    """
    A cloud provider, e.g. "Exoscale".
    """

    name = models.CharField(max_length=100, verbose_name=_("Name"))
    description = models.TextField(blank=True, verbose_name=_("Description"))
    logo = models.ImageField(
        upload_to="public/service_providers",
        blank=True,
        null=True,
        verbose_name=_("Logo"),
    )
    external_links = models.JSONField(
        null=True, blank=True, verbose_name=_("External links")
    )

    class Meta:
        verbose_name = _("Cloud provider")
        verbose_name_plural = _("Cloud providers")

    def __str__(self):
        return self.name


class Plan(models.Model):
    """
    Each service offering can have multiple plans, e.g. for different tiers.
    """

    name = models.CharField(max_length=100, verbose_name=_("Name"))
    description = models.TextField(blank=True, verbose_name=_("Description"))
    # TODO schema
    features = models.JSONField(verbose_name=_("Features"), null=True, blank=True)
    # TODO schema
    pricing = models.JSONField(verbose_name=_("Pricing"), null=True, blank=True)
    term = models.PositiveIntegerField(
        verbose_name=_("Term"), help_text=_("Term in months")
    )
    service_offering = models.ForeignKey(
        to="ServiceOffering",
        on_delete=models.PROTECT,
        related_name="plans",
        verbose_name=_("Service offering"),
    )

    class Meta:
        verbose_name = _("Plan")
        verbose_name_plural = _("Plans")

    def __str__(self):
        return self.name


def validate_api_definition(value):
    required_fields = ("group", "version", "kind")
    return validate_dict(value, required_fields)


class ServiceDefinition(models.Model):
    """
    Configuration/service implementation: contains information on which
    CompositeResourceDefinition (aka XRD) implements a service on a ControlPlane.

    Is required in order to query the OpenAPI spec for dynamic form generation.
    """

    name = models.CharField(max_length=100, verbose_name=_("Name"))
    description = models.TextField(blank=True, verbose_name=_("Description"))
    api_definition = models.JSONField(
        verbose_name=_("API Definition"),
        help_text=_("Contains group, version, and kind information"),
        null=True,
        blank=True,
    )
    service = models.ForeignKey(
        to="Service",
        on_delete=models.CASCADE,
        related_name="service_definitions",
        verbose_name=_("Service"),
    )

    @property
    def control_planes(self):
        return ControlPlane.objects.filter(
            offering_connections__service_definition=self
        ).distinct()

    @property
    def service_offerings(self):
        return ServiceOffering.objects.filter(
            control_plane_connections__service_definition=self
        ).distinct()

    class Meta:
        verbose_name = _("Service definition")
        verbose_name_plural = _("Service definitions")

    def __str__(self):
        return self.name


class ServiceOfferingControlPlane(models.Model):
    """
    Each combination of ServiceOffering and ControlPlane can have a different
    ServiceDefinition, which is here modeled as the "through" model.
    """

    service_offering = models.ForeignKey(
        to="ServiceOffering",
        on_delete=models.CASCADE,
        related_name="control_plane_connections",
        verbose_name=_("Service offering"),
    )
    control_plane = models.ForeignKey(
        to="ControlPlane",
        on_delete=models.CASCADE,
        related_name="offering_connections",
        verbose_name=_("Control plane"),
    )
    service_definition = models.ForeignKey(
        to="ServiceDefinition",
        on_delete=models.PROTECT,
        related_name="offering_control_planes",
        verbose_name=_("Service definition"),
    )

    class Meta:
        verbose_name = _("Service offering control plane connection")
        verbose_name_plural = _("Service offering control planes connections")
        unique_together = [("service_offering", "control_plane")]

    def __str__(self):
        return f"{self.service_offering} on {self.control_plane} with {self.service_definition}"

    @cached_property
    def resource_definition(self):
        client = self.control_plane.get_kubernetes_client()
        api_instance = kubernetes.client.ApiextensionsV1Api(client)
        kind = self.service_definition.api_definition["kind"].lower()
        group = self.service_definition.api_definition["group"]
        name = f"{kind}.{group}"
        crd = api_instance.read_custom_resource_definition(name)
        return crd

    @cached_property
    def resource_schema(self):
        for version in self.resource_definition.spec.versions:
            if self.service_definition.api_definition["version"] != version.name:
                continue
            if version.schema and version.schema.open_apiv3_schema:
                schema_dict = kubernetes.client.ApiClient().sanitize_for_serialization(
                    version.schema.open_apiv3_schema
                )
                return schema_dict

    @cached_property
    def django_model(self):
        from servala.core.crd import generate_django_model

        kwargs = {
            key: value
            for key, value in self.service_definition.api_definition.items()
            if key in ("group", "version", "kind")
        }
        return generate_django_model(self.resource_schema, **kwargs)

    @cached_property
    def model_form_class(self):
        from servala.core.crd import generate_model_form_class

        return generate_model_form_class(self.django_model)


class ServiceOffering(models.Model):
    """
    A service offering, e.g. "PostgreSQL on AWS", "MinIO on GCP".
    """

    service = models.ForeignKey(
        to="Service",
        on_delete=models.PROTECT,
        related_name="offerings",
        verbose_name=_("Service"),
    )
    provider = models.ForeignKey(
        to="CloudProvider",
        on_delete=models.PROTECT,
        related_name="offerings",
        verbose_name=_("Provider"),
    )
    control_planes = models.ManyToManyField(
        to="ControlPlane",
        through="ServiceOfferingControlPlane",
        related_name="offerings",
        verbose_name=_("Control planes"),
    )
    description = models.TextField(blank=True, verbose_name=_("Description"))

    class Meta:
        verbose_name = _("Service offering")
        verbose_name_plural = _("Service offerings")

    def __str__(self):
        return _("{service_name} at {provider_name}").format(
            service_name=self.service.name, provider_name=self.provider.name
        )
