#
# 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 re
import traceback

from . import classes, db, logger
from .helpers import try_int
from .indexers.indexer_config import TVINFO_IMDB, TVINFO_TMDB, TVINFO_TRAKT, TVINFO_TVDB, TVINFO_TVMAZE

import sickgear

from lib.dateutil.parser import parse

from _23 import unidecode
from six import iteritems, moves, string_types, PY2

# noinspection PyUnreachableCode
if False:
    # noinspection PyUnresolvedReferences
    from typing import Any, AnyStr, Dict, List, Optional, Tuple, Union
    from six import integer_types
    from sickgear.tv import TVShow

tv_maze_retry_wait = 10
defunct_indexer = []
indexer_list = []


class NewIdDict(dict):
    def __init__(self, *args, **kwargs):
        tv_src = kwargs.pop('tv_src')
        super(NewIdDict, self).__init__(*args, **kwargs)
        self.verified = {s: (False, True)[s == tv_src] for s in indexer_list}

    def set_value(self, value, old_value=None, tv_src=None, key=None):
        # type: (Any, Any, int, int) -> Any
        if (None is tv_src or tv_src != key) and old_value is MapStatus.MISMATCH or (
                0 < value and old_value not in [None, value] and 0 < old_value):
            return MapStatus.MISMATCH
        if value and tv_src and tv_src == key:
            self.verified[tv_src] = True
        return value

    @staticmethod
    def get_value(value):
        if value in [None, 0]:
            return MapStatus.NOT_FOUND
        return value

    def __getitem__(self, key):
        return self.get_value(super(NewIdDict, self).get(key))

    def get(self, key, default=None):
        return self.get_value(super(NewIdDict, self).get(key, default))

    def __setitem__(self, key, value):
        super(NewIdDict, self).__setitem__(key, self.set_value(value, self.get(key)))

    def update(self, other=None, tv_src=None, **kwargs):
        # type: (Dict[int, Any], int, Any) -> None
        """
        updates dict with new ids
        set MapStatus.MISMATCH if values mismatch, except if it's tv_src (this will be treated as verified source id)

        :param other: new data dict
        :param tv_src: verified tv src id
        :param kwargs:
        """
        if isinstance(other, dict):
            other = {o: self.set_value(v, self.get(o), tv_src, o) for o, v in iteritems(other)}
        super(NewIdDict, self).update(other, **kwargs)


def get_missing_ids(show_ids, show_obj, tv_src):
    # type: (Dict[int, integer_types], TVShow, int) -> Dict[int, integer_types]
    """

    :param show_ids:
    :param show_obj:
    :param tv_src:
    :return:
    """
    try:
        tvinfo_config = sickgear.TVInfoAPI(tv_src).api_params.copy()
        tvinfo_config['cache_search'] = True
        tvinfo_config['custom_ui'] = classes.AllShowInfosNoFilterListUI
        t = sickgear.TVInfoAPI(tv_src).setup(**tvinfo_config)
        show_name, f_date = None, None
        if any(1 for k, v in iteritems(show_ids) if v and k in t.supported_id_searches):
            try:
                found_shows = t.search_show(ids=show_ids)
                res_count = len(found_shows or [])
                if 1 < res_count:
                    show_name, f_date = get_show_name_date(show_obj)
                for show in found_shows or []:
                    if 1 == res_count or confirm_show(f_date, show['firstaired'], show_name,
                                                      clean_show_name(show['seriesname'])):
                        return combine_new_ids(show_ids, show['ids'], tv_src)
            except (BaseException, Exception):
                pass
        found_shows = t.search_show(name=clean_show_name(show_obj.name))
        if not show_name:
            show_name, f_date = get_show_name_date(show_obj)
        for show in found_shows or []:
            if confirm_show(f_date, show['firstaired'], show_name, clean_show_name(show['seriesname'])):
                if any(v for k, v in iteritems(show['ids']) if tv_src != k and v):
                    f_show = [show]
                else:
                    f_show = t.search_show(ids={tv_src: show['id']})
                if f_show and 1 == len(f_show):
                    return combine_new_ids(show_ids, f_show[0]['ids'], tv_src)
    except (BaseException, Exception):
        pass
    return {}


