"""
API for imdb.com
"""

import functools
import re
import string

from .. import html, http
from ..types import ReleaseType
from . import common
from .base import WebDbApiBase

import logging  # isort:skip
_log = logging.getLogger(__name__)


class ImdbApi(WebDbApiBase):
    """API for imdb.com"""

    name = 'imdb'
    label = 'IMDb'
    _url_base = 'https://imdb.com'

    _soup_cache = {}

    async def _get_soup(self, path, params={}):
        cache_id = (path, tuple(sorted(params.items())))
        if cache_id in self._soup_cache:
            return self._soup_cache[cache_id]
        text = await http.get(
            url=f'{self._url_base}/{path}',
            params=params,
            cache=True,
        )
        self._soup_cache[cache_id] = html.parse(text)
        return self._soup_cache[cache_id]

    _title_types = {
        ReleaseType.movie: 'feature,tv_movie,documentary,short,video,tv_short',
        ReleaseType.season: 'tv_series,tv_miniseries',
        # Searching for single episodes is currently not supported
        ReleaseType.episode: 'tv_series,tv_miniseries',
    }

    async def search(self, query):
        _log.debug('Searching IMDb for %s', query)

        if query.id:
            _log.debug('Getting ID: %r', query.id)
            return [_ImdbSearchResult(
                imdb_api=self,
                cast=functools.partial(self.cast, query.id),
                countries=functools.partial(self.countries, query.id),
                directors=functools.partial(self.directors, query.id),
                genres=functools.partial(self.genres, query.id),
                id=query.id,
                summary=functools.partial(self.summary, query.id),
                title=await self.title_english(query.id),
                title_english=functools.partial(self.title_english, query.id),
                title_original=functools.partial(self.title_original, query.id),
                type=await self.type(query.id),
                url=await self.url(query.id),
                year=await self.year(query.id),
            )]

        elif not query.title:
            return []

        else:
            path = 'search/title/'
            params = {'title': query.title_normalized if not query.id else query.id}
            _log.debug('PARAMS: %r', params)
            if query.type is not ReleaseType.unknown:
                params['title_type'] = self._title_types[query.type]
            if query.year is not None:
                params['release_date'] = f'{query.year}-01-01,{query.year}-12-31'

            soup = await self._get_soup(path, params=params)
            items = soup.find_all('div', class_='lister-item-content')
            results = [_ImdbSearchResult(soup=item, imdb_api=self)
                       for item in items]
            return results

    _person_url_path_regex = re.compile(r'(/name/nm\d+)')

    def _get_persons(self, tag):
        a_tags = tag.find_all('a', href=self._person_url_path_regex)
        persons = []
        for a_tag in a_tags:
            if a_tag.string:
                name = a_tag.string.strip()
                url_path = self._person_url_path_regex.match(a_tag["href"]).group(1)
                url = f'{self._url_base.rstrip("/")}/{url_path.lstrip("/")}'
                persons.append(common.Person(name, url))
        return tuple(persons)

    async def cast(self, id):
        cast = []
        if id:
            soup = await self._get_soup(f'title/{id}')
            cast_tag = soup.find(class_='cast_list')
            if cast_tag:
                tr_tags = cast_tag.find_all('tr')
                for tr_tag in tr_tags:
                    cast.extend(self._get_persons(tr_tag))
        return tuple(cast)

    async def countries(self, id):
        countries = []
        if id:
            soup = await self._get_soup(f'title/{id}')
            details_tag = soup.find(id='titleDetails')
            if details_tag:
                tag = details_tag.find(string='Country:')
                if tag:
                    tag = tag.parent
                    if tag:
                        tag = tag.parent
                        countries = []
                        for country_tag in tag.find_all('a'):
                            countries.append(''.join(country_tag.stripped_strings))
        return tuple(countries)

    async def creators(self, id):
        creators = []
        if id:
            soup = await self._get_soup(f'title/{id}')
            creators = []
            for tag in soup.find_all(class_='credit_summary_item'):
                strings = tuple(tag.stripped_strings)
                if 'Creator:' in strings or 'Creators:' in strings:
                    creators.extend(self._get_persons(tag))
        return tuple(creators)

    async def directors(self, id):
        directors = []
        if id:
            soup = await self._get_soup(f'title/{id}')
            for tag in soup.find_all(class_='credit_summary_item'):
                strings = tuple(tag.stripped_strings)
                if 'Director:' in strings or 'Directors:' in strings:
                    directors.extend(self._get_persons(tag))
        return tuple(directors)

    async def genres(self, id):
        if id:
            soup = await self._get_soup(f'title/{id}')
            subtext_tag = soup.find(class_='subtext')
            if subtext_tag:
                genre_links = subtext_tag.find_all('a', href=re.compile(r'/search/title\?genres='))
                return tuple(
                    link.string.lower().strip()
                    for link in genre_links
                )
        return ()

    async def poster_url(self, id):
        if id:
            soup = await self._get_soup(f'title/{id}')
            poster_tag = soup.find(class_='poster')
            if poster_tag:
                link_tag = poster_tag.find('a')
                if link_tag and link_tag.get('href'):
                    soup = await self._get_soup(link_tag['href'])
                    img_tags = [
                        tag
                        for tag in soup.find_all('img', class_=re.compile(r'^MediaViewerImagestyles__PortraitImage'))
                        if 'peek' not in tag['class']
                    ]
                    if img_tags:
                        url = img_tags[0].get('src')
                        if url:
                            return url
        return ''

    rating_min = 0.0
    rating_max = 10.0

    async def rating(self, id):
        if id:
            soup = await self._get_soup(f'title/{id}')
            rating_tag = soup.find(itemprop='ratingValue')
            if rating_tag:
                try:
                    return float(rating_tag.string)
                except (ValueError, TypeError):
                    pass
        return None

    async def summary(self, id):
        if id:
            soup = await self._get_soup(f'title/{id}')
            storyline_tag = soup.find(id='titleStoryLine')
            try:
                summary_tag = storyline_tag.div.p.span
            except AttributeError:
                pass
            else:
                if summary_tag:
                    return ''.join(summary_tag.strings).strip()
        return ''

    async def title_english(self, id, allow_empty=True):
        if id:
            akas = await self._get_akas(id)
            original_title = akas.get('(original title)', '')
            for key, english_title in akas.items():
                for regex in self._english_akas_keys:
                    if regex.search(key):
                        # _log.debug('Interesting English title: %r -> %r', key, english_title)
                        if not allow_empty:
                            # _log.debug('Forcing first match: %r', english_title)
                            return english_title
                        if not self._titles_are_similar(english_title, original_title):
                            # _log.debug('English title: %r', english_title)
                            # _log.debug('Original title: %r', original_title)
                            return english_title
                        # else:
                        #     _log.debug('Similar to original title %r: %r', original_title, english_title)
        return ''

    async def title_original(self, id):
        if id:
            akas = await self._get_akas(id)
            original_title = akas.get('(original title)', '')
            english_title = await self.title_english(id, allow_empty=False)
            if original_title:
                if not self._titles_are_similar(original_title, english_title):
                    # _log.debug('Original title: %r', original_title)
                    # _log.debug('English title: %r', english_title)
                    return original_title
                # else:
                #     _log.debug('Similar to English title %r: %r', english_title, original_title)
            else:
                # Default to getting title from link to main page
                # _log.debug('No original title in AKAs found')
                soup = await self._get_soup(f'title/{id}/releaseinfo')
                title_tag = soup.find(class_='subpage_title_block__right-column')
                if title_tag:
                    a_tag = title_tag.find('a', href=re.compile(r'/title/tt'))
                    if a_tag:
                        return a_tag.string

            _log.debug('Defaulting to English title: %r', english_title)
            return english_title
        return ''

    def _titles_are_similar(self, a, b):
        """Whether normalized `a` contains normalized `b` or vice versa"""
        an = self._normalize_title(a)
        bn = self._normalize_title(b)
        return an and bn and (an in bn or bn in an)

    _normalize_title_translation = str.maketrans('', '', string.punctuation)

    def _normalize_title(self, title):
        """Return casefolded `title` without punctuation and deduplicated whitespace"""
        return ' '.join(title.translate(self._normalize_title_translation).casefold().split())

    _ignored_akas_keys = (
        re.compile(r'\(TV title\)$'),
        re.compile(r'\(alternative spelling\)$'),
        re.compile(r'\(dubbed version\)$'),
        re.compile(r'\(literal title\)$'),
        re.compile(r'\(original script title\)$'),
        re.compile(r'\(short title\)$'),
        re.compile(r'\(video box title\)$'),
        re.compile(r'\(working title\)$'),
    )
    _english_akas_keys = (
        re.compile(r'^USA.*English'),
        re.compile(r'^USA$'),
        re.compile(r'^World-wide.*English'),
    )

    async def _get_akas(self, id):
        soup = await self._get_soup(f'title/{id}/releaseinfo')
        akas = {}

        def is_item_key_class(class_):
            # Class may also be named "aka-item__name-empty"
            return class_ and class_.startswith('aka-item__name')

        for item in soup.find_all('tr', class_='aka-item'):
            key_tag = item.find('td', class_=is_item_key_class)
            key = ''.join(key_tag.stripped_strings).strip()

            title_tag = item.find('td', class_='aka-item__title')
            title = ''.join(title_tag.stripped_strings).strip()

            if title:
                if not any(regex.search(key) for regex in self._ignored_akas_keys):
                    akas[key] = title
            #     else:
            #         _log.debug('Ignoring AKA: %r -> %r', key, title)
            # else:
            #     _log.debug('Ignoring empty title: %r -> %r', key, title)

        return akas

    async def type(self, id):
        if id:
            soup = await self._get_soup(f'title/{id}')
            subtext_tag = soup.find(class_='subtext')
            if subtext_tag:
                # reversed() because interesting info is on the right side
                subtexts = ''.join(subtext_tag.stripped_strings).lower().split('|')
                genres = [genre.string.lower() for genre in subtext_tag.find_all(
                    'a', href=re.compile(r'/search/title\?genres='))]
                if 'episode' in subtexts[-1]:
                    return ReleaseType.episode
                elif 'tv series' in subtexts[-1]:
                    return ReleaseType.season
                elif 'tv mini-series' in subtexts[-1]:
                    return ReleaseType.season
                elif 'tv short' in subtexts[-1]:
                    return ReleaseType.movie
                elif 'short' in genres:
                    return ReleaseType.movie
                elif 'video' in subtexts[-1].split(' '):
                    return ReleaseType.movie
                elif 'tv movie' in subtexts[-1]:
                    return ReleaseType.movie
                elif re.search(r'^\d+ [a-z]+ \d{4}', subtexts[-1]):
                    return ReleaseType.movie
        return ReleaseType.unknown

    async def url(self, id):
        if id:
            return f'{self._url_base.rstrip("/")}/title/{id}'
        return ''

    async def year(self, id):
        if id:
            soup = await self._get_soup(f'title/{id}')

            # Movies
            year_tag = soup.find(id='titleYear')
            if year_tag:
                return ''.join(year_tag.stripped_strings).strip('()')

            # Series
            subtext_tag = soup.find(class_='subtext')
            if subtext_tag:
                # reversed() because year is on the very right
                for tag in reversed(subtext_tag.find_all()):
                    text = ''.join(tag.stripped_strings)
                    # Looking for one of these:
                    #   - "TV Mini-Series (<YEAR>)"
                    #   - "TV Series (<YEAR>–<year>)" (the "–" is an EN DASH / U+2013)
                    #   - "<day> <month> <YEAR>"
                    match = re.search(r'\b(\d{4})\b', text)
                    if match:
                        return match.group(1)

        return ''


