import requests
import random
import time
import re
from pathlib import Path

import wikipediaapi
from bs4 import BeautifulSoup
from django.conf import settings
from django.db import models
from unidecode import unidecode


class Gender(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=250)

    class Meta:
        unique_together = ["name"]
        ordering = ["name"]
        verbose_name = "Género"
        verbose_name_plural = "Generos"

    def __str__(self):
        return self.name


class Country(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=250, verbose_name="País")

    class Meta:
        unique_together = ["name"]
        ordering = ["name"]
        verbose_name = "País"
        verbose_name_plural = "Paises"

    def __str__(self):
        return self.name


class PersonQuerySet(models.QuerySet):
    def directors(self):
        rows = self.filter(is_director=True).values_list("name", flat=True)
        return rows

    def actors(self):
        rows = self.filter(is_actor=True).values_list("name", flat=True)
        return rows


class Person(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField("Nombre", max_length=500)
    country = models.ForeignKey(
        Country, related_name="country", on_delete=models.PROTECT
    )
    date_born = models.DateField("Fecha de nacimiento", null=True, blank=True)
    is_actor = models.BooleanField("Es Actor", default=False)
    is_director = models.BooleanField("Es Director", default=False)
    is_woman = models.BooleanField("Es mujer", default=False)
    photo = models.ImageField(
        "Fotografía", upload_to="%Y/%m/%d/", null=True, blank=True
    )
    objects = PersonQuerySet.as_manager()

    class Meta:
        unique_together = ["name"]
        ordering = ["name"]
        verbose_name = "Persona"
        verbose_name_plural = "Personas"

    def __str__(self):
        return self.name


class MovieQuerySet(models.QuerySet):
    def random_pick(self, random_max=6, min_items=20, all=None, **kwargs):
        """
        Regresa películas de manera aleatoria.

        Por defecto tiene que haber al menos 20 películas en la consulta.
        Por defecto regresa un máximo de 6 películas.
        """
        if all is None:
            all = list(Movie.objects.filter(**kwargs).values())
        if len(all) < min_items:
            return None
        elif len(all) <= random_max:
            return all
        else:
            return random.sample(all, random_max)

    def top_pick(self, key, top_max=6):
        """
        Regresa el top de películas.

        El top corresponde al criterio de búsqueda en 'key'.
        Por defecto regresa las primeras 6 películas como máximo.
        """
        all = list(Movie.objects.order_by(f"-{key}").values())
        return self.fix_all(all[:top_max])

    def top_random_pick(self, key, top_max=6):
        """
        Regresa el top de películas de manera aleatorias.

        El top corresponde al criterio de búsqueda en 'key'.
        Por defecto regresa un máximo de 6 películas.
        A diferencia de 'random_pick', de todo el top selecciona de manera
        aleatoria según el 'top_max'.
        """
        all = list(Movie.objects.order_by(f"-{key}").values())
        top = []
        for movie in all:
            if movie[key] == all[0][key]:
                top.append(movie)
            else:
                break
        if len(top) > top_max:
            movies = random.sample(top, top_max)
        else:
            movies = top
        return self.fix_all(movies)

    def home_pick(self):
        """
        Regresa la selección de películas para el 'home'.

        El 'home' es la página principal.
        """
        genders = list(Gender.objects.values_list("id", flat=True))
        sections = self.home_sections()
        for gender in genders:
            key = Gender.objects.get(pk=gender).name
            picked = self.random_pick(genders=gender)
            if picked:
                sections["genders"][key] = self.fix_all(picked)
        return sections

    def home_sections(self):
        """
        Regresa la selección de películas en secciones para el 'home'.

        Las secciones son novedades, mejor valorados y más descargados.
        """
        sections = {
            "Novedades": self.top_pick("id"),
            "Mejor valorados": self.top_random_pick("stars"),
            "Más descargados": self.top_pick("count"),
        }
        sections["genders"] = {}
        return sections

    def fix_all(self, movies):
        """
        Enmienda los datos de las películas.
        """
        for movie in movies:
            self.fix_data(movie, wikipedia=False)
        return movies

    def fix_data(self, movie, wikipedia=True):
        """
        Enmienda los datos de una película.

        Los enmiendos son formateos de unos valores (que se guardan en nuevas
        llaves) y de rutas a medios.
        """
        if len(movie["file_name"]) == 0 or len(movie["cartel"]) == 0:
            print(f"WARN: película sin ruta de video o cartel:\n{movie}")
        movie["duration_formatted"] = self.format_duration(movie["duration"])
        movie["count_formatted"] = self.format_count(movie["count"])
        movie["stars_icons"] = self.format_stars(movie["stars"])
        movie["file_name"] = self._fix_path_movie(movie["file_name"])
        movie["cartel"] = self._fix_path_cartel(movie["cartel"])
        if wikipedia:
            movie["wiki"] = self.get_wiki(movie)

    def _fix_path_movie(self, file_name):
        """
        Enmienda ruta a medio.
        """
        if not file_name:
            return ""

        letter = file_name[0].upper()
        if letter.isdigit():
            letter = "0"
        url = f"{settings.URL_CDN}/{letter}/{file_name}"
        return url

    def _fix_path_cartel(self, file_name):
        """Si no existe el cartel regresa el default"""
        url = f"{settings.MEDIA_URL}{file_name}"

        path = Path(f"{settings.MEDIA_ROOT}{file_name}")
        if not path.exists():
            url = f"{settings.MEDIA_URL}default.png"

        return url

    def fix_summ(self, raw):
        """
        Enmienda sinopsis de Wikipedia.

        Los sumarios de artículos a la Wikipedia vienen con notas al pie que no
        son necesarias. Para la sinopsis estas notas son eliminadas y se
        regresa el código HTML como una cadena de caracteres.
        """
        html = BeautifulSoup(raw, "lxml")
        for ref in html.find_all("sup", "reference"):
            ref.decompose()
        for crossref in html.find_all("dl"):
            crossref.decompose()
        clean = list(map(lambda x: str(x), html.body.children))
        return " ".join(clean)

    def format_stars(self, num):
        """
        Da formato a la cantidad de estrellas.

        Regresa la cantidad de estrellas en lugar del número de estas.
        """
        stars = "★" * num
        while len(stars) < 5:
            stars += "☆"
        return stars

    def format_count(self, num):
        """
        Da formato a la cantidad de descargas.

        Regresa la cantidad en una cifra separada por comas.
        """
        return "{:,}".format(num)

    def format_duration(self, num):
        """
        Da formato a duración.

        Regresa la duración en 'Nh Nm'; p. ej.: 1h 22m, 2h, 15m.
        """
        secs = num * 60
        hours = self.format_duration_num("%H", secs)
        mins = self.format_duration_num("%M", secs)
        if hours == "":
            return mins
        elif mins == "":
            return hours
        else:
            return f"{hours}&nbsp;{mins}"

    def format_duration_num(self, num_type, secs):
        """
        Da formato a cada número de la duración.

        Extrae hora o minuto de la duración, la pasa a int y si no es cero,
        regresa Ns; p. ej.: 1h, 55m.
        """
        num = int(time.strftime(num_type, time.gmtime(secs)))
        sym = num_type[-1].lower()
        if num == 0:
            return ""
        else:
            return f"{num}{sym}"

    # ~ def get_sub_lang(self, key):
        # ~ """
        # ~ Obtiene los lenguajes de los subtítulos en diversas nomenclaturas.

        # ~ La llave está en ISO-639-2 o ISO-639-3.
        # ~ Cfr. https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
             # ~ https://en.wikipedia.org/wiki/ISO_639-2
        # ~ OJO: hay más variantes de Tarahumara y de Mixteco
        # ~ """
        # ~ langs = {
            # ~ "spa": {"iso639_1": "es", "label": "Español"},
            # ~ "eng": {"iso639_1": "en", "label": "Inglés"},
            # ~ "fra": {"iso639_1": "fr", "label": "Francés"},
            # ~ "por": {"iso639_1": "pt", "label": "Portugués"},
            # ~ "rum": {"iso639_1": "ro", "label": "Rumano"},
            # ~ "cat": {"iso639_1": "ca", "label": "Catalán"},
            # ~ "glg": {"iso639_1": "gl", "label": "Gallego"},
            # ~ "grn": {"iso639_1": "gn", "label": "Guaraní"},
            # ~ "gug": {"iso639_1": "gn", "label": "Guaraní"},
            # ~ "nah": {"iso639_1": "!!", "label": "Náhuatl"},
            # ~ "myn": {"iso639_1": "!!", "label": "Maya"},
            # ~ "zap": {"iso639_1": "!!", "label": "Zapoteco"},
            # ~ "oto": {"iso639_1": "!!", "label": "Otomí"},
            # ~ "tar": {"iso639_1": "!!", "label": "Tarahumara"},
            # ~ "mix": {"iso639_1": "!!", "label": "Mixteco"},
            # ~ "apa": {"iso639_1": "!!", "label": "Apache"},
            # ~ "rus": {"iso639_1": "ru", "label": "Ruso"},
            # ~ "deu": {"iso639_1": "de", "label": "Alemán"},
            # ~ "ger": {"iso639_1": "de", "label": "Alemán"},
            # ~ "chi": {"iso639_1": "zh", "label": "Chino"},
            # ~ "zho": {"iso639_1": "zh", "label": "Chino"},
        # ~ }
        # ~ if key in langs:
            # ~ lang = langs[key]
            # ~ lang["suffix"] = key
            # ~ return lang
        # ~ return {"suffix": key, "iso639_1": "!!", "label": key}

    def get_subs(self, id):
        """
        Obtiene subtítulos de película por id.

        TODO: en la restructuración de la DB no debería hacer requests.
        """
        file_name = Movie.objects.get(pk=id).file_name
        if not file_name:
            return []

        letter = file_name[0].upper()
        if letter.isdigit():
            letter = "0"

        url_base = f"{settings.URL_CDN}/{letter}"
        base_name = Path(file_name).stem

        subs = []
        for k, v in settings.LANG_SUBTITLES.items():
            url_sub = f'{settings.URL_BASE}{url_base}/{base_name}.{k}.vtt'
            response = requests.get(url_sub)
            if response.status_code==200:
                subs.append(v)

        return subs

    def get_wiki(self, movie, again=True):
        """
        Obtiene artículo de Wikipedia.

        Primero intenta obtener el artículo por el nombre original. Si no tiene
        éxito, intenta obtenerlo por el nombre en español. Regresa None si no
        tuvo ningún éxito.
        """
        wiki = self.get_wiki_page(movie["original_name"])
        if not wiki:
            wiki = self.get_wiki_page(movie["name"])
        return wiki

    def get_wiki_page(self, title):
        """
        Intenta obtener artículo de Wikipedia.

        Si no tiene éxito, regresa 'None'.
        """
        try:
            lang = settings.LANGUAGE_CODE.split("-")[0]
            wiki = wikipediaapi.Wikipedia(
                lang, extract_format=wikipediaapi.ExtractFormat.HTML
            )
            page = wiki.page(title)
            if page.exists():
                return {
                    "title": page.title,
                    "url": page.fullurl,
                    "summary": self.fix_summ(page.summary),
                }
            else:
                return None
        except Exception:
            return None

    def get_related(self):
        """
        Regresa los campos relacionados a las películas.
        """
        return ["countries", "genders", "directors", "actors"]

    def get_movie_by_id(self, id):
        """
        Obtiene película por id.

        Esta obtención también añade objetos relacionados.
        """
        related = self.get_related()
        movie = Movie.objects.prefetch_related(*related).get(pk=id).__dict__
        for key, val in movie["_prefetched_objects_cache"].items():
            movie[key] = list(map(lambda x: x["name"], val.values()))
        movie.pop("_state")
        movie.pop("_prefetched_objects_cache")
        self.fix_data(movie)
        return movie

    def get_movies(self, query):
        """
        Obtiene películas buscadas.
        """
        movies = Movie.objects.prefetch_related(*self.get_related())
        for q in self.get_queries(query):
            selector = None
            if re.match(r"^\w:", q) is not None:
                selector = self.get_selector(re.sub(r"^(\w):.*", r"\1", q))
                q = re.sub(r"^\w:(.*)", r"\1", q)
            movies = self.get_movies_by_query(selector, q, movies)
        if len(movies) != len(Movie.objects.all()):
            if hasattr(movies, "values"):
                movies = self.fix_all(list(movies.values()))
            movies = self.random_pick(random_max=100, min_items=0, all=movies)
            return movies
        else:
            return []

    def get_movies_by_query(self, selector, query, movies):
        """
        Obtiene película por query.
        """
        if selector is None:
            return self.get_movies_by_query_any(query, movies)
        elif selector != "section":
            return self.get_movies_by_query_selector(selector, query, movies)
        else:
            return self.get_movies_by_query_section(query, movies)

    def get_movies_by_query_selector(self, selector, query, movies):
        """
        Obtiene película por query que tiene selector 'w:'.
        """
        if selector == "year":
            selector = f"{selector}__iregex"
            try:
                query = int(query)
            except Exception:
                query = 0
        else:
            if selector == "name" or selector == "original_name":
                selector = f"{selector}__unaccent__iregex"
            else:
                selector = f"{selector}__name__unaccent__iregex"
        query = f"[[:<:]]{query}"
        kwargs = {selector: query}
        return movies.filter(**kwargs)

    def get_movies_by_query_section(self, query, movies):
        """
        Obtiene película por query que tiene selector 'w:' para sección.
        """
        if query == "mejor valorados":
            return self.top_random_pick("stars", top_max=100)
        elif query == "novedades":
            return self.top_pick("id", top_max=100)
        elif query == "mas descargados":
            return self.top_pick("count", top_max=100)
        else:
            return list(movies.values())

    def get_movies_by_query_any(self, query, movies):
        """
        Obtiene película por query que no tiene ningún selector.
        """
        for field in ["name"] + self.get_related():
            if field != "name":
                field = f"{field}__name"
            kwargs = {f"{field}__unaccent__iregex": f"[[:<:]]{query}"}
            result = movies.filter(**kwargs)
            if len(result) > 0:
                movies = result
        return movies

    def get_queries(self, query):
        """
        Devuelve un conjunto de queries sanitizado; p. ej.:
            de: d:Bruno--Stagñaro y:1997 pizzá, birra,         faso
            a: ['d:bruno-stagnaro', 'y:1997', 'pizza', 'birra', 'faso']
        """
        queries = re.sub(r"\s+", " ", unidecode(str(query)).lower()).split()
        return list(map(lambda q: self.clean_query(q), queries))

    def clean_query(self, word):
        """
        Limpia query de caracteres 'W' al inicio y al final. También sustituye
        '-' por un espacio.
        """
        word = re.sub(r"^\W+", "", word)
        word = re.sub(r"\W+$", "", word)
        word = re.sub(r"-+", " ", word)
        return word

    def get_selector(self, prefix):
        """
        Obtiene el selector para filtrar la consulta de una búsqueda.

        Esto permite restricciones en la búsqueda con esta sintaxis:
        * t:Un-Título => Buscará las películas que tengan 'Un T…' en sus 'name'
        * d:Nombre => Buscará las películas que tengan 'N…' en sus 'directors'
        El guion (-) se usa como separador de palabras, puede contener acentos
        o eñe, aunque va a ser decodificado.
        """
        prefixes = {
            "t": "name",
            "o": "original_name",
            "y": "year",
            "c": "countries",
            "p": "countries",
            "d": "directors",
            "a": "actors",
            "g": "genders",
            "s": "section",
        }
        if prefix in prefixes:
            return prefixes[prefix]
        else:
            return None

    def api(self, request):
        """
        Obtiene resultados de la API.
        """
        msg = f"Vista {request.get_host()}/help/#api para más información."
        result = {"ERROR": msg}
        try:
            if request.GET.get("q"):
                query = request.GET["q"]
                result = {"movies": Movie.objects.get_movies(query)}
            elif request.GET.get("id"):
                query = request.GET["id"]
                result = {"movie": Movie.objects.get_movie_by_id(query)}
            else:
                result["Exception"] = f"Invalid {request.GET}"
        except Exception:
            result["Exception"] = f"Invalid {request.GET}"
        if request.user.is_authenticated:
            return result
        else:
            return self.clean_api_response(result)

    def clean_api_response(self, result):
        """
        Elimina la URL de la película para usuarios no autenticados.
        """
        url = "file_name"
        if "movies" in result:
            for movie in result["movies"]:
                del movie[url]
        elif "movie" in result:
            del result["movie"][url]
        return result


def upload_cartel(instance, filename):
    first = filename[0].upper()
    if first.isdigit():
        first = "0"
    return f"{first}/{filename}"


class Movie(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField("Nombre", max_length=1000)
    original_name = models.CharField(
        "Nombre original", max_length=1000, default="", blank=True
    )
    file_name = models.CharField(
        "Nombre archivo", max_length=1000, default="", blank=True
    )
    year = models.PositiveSmallIntegerField("Año", default=1900)
    duration = models.PositiveSmallIntegerField("Duración", default=0)
    directors = models.ManyToManyField(
        Person, verbose_name="Director", related_name="directors"
    )
    actors = models.ManyToManyField(Person, related_name="actors", blank=True)
    countries = models.ManyToManyField(
        Country, related_name="countries", verbose_name="País", blank=True
    )
    genders = models.ManyToManyField(
        Gender, related_name="genders", verbose_name="Género", blank=True
    )
    cartel = models.ImageField(
        "Cartel", upload_to=upload_cartel, null=True, blank=True
    )
    count = models.PositiveIntegerField("Descargas", default=0)
    stars = models.PositiveSmallIntegerField("Estrellas", default=0)
    published = models.BooleanField("¿Publicar?", default=True)
    is_digital = models.BooleanField("Es digital", default=False)
    objects = MovieQuerySet.as_manager()

    class Meta:
        unique_together = ["name", "original_name"]
        ordering = ["name"]
        verbose_name = "Película"
        verbose_name_plural = "Películas"

    def __str__(self):
        return self.name
