# -*- coding: utf-8 -*-
#

import difflib
import time
import re
import logging
import traceback
from typing import Tuple
import dateutil.parser
from datetime import datetime
from dateutil.tz import tzutc, tzlocal

# make strptime call prior to doing anything, to try and prevent threading
# errors
time.strptime("1970-01-01 12:00:00", "%Y-%m-%d %H:%M:%S")

logger = logging.getLogger(__name__)


def isMovie(type):
    return type == "movie"


def isEpisode(type):
    return type == "episode"


def isShow(type):
    return type == "show"


def isSeason(type):
    return type == "season"


def isValidMediaType(type):
    return type in ["movie", "show", "season", "episode"]


def chunks(list, n):
    return [list[i : i + n] for i in range(0, len(list), n)]


def getFormattedItemName(type, info):
    s = ""
    try:
        if isShow(type):
            s = info["title"]
        elif isEpisode(type):
            s = "S%02dE%02d - %s" % (info["season"], info["number"], info["title"])
        elif isSeason(type):
            if isinstance(info, list):
                info = info[0]
            if info["season"] > 0:
                s = "%s - Season %d" % (info["title"], info["season"])
            else:
                s = "%s - Specials" % info["title"]
        elif isMovie(type):
            s = "%s (%s)" % (info["title"], info["year"])
    except KeyError:
        s = ""
    return s


def __findInList(list, case_sensitive=True, **kwargs):
    for item in list:
        i = 0
        for key in kwargs:
            # because we can need to find at the root level and inside ids this
            # is is required
            if key in item:
                key_val = item[key]
            else:
                if "ids" in item and key in item["ids"]:
                    key_val = item["ids"][key]
                else:
                    continue
            if not case_sensitive and isinstance(key_val, str):
                if key_val.lower() == kwargs[key].lower():
                    i = i + 1
            else:
                # forcing the compare to be done at the string level
                if str(key_val) == str(kwargs[key]):
                    i = i + 1
        if i == len(kwargs):
            return item
    return None


def findMediaObject(mediaObjectToMatch, listToSearch, matchByTitleAndYear):
    result = None
    if (
        result is None
        and "ids" in mediaObjectToMatch
        and "imdb" in mediaObjectToMatch["ids"]
        and str(mediaObjectToMatch["ids"]["imdb"]).startswith("tt")
    ):
        result = __findInList(listToSearch, imdb=mediaObjectToMatch["ids"]["imdb"])
    # we don't want to give up if we don't find a match based on the first
    # field so we use if instead of elif
    if (
        result is None
        and "ids" in mediaObjectToMatch
        and "tmdb" in mediaObjectToMatch["ids"]
        and mediaObjectToMatch["ids"]["tmdb"]
    ):
        result = __findInList(listToSearch, tmdb=mediaObjectToMatch["ids"]["tmdb"])
    if (
        result is None
        and "ids" in mediaObjectToMatch
        and "tvdb" in mediaObjectToMatch["ids"]
        and mediaObjectToMatch["ids"]["tvdb"]
    ):
        result = __findInList(listToSearch, tvdb=mediaObjectToMatch["ids"]["tvdb"])

    if matchByTitleAndYear:
        # match by title and year it will result in movies with the same title and
        # year to mismatch - but what should we do instead?
        if (
            result is None
            and "title" in mediaObjectToMatch
            and "year" in mediaObjectToMatch
        ):
            result = __findInList(
                listToSearch,
                title=mediaObjectToMatch["title"],
                year=mediaObjectToMatch["year"],
            )
        # match only by title, as some items don't have a year on trakt
        elif result is None and "title" in mediaObjectToMatch:
            result = __findInList(listToSearch, title=mediaObjectToMatch["title"])

    return result


