# coding=utf-8

import logging
import re
import traceback
import types
import os

import time

import datetime

from ignore import get_decision_list
from helpers import is_recent, get_plex_item_display_title, query_plex, PartUnknownException
from lib import Plex, get_intent
from config import config
from subliminal_patch.subtitle import ModifiedSubtitle
from subzero.modification import registry as mod_registry, SubtitleModifications
from socket import timeout

logger = logging.getLogger(__name__)

MI_KIND, MI_TITLE, MI_KEY, MI_DEEPER, MI_ITEM = 0, 1, 2, 3, 4

container_size_re = re.compile(ur'totalSize="(\d+)"')


def get_item(key):
    try:
        item_id = int(key)
    except ValueError:
        return

    try:
        item_container = Plex["library"].metadata(item_id)
    except timeout:
        Log.Debug("PMS API timed out when querying information about item %d", item_id)
        return

    try:
        return list(item_container)[0]
    except:
        pass


def get_item_kind(item):
    return type(item).__name__


PLEX_API_TYPE_MAP = {
    "Show": "series",
    "Season": "season",
    "Episode": "episode",
    "Movie": "movie",
}


def get_item_kind_from_rating_key(key):
    item = get_item(key)
    return PLEX_API_TYPE_MAP.get(get_item_kind(item))


def get_item_kind_from_item(item):
    return PLEX_API_TYPE_MAP.get(get_item_kind(item))


def get_item_title(item):
    kind = get_item_kind_from_item(item)
    if kind not in ("episode", "movie", "season", "series"):
        return

    if kind == "episode":
        return get_plex_item_display_title(item, "show", parent=item.season, section_title=None,
                                                 parent_title=item.show.title)
    elif kind == "season":
        return get_plex_item_display_title(item, "season", parent=item.show, section_title="Season",
                                           parent_title=item.show.title)
    else:
        return get_plex_item_display_title(item, kind, section_title=None)


def get_item_thumb(item):
    kind = get_item_kind(item)
    if kind == "Episode":
        return item.show.thumb
    elif kind == "Section":
        return item.art
    return item.thumb


def get_items_info(items):
    return items[0][MI_KIND], items[0][MI_DEEPER]


def get_kind(items):
    return items[0][MI_KIND]


def get_section_size(key):
    """
    quick query to determine the section size
    :param key:
    :return:
    """
    size = None
    url = "http://127.0.0.1:32400/library/sections/%s/all" % int(key)
    use_args = {
        "X-Plex-Container-Size": "0",
        "X-Plex-Container-Start": "0"
    }
    response = query_plex(url, use_args)
    matches = container_size_re.findall(response.content)
    if matches:
        size = int(matches[0])

    return size


def get_items(key="recently_added", base="library", value=None, flat=False, add_section_title=False):
    """
    try to handle all return types plex throws at us and return a generalized item tuple
    """
    items = []
    apply_value = None
    if value:
        if isinstance(value, types.ListType):
            apply_value = value
        else:
            apply_value = [value]
    result = getattr(Plex[base], key)(*(apply_value or []))

    for item in result:
        cls = getattr(getattr(item, "__class__"), "__name__")
        if hasattr(item, "scanner"):
            kind = "section"
        elif cls == "Directory":
            kind = "directory"
        else:
            kind = item.type

        # only return items for our enabled sections
        section_key = None
        if kind == "section":
            section_key = item.key
        else:
            if hasattr(item, "section_key"):
                section_key = getattr(item, "section_key")

        if section_key and section_key not in config.enabled_sections:
            continue

        if kind == "season":
            # fixme: i think this case is unused now
            if flat:
                # return episodes
                for child in item.children():
                    items.append(("episode", get_plex_item_display_title(child, "show", parent=item, add_section_title=add_section_title), int(item.rating_key),
                                  False, child))
            else:
                # return seasons
                items.append(("season", item.title, int(item.rating_key), True, item))

        elif kind == "directory":
            items.append(("directory", item.title, item.key, True, item))

        elif kind == "section":
            if item.type in ['movie', 'show']:
                item.size = get_section_size(item.key)
                items.append(("section", item.title, int(item.key), True, item))

        elif kind == "episode":
            items.append(
                (kind, get_plex_item_display_title(item, "show", parent=item.season, parent_title=item.show.title, section_title=item.section.title,
                                                   add_section_title=add_section_title), int(item.rating_key), False, item))

        elif kind in ("movie", "artist", "photo"):
            items.append((kind, get_plex_item_display_title(item, kind, section_title=item.section.title, add_section_title=add_section_title),
                          int(item.rating_key), False, item))

        elif kind == "show":
            items.append((
                kind, get_plex_item_display_title(item, kind, section_title=item.section.title, add_section_title=add_section_title), int(item.rating_key), True,
                item))

    return items


