import collections

import persisting_theory
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.urls import reverse_lazy

from funkwhale_api.music import models


class RadioFilterRegistry(persisting_theory.Registry):
    def prepare_data(self, data):
        return data()

    def prepare_name(self, data, name=None):
        return data.code

    @property
    def exposed_filters(self):
        return [f for f in self.values() if f.expose_in_api]


registry = RadioFilterRegistry()


def run(filters, **kwargs):
    candidates = kwargs.pop("candidates", models.Track.objects.all())
    final_query = None
    final_query = registry["group"].get_query(candidates, filters=filters, **kwargs)

    if final_query:
        candidates = candidates.filter(final_query)
    return candidates.order_by("pk").distinct()


def validate(filter_config):
    try:
        f = registry[filter_config["type"]]
    except KeyError:
        raise ValidationError('Invalid type "{}"'.format(filter_config["type"]))
    f.validate(filter_config)
    return True


def test(filter_config, **kwargs):
    """
    Run validation and also gather the candidates for the given config
    """
    data = {"errors": [], "candidates": {"count": None, "sample": None}}
    try:
        validate(filter_config)
    except ValidationError as e:
        data["errors"] = [e.message]
        return data

    candidates = run([filter_config], **kwargs)
    data["candidates"]["count"] = candidates.count()
    data["candidates"]["sample"] = candidates[:10]

    return data


def clean_config(filter_config):
    f = registry[filter_config["type"]]
    return f.clean_config(filter_config)


class RadioFilter(object):
    help_text = None
    label = None
    fields = []
    expose_in_api = True

    def get_query(self, candidates, **kwargs):
        return candidates

    def clean_config(self, filter_config):
        return filter_config

    def validate(self, config):
        operator = config.get("operator", "and")
        try:
            assert operator in ["or", "and"]
        except AssertionError:
            raise ValidationError('Invalid operator "{}"'.format(config["operator"]))


@registry.register
class GroupFilter(RadioFilter):
    code = "group"
    expose_in_api = False

    def get_query(self, candidates, filters, **kwargs):
        if not filters:
            return

        final_query = None
        for filter_config in filters:
            f = registry[filter_config["type"]]
            conf = collections.ChainMap(filter_config, kwargs)
            query = f.get_query(candidates, **conf)
            if filter_config.get("not", False):
                # query = ~query *should* work but it doesn't (see #950)
                # The line below generate a proper subquery
                query = ~Q(pk__in=candidates.filter(query).values_list("pk", flat=True))

            if not final_query:
                final_query = query
            else:
                operator = filter_config.get("operator", "and")
                if operator == "and":
                    final_query &= query
                elif operator == "or":
                    final_query |= query
                else:
                    raise ValueError('Invalid query operator "{}"'.format(operator))
        return final_query

    def validate(self, config):
        super().validate(config)
        for fc in config["filters"]:
            registry[fc["type"]].validate(fc)


@registry.register
class ArtistFilter(RadioFilter):
    code = "artist"
    label = "Artist"
    help_text = "Select tracks for a given artist"
    fields = [
        {
            "name": "ids",
            "type": "list",
            "subtype": "number",
            "autocomplete": reverse_lazy("api:v1:artists-list"),
            "autocomplete_qs": "q={query}",
            "autocomplete_fields": {"name": "name", "value": "id"},
            "label": "Artist",
            "placeholder": "Select artists",
        }
    ]

    def clean_config(self, filter_config):
        filter_config = super().clean_config(filter_config)
        filter_config["ids"] = sorted(filter_config["ids"])
        names = (
            models.Artist.objects.filter(pk__in=filter_config["ids"])
            .order_by("id")
            .values_list("name", flat=True)
        )
        filter_config["names"] = list(names)
        return filter_config

    def get_query(self, candidates, ids, **kwargs):
        return Q(artist__pk__in=ids)

    def validate(self, config):
        super().validate(config)
        try:
            pks = models.Artist.objects.filter(pk__in=config["ids"]).values_list(
                "pk", flat=True
            )
            diff = set(config["ids"]) - set(pks)
            assert len(diff) == 0
        except KeyError:
            raise ValidationError("You must provide an id")
        except AssertionError:
            raise ValidationError('No artist matching ids "{}"'.format(diff))


@registry.register
class TagFilter(RadioFilter):
    code = "tag"
    fields = [
        {
            "name": "names",
            "type": "list",
            "subtype": "string",
            "autocomplete": reverse_lazy("api:v1:tags-list"),
            "autocomplete_fields": {
                "remoteValues": "results",
                "name": "name",
                "value": "name",
            },
            "autocomplete_qs": "q={query}&ordering=length",
            "label": "Tags",
            "placeholder": "Select tags",
        }
    ]
    help_text = "Select tracks with a given tag"
    label = "Tag"

    def get_query(self, candidates, names, **kwargs):
        return (
            Q(tagged_items__tag__name__in=names)
            | Q(artist__tagged_items__tag__name__in=names)
            | Q(album__tagged_items__tag__name__in=names)
        )