def regex_tvshow(label):
    regexes = [
        # ShowTitle.S01E09; s01e09, s01.e09, s01-e09
        r"(.*?)[._ -]s([0-9]+)[._ -]*e([0-9]+)",
        r"(.*?)[._ -]([0-9]+)x([0-9]+)",  # Showtitle.1x09
        r"(.*?)[._ -]([0-9]+)([0-9][0-9])",  # ShowTitle.109
        # ShowTitle.Season 01 - Episode 02, Season 01 Episode 02
        "(.*?)[._ -]?season[._ -]*([0-9]+)[._ -]*-?[._ -]*episode[._ -]*([0-9]+)",
        # ShowTitle_[s01]_[e01]
        r"(.*?)[._ -]\[s([0-9]+)\][._ -]*\[[e]([0-9]+)",
        r"(.*?)[._ -]s([0-9]+)[._ -]*ep([0-9]+)",
    ]  # ShowTitle - s01ep03, ShowTitle - s1ep03

    for regex in regexes:
        match = re.search(regex, label, re.I)
        if match:
            show_title, season, episode = match.groups()
            if show_title:
                show_title = re.sub(r"[\[\]_\(\).-]", " ", show_title)
                show_title = re.sub(r"\s\s+", " ", show_title)
                show_title = show_title.strip()
            return show_title, int(season), int(episode)

    return "", -1, -1


def regex_year(title):
    prog = re.compile(r"^(.+) \((\d{4})\)$")
    result = prog.match(title)

    if result:
        return result.group(1), result.group(2)
    else:
        return "", ""


def findMovieMatchInList(id, listToMatch, idType):
    return next(
        (
            item.to_dict()
            for key, item in list(listToMatch.items())
            if any(idType in key for key, value in item.keys if str(value) == str(id))
        ),
        {},
    )


def findShowMatchInList(id, listToMatch, idType):
    return next(
        (
            item.to_dict()
            for key, item in list(listToMatch.items())
            if any(idType in key for key, value in item.keys if str(value) == str(id))
        ),
        {},
    )


def findSeasonMatchInList(id, seasonNumber, listToMatch, idType):
    show = findShowMatchInList(id, listToMatch, idType)
    logger.debug("findSeasonMatchInList %s" % show)
    if "seasons" in show:
        for season in show["seasons"]:
            if season["number"] == seasonNumber:
                return season

    return {}


def findEpisodeMatchInList(id, seasonNumber, episodeNumber, list, idType):
    season = findSeasonMatchInList(id, seasonNumber, list, idType)
    if season:
        for episode in season["episodes"]:
            if episode["number"] == episodeNumber:
                return episode

    return {}


def convertDateTimeToUTC(toConvert):
    if toConvert:
        dateFormat = "%Y-%m-%d %H:%M:%S"
        try:
            naive = datetime.strptime(toConvert, dateFormat)
        except TypeError:
            naive = datetime(*(time.strptime(toConvert, dateFormat)[0:6]))

        try:
            local = naive.replace(tzinfo=tzlocal())
            utc = local.astimezone(tzutc())
        except ValueError:
            logger.debug(
                "convertDateTimeToUTC() ValueError: movie/show was collected/watched outside of the unix timespan. Fallback to datetime utcnow"
            )
            utc = datetime.utcnow()
        return str(utc)
    else:
        return toConvert


def convertUtcToDateTime(toConvert):
    if toConvert:
        dateFormat = "%Y-%m-%d %H:%M:%S"
        try:
            naive = dateutil.parser.parse(toConvert)
            utc = naive.replace(tzinfo=tzutc())
            local = utc.astimezone(tzlocal())
        except ValueError:
            logger.debug(
                "convertUtcToDateTime() ValueError: movie/show was collected/watched outside of the unix timespan. Fallback to datetime now"
            )
            local = datetime.now()
        return local.strftime(dateFormat)
    else:
        return toConvert


def createError(ex):
    template = (
        "EXCEPTION Thrown (PythonToCppException) : -->Python callback/script returned the following error<--\n"
        " - NOTE: IGNORING THIS CAN LEAD TO MEMORY LEAKS!\n"
        "Error Type: <type '{0}'>\n"
        "Error Contents: {1!r}\n"
        "{2}"
        "-->End of Python script error report<--"
    )
    return template.format(type(ex).__name__, ex.args, traceback.format_exc())


def guessBestTraktId(id, type) -> Tuple[dict, str]:
    data = {}
    id_type = ""
    if id.startswith("tt"):
        data["imdb"] = id
        id_type = "imdb"
    elif id.isdigit() and isMovie(type):
        data["tmdb"] = id
        id_type = "tmdb"
    elif id.isdigit() and (isEpisode(type) or isSeason(type) or isShow(type)):
        data["tvdb"] = id
        id_type = "tvdb"
    else:
        data["slug"] = id
        id_type = "slug"
    return data, id_type