def get_recent_items():
    """
    actually get the recent items, not limited like /library/recentlyAdded
    :return:
    """
    args = {
        "sort": "addedAt:desc",
        "X-Plex-Container-Start": "0",
        "X-Plex-Container-Size": "%s" % config.max_recent_items_per_library
    }

    episode_re = re.compile(ur'(?su)ratingKey="(?P<key>\d+)"'
                            ur'.+?grandparentRatingKey="(?P<parent_key>\d+)"'
                            ur'.+?title="(?P<title>.*?)"'
                            ur'.+?grandparentTitle="(?P<parent_title>.*?)"'
                            ur'.+?index="(?P<episode>\d+?)"'
                            ur'.+?parentIndex="(?P<season>\d+?)".+?addedAt="(?P<added>\d+)"'
                            ur'.+?<Part.+? file="(?P<filename>[^"]+?)"')
    movie_re = re.compile(ur'(?su)ratingKey="(?P<key>\d+)".+?title="(?P<title>.*?)'
                          ur'".+?addedAt="(?P<added>\d+)"'
                          ur'.+?<Part.+? file="(?P<filename>[^"]+?)"')
    available_keys = ("key", "title", "parent_key", "parent_title", "season", "episode", "added", "filename")
    recent = []

    ref_list = get_decision_list()

    for section in Plex["library"].sections():
        if section.type not in ("movie", "show") \
                or section.key not in config.enabled_sections \
                or ((section.key not in ref_list.sections and config.include)
                    or (section.key in ref_list.sections and not config.include)):
            Log.Debug(u"Skipping section: %s" % section.title)
            continue

        use_args = args.copy()
        plex_item_type = "Movie"
        if section.type == "show":
            use_args["type"] = "4"
            plex_item_type = "Episode"

        url = "http://127.0.0.1:32400/library/sections/%s/all" % int(section.key)
        response = query_plex(url, use_args)

        matcher = episode_re if section.type == "show" else movie_re
        matches = [m.groupdict() for m in matcher.finditer(response.content)]
        for match in matches:
            data = dict((key, match[key] if key in match else None) for key in available_keys)
            if section.type == "show" and ((data["parent_key"] not in ref_list.series and config.include) or
                                           (data["parent_key"] in ref_list.series and not config.include)):
                Log.Debug(u"Skipping series: %s" % data["parent_title"])
                continue
            if (data["key"] not in ref_list.videos and config.include) or \
                    (data["key"] in ref_list.videos and not config.include):
                Log.Debug(u"Skipping item: %s" % data["title"])
                continue
            if not is_physically_wanted(data["filename"], plex_item_type):
                Log.Debug(u"Skipping item (physically not wanted): %s" % data["title"])
                continue

            if is_recent(int(data["added"])):
                recent.append((int(data["added"]), section.type, section.title, data["key"]))

    return recent


def get_on_deck_items():
    return get_items(key="on_deck", add_section_title=True)


