import re

import reversion
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.db import models
from gm2m import GM2MField
from crum import get_current_request
from django.urls import reverse


##############################################
#
#   Project classes
#
##############################################


class Project(models.Model):
    """Holds information on registered project.
    Id is used by the JavaScript function to target the right endpoints
    """

    name = models.CharField(max_length=255)  # name of the project registered
    user = models.ForeignKey(
        User, on_delete=models.CASCADE
    )  # foreignkey to the User who created the project
    description = models.TextField(blank=True, null=True)
    base_url = models.URLField(
        blank=True, null=True
    )  # optional base URL to restrict highlights to a base URL
    store_text = models.BooleanField(
        default=False
    )  # Whether to store the text in the tool or work with ids only.

    def __str__(self):
        return self.name

    class Meta:
        db_table = "highlighter_project"


class TextHigh(models.Model):
    """Holds unstructured text associated with
    one ore many entities/relations.
    """

    title = models.CharField(max_length=255, blank=True, null=True)
    text_choices = (("ft", "Full Text"), ("id", "Text ID"), ("cl", "Text Class"))
    text_type = models.CharField(max_length=3, choices=text_choices, default="cl")
    uri = models.URLField(
        blank=True, null=True
    )  # uri of vocab used to identify the text type
    text = models.TextField(blank=True, null=True)
    text_id = models.PositiveIntegerField(
        blank=True, null=True
    )  # UID to identify the text within the project. Allows to not upload the texts.
    text_class = models.CharField(max_length=255, blank=True, null=True)
    project = models.ForeignKey(
        Project, blank=True, null=True, on_delete=models.SET_NULL
    )

    def __str__(self):
        return self.title

    class Meta:
        db_table = "highlighter_texthigh"


class AnnotationProject(models.Model):
    """Every Project can have several Annotation Projects. Annotation Projects are used to track the precission etc.
    of automatic annotation tools.
    """

    name = models.CharField(max_length=255)
    description = models.TextField(blank=True, null=True)
    published = models.BooleanField(default=False)

    @classmethod
    def from_db(cls, db, field_names, values):
        instance = super().from_db(db, field_names, values)
        instance._loaded_values = dict(zip(field_names, values))
        return instance

    def save(self, *args, **kwargs):
        if hasattr(self, "_loaded_values"):
            if self.published != self._loaded_values["published"]:
                for ann in self.annotation_set.all():
                    for ent in ann.entity_link.all():
                        if ent.published != self.published:
                            ent.published = self.published
                            ent.save()
        super().save(*args, **kwargs)

    def __str__(self):
        return self.name

    class Meta:
        db_table = "highlighter_annotationproject"