def best_id(ids, type) -> Tuple[str, str]:
    if "trakt" in ids:
        return ids["trakt"], "trakt"
    elif "imdb" in ids and isMovie(type):
        return ids["imdb"], "imdb"
    elif "tmdb" in ids:
        return ids["tmdb"], "tmdb"
    elif "tvdb" in ids:
        return ids["tvdb"], "tvdb"
    elif "tvrage" in ids:
        return ids["tvrage"], "tvrage"
    elif "slug" in ids:
        return ids["slug"], "slug"


def checkExcludePath(excludePath, excludePathEnabled, fullpath, x):
    if excludePath != "" and excludePathEnabled and fullpath.startswith(excludePath):
        logger.debug(
            "checkExclusion(): Video is from location, which is currently set as excluded path %i."
            % x
        )
        return True
    else:
        return False


def sanitizeMovies(movies):
    # do not remove watched_at and collected_at may cause problems between the
    # 4 sync types (would probably have to deepcopy etc)
    for movie in movies:
        if "collected" in movie:
            del movie["collected"]
        if "watched" in movie:
            del movie["watched"]
        if "movieid" in movie:
            del movie["movieid"]
        if "plays" in movie:
            del movie["plays"]
        if "userrating" in movie:
            del movie["userrating"]


# todo add tests


def sanitizeShows(shows):
    # do not remove watched_at and collected_at may cause problems between the
    # 4 sync types (would probably have to deepcopy etc)
    for show in shows["shows"]:
        for season in show["seasons"]:
            for episode in season["episodes"]:
                if "collected" in episode:
                    del episode["collected"]
                if "watched" in episode:
                    del episode["watched"]
                if "season" in episode:
                    del episode["season"]
                if "plays" in episode:
                    del episode["plays"]
                if "ids" in episode and "episodeid" in episode["ids"]:
                    del episode["ids"]["episodeid"]


def compareMovies(
    movies_col1,
    movies_col2,
    matchByTitleAndYear,
    watched=False,
    restrict=False,
    playback=False,
    rating=False,
):
    movies = []
    for movie_col1 in movies_col1:
        if movie_col1:
            movie_col2 = findMediaObject(movie_col1, movies_col2, matchByTitleAndYear)
            # logger.debug("movie_col1 %s" % movie_col1)
            # logger.debug("movie_col2 %s" % movie_col2)

            if movie_col2:  # match found
                if watched:  # are we looking for watched items
                    if movie_col2["watched"] == 0 and movie_col1["watched"] == 1:
                        if "movieid" not in movie_col1:
                            movie_col1["movieid"] = movie_col2["movieid"]
                        movies.append(movie_col1)
                elif playback:
                    if "movieid" not in movie_col1:
                        movie_col1["movieid"] = movie_col2["movieid"]
                    movie_col1["runtime"] = movie_col2["runtime"]
                    movies.append(movie_col1)
                elif rating:
                    if (
                        "rating" in movie_col1
                        and movie_col1["rating"] != 0
                        and ("rating" not in movie_col2 or movie_col2["rating"] == 0)
                    ):
                        if "movieid" not in movie_col1:
                            movie_col1["movieid"] = movie_col2["movieid"]
                        movies.append(movie_col1)
                else:
                    if "collected" in movie_col2 and not movie_col2["collected"]:
                        movies.append(movie_col1)
            else:  # no match found
                if not restrict:
                    if "collected" in movie_col1 and movie_col1["collected"]:
                        if watched and (movie_col1["watched"] == 1):
                            movies.append(movie_col1)
                        elif rating and movie_col1["rating"] != 0:
                            movies.append(movie_col1)
                        elif not watched and not rating:
                            movies.append(movie_col1)
    return movies


