#
# This file is part of SickGear.
#
# SickGear is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SickGear is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SickGear.  If not, see <http://www.gnu.org/licenses/>.
import datetime
import fnmatch
import os
import copy
import re

from exceptions_helper import ex

import sickgear
from . import common, db, logger
from .helpers import sanitize_scene_name
from .name_parser.parser import InvalidNameException, InvalidShowException, NameParser
from .scene_exceptions import ReleaseMap
from sg_helpers import scantree

from _23 import quote_plus
from six import iterkeys, itervalues

# noinspection PyUnreachableCode
if False:
    from typing import AnyStr, List, Optional, Set, Union
    from .tv import TVShow
    # noinspection PyUnresolvedReferences
    from re import Pattern


def pass_wordlist_checks(name,  # type: AnyStr
                         parse=True,  # type: bool
                         indexer_lookup=True,  # type: bool
                         show_obj=None  # type: TVShow
                         ):  # type: (...) -> bool
    """
    Filters out non-english and just all-around stupid releases by comparing
    the word list contents at boundaries or the end of name.

    :param name: the release name to check
    :type name: basestring
    :param parse: parse release name
    :type parse: bool
    :param indexer_lookup: use indexer lookup during paring
    :type indexer_lookup: bool
    :param show_obj: TVShow object
    :type show_obj: TVShow

    :return: True if the release name is OK, False if it's bad.
    :rtype: bool
    """

    if parse:
        err_msg = f'Unable to parse the filename {name} into a valid '
        try:
            NameParser(indexer_lookup=indexer_lookup).parse(name)
        except InvalidNameException:
            logger.debug(err_msg + 'episode')
            return False
        except InvalidShowException:
            logger.debug(err_msg + 'show')
            return False

    word_list = {'sub(bed|ed|pack|s)', '(dk|fin|heb|kor|nor|nordic|pl|swe)sub(bed|ed|s)?',
                 '(dir|sample|sub|nfo)fix', 'sample', '(dvd)?extras',
                 'dub(bed)?'}

    # if any of the bad strings are in the name then say no
    if sickgear.IGNORE_WORDS:
        word_list.update(sickgear.IGNORE_WORDS)

    req_word_list = copy.copy(sickgear.REQUIRE_WORDS)

    result = None
    if show_obj:
        if show_obj.rls_ignore_words and isinstance(show_obj.rls_ignore_words, set):
            if sickgear.IGNORE_WORDS_REGEX == show_obj.rls_ignore_words_regex:
                word_list.update(show_obj.rls_ignore_words)
            else:
                result = contains_any(name, show_obj.rls_ignore_words, rx=show_obj.rls_ignore_words_regex)
        if show_obj.rls_global_exclude_ignore and isinstance(show_obj.rls_global_exclude_ignore, set):
            word_list = word_list - show_obj.rls_global_exclude_ignore

    result = result or contains_any(name, word_list, rx=sickgear.IGNORE_WORDS_REGEX)
    if None is not result and result:
        logger.debug(f'Ignored: {name} for containing ignore word')
        return False

    result = None
    if show_obj:
        if show_obj.rls_require_words and isinstance(show_obj.rls_require_words, set):
            result = not contains_any(name, show_obj.rls_require_words, rx=show_obj.rls_require_words_regex)

        if show_obj.rls_global_exclude_require and isinstance(show_obj.rls_global_exclude_require, set):
            req_word_list = req_word_list - show_obj.rls_global_exclude_require

    # if any of the good strings aren't in the name then say no
    result = result or not_contains_any(name, req_word_list, rx=sickgear.REQUIRE_WORDS_REGEX)
    if None is not result and result:
        logger.debug(f'Ignored: {name} for not containing required word match')
        return False

    return True


def not_contains_any(subject,  # type: AnyStr
                     lookup_words,  # type: Union[AnyStr, Set[AnyStr]]
                     rx=None,
                     **kwargs
                     ):  # type: (...) -> bool

    return contains_any(subject, lookup_words, invert=True, rx=rx, **kwargs)