class _ImdbSearchResult(common.SearchResult):
    def __init__(self, *, imdb_api, soup=None, cast=None, countries=None,
                 directors=None, id=None, genres=None, summary=None, title=None,
                 title_english=None, title_original=None, type=None, url=None,
                 year=None):
        soup = soup or html.parse('')
        id = id or self._get_id(soup)
        return super().__init__(
            cast=cast or self._get_cast(soup),
            countries=countries or functools.partial(imdb_api.countries, id),
            directors=directors or self._get_directors(soup),
            genres=genres or self._get_genres(soup),
            id=id or self._get_id(soup),
            summary=summary or self._get_summary(soup),
            title=title or self._get_title(soup),
            title_english=title_english or functools.partial(imdb_api.title_english, id),
            title_original=title_original or functools.partial(imdb_api.title_original, id),
            type=type or self._get_type(soup),
            url=url or self._get_url(soup),
            year=year or self._get_year(soup),
        )

    def _get_cast(self, soup):
        people = soup.find(string=re.compile(r'Stars?.*'))
        if people:
            names = tuple(name.string.strip() for name in people.parent.find_all('a'))
            return names[1:]
        else:
            return ()

    def _get_directors(self, soup):
        people = soup.find(string=re.compile(r'Director?.*'))
        if people:
            director = people.parent.find('a')
            if director:
                return (director.string.strip(),)
        else:
            return ()

    def _get_id(self, soup):
        a_tag = soup.find('a')
        if a_tag:
            href = a_tag.get('href')
            return re.sub(r'^.*/([t0-9]+)/.*$', r'\1', href)
        return ''

    def _get_genres(self, soup):
        try:
            genres = soup.find(class_='genre').string.strip()
        except AttributeError:
            genres = ''
        if genres:
            return tuple(g.strip().casefold() for g in genres.split(','))
        else:
            return ()

    def _get_summary(self, soup):
        summary = ''

        tags = soup.find_all(class_='text-muted')
        if len(tags) >= 3:
            summary = (tags[2].string or '').strip()

        # Look for "See full summary" link. Preceding text is summary.
        if not summary:
            summary_link = soup.find('a', text=re.compile(r'(?i:full\s+summary)'))
            if summary_link:
                summary_tag = summary_link.parent
                if summary_tag:
                    summary = ''.join(summary_tag.strings)
                    summary = re.sub(r'See full summary.*', '', summary).strip()

        return summary

    def _get_title(self, soup):
        a_tag = soup.find('a')
        if a_tag:
            return a_tag.string.strip()
        return ''

    def _get_type(self, soup):
        if soup.find(string=re.compile(r'Directors?:')):
            return ReleaseType.movie
        else:
            return ReleaseType.series

    def _get_url(self, soup):
        id = self._get_id(soup)
        if id:
            return f'{ImdbApi._url_base}/title/{id}'
        return ''

    def _get_year(self, soup):
        try:
            year = soup.find(class_='lister-item-year').string or ''
        except AttributeError:
            return ''
        # Year may be preceded by roman number
        year = re.sub(r'\([IVXLCDM]+\)\s*', '', year)
        # Remove parentheses
        year = year.strip('()')
        # Possible formats:
        # - YYYY
        # - YYYY–YYYY  ("–" is NOT "U+002D / HYPHEN-MINUS" but "U+2013 / EN DASH")
        # - YYYY–
        try:
            return str(int(year[:4]))
        except (ValueError, TypeError):
            return ''
