"""blueprint models"""
from pathlib import Path
from uuid import uuid4

from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from structlog import get_logger

from authentik.blueprints.v1.oci import OCI_PREFIX, BlueprintOCIClient, OCIException
from authentik.lib.config import CONFIG
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
from authentik.lib.sentry import SentryIgnoredException

LOGGER = get_logger()


class BlueprintRetrievalFailed(SentryIgnoredException):
    """Error raised when we are unable to fetch the blueprint contents, whether it be HTTP files
    not being accessible or local files not being readable"""


class ManagedModel(models.Model):
    """Model that can be managed by authentik exclusively"""

    managed = models.TextField(
        default=None,
        null=True,
        verbose_name=_("Managed by authentik"),
        help_text=_(
            "Objects that are managed by authentik. These objects are created and updated "
            "automatically. This flag only indicates that an object can be overwritten by "
            "migrations. You can still modify the objects via the API, but expect changes "
            "to be overwritten in a later update."
        ),
        unique=True,
    )

    class Meta:
        abstract = True


class BlueprintInstanceStatus(models.TextChoices):
    """Instance status"""

    SUCCESSFUL = "successful"
    WARNING = "warning"
    ERROR = "error"
    ORPHANED = "orphaned"
    UNKNOWN = "unknown"


class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
    """Instance of a single blueprint. Can be parameterized via context attribute when
    blueprint in `path` has inputs."""

    instance_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)

    name = models.TextField(unique=True)
    metadata = models.JSONField(default=dict)
    path = models.TextField(default="", blank=True)
    content = models.TextField(default="", blank=True)
    context = models.JSONField(default=dict)
    last_applied = models.DateTimeField(auto_now=True)
    last_applied_hash = models.TextField()
    status = models.TextField(
        choices=BlueprintInstanceStatus.choices, default=BlueprintInstanceStatus.UNKNOWN
    )
    enabled = models.BooleanField(default=True)
    managed_models = ArrayField(models.TextField(), default=list)

    def retrieve_oci(self) -> str:
        """Get blueprint from an OCI registry"""
        client = BlueprintOCIClient(self.path.replace(OCI_PREFIX, "https://"))
        try:
            manifests = client.fetch_manifests()
            return client.fetch_blobs(manifests)
        except OCIException as exc:
            raise BlueprintRetrievalFailed(exc) from exc

    def retrieve_file(self) -> str:
        """Get blueprint from path"""
        try:
            base = Path(CONFIG.get("blueprints_dir"))
            full_path = base.joinpath(Path(self.path)).resolve()
            if not str(full_path).startswith(str(base.resolve())):
                raise BlueprintRetrievalFailed("Invalid blueprint path")
            with full_path.open("r", encoding="utf-8") as _file:
                return _file.read()
        except (IOError, OSError) as exc:
            raise BlueprintRetrievalFailed(exc) from exc

    def retrieve(self) -> str:
        """Retrieve blueprint contents"""
        if self.path.startswith(OCI_PREFIX):
            return self.retrieve_oci()
        if self.path != "":
            return self.retrieve_file()
        return self.content

    @property
    def serializer(self) -> Serializer:
        from authentik.blueprints.api import BlueprintInstanceSerializer

        return BlueprintInstanceSerializer

    def __str__(self) -> str:
        return f"Blueprint Instance {self.name}"

    class Meta:
        verbose_name = _("Blueprint Instance")
        verbose_name_plural = _("Blueprint Instances")
        unique_together = (
            (
                "name",
                "path",
            ),
        )
