import shlex
from collections.abc import Iterator

from django.contrib.postgres.search import SearchQuery

from .models.assemblies import Assembly
from .models.conference import Conference, ConferenceTrack
from .models.events import Event
from .models.rooms import Room
from .models.tags import ConferenceTag
from .models.users import PlatformUser


def search(
    user: PlatformUser, conference: Conference, search_term: str, max_per_category: int = 10
) -> Iterator[ConferenceTag | ConferenceTrack | Assembly | Event | Room]:
    """
    Search assemblies, events, pages and tags for the search_term(s).
    Matches on the name are ranked higher than those only in the description.
    """

    search_q = SearchQuery(search_term, search_type='websearch')
    try:
        terms = shlex.split(search_term)
    except ValueError:
        return
    include_terms = [term for term in terms if not term.startswith('-')]
    exclude_terms = [term for term in terms if term.startswith('-')]

    def apply_terms_name(qs):
        for term in include_terms:
            qs = qs.filter(name__icontains=term)
        for term in exclude_terms:
            qs = qs.exclude(name__icontains=term)
        return qs

    def apply_terms_description(qs):
        for term in include_terms:
            qs = qs.filter(description__icontains=term)
        for term in exclude_terms:
            qs = qs.exclude(description__icontains=term)
        return qs

    # matching tags are the best
    tags = ConferenceTag.objects.filter(conference=conference, is_public=True)
    if len(include_terms) > 0:
        for term in include_terms:
            tags = tags.filter(slug__icontains=term)
    if len(exclude_terms) > 0:
        for term in exclude_terms:
            tags = tags.exclude(slug__icontains=term)
    yield from tags[:max_per_category]

    # matching tracks are good, too
    tracks = conference.tracks.filter(is_public=True)
    for term in include_terms:
        tracks = tracks.filter(name__icontains=term)
    for term in exclude_terms:
        tracks = tracks.exclude(name__icontains=term)
    yield from tracks[:max_per_category]

    # search static pages
    pages = conference.pages.filter(public_revision__gte=0)
    yield from pages.filter(search_vector=search_q)

    # tracking which assemblies we've already seen so we don't return them twice
    assemblies_seen = []
    events_seen = []
    rooms_seen = []

    # name match on Assembly
    assemblies = apply_terms_name(Assembly.objects.conference_accessible(conference=conference))[:max_per_category]
    for v in assemblies:
        assemblies_seen.append(v.pk)
        yield v

    # name match on Event
    events = apply_terms_name(Event.objects.conference_accessible(conference=conference))[:max_per_category]
    for e in events:
        events_seen.append(e.pk)
        yield e

    # name match on Room
    rooms = apply_terms_name(Room.objects.conference_accessible(conference=conference))[:max_per_category]
    for r in rooms:
        rooms_seen.append(r.pk)
        yield r

    # description match on Assembly
    if len(assemblies) < max_per_category:
        description_matches = Assembly.objects.conference_accessible(conference=conference).exclude(pk__in=assemblies_seen)
        yield from apply_terms_description(description_matches)[: max_per_category - len(assemblies)]

    # description match on Event
    if len(events) < max_per_category:
        description_matches = Event.objects.conference_accessible(conference=conference).exclude(pk__in=events_seen)
        yield from apply_terms_description(description_matches)[: max_per_category - len(events)]

    # description match on Room
    if len(rooms) < max_per_category:
        description_matches = Room.objects.conference_accessible(conference=conference).exclude(pk__in=rooms_seen)
        yield from apply_terms_description(description_matches)[: max_per_category - len(rooms)]