def get_recently_added_items():
    return get_items(key="recently_added", add_section_title=True, flat=False)


def get_all_items(key, base="library", value=None, flat=False):
    return get_items(key, base=base, value=value, flat=flat)


def is_wanted(rating_key, item=None):
    """
    check whether an item, its show/season/section is in the soft or the hard ignore list
    :param rating_key:
    :param item:
    :return:
    """

    ref_list = get_decision_list()
    ret_val = ref_list.store == "include"
    inc_exc_verbose = "exclude" if not ret_val else "include"

    # item in soft include/exclude list
    if ref_list["videos"] and rating_key in ref_list["videos"]:
        Log.Debug("Item %s is in the soft %s list" % (rating_key, inc_exc_verbose))
        return ret_val

    item = item or get_item(rating_key)
    kind = get_item_kind(item)

    # show in soft include/exclude list
    if kind == "Episode" and ref_list["series"] and item.show.rating_key in ref_list["series"]:
        Log.Debug("Item %s's show is in the soft %s list" % (rating_key, inc_exc_verbose))
        return ret_val

    # season in soft include/exclude list
    if kind == "Episode" and ref_list["seasons"] and item.season.rating_key in ref_list["seasons"]:
        Log.Debug("Item %s's season is in the soft %s list" % (rating_key, inc_exc_verbose))
        return ret_val

    # section in soft include/exclude list
    if ref_list["sections"] and item.section.key in ref_list["sections"]:
        Log.Debug("Item %s's section is in the soft %s list" % (rating_key, inc_exc_verbose))
        return ret_val

    # physical/path include/exclude
    if config.include_exclude_sz_files or config.include_exclude_paths:
        for media in item.media:
            for part in media.parts:
                return is_physically_wanted(part.file, kind)

    return not ret_val


def is_physically_wanted(fn, kind):
    if config.include_exclude_sz_files or config.include_exclude_paths:
        # normally check current item folder and the library
        check_paths = [".", "../"]
        if kind == "Episode":
            # series/episode, we've got a season folder here, also
            check_paths.append("../../")

        wanted_results = []
        if config.include_exclude_sz_files:
            for sub_path in check_paths:
                wanted_results.append(config.is_physically_wanted(
                    os.path.normpath(os.path.join(os.path.dirname(fn), sub_path)), fn))

        if config.include_exclude_paths:
            wanted_results.append(config.is_path_wanted(fn))

        if config.include and any(wanted_results):
            return True
        elif not config.include and not all(wanted_results):
            return False

    return not config.include


def refresh_item(rating_key, force=False, timeout=8000, refresh_kind=None, parent_rating_key=None):
    intent = get_intent()

    # timeout actually is the time for which the intent will be valid
    if force:
        Log.Debug("Setting intent for force-refresh of %s to timeout: %s", rating_key, timeout)
        intent.set("force", rating_key, timeout=timeout)

        # force Dict.Save()
        intent.store.save()

    refresh = [rating_key]

    if refresh_kind == "season":
        # season refresh, needs explicit per-episode refresh
        refresh = [item.rating_key for item in list(Plex["library/metadata"].children(int(rating_key)))]

    multiple = len(refresh) > 1
    wait = config.advanced.get("refresh_after_called", 5)
    Thread.Sleep(wait)
    for key in refresh:
        Log.Info("%s item %s", "Refreshing" if not force else "Forced-refreshing", key)
        Plex["library/metadata"].refresh(key)
        if multiple:
            Thread.Sleep(wait)


def get_current_sub(rating_key, part_id, language, plex_item=None):
    from support.storage import get_subtitle_storage

    item = plex_item or get_item(rating_key)
    subtitle_storage = get_subtitle_storage()
    stored_subs = subtitle_storage.load_or_new(item)
    current_sub = stored_subs.get_any(part_id, language)
    return current_sub, stored_subs, subtitle_storage


