from collections import OrderedDict, defaultdict
from operator import attrgetter

from django.conf import settings
from django.core.cache import cache
from django.db import models
from django.db.models import CASCADE
from django.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _

from judge.judgeapi import disconnect_judge

__all__ = ["Language", "RuntimeVersion", "Judge"]


class Language(models.Model):
    key = models.CharField(
        max_length=6,
        verbose_name=_("short identifier"),
        help_text=_(
            "The identifier for this language; the same as its executor id for judges."
        ),
        unique=True,
    )
    name = models.CharField(
        max_length=20,
        verbose_name=_("long name"),
        help_text=_('Longer name for the language, e.g. "Python 2" or "C++11".'),
    )
    short_name = models.CharField(
        max_length=10,
        verbose_name=_("short name"),
        help_text=_(
            'More readable, but short, name to display publicly; e.g. "PY2" or '
            '"C++11". If left blank, it will default to the '
            "short identifier."
        ),
        null=True,
        blank=True,
    )
    common_name = models.CharField(
        max_length=10,
        verbose_name=_("common name"),
        help_text=_(
            "Common name for the language. For example, the common name for C++03, "
            'C++11, and C++14 would be "C++"'
        ),
    )
    ace = models.CharField(
        max_length=20,
        verbose_name=_("ace mode name"),
        help_text=_(
            'Language ID for Ace.js editor highlighting, appended to "mode-" to determine '
            'the Ace JavaScript file to use, e.g., "python".'
        ),
    )
    pygments = models.CharField(
        max_length=20,
        verbose_name=_("pygments name"),
        help_text=_("Language ID for Pygments highlighting in source windows."),
    )
    template = models.TextField(
        verbose_name=_("code template"),
        help_text=_("Code template to display in submission editor."),
        blank=True,
    )
    info = models.CharField(
        max_length=50,
        verbose_name=_("runtime info override"),
        blank=True,
        help_text=_(
            "Do not set this unless you know what you're doing! It will override the "
            "usually more specific, judge-provided runtime info!"
        ),
    )
    description = models.TextField(
        verbose_name=_("language description"),
        help_text=_(
            "Use this field to inform users of quirks with your environment, "
            "additional restrictions, etc."
        ),
        blank=True,
    )
    extension = models.CharField(
        max_length=10,
        verbose_name=_("extension"),
        help_text=_('The extension of source files, e.g., "py" or "cpp".'),
    )

    def runtime_versions(self):
        runtimes = OrderedDict()
        # There be dragons here if two judges specify different priorities
        for runtime in self.runtimeversion_set.all():
            id = runtime.name
            if id not in runtimes:
                runtimes[id] = set()
            if (
                not runtime.version
            ):  # empty str == error determining version on judge side
                continue
            runtimes[id].add(runtime.version)

        lang_versions = []
        for id, version_list in runtimes.items():
            lang_versions.append(
                (id, sorted(version_list, key=lambda a: tuple(map(int, a.split(".")))))
            )
        return lang_versions

    @classmethod
    def get_common_name_map(cls):
        result = cache.get("lang:cn_map")
        if result is not None:
            return result
        result = defaultdict(set)
        for id, cn in Language.objects.values_list("id", "common_name"):
            result[cn].add(id)
        result = {id: cns for id, cns in result.items() if len(cns) > 1}
        cache.set("lang:cn_map", result, 86400)
        return result

    @cached_property
    def short_display_name(self):
        return self.short_name or self.key

    def __str__(self):
        return self.name

    @cached_property
    def display_name(self):
        if self.info:
            return "%s (%s)" % (self.name, self.info)
        else:
            return self.name

    @classmethod
    def get_python3(cls):
        # We really need a default language, and this app is in Python 3
        return Language.objects.get_or_create(key="PY3", defaults={"name": "Python 3"})[
            0
        ]

    def get_absolute_url(self):
        return reverse("runtime_list") + "#" + self.key

    @classmethod
    def get_default_language(cls):
        try:
            return Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE)
        except Language.DoesNotExist:
            return cls.get_python3()

    @classmethod
    def get_default_language_pk(cls):
        return cls.get_default_language().pk

    class Meta:
        ordering = ["key"]
        verbose_name = _("language")
        verbose_name_plural = _("languages")


class RuntimeVersion(models.Model):
    language = models.ForeignKey(
        Language,
        verbose_name=_("language to which this runtime belongs"),
        on_delete=CASCADE,
    )
    judge = models.ForeignKey(
        "Judge", verbose_name=_("judge on which this runtime exists"), on_delete=CASCADE
    )
    name = models.CharField(max_length=64, verbose_name=_("runtime name"))
    version = models.CharField(
        max_length=64, verbose_name=_("runtime version"), blank=True
    )
    priority = models.IntegerField(
        verbose_name=_("order in which to display this runtime"), default=0
    )


class Judge(models.Model):
    name = models.CharField(
        max_length=50, help_text=_("Server name, hostname-style"), unique=True
    )
    created = models.DateTimeField(
        auto_now_add=True, verbose_name=_("time of creation")
    )
    auth_key = models.CharField(
        max_length=100,
        help_text=_("A key to authenticate this judge"),
        verbose_name=_("authentication key"),
    )
    is_blocked = models.BooleanField(
        verbose_name=_("block judge"),
        default=False,
        help_text=_(
            "Whether this judge should be blocked from connecting, "
            "even if its key is correct."
        ),
    )
    online = models.BooleanField(verbose_name=_("judge online status"), default=False)
    start_time = models.DateTimeField(verbose_name=_("judge start time"), null=True)
    ping = models.FloatField(verbose_name=_("response time"), null=True)
    load = models.FloatField(
        verbose_name=_("system load"),
        null=True,
        help_text=_("Load for the last minute, divided by processors to be fair."),
    )
    description = models.TextField(blank=True, verbose_name=_("description"))
    last_ip = models.GenericIPAddressField(
        verbose_name="Last connected IP", blank=True, null=True
    )
    problems = models.ManyToManyField(
        "Problem", verbose_name=_("problems"), related_name="judges"
    )
    runtimes = models.ManyToManyField(
        Language, verbose_name=_("judges"), related_name="judges"
    )

    def __str__(self):
        return self.name

    def disconnect(self, force=False):
        disconnect_judge(self, force=force)

    disconnect.alters_data = True

    @cached_property
    def runtime_versions(self):
        qs = self.runtimeversion_set.values(
            "language__key", "language__name", "version", "name"
        ).order_by("language__key", "priority")

        ret = OrderedDict()

        for data in qs:
            key = data["language__key"]
            if key not in ret:
                ret[key] = {"name": data["language__name"], "runtime": []}
            ret[key]["runtime"].append((data["name"], (data["version"],)))

        return list(ret.items())

    @cached_property
    def uptime(self):
        return timezone.now() - self.start_time if self.online else "N/A"

    @cached_property
    def ping_ms(self):
        return self.ping * 1000 if self.ping is not None else None

    @cached_property
    def runtime_list(self):
        return map(attrgetter("name"), self.runtimes.all())

    class Meta:
        ordering = ["name"]
        verbose_name = _("judge")
        verbose_name_plural = _("judges")