def confirm_show(premiere_date, shows_premiere, expected_name, show_name):
    # type: (Optional[datetime.date], Optional[Union[AnyStr, datetime.date]], AnyStr, AnyStr) -> bool
    """
    confirm show possible confirmations:
    1. premiere dates are less then 2 days apart
    2. show name is the same and premiere year is 1 year or less apart

    :param premiere_date: expected show premiere date
    :param shows_premiere: compare date
    :param expected_name:
    :param show_name:
    """
    if any(t is None for t in (premiere_date, shows_premiere)):
        return False
    if isinstance(shows_premiere, string_types):
        try:
            shows_premiere = parse(shows_premiere).date()
        except (BaseException, Exception):
            return False
    start_year = (shows_premiere and shows_premiere.year) or 0
    return abs(premiere_date - shows_premiere) < datetime.timedelta(days=2) or (
            expected_name == show_name and abs(premiere_date.year - start_year) <= 1)


def get_premieredate(show_obj):
    """

    :param show_obj: show object
    :type show_obj: sickgear.tv.TVShow
    :return:
    :rtype: datetime.date or None
    """
    try:
        ep_obj = show_obj.first_aired_regular_episode
        if ep_obj and ep_obj.airdate:
            return ep_obj.airdate
    except (BaseException, Exception):
        pass
    return None


def clean_show_name(showname):
    """

    :param showname: show name
    :type showname: AnyStr
    :return:
    :rtype: AnyStr
    """
    if not PY2:
        return re.sub(r'[(\s]*(?:19|20)\d\d[)\s]*$', '', showname)
    return re.sub(r'[(\s]*(?:19|20)\d\d[)\s]*$', '', unidecode(showname))


def get_show_name_date(show_obj):
    # type: (TVShow) -> Tuple[Optional[AnyStr], Optional[datetime.date]]
    return clean_show_name(show_obj.name), get_premieredate(show_obj)


def combine_mapped_new_dict(mapped, new_ids):
    # type: (Dict[int, Dict], Dict[int, integer_types]) -> Dict[int, integer_types]
    return {n: m for d in ({k: v['id'] for k, v in iteritems(mapped) if v['id']}, new_ids) for n, m in iteritems(d)}


def combine_new_ids(cur_ids, new_ids, src_id):
    # type: (Dict[int, integer_types], Dict[int, integer_types], int) -> Dict[int, integer_types]
    """
    combine cur_ids with new_ids, priority has cur_ids with exception of src_id key

    :param cur_ids:
    :param new_ids:
    :param src_id:
    """
    return {k: v for d in (cur_ids, new_ids) for k, v in iteritems(d)
            if v and (k == src_id or not cur_ids.get(k) or v == cur_ids.get(k, ''))}