def contains_any(subject,  # type: AnyStr
                 lookup_words,  # type: Union[AnyStr, Set[AnyStr]]
                 invert=False,  # type: bool
                 rx=None,
                 **kwargs
                 ):  # type: (...) -> Optional[bool]
    """
    Check if subject does or does not contain a match from a list or string of regular expression lookup words

    :param subject: word to test existence of
    :type subject: basestring
    :param lookup_words: List or comma separated string of words to search
    :type lookup_words: Union(list, set, basestring)
    :param re_prefix: insert string to all lookup words
    :type re_prefix: basestring
    :param re_suffix: append string to all lookup words
    :type re_suffix: basestring
    :param invert: invert function logic "contains any" into "does not contain any"
    :type invert: bool
    :param rx: lookup_words are regex
    :type rx: Union(NoneType, bool)

    :return: None if no checking was done. True for first match found, or if invert is False,
    :param lookup_words: List or comma separated string of words to search
    :param invert: invert function logic "contains any" into "does not contain any"
    :param kwargs:
    :return: None if no checking was done. True for first match found, or if invert is False,
             then True for first pattern that does not match, or False
    :rtype: Union(NoneType, bool)
    """
    compiled_words = compile_word_list(lookup_words, rx=rx, **kwargs)
    if subject and compiled_words:
        for rc_filter in compiled_words:
            match = rc_filter.search(subject)
            if (match and not invert) or (not match and invert):
                msg = match and not invert and 'Found match' or ''
                msg = not match and invert and 'No match found' or msg
                logger.debug(f'{msg} from pattern: {rc_filter.pattern} in text: {subject} ')
                return True
        return False
    return None


def compile_word_list(lookup_words,  # type: Union[AnyStr, Set[AnyStr]]
                      re_prefix=r'(^|[\W_])',  # type: AnyStr
                      re_suffix=r'($|[\W_])',  # type: AnyStr
                      rx=None
                      ):  # type: (...) -> List[Pattern[AnyStr]]

    result = []
    if lookup_words:
        if None is rx:
            search_raw = isinstance(lookup_words, list)
            if not search_raw:
                # noinspection PyUnresolvedReferences
                search_raw = not lookup_words.startswith('regex:')
                # noinspection PyUnresolvedReferences
                lookup_words = lookup_words[(6, 0)[search_raw]:].split(',')
            lookup_words = [x.strip() for x in lookup_words if x.strip()]
        else:
            search_raw = not rx
        for word in lookup_words:
            try:
                # !0 == regex and subject = s / 'what\'s the "time"' / what\'s\ the\ \"time\"
                subject = search_raw and re.escape(word) or re.sub(r'([\" \'])', r'\\\1', word)
                result.append(re.compile('(?i)%s%s%s' % (re_prefix, subject, re_suffix)))
            except re.error as e:
                logger.debug(f'Failure to compile filter expression: {word} ... Reason: {ex(e)}')

        diff = len(lookup_words) - len(result)
        if diff:
            logger.debug(f'From {len(lookup_words)} expressions, {diff} was discarded during compilation')

    return result


def url_encode(show_names, spacer='.'):
    # type: (List[AnyStr], AnyStr) -> List[AnyStr]
    """

    :param show_names: show name
    :param spacer: spacer
    :return:
    """
    return [quote_plus(n.replace('.', spacer).encode('utf-8', errors='replace')) for n in show_names]


def get_show_names(ep_obj, spacer='.'):
    # type: (sickgear.tv.TVEpisode, AnyStr) -> List[AnyStr]
    """

    :param ep_obj: episode object
    :param spacer: spacer
    :return:
    """
    return get_show_names_all_possible(ep_obj.show_obj, season=ep_obj.season, spacer=spacer, force_anime=True)


def get_show_names_all_possible(show_obj, season=-1, scenify=True, spacer='.', force_anime=False):
    # type: (sickgear.tv.TVShow, int, bool, AnyStr, bool) -> List[AnyStr]
    """

    :param show_obj: show object
    :param season: season
    :param scenify:
    :param spacer: spacer
    :param force_anime:
    :return:
    """
    show_names = list(set(
        all_possible_show_names(show_obj, season=season, force_anime=force_anime)))  # type: List[AnyStr]
    if scenify:
        show_names = list(map(sanitize_scene_name, show_names))
    return url_encode(show_names, spacer)


