import uuid
from datetime import timedelta
from typing import Any, Optional, Union

from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
from django.db import models
from django.db.models import Q
from django.template import Context, Template
from django.utils import timezone
from django.utils.translation import gettext as _

from celery.result import AsyncResult
from model_utils.models import TimeStampedModel

from aleksis.core.mixins import ExtensibleModel
from aleksis.core.models import OAuthApplication, Person
from aleksis.core.util.pdf import process_context_for_pdf


class CardPrinterStatus(models.TextChoices):
    ONLINE = "online", _("Online")
    OFFLINE = "offline", _("Offline")
    WITH_ERRORS = "with_errors", _("With errors")
    NOT_REGISTERED = "not_registered", _("Not registered")

    @classmethod
    def get_color(cls, value):
        _colors = {
            CardPrinterStatus.ONLINE.value: "green",
            CardPrinterStatus.OFFLINE.value: "red",
            CardPrinterStatus.WITH_ERRORS.value: "orange",
            CardPrinterStatus.NOT_REGISTERED.value: "grey",
        }
        return _colors.get(value)

    @classmethod
    def get_icon(cls, value):
        _icons = {
            CardPrinterStatus.ONLINE.value: "mdi:printer-check",
            CardPrinterStatus.OFFLINE.value: "mdi:printer-off",
            CardPrinterStatus.WITH_ERRORS.value: "mdi:printer-alert",
            CardPrinterStatus.NOT_REGISTERED.value: "mdi:printer-search",
        }
        return _icons.get(value)

    @classmethod
    def get_label(cls, value):
        _labels = {x: y for x, y in cls.choices}
        return _labels.get(value)


class PrintStatus(models.TextChoices):
    REGISTERED = "registered", _("Registered")
    IN_PROGRESS = "in_progress", _("In progress")
    FINISHED = "finished", _("Finished")
    FAILED = "failed", _("Failed")


class CardPrinter(ExtensibleModel):
    SCOPE_PREFIX = "card_printer"
    name = models.CharField(max_length=255, verbose_name=_("Name"))
    description = models.TextField(verbose_name=_("Description"), blank=True)
    location = models.CharField(max_length=255, verbose_name=_("Location"), blank=True)

    status = models.CharField(
        max_length=255,
        verbose_name=_("Status"),
        choices=CardPrinterStatus.choices,
        default=CardPrinterStatus.NOT_REGISTERED,
    )
    status_text = models.TextField(verbose_name=_("Status text"), blank=True)
    last_seen_at = models.DateTimeField(verbose_name=_("Last seen at"), blank=True, null=True)

    oauth2_application = models.ForeignKey(
        to=OAuthApplication,
        on_delete=models.CASCADE,
        verbose_name=_("OAuth2 application"),
        blank=True,
        null=True,
        related_name="card_printers",
    )

    # Settings
    cups_printer = models.CharField(max_length=255, verbose_name=_("CUPS printer"), blank=True)
    generate_number_on_server = models.BooleanField(
        default=True, verbose_name=_("Generate card number on server")
    )
    card_detector = models.CharField(max_length=255, verbose_name=_("Card detector"), blank=True)

    def save(self, *args, **kwargs):
        if not self.oauth2_application:
            application = OAuthApplication(
                client_type=OAuthApplication.CLIENT_CONFIDENTIAL,
                authorization_grant_type=OAuthApplication.GRANT_AUTHORIZATION_CODE,
                name=f"Card printer: {self.name}",
                redirect_uris="urn:ietf:wg:oauth:2.0:oob",
            )
            application.save()
            self.oauth2_application = application

        super().save(*args, **kwargs)

    def __str__(self):
        return self.name

    @property
    def status_label(self) -> str:
        """Return the verbose name of the status."""
        return CardPrinterStatus.get_label(self.status)

    @property
    def status_color(self) -> str:
        """Return a color for the status."""
        return CardPrinterStatus.get_color(self.status)

    @property
    def status_icon(self) -> str:
        """Return an iconify icon for the status."""
        return CardPrinterStatus.get_icon(self.status)

    def generate_config(self) -> dict[str, Any]:
        """Generate the configuration for the printer client."""
        config = {
            "base_url": settings.BASE_URL,
            "client_id": self.oauth2_application.client_id,
            "client_secret": self.oauth2_application.client_secret,
        }
        return config

    @property
    def config_filename(self) -> str:
        """Return the filename for the printer client configuration."""
        return f"card-printer-config-{self.pk}.json"

    def check_online_status(self):
        if (
            self.status
            not in (CardPrinterStatus.NOT_REGISTERED.value, CardPrinterStatus.OFFLINE.value)
            and self.last_seen_at
        ):
            if self.last_seen_at < timezone.now() - timedelta(minutes=1):
                self.status = CardPrinterStatus.OFFLINE.value
                self.save()

    @classmethod
    def check_online_status_for_all(cls, qs=None):
        if not qs:
            qs = cls.objects.all()
        for printer in cls.objects.all():
            printer.check_online_status()

    def get_next_print_job(self) -> Optional["CardPrintJob"]:
        if not self.generate_number_on_server:
            self.jobs.filter(
                (Q(card__pdf_file="") & ~Q(card__chip_number=""))
                | Q(status=PrintStatus.IN_PROGRESS)
            ).update(status=PrintStatus.FAILED)
            Card.objects.filter(
                jobs__in=self.jobs.filter(status=PrintStatus.FAILED), chip_number=""
            ).update(chip_number="")
        else:
            self.jobs.filter(status=PrintStatus.IN_PROGRESS).update(status=PrintStatus.FAILED)

        jobs = self.jobs.order_by("created").filter(status=PrintStatus.REGISTERED)
        if self.generate_number_on_server:
            jobs = jobs.filter(card__pdf_file__isnull=False)
        if jobs.exists():
            return jobs.first()
        return None

    class Meta:
        verbose_name = _("Card printer")
        verbose_name_plural = _("Card printers")