def save_stored_sub(stored_subtitle, rating_key, part_id, language, item_type, plex_item=None, storage=None,
                    stored_subs=None):
    """
    in order for this to work, if the calling supplies stored_subs and storage, it has to trigger its saving and
    destruction explicitly
    :param stored_subtitle:
    :param rating_key:
    :param part_id:
    :param language:
    :param item_type:
    :param plex_item:
    :param storage:
    :param stored_subs:
    :return:
    """
    from support.plex_media import get_plex_metadata
    from support.scanning import scan_videos
    from support.storage import save_subtitles, get_subtitle_storage

    plex_item = plex_item or get_item(rating_key)

    stored_subs_was_provided = True
    if not stored_subs or not storage:
        storage = get_subtitle_storage()
        stored_subs = storage.load(plex_item.rating_key)
        stored_subs_was_provided = False

    if not all([plex_item, stored_subs]):
        return

    try:
        metadata = get_plex_metadata(rating_key, part_id, item_type, plex_item=plex_item)
    except PartUnknownException:
        return

    scanned_parts = scan_videos([metadata], ignore_all=True, skip_hashing=True)
    video, plex_part = scanned_parts.items()[0]

    subtitle = ModifiedSubtitle(language, mods=stored_subtitle.mods)
    subtitle.content = stored_subtitle.content
    if stored_subtitle.encoding:
        # thanks plex
        setattr(subtitle, "_guessed_encoding", stored_subtitle.encoding)

        if stored_subtitle.encoding != "utf-8":
            subtitle.normalize()
            stored_subtitle.content = subtitle.content
            stored_subtitle.encoding = "utf-8"
            storage.save(stored_subs)

    subtitle.plex_media_fps = plex_part.fps
    subtitle.page_link = stored_subtitle.id
    subtitle.language = language
    subtitle.id = stored_subtitle.id

    try:
        save_subtitles(scanned_parts, {video: [subtitle]}, mode="m", bare_save=True)
        stored_subtitle.mods = subtitle.mods
        Log.Debug("Modified %s subtitle for: %s:%s with: %s", language.name, rating_key, part_id,
                  ", ".join(subtitle.mods) if subtitle.mods else "none")
    except:
        Log.Error("Something went wrong when modifying subtitle: %s", traceback.format_exc())

    if subtitle.storage_path:
        stored_subtitle.last_mod = datetime.datetime.fromtimestamp(os.path.getmtime(subtitle.storage_path))

    if not stored_subs_was_provided:
        storage.save(stored_subs)
        storage.destroy()


def set_mods_for_part(rating_key, part_id, language, item_type, mods, mode="add"):
    plex_item = get_item(rating_key)

    if not plex_item:
        return

    current_sub, stored_subs, storage = get_current_sub(rating_key, part_id, language, plex_item=plex_item)
    if mode == "add":
        for mod in mods:
            identifier, args = SubtitleModifications.parse_identifier(mod)
            mod_class = SubtitleModifications.get_mod_class(identifier)

            if identifier not in mod_registry.mods_available:
                raise NotImplementedError("Mod unknown or not registered")

            # clean exclusive mods
            if mod_class.exclusive and current_sub.mods:
                for current_mod in current_sub.mods[:]:
                    if current_mod.startswith(identifier):
                        current_sub.mods.remove(current_mod)
                        Log.Info("Removing superseded mod %s" % current_mod)

            current_sub.add_mod(mod)
    elif mode == "clear":
        current_sub.add_mod(None)
    elif mode == "remove":
        for mod in mods:
            current_sub.mods.remove(mod)

    elif mode == "remove_last":
        if current_sub.mods:
            current_sub.mods.pop()
    else:
        raise NotImplementedError("Wrong mode given")

    save_stored_sub(current_sub, rating_key, part_id, language, item_type, plex_item=plex_item, storage=storage,
                    stored_subs=stored_subs)

    storage.save(stored_subs)
    storage.destroy()