def make_scene_season_search_string(show_obj,  # type: sickgear.tv.TVShow
                                    ep_obj,  # type: sickgear.tv.TVEpisode
                                    ignore_allowlist=False,  # type: bool
                                    extra_search_type=None
                                    ):  # type: (...) -> List[AnyStr]
    """

    :param show_obj: show object
    :param ep_obj: episode object
    :param ignore_allowlist:
    :param extra_search_type:
    :return: list of search strings
    """
    if show_obj.air_by_date or show_obj.sports:
        numseasons = 0

        # the search string for air by date shows is just
        season_strings = [str(ep_obj.airdate).split('-')[0]]
    elif show_obj.is_anime:
        numseasons = 0
        ep_obj_list = show_obj.get_all_episodes(ep_obj.season)

        # get show qualities
        any_qualities, best_qualities = common.Quality.split_quality(show_obj.quality)

        # compile a list of all the episode numbers we need in this 'season'
        season_strings = []
        for episode in ep_obj_list:

            # get quality of the episode
            cur_composite_status = episode.status
            cur_status, cur_quality = common.Quality.split_composite_status(cur_composite_status)

            if best_qualities:
                highest_best_quality = max(best_qualities)
            else:
                highest_best_quality = 0

            # if we need a better one then add it to the list of episodes to fetch
            if (cur_status in (
                    common.DOWNLOADED,
                    common.SNATCHED) and cur_quality < highest_best_quality) or cur_status == common.WANTED:
                ab_number = episode.scene_absolute_number
                if 0 < ab_number:
                    season_strings.append("%02d" % ab_number)

    else:
        my_db = db.DBConnection()
        sql_result = my_db.select(
            'SELECT COUNT(DISTINCT season) AS numseasons'
            ' FROM tv_episodes'
            ' WHERE indexer = ? AND showid = ?'
            ' AND season != 0',
            [show_obj.tvid, show_obj.prodid])

        numseasons = int(sql_result[0][0])
        season_strings = ["S%02d" % int(ep_obj.scene_season)]

    show_names = get_show_names_all_possible(show_obj, ep_obj.scene_season)

    to_return = []

    # search each show name
    for cur_name in show_names:
        # most providers all work the same way
        if not extra_search_type:
            # if there's only one season then we can just use the show name straight up
            if 1 == numseasons:
                to_return.append(cur_name)
            # for providers that don't allow multiple searches in one request we only search for Sxx style stuff
            else:
                for cur_season in season_strings:
                    if not ignore_allowlist and show_obj.is_anime \
                            and None is not show_obj.release_groups and show_obj.release_groups.allowlist:
                        for keyword in show_obj.release_groups.allowlist:

                            to_return.append(keyword + '.' + cur_name + "." + cur_season)
                    else:
                        to_return.append(cur_name + "." + cur_season)

    return to_return


def make_scene_search_string(show_obj,  # type: sickgear.tv.TVShow
                             ep_obj,  # type: sickgear.tv.TVEpisode
                             ignore_allowlist=False  # type: bool
                             ):  # type: (...) -> List[AnyStr]
    """

    :param show_obj: show object
    :param ep_obj: episode object
    :param ignore_allowlist:
    :return: list or search strings
    """
    my_db = db.DBConnection()
    sql_result = my_db.select(
        'SELECT COUNT(DISTINCT season) AS numseasons'
        ' FROM tv_episodes'
        ' WHERE indexer = ? AND showid = ? AND season != 0',
        [show_obj.tvid, show_obj.prodid])
    num_seasons = int(sql_result[0][0])

    # see if we should use dates instead of episodes
    if (show_obj.air_by_date or show_obj.sports) and ep_obj.airdate != datetime.date.fromordinal(1):
        ep_strings = [str(ep_obj.airdate)]
    elif show_obj.is_anime:
        ep_strings = ['%02i' % int(ep_obj.scene_absolute_number
                                   if 0 < ep_obj.scene_absolute_number else ep_obj.scene_episode)]
    else:
        ep_strings = ['S%02iE%02i' % (int(ep_obj.scene_season), int(ep_obj.scene_episode)),
                      '%ix%02i' % (int(ep_obj.scene_season), int(ep_obj.scene_episode))]

    # for single-season shows just search for the show name -- if total ep count (exclude s0) is less than 11
    # due to the amount of qualities and releases, it is easy to go over the 50 result limit on rss feeds otherwise
    if 1 == num_seasons and not ep_obj.show_obj.is_anime:
        ep_strings = ['']

    show_names = get_show_names_all_possible(show_obj, ep_obj.scene_season)

    to_return = []

    for cur_show_obj in show_names:
        for cur_ep_string in ep_strings:
            if not ignore_allowlist and ep_obj.show_obj.is_anime and \
                    None is not ep_obj.show_obj.release_groups and ep_obj.show_obj.release_groups.allowlist:
                for keyword in ep_obj.show_obj.release_groups.allowlist:
                    to_return.append(keyword + '.' + cur_show_obj + '.' + cur_ep_string)
            else:
                to_return.append(cur_show_obj + '.' + cur_ep_string)

    return to_return


