from typing import List
import urllib.parse
import nh3
from django.conf import settings as s
from django.db import models, transaction
from django.db.models import QuerySet
from django.urls import reverse

from epdb.models import (
    EnviPathModel,
    Compound,
    CompoundStructure,
    ParallelRule,
    SequentialRule,
    SimpleAmbitRule,
    SimpleRDKitRule,
)
from utilities.chem import FormatConverter


class Package(EnviPathModel):
    reviewed = models.BooleanField(verbose_name="Reviewstatus", default=False)
    license = models.ForeignKey(
        "epdb.License", on_delete=models.SET_NULL, blank=True, null=True, verbose_name="License"
    )

    class Classification(models.IntegerChoices):
        INTERNAL = 0, "Internal"
        RESTRICTED = 10 , "Restricted"
        SECRET = 20, "Secret"

    classification_level = models.IntegerField(
        choices=Classification,
        default=Classification.RESTRICTED,
    )

    data_pool = models.ForeignKey("epdb.Group", on_delete=models.SET_NULL, blank=True, null=True,
                                  verbose_name="Data pool", default=None)

    def delete(self, *args, **kwargs):
        # explicitly handle related Rules
        for r in self.rules.all():
            r.delete()
        super().delete(*args, **kwargs)

    def __str__(self):
        return f"{self.name} (pk={self.pk})"

    @property
    def compounds(self) -> QuerySet:
        return self.compound_set.all()

    @property
    def rules(self) -> QuerySet:
        return self.rule_set.all()

    @property
    def reactions(self) -> QuerySet:
        return self.reaction_set.all()

    @property
    def pathways(self) -> QuerySet:
        return self.pathway_set.all()

    @property
    def scenarios(self) -> QuerySet:
        return self.scenario_set.all()

    @property
    def models(self) -> QuerySet:
        return self.epmodel_set.all()

    def _url(self):
        return "{}/package/{}".format(s.SERVER_URL, self.uuid)

    def get_applicable_rules(self) -> List["Rule"]:
        """
        Returns a ordered set of rules where the following applies:
        1. All Composite will be added to result
        2. All SimpleRules will be added if theres no CompositeRule present using the SimpleRule
        Ordering is based on "url" field.
        """
        rules = []
        rule_qs = self.rules

        reflected_simple_rules = set()

        for r in rule_qs:
            if isinstance(r, ParallelRule) or isinstance(r, SequentialRule):
                rules.append(r)
                for sr in r.simple_rules.all():
                    reflected_simple_rules.add(sr)

        for r in rule_qs:
            if isinstance(r, SimpleAmbitRule) or isinstance(r, SimpleRDKitRule):
                if r not in reflected_simple_rules:
                    rules.append(r)

        rules = sorted(rules, key=lambda x: x.url)
        return rules

    class Meta:
        db_table = "epdb_package"


class PESCompound(Compound):

    @staticmethod
    @transaction.atomic
    def create(
        package: "Package", pes_data: dict, name: str = None, description: str = None, *args, **kwargs
    ) -> "Compound":

        pes_url = pes_data["pes_url"]

        # Check if we find a direct match for a given pes_link
        if PESStructure.objects.filter(pes_link=pes_url, compound__package=package).exists():
            # Due to normalization we might end up in having multiple structures
            # All of them point to the same compound -> pick any
            return PESStructure.objects.filter(pes_link=pes_url, compound__package=package).first().compound

        # Generate Compound
        c = PESCompound()
        c.package = package

        if name is not None:
            # Clean for potential XSS
            name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()

        if name is None or name == "":
            name = f"Compound {Compound.objects.filter(package=package).count() + 1}"

        c.name = name

        # We have a default here only set the value if it carries some payload
        if description is not None and description.strip() != "":
            c.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()

        c.save()

        molfile = pes_data.get("representativeStructures", [{}])[0].get("ctab")

        if molfile is None:
            raise ValueError("PES data does not contain a valid mol file!")

        smiles = FormatConverter.to_smiles(FormatConverter.from_molfile(molfile))

        standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True)

        is_standardized = standardized_smiles == smiles

        if not is_standardized:
            _ = PESStructure.create(
                c,
                pes_url,
                molfile,
                standardized_smiles,
                name="Normalized structure of {}".format(name),
                description="{} (in its normalized form)".format(description),
                normalized_structure=True,
            )


        cs = PESStructure.create(
            c,
            pes_url,
            molfile,
            smiles,
            name=name,
            description=description,
            normalized_structure=is_standardized
        )

        c.default_structure = cs
        c.save()

        return c


class PESStructure(CompoundStructure):
    pes_link = models.URLField(blank=False, null=False, verbose_name="PES Link")

    @staticmethod
    @transaction.atomic
    def create(
            compound: Compound,
            pes_link: str,
            mol_file: str,
            smiles: str,
            name: str = None,
            description: str = None,
            *args,
            **kwargs
    ):
        if compound.pk is None:
            raise ValueError("Unpersisted Compound! Persist compound first!")

        cs = PESStructure()
        # Clean for potential XSS
        if name is not None:
            cs.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()

        if description is not None:
            cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()

        cs.smiles = smiles
        cs.mol_file = mol_file
        cs.pes_link = pes_link
        cs.compound = compound

        if "normalized_structure" in kwargs:
            cs.normalized_structure = kwargs["normalized_structure"]

        cs.save()

        return cs

    @transaction.atomic
    def add_structure(
        self,
        smiles: str,
        name: str = None,
        description: str = None,
        default_structure: bool = False,
        *args,
        **kwargs,
    ) -> "CompoundStructure":
        raise ValueError("Not supported!")

    def d3_json(self):
        return {
            "is_pes": True,
            "pes_link": self.pes_link,
            # Will overwrite image from Node
            "image": f"{reverse('depict_pes')}?pesLink={urllib.parse.quote(self.pes_link)}",
            "image_type": "png",
        }