def map_indexers_to_show(show_obj, update=False, force=False, recheck=False, im_sql_result=None):
    # type: (sickgear.tv.TVShow, Optional[bool], Optional[bool], Optional[bool], Optional[list]) -> dict
    """

    :param show_obj: TVShow Object
    :param update: add missing + previously not found ids
    :param force: search for and replace all mapped/missing ids (excluding NO_AUTOMATIC_CHANGE flagged)
    :param recheck: load all ids, don't remove existing
    :param im_sql_result:
    :return: mapped ids
    """
    mapped = {}

    # init mapped tvids object
    for tvid in indexer_list:
        mapped[tvid] = {'id': (0, show_obj.prodid)[int(tvid) == int(show_obj.tvid)],
                        'status': (MapStatus.NONE, MapStatus.SOURCE)[int(tvid) == int(show_obj.tvid)],
                        'date': datetime.date.fromordinal(1)}

    sql_result = []
    for cur_row in im_sql_result or []:
        if show_obj.prodid == cur_row['indexer_id'] and show_obj.tvid == cur_row['indexer']:
            sql_result.append(cur_row)

    if not sql_result:
        my_db = db.DBConnection()
        sql_result = my_db.select(
            'SELECT * FROM indexer_mapping WHERE indexer = ? AND indexer_id = ?', [show_obj.tvid, show_obj.prodid])

    # for each mapped entry
    for cur_row in sql_result or []:
        date = try_int(cur_row['date'])
        mapped[int(cur_row['mindexer'])] = {'status': int(cur_row['status']),
                                            'id': int(cur_row['mindexer_id']),
                                            'date': datetime.date.fromordinal(date if 0 < date else 1)}

    # get list of needed ids
    mis_map = [k for k, v in iteritems(mapped) if (v['status'] not in [
        MapStatus.NO_AUTOMATIC_CHANGE, MapStatus.SOURCE])
               and ((0 == v['id'] and MapStatus.NONE == v['status'])
                    or force or recheck or (update and 0 == v['id'] and k not in defunct_indexer))]
    if mis_map:
        src_tv_id = show_obj._tvid
        new_ids = NewIdDict(tv_src=src_tv_id)  # type: NewIdDict
        if show_obj.imdbid and re.search(r'\d+$', show_obj.imdbid):
            new_ids[TVINFO_IMDB] = try_int(re.search(r'(?:tt)?(\d+)', show_obj.imdbid).group(1))
        all_ids_srcs = [src_tv_id] + [s for s in (TVINFO_TRAKT, TVINFO_TMDB, TVINFO_TVMAZE, TVINFO_TVDB, TVINFO_IMDB)
                                      if s != src_tv_id]
        searched, confirmed = {}, False
        for r in moves.range(len(all_ids_srcs)):
            search_done = False
            for i in all_ids_srcs:
                if new_ids.verified.get(i):
                    continue
                search_ids = {k: v for k, v in iteritems(combine_mapped_new_dict(mapped, new_ids))
                              if k not in searched.setdefault(i, {})}
                if search_ids:
                    search_done = True
                    searched[i].update(search_ids)
                    new_ids.update(get_missing_ids(search_ids, show_obj, tv_src=i), tv_src=i)
                    if new_ids.get(i) and 0 < new_ids.get(i):
                        searched[i].update({i: new_ids[i]})
                confirmed = all(v for k, v in iteritems(new_ids.verified) if k not in defunct_indexer)
                if confirmed:
                    break
            if confirmed or not search_done:
                break

        for i in indexer_list:
            if i != show_obj.tvid and ((i in mis_map and 0 != new_ids.get(i, 0)) or
                                       (new_ids.verified.get(i) and 0 < new_ids.get(i, 0))):
                if i not in new_ids:
                    mapped[i] = {'status': MapStatus.NOT_FOUND, 'id': 0}
                    continue
                if new_ids.verified.get(i) and 0 < new_ids[i] and mapped.get(i, {'id': 0})['id'] != new_ids[i]:
                    if i not in mis_map:
                        mis_map.append(i)
                    mapped[i] = {'status': MapStatus.NONE, 'id': new_ids[i]}
                    continue
                if 0 > new_ids[i]:
                    mapped[i] = {'status': new_ids[i], 'id': 0}
                elif force or not recheck or 0 >= mapped.get(i, {'id': 0}).get('id', 0):
                    mapped[i] = {'status': MapStatus.NONE, 'id': new_ids[i]}

        if [k for k in mis_map if 0 != mapped.get(k, {'id': 0, 'status': 0})['id'] or
                mapped.get(k, {'id': 0, 'status': 0})['status'] not in [MapStatus.NONE, MapStatus.SOURCE]]:
            sql_l = []
            today = datetime.date.today()
            date = today.toordinal()
            for tvid in indexer_list:

                if show_obj.tvid == tvid or tvid not in mis_map:
                    continue

                if 0 != mapped[tvid]['id'] or MapStatus.NONE != mapped[tvid]['status']:
                    mapped[tvid]['date'] = today
                    sql_l.append([
                        'REPLACE INTO indexer_mapping (indexer_id, indexer, mindexer_id, mindexer, date, status)'
                        ' VALUES (?,?,?,?,?,?)',
                        [show_obj.prodid, show_obj.tvid, mapped[tvid]['id'], tvid, date, mapped[tvid]['status']]])
                else:
                    sql_l.append([
                        'DELETE FROM indexer_mapping'
                        ' WHERE indexer = ? AND indexer_id = ? AND mindexer = ?',
                        [show_obj.tvid, show_obj.prodid, tvid]])

            if 0 < len(sql_l):
                logger.debug('Adding TV info mapping to DB for show: %s' % show_obj.unique_name)
                my_db = db.DBConnection()
                my_db.mass_action(sql_l)

    show_obj.ids = mapped
    return mapped