def all_possible_show_names(show_obj, season=-1, force_anime=False):
    # type: (sickgear.tv.TVShow, int, bool) -> List[AnyStr]
    """
    Figures out every possible variation of the name for a particular show. Includes TVDB name, TVRage name,
    country codes on the end, e.g. "Show Name (AU)", and any scene exception names.

    :param show_obj: a TVShow object that we should get the names of
    :param season: season
    :param force_anime:
    :return: a list of all the possible show names
    """

    show_names = ReleaseMap().get_alt_names(show_obj.tvid, show_obj.prodid, season)[:]
    if -1 != season and not show_names:  # fallback to generic exceptions if no season specific exceptions
        season = -1
        show_names = ReleaseMap().get_alt_names(show_obj.tvid, show_obj.prodid)[:]

    if -1 == season:
        show_names.append(show_obj.name)

    if not show_obj.is_anime and not force_anime:
        new_show_names = []
        country_list = common.countryList
        country_list.update(dict(zip(itervalues(common.countryList), iterkeys(common.countryList))))
        for cur_name in set(show_names):
            if not cur_name:
                continue

            # if we have "Show Name Australia" or "Show Name (Australia)" this will add "Show Name (AU)" for
            # any countries defined in common.countryList
            # (and vice versa)
            for cur_country in country_list:
                if cur_name.endswith(' ' + cur_country):
                    new_show_names.append(cur_name.replace(' ' + cur_country,
                                                           ' (' + country_list[cur_country] + ')'))
                elif cur_name.endswith(' (' + cur_country + ')'):
                    new_show_names.append(cur_name.replace(' (' + cur_country + ')',
                                                           ' (' + country_list[cur_country] + ')'))

            # if we have "Show Name (2013)" this will strip the (2013) show year from the show name
            # newShowNames.append(re.sub('\(\d{4}\)','',curName))

        show_names += new_show_names

    return show_names


def determine_release_name(dir_name=None, nzb_name=None):
    # type: (AnyStr, AnyStr) -> Union[AnyStr, None]
    """Determine a release name from a nzb and/or folder name
    :param dir_name: dir name
    :param nzb_name: nzb name
    :return: None or release name
    """

    if None is not nzb_name:
        logger.log('Using nzb name for release name.')
        return nzb_name.rpartition('.')[0]

    if not dir_name or not os.path.isdir(dir_name):
        return None

    # try to get the release name from nzb/nfo
    file_types = ['*.nzb', '*.nfo']

    for search in file_types:
        results = [direntry.name for direntry in scantree(dir_name, include=[fnmatch.translate(search)],
                                                          filter_kind=False, recurse=False)]

        if 1 == len(results):
            found_file = results[0].rpartition('.')[0]
            if pass_wordlist_checks(found_file):
                logger.log(f'Release name ({found_file}) found from file ({results[0]})')
                return found_file.rpartition('.')[0]

    # If that fails, we try the folder
    folder = os.path.basename(dir_name)
    if pass_wordlist_checks(folder):
        # NOTE: Multiple failed downloads will change the folder name.
        # (e.g., appending #s)
        # Should we handle that?
        logger.log(f'Folder name ({folder}) appears to be a valid release name. Using it.')
        return folder

    return None


def abbr_showname(name):
    # type: (AnyStr) -> AnyStr
    result = name
    for cur_from, cur_to in (
            (r'^Star Trek\s*:\s*', r'ST: '), (r'^The Walking Dead\s*:\s*', r'TWD: '),
    ):
        result = re.sub('(?i)%s' % cur_from, cur_to, result)
        if name != result:
            break
    return result