@reversion.register()
class Annotation(models.Model):
    """Class storing highlights in full-texts"""

    set_highlighter = getattr(
        settings,
        "APIS_HIGHLIGHTER_ENTITIES",
        (
            "apis_entities.Person",
            "apis_entities.Institution",
            "apis_entities.Place",
            "apis_entities.Event",
            "apis_entities.Work",
            "apis_relations.PersonPerson",
            "apis_relations.PersonPlace",
            "apis_relations.PersonInstitution",
            "apis_relations.PersonEvent",
            "apis_relations.PersonWork",
            "apis_relations.InstitutionPlace",
            "apis_relations.InstitutionEvent",
            "apis_relations.InstitutionWork",
            "apis_relations.InstitutionInstitution",
            "apis_relations.PlaceEvent",
            "apis_relations.PlaceWork",
            "apis_relations.PlacePlace",
            "apis_relations.EventWork",
            "apis_relations.EventEvent",
            "apis_relations.WorkWork",
        ),
    )
    status_choices = (("del", "deleted"), ("ap", "approved"))
    start = models.PositiveIntegerField()  # number of string to start highlight
    end = models.PositiveIntegerField()  # number of string to end highlight
    entity_link = GM2MField(
        *set_highlighter
    )  # generic field to store the relation object
    entity_candidate = models.ManyToManyField("apis_metainfo.UriCandidate", blank=True)
    orig_string = models.CharField(
        max_length=255, blank=True, null=True
    )  # string originally highlighted
    text = models.ForeignKey("apis_metainfo.Text", on_delete=models.CASCADE)
    parent = models.ForeignKey(
        "self",
        related_name="parent_annotation",
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
    )  # parent annotations are used to allow for a stacked design of the annotation project
    user_added = models.ForeignKey(
        User, blank=True, null=True, on_delete=models.SET_NULL
    )  # changed from default=12
    annotation_project = models.ForeignKey(
        AnnotationProject, blank=True, null=True, on_delete=models.CASCADE
    )
    status = models.CharField(
        max_length=4, choices=status_choices, blank=True, null=True
    )

    def __str__(self):
        return "{}".format(self.pk)

    def trim_whitespaces(self, char):
        test = True
        while test and char < self.text_length:
            if self.text.text[char] == " ":
                test = True
                char += 1
            else:
                return char

    def annotation_hash(self, format_string="start_end_text_ent_entid"):
        """
        Function that returns a hash of the annotaion used to calculate inter-annotator agreement.

        :return:
        """
        matching = {"start": "start", "end": "end", "text": "text_id"}
        f_list = format_string.split("_")
        if "start" not in f_list or "end" not in f_list or "text" not in f_list:
            raise AttributeError(
                "Start, end and text must be present in the format_string parameter"
            )
        res = ""
        self.text_length = len(self.text.text)
        self.start = self.trim_whitespaces(self.start)
        self.end = self.trim_whitespaces(self.end)
        for f in f_list:
            if f in matching.keys():
                res += str(getattr(self, matching[f]))
            else:
                ent_link = self.entity_link.all()
                if len(ent_link) > 0:
                    cont_lst = str(
                        ContentType.objects.get_for_model(ent_link[0])
                    ).split()
                    if f == "ent":
                        res += "".join(cont_lst)
                    elif f == "entid":
                        for cont in cont_lst:
                            if hasattr(ent_link[0], "related_" + cont.strip()):
                                res += str(
                                    getattr(
                                        ent_link[0], "related_" + cont.strip() + "_id"
                                    )
                                )
                            elif hasattr(ent_link[0], "related_" + cont.strip() + "A"):
                                res += str(
                                    getattr(
                                        ent_link[0], "related_" + cont.strip() + "A_id"
                                    )
                                )
                            elif hasattr(ent_link[0], "related_" + cont.strip() + "B"):
                                res += str(
                                    getattr(
                                        ent_link[0], "related_" + cont.strip() + "B_id"
                                    )
                                )
                            res += "-"
                        res = res[:-1]
                    elif f == "rel":
                        res += str(getattr(ent_link[0], "relation_type_id"))
                else:
                    res += "NONE"
            res += "_"
        return res[:-1]

    def get_related_entity(self):
        rel = self.entity_link.all()
        if not len(rel) == 1:
            return None
        else:
            return rel[0]

    def get_html_markup(self, include_object=False):
        if not len(self.entity_link.all()) == 1:
            return None
        rel_entity = self.entity_link.all()[0]
        if hasattr(rel_entity, "relation_type"):
            entity_kind = str(rel_entity.relation_type.pk)
        elif hasattr(rel_entity, "kind"):
            entity_kind = str(rel_entity.kind.pk)
        else:
            entity_kind = str(0)
        entity_type = type(rel_entity).__name__
        entity_type_app = ContentType.objects.get(model=entity_type.lower()).app_label
        ent_lst_pk = []
        if self.user_added is not None:
            user_added = self.user_added.username[:2]
        else:
            user_added = "na"
        if entity_type_app.lower() == "apis_relations":
            for x in dir(rel_entity):
                c = re.match("related_\w+_id", x)
                if c:
                    ent_lst_pk.append(str(getattr(rel_entity, c.group(0))))
        start_span = """<mark class="highlight hl_text_{}" data-hl-type="simple" data-hl-start="{}"
            data-hl-end="{}" data-hl-text-id="{}" data-hl-ann-id="{}" data-entity-class="{}" data-entity-pk="{}"
            data-related-entity-pk="{}" data-entity-type="{}" data-user-added={}>""".format(
            entity_kind,
            self.start,
            self.end,
            self.text_id,
            self.pk,
            entity_type,
            rel_entity.pk,
            ",".join(ent_lst_pk),
            entity_type_app,
            user_added,
        )
        start_span = start_span.replace("\n", "")
        if include_object:
            request = get_current_request()
            if request is None:
                base = getattr(settings, "APIS_BASE_URI", "http://to_change.com")
                if base.endswith("/"):
                    base = base[:-1]
                url = f'{base}{reverse("apis:apis_api:{}-detail".format(entity_type.lower()), kwargs={"pk": rel_entity.pk},)}'
            else:
                url = request.build_absolute_uri(
                    reverse(
                        "apis:apis_api:{}-detail".format(entity_type.lower()),
                        kwargs={"pk": rel_entity.pk},
                    )
                )
            res_obj = {
                "id": self.id,
                "start": self.start,
                "end": self.end,
                "related_object": {
                    "id": rel_entity.id,
                    "url": url,
                    "type": entity_type,
                    "label": str(rel_entity),
                },
            }
            return start_span, res_obj
        return start_span

    class Meta:
        db_table = "highlighter_annotation"


##############################################
#
#   Configuration Classes
#
##############################################


class VocabularyAPI(models.Model):
    """Class storing the information of the endpoints of the vocabularies."""

    method_lst = (("l", "local"), ("o", "open skos"))
    name = models.CharField(max_length=255, null=True, blank=True)
    api_endpoint = models.CharField(max_length=255, null=True, blank=True)
    method = models.CharField(max_length=2, choices=method_lst, default="l")

    def __str__(self):
        return self.name

    class Meta:
        db_table = "highlighter_vocabularyapi"


class MenuEntry(models.Model):
    """Class that defines the menu structure of the context menu"""

    choices_kind = (
        ("txt", "Text field"),
        ("frm", "Form"),
        ("m", "menu entry"),
        ("fn", "Javascript function"),
    )
    kind = models.CharField(max_length=4, choices=choices_kind, default="txt")
    name = models.CharField(
        max_length=255, null=True, blank=True
    )  # Charfield used when txt is as type of entry specified
    api = models.ForeignKey(
        VocabularyAPI, blank=True, null=True, on_delete=models.SET_NULL
    )  # Foreignkey used to link the API entry in case api is set as kind
    parent = models.ForeignKey(
        "self",
        related_name="parent_menuEntry",
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
    )
    project = models.ForeignKey(
        Project, null=True, blank=True, on_delete=models.CASCADE
    )

    def __str__(self):
        return self.name

    class Meta:
        db_table = "highlighter_menuentry"


#######################################################################
#
# DL-Classes
#
#######################################################################


class ActiveLearningProject(models.Model):
    """Used to store active learning project settings."""

    choices = ""
    name = models.CharField(max_length=255)
    sampling_strategy = models.CharField(max_length=4, choices=choices)
    rebuild = models.IntegerField()
    collection = models
    log_dir = models.CharField(max_length=255)

    class Meta:
        db_table = "highlighter_activelearningproject"