class CardLayoutMediaFile(ExtensibleModel):
    media_file = models.FileField(upload_to="card_layouts/media/", verbose_name=_("Media file"))
    card_layout = models.ForeignKey(
        "CardLayout",
        on_delete=models.CASCADE,
        related_name="media_files",
        verbose_name=_("Card layout"),
    )

    def __str__(self):
        return self.media_file.name

    class Meta:
        verbose_name = _("Media file for a card layout")
        verbose_name_plural = _("Media files for card layouts")


class CardLayout(ExtensibleModel):
    BASE_TEMPLATE = """
    {{% extends "core/base_simple_print.html" %}}
    {{% load i18n static barcode %}}

    {{% block size %}}
      {{% with width={width} height={height} %}}
        {{{{ block.super }}}}
      {{% endwith %}}
    {{% endblock %}}

    {{% block extra_head %}}
      <style>
        {css}
      </style>
    {{% endblock %}}

    {{% block content %}}
      {template}
    {{% endblock %}}
    """
    name = models.CharField(max_length=255, verbose_name=_("Name"))
    template = models.TextField(verbose_name=_("Template"))
    css = models.TextField(verbose_name=_("Custom CSS"), blank=True)
    width = models.PositiveIntegerField(verbose_name=_("Width"), help_text=_("in mm"))
    height = models.PositiveIntegerField(verbose_name=_("Height"), help_text=_("in mm"))
    required_fields = ArrayField(models.TextField(), verbose_name=_("Required data fields"))

    def get_template(self) -> Template:
        template = self.BASE_TEMPLATE.format(
            width=self.width, height=self.height, css=self.css, template=self.template
        )
        return Template(template)

    def render(self, card: "Card"):
        t = self.get_template()
        context = card.get_context()
        processed_context = process_context_for_pdf(context)

        return t.render(Context(processed_context))

    def validate_template(self):
        try:
            t = Template(self.template)
            t.render(Context())
        except Exception as e:
            raise ValidationError(_("Template is invalid: {}").format(e))

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = _("Card Layout")
        verbose_name_plural = _("Card Layouts")


class Card(ExtensibleModel):
    person = models.ForeignKey(
        Person, models.CASCADE, verbose_name=_("Person"), related_name="cards"
    )
    chip_number = models.CharField(verbose_name=_("Chip Number"), blank=True, max_length=255)
    valid_until = models.DateField(verbose_name=_("Valid until"))
    deactivated = models.BooleanField(verbose_name=_("Deactivated"), default=False)

    layout = models.ForeignKey(
        CardLayout, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_("Card Layout")
    )
    pdf_file = models.FileField(
        verbose_name=_("PDF file"),
        blank=True,
        upload_to="cards/",
        validators=[FileExtensionValidator(["pdf"])],
    )

    @property
    def is_valid(self):
        return (
            self.valid_until >= timezone.now().date() and not self.deactivated and self.chip_number
        )

    def deactivate(self):
        self.deactivated = True
        self.save()

    def get_context(self):
        return {
            "person": self.person,
            "chip_number": self.chip_number,
            "valid_until": self.valid_until,
            "media_files": self.layout.media_files.all(),
        }

    def generate_pdf(self) -> Union[bool, AsyncResult]:
        from .tasks import generate_card_pdf

        if self.pdf_file:
            return True
        return generate_card_pdf.delay(self.pk)

    def print_card(self, printer: CardPrinter):
        if not self.layout:
            raise ValueError(_("There is no layout provided for the card."))
        job = CardPrintJob(card=self, printer=printer)
        job.save()
        if not self.chip_number and printer.generate_number_on_server:
            self.chip_number = str(self.generate_number())
            self.save()
        if self.chip_number:
            self.generate_pdf()
        return job

    def generate_number(self) -> int:
        return uuid.uuid1().int >> 32

    def __str__(self):
        if self.chip_number:
            return f"{self.person} ({self.chip_number})"
        return f"{self.person}"

    class Meta:
        verbose_name = _("Card")
        verbose_name_plural = _("Cards")


class CardPrintJob(TimeStampedModel, ExtensibleModel):
    printer = models.ForeignKey(
        CardPrinter, on_delete=models.CASCADE, verbose_name=_("Printer"), related_name="jobs"
    )
    card = models.ForeignKey(
        Card, on_delete=models.CASCADE, verbose_name=_("Card"), related_name="jobs"
    )

    status = models.CharField(
        max_length=255,
        verbose_name=_("Status"),
        choices=PrintStatus.choices,
        default=PrintStatus.REGISTERED,
    )
    status_text = models.TextField(verbose_name=_("Status text"), blank=True)

    class Meta:
        verbose_name = _("Card print job")
        verbose_name_plural = _("Card print jobs")