def save_mapping(show_obj, save_map=None):
    # type: (sickgear.tv.TVShow, Optional[List[int]]) -> None
    """

    :param show_obj: show object
    :param save_map: list of tvid ints
    """
    sql_l = []
    today = datetime.date.today()
    date = today.toordinal()
    for tvid in indexer_list:

        if show_obj.tvid == tvid or (isinstance(save_map, list) and tvid not in save_map):
            continue

        if 0 != show_obj.ids[tvid]['id'] or MapStatus.NONE != show_obj.ids[tvid]['status']:
            show_obj.ids[tvid]['date'] = today
            sql_l.append([
                'REPLACE INTO indexer_mapping'
                ' (indexer_id, indexer, mindexer_id, mindexer, date, status) VALUES (?,?,?,?,?,?)',
                [show_obj.prodid, show_obj.tvid, show_obj.ids[tvid]['id'],
                 tvid, date, show_obj.ids[tvid]['status']]])
        else:
            sql_l.append([
                'DELETE FROM indexer_mapping WHERE indexer = ? AND indexer_id = ? AND mindexer = ?',
                [show_obj.tvid, show_obj.prodid, tvid]])

    if 0 < len(sql_l):
        logger.debug('Saving TV info mapping to DB for show: %s' % show_obj.unique_name)
        my_db = db.DBConnection()
        my_db.mass_action(sql_l)


def del_mapping(tvid, prodid):
    """

    :param tvid: tvid
    :type tvid: int
    :param prodid: prodid
    :type prodid: int or long
    """
    my_db = db.DBConnection()
    my_db.action('DELETE FROM indexer_mapping WHERE indexer = ? AND indexer_id = ?', [tvid, prodid])


def should_recheck_update_ids(show_obj):
    """

    :param show_obj: show object
    :type show_obj: sickgear.tv.TVShow
    :return:
    :rtype: bool
    """
    try:
        today = datetime.date.today()
        ids_updated = min([v.get('date') for k, v in iteritems(show_obj.ids) if k != show_obj.tvid and
                           k not in defunct_indexer] or [datetime.date.fromtimestamp(1)])
        if today - ids_updated >= datetime.timedelta(days=365):
            return True
        ep_obj = show_obj.first_aired_regular_episode
        if ep_obj and ep_obj.airdate and ep_obj.airdate > datetime.date.fromtimestamp(1):
            show_age = (today - ep_obj.airdate).days
            # noinspection PyTypeChecker
            for d in [365, 270, 180, 135, 90, 60, 30, 16, 9] + range(4, -4, -1):
                if d <= show_age:
                    return ids_updated < (ep_obj.airdate + datetime.timedelta(days=d))
    except (BaseException, Exception):
        pass
    return False


def load_mapped_ids(**kwargs):
    logger.log('Start loading TV info mappings...')
    if 'load_all' in kwargs:
        del kwargs['load_all']
        my_db = db.DBConnection()
        sql_result = my_db.select('SELECT * FROM indexer_mapping ORDER BY indexer, indexer_id')
    else:
        sql_result = None
    for cur_show_obj in sickgear.showList:
        with cur_show_obj.lock:
            n_kargs = kwargs.copy()
            if 'update' in kwargs and should_recheck_update_ids(cur_show_obj):
                n_kargs['recheck'] = True
            if sql_result:
                n_kargs['im_sql_result'] = sql_result
            try:
                cur_show_obj.ids = sickgear.indexermapper.map_indexers_to_show(cur_show_obj, **n_kargs)
            except (BaseException, Exception):
                logger.debug('Error loading mapped id\'s for show: %s' % cur_show_obj.unique_name)
                logger.log('Traceback: %s' % traceback.format_exc(), logger.ERROR)
    logger.log('TV info mappings loaded')


class MapStatus(object):
    def __init__(self):
        pass

    SOURCE = 1
    NONE = 0
    NOT_FOUND = -1
    MISMATCH = -2
    NO_AUTOMATIC_CHANGE = -100

    allstatus = [SOURCE, NONE, NOT_FOUND, MISMATCH, NO_AUTOMATIC_CHANGE]