def compareShows(
    shows_col1, shows_col2, matchByTitleAndYear, rating=False, restrict=False
):
    shows = []
    # logger.debug("shows_col1 %s" % shows_col1)
    # logger.debug("shows_col2 %s" % shows_col2)
    for show_col1 in shows_col1["shows"]:
        if show_col1:
            show_col2 = findMediaObject(
                show_col1, shows_col2["shows"], matchByTitleAndYear
            )
            # logger.debug("show_col1 %s" % show_col1)
            # logger.debug("show_col2 %s" % show_col2)

            if show_col2:
                show = {
                    "title": show_col1["title"],
                    "ids": {},
                    "year": show_col1["year"],
                }
                if show_col1["ids"]:
                    show["ids"].update(show_col1["ids"])
                if show_col2["ids"]:
                    show["ids"].update(show_col2["ids"])
                if "tvshowid" in show_col2:
                    show["tvshowid"] = show_col2["tvshowid"]

                if (
                    rating
                    and "rating" in show_col1
                    and show_col1["rating"] != 0
                    and ("rating" not in show_col2 or show_col2["rating"] == 0)
                ):
                    show["rating"] = show_col1["rating"]
                    shows.append(show)
                elif not rating:
                    shows.append(show)
            else:
                if not restrict:
                    show = {
                        "title": show_col1["title"],
                        "ids": {},
                        "year": show_col1["year"],
                    }
                    if show_col1["ids"]:
                        show["ids"].update(show_col1["ids"])

                    if rating and "rating" in show_col1 and show_col1["rating"] != 0:
                        show["rating"] = show_col1["rating"]
                        shows.append(show)
                    elif not rating:
                        shows.append(show)

    result = {"shows": shows}
    return result


# always return shows_col1 if you have enrich it, but don't return shows_col2
def compareEpisodes(
    shows_col1,
    shows_col2,
    matchByTitleAndYear,
    watched=False,
    restrict=False,
    collected=False,
    playback=False,
    rating=False,
):
    shows = []
    # logger.debug("epi shows_col1 %s" % shows_col1)
    # logger.debug("epi shows_col2 %s" % shows_col2)
    for show_col1 in shows_col1["shows"]:
        if show_col1:
            show_col2 = findMediaObject(
                show_col1, shows_col2["shows"], matchByTitleAndYear
            )
            # logger.debug("show_col1 %s" % show_col1)
            # logger.debug("show_col2 %s" % show_col2)

            if show_col2:
                season_diff = {}
                # format the data to be easy to compare Trakt and KODI data
                season_col1 = __getEpisodes(show_col1["seasons"])
                season_col2 = __getEpisodes(show_col2["seasons"])
                for season in season_col1:
                    a = season_col1[season]
                    if season in season_col2:
                        b = season_col2[season]
                        diff = list(set(a).difference(set(b)))
                        if playback:
                            t = list(set(a).intersection(set(b)))
                            if len(t) > 0:
                                eps = {}
                                for ep in t:
                                    eps[ep] = a[ep]
                                    if "episodeid" in season_col2[season][ep]["ids"]:
                                        if "ids" in eps:
                                            eps[ep]["ids"]["episodeid"] = season_col2[
                                                season
                                            ][ep]["ids"]["episodeid"]
                                        else:
                                            eps[ep]["ids"] = {
                                                "episodeid": season_col2[season][ep][
                                                    "ids"
                                                ]["episodeid"]
                                            }
                                    eps[ep]["runtime"] = season_col2[season][ep][
                                        "runtime"
                                    ]
                                season_diff[season] = eps
                        elif rating:
                            t = list(set(a).intersection(set(b)))
                            if len(t) > 0:
                                eps = {}
                                for ep in t:
                                    if (
                                        "rating" in a[ep]
                                        and a[ep]["rating"] != 0
                                        and season_col2[season][ep]["rating"] == 0
                                    ):
                                        eps[ep] = a[ep]
                                        if (
                                            "episodeid"
                                            in season_col2[season][ep]["ids"]
                                        ):
                                            if "ids" in eps:
                                                eps[ep]["ids"]["episodeid"] = (
                                                    season_col2[season][ep]["ids"][
                                                        "episodeid"
                                                    ]
                                                )
                                            else:
                                                eps[ep]["ids"] = {
                                                    "episodeid": season_col2[season][
                                                        ep
                                                    ]["ids"]["episodeid"]
                                                }
                                if len(eps) > 0:
                                    season_diff[season] = eps
                        elif len(diff) > 0:
                            if restrict:
                                # get all the episodes that we have in Kodi, watched or not - update kodi
                                collectedShow = findMediaObject(
                                    show_col1, collected["shows"], matchByTitleAndYear
                                )
                                # logger.debug("collected %s" % collectedShow)
                                collectedSeasons = __getEpisodes(
                                    collectedShow["seasons"]
                                )
                                t = list(
                                    set(collectedSeasons[season]).intersection(
                                        set(diff)
                                    )
                                )
                                if len(t) > 0:
                                    eps = {}
                                    for ep in t:
                                        eps[ep] = a[ep]
                                        if (
                                            "episodeid"
                                            in collectedSeasons[season][ep]["ids"]
                                        ):
                                            if "ids" in eps:
                                                eps[ep]["ids"]["episodeid"] = (
                                                    collectedSeasons[season][ep]["ids"][
                                                        "episodeid"
                                                    ]
                                                )
                                            else:
                                                eps[ep]["ids"] = {
                                                    "episodeid": collectedSeasons[
                                                        season
                                                    ][ep]["ids"]["episodeid"]
                                                }
                                    season_diff[season] = eps
                            else:
                                eps = {}
                                for ep in diff:
                                    eps[ep] = a[ep]
                                if len(eps) > 0:
                                    season_diff[season] = eps
                    else:
                        if not restrict and not rating:
                            if len(a) > 0:
                                season_diff[season] = a
                # logger.debug("season_diff %s" % season_diff)
                if len(season_diff) > 0:
                    # logger.debug("Season_diff")
                    show = {
                        "title": show_col1["title"],
                        "ids": {},
                        "year": show_col1["year"],
                        "seasons": [],
                    }
                    if show_col1["ids"]:
                        show["ids"].update(show_col1["ids"])
                    if show_col2["ids"]:
                        show["ids"].update(show_col2["ids"])
                    for seasonKey in season_diff:
                        episodes = []
                        for episodeKey in season_diff[seasonKey]:
                            episodes.append(season_diff[seasonKey][episodeKey])
                        show["seasons"].append(
                            {"number": seasonKey, "episodes": episodes}
                        )
                    if "tvshowid" in show_col2:
                        show["tvshowid"] = show_col2["tvshowid"]
                    # logger.debug("show %s" % show)
                    shows.append(show)
            else:
                if not restrict:
                    if countEpisodes([show_col1]) > 0:
                        show = {
                            "title": show_col1["title"],
                            "ids": {},
                            "year": show_col1["year"],
                            "seasons": [],
                        }
                        if show_col1["ids"]:
                            show["ids"].update(show_col1["ids"])
                        for seasonKey in show_col1["seasons"]:
                            episodes = []
                            for episodeKey in seasonKey["episodes"]:
                                if watched and (episodeKey["watched"] == 1):
                                    episodes.append(episodeKey)
                                elif rating and episodeKey["rating"] != 0:
                                    episodes.append(episodeKey)
                                elif not watched and not rating:
                                    episodes.append(episodeKey)
                            if len(episodes) > 0:
                                show["seasons"].append(
                                    {
                                        "number": seasonKey["number"],
                                        "episodes": episodes,
                                    }
                                )

                        if "tvshowid" in show_col1:
                            del show_col1["tvshowid"]
                        if countEpisodes([show]) > 0:
                            shows.append(show)
    result = {"shows": shows}
    return result


def countEpisodes(shows, collection=True):
    count = 0
    if "shows" in shows:
        shows = shows["shows"]
    for show in shows:
        for seasonKey in show["seasons"]:
            if seasonKey is not None and "episodes" in seasonKey:
                for episodeKey in seasonKey["episodes"]:
                    if episodeKey is not None:
                        if (
                            "collected" in episodeKey
                            and not episodeKey["collected"] == collection
                        ):
                            continue
                        if "number" in episodeKey and episodeKey["number"]:
                            count += 1
    return count


def __getEpisodes(seasons):
    data = {}
    for season in seasons:
        episodes = {}
        for episode in season["episodes"]:
            episodes[episode["number"]] = episode
        data[season["number"]] = episodes

    return data


def checkIfNewVersion(old, new):
    # Check if old is empty, it might be the first time we check
    if old == "":
        return True
    # Major
    if old[0] < new[0]:
        return True
    # Minor
    if old[1] < new[1]:
        return True
    # Revision
    if old[2] < new[2]:
        return True
    return False


def _to_sec(timedelta_string, factors=(1, 60, 3600, 86400)):
    """[[[days:]hours:]minutes:]seconds -> seconds"""
    return sum(
        x * y
        for x, y in zip(list(map(float, timedelta_string.split(":")[::-1])), factors)
    )


def _fuzzyMatch(string1, string2, match_percent=55.0):
    s = difflib.SequenceMatcher(None, string1, string2)
    s.find_longest_match(0, len(string1), 0, len(string2))
    return (
        difflib.SequenceMatcher(None, string1, string2).ratio() * 100
    ) >= match_percent
