import time
import re
import collections

import gevent

from util import helper
from Plugin import PluginManager
from . import ContentDbPlugin


# We can only import plugin host clases after the plugins are loaded
@PluginManager.afterLoad
def importPluginnedClasses():
    global config
    from Config import config


def processAccessLog():
    global access_log
    if access_log:
        content_db = ContentDbPlugin.content_db
        if not content_db.conn:
            return False

        s = time.time()
        access_log_prev = access_log
        access_log = collections.defaultdict(dict)
        now = int(time.time())
        num = 0
        for site_id in access_log_prev:
            content_db.execute(
                "UPDATE file_optional SET time_accessed = %s WHERE ?" % now,
                {"site_id": site_id, "inner_path": list(access_log_prev[site_id].keys())}
            )
            num += len(access_log_prev[site_id])

        content_db.log.debug("Inserted %s web request stat in %.3fs" % (num, time.time() - s))


def processRequestLog():
    global request_log
    if request_log:
        content_db = ContentDbPlugin.content_db
        if not content_db.conn:
            return False

        s = time.time()
        request_log_prev = request_log
        request_log = collections.defaultdict(lambda: collections.defaultdict(int))  # {site_id: {inner_path1: 1, inner_path2: 1...}}
        num = 0
        for site_id in request_log_prev:
            for inner_path, uploaded in request_log_prev[site_id].items():
                content_db.execute(
                    "UPDATE file_optional SET uploaded = uploaded + %s WHERE ?" % uploaded,
                    {"site_id": site_id, "inner_path": inner_path}
                )
                num += 1
        content_db.log.debug("Inserted %s file request stat in %.3fs" % (num, time.time() - s))


if "access_log" not in locals().keys():  # To keep between module reloads
    access_log = collections.defaultdict(dict)  # {site_id: {inner_path1: 1, inner_path2: 1...}}
    request_log = collections.defaultdict(lambda: collections.defaultdict(int))  # {site_id: {inner_path1: 1, inner_path2: 1...}}
    helper.timer(61, processAccessLog)
    helper.timer(60, processRequestLog)


@PluginManager.registerTo("ContentManager")
class ContentManagerPlugin(object):
    def __init__(self, *args, **kwargs):
        self.cache_is_pinned = {}
        super(ContentManagerPlugin, self).__init__(*args, **kwargs)

    def optionalDownloaded(self, inner_path, hash_id, size=None, own=False):
        if "|" in inner_path:  # Big file piece
            file_inner_path, file_range = inner_path.split("|")
        else:
            file_inner_path = inner_path

        self.contents.db.executeDelayed(
            "UPDATE file_optional SET time_downloaded = :now, is_downloaded = 1, peer = peer + 1 WHERE site_id = :site_id AND inner_path = :inner_path AND is_downloaded = 0",
            {"now": int(time.time()), "site_id": self.contents.db.site_ids[self.site.address], "inner_path": file_inner_path}
        )

        return super(ContentManagerPlugin, self).optionalDownloaded(inner_path, hash_id, size, own)

    def optionalRemoved(self, inner_path, hash_id, size=None):
        res = self.contents.db.execute(
            "UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE site_id = :site_id AND inner_path = :inner_path AND is_downloaded = 1",
            {"site_id": self.contents.db.site_ids[self.site.address], "inner_path": inner_path}
        )

        if res.rowcount > 0:
            back = super(ContentManagerPlugin, self).optionalRemoved(inner_path, hash_id, size)
            # Re-add to hashfield if we have other file with the same hash_id
            if self.isDownloaded(hash_id=hash_id, force_check_db=True):
                self.hashfield.appendHashId(hash_id)
        else:
            back = False
        self.cache_is_pinned = {}
        return back

    def optionalRenamed(self, inner_path_old, inner_path_new):
        back = super(ContentManagerPlugin, self).optionalRenamed(inner_path_old, inner_path_new)
        self.cache_is_pinned = {}
        self.contents.db.execute(
            "UPDATE file_optional SET inner_path = :inner_path_new WHERE site_id = :site_id AND inner_path = :inner_path_old",
            {"site_id": self.contents.db.site_ids[self.site.address], "inner_path_old": inner_path_old, "inner_path_new": inner_path_new}
        )
        return back

    def isDownloaded(self, inner_path=None, hash_id=None, force_check_db=False):
        if hash_id and not force_check_db and hash_id not in self.hashfield:
            return False

        if inner_path:
            res = self.contents.db.execute(
                "SELECT is_downloaded FROM file_optional WHERE site_id = :site_id AND inner_path = :inner_path LIMIT 1",
                {"site_id": self.contents.db.site_ids[self.site.address], "inner_path": inner_path}
            )
        else:
            res = self.contents.db.execute(
                "SELECT is_downloaded FROM file_optional WHERE site_id = :site_id AND hash_id = :hash_id AND is_downloaded = 1 LIMIT 1",
                {"site_id": self.contents.db.site_ids[self.site.address], "hash_id": hash_id}
            )
        row = res.fetchone()
        if row and row["is_downloaded"]:
            return True
        else:
            return False

    def isPinned(self, inner_path):
        if inner_path in self.cache_is_pinned:
            self.site.log.debug("Cached is pinned: %s" % inner_path)
            return self.cache_is_pinned[inner_path]

        res = self.contents.db.execute(
            "SELECT is_pinned FROM file_optional WHERE site_id = :site_id AND inner_path = :inner_path LIMIT 1",
            {"site_id": self.contents.db.site_ids[self.site.address], "inner_path": inner_path}
        )
        row = res.fetchone()

        if row and row[0]:
            is_pinned = True
        else:
            is_pinned = False

        self.cache_is_pinned[inner_path] = is_pinned
        self.site.log.debug("Cache set is pinned: %s %s" % (inner_path, is_pinned))

        return is_pinned

    def setPin(self, inner_path, is_pinned):
        content_db = self.contents.db
        site_id = content_db.site_ids[self.site.address]
        content_db.execute("UPDATE file_optional SET is_pinned = %d WHERE ?" % is_pinned, {"site_id": site_id, "inner_path": inner_path})
        self.cache_is_pinned = {}

    def optionalDelete(self, inner_path):
        if self.isPinned(inner_path):
            self.site.log.debug("Skip deleting pinned optional file: %s" % inner_path)
            return False
        else:
            return super(ContentManagerPlugin, self).optionalDelete(inner_path)


@PluginManager.registerTo("WorkerManager")
class WorkerManagerPlugin(object):
    def doneTask(self, task):
        super(WorkerManagerPlugin, self).doneTask(task)

        if task["optional_hash_id"] and not self.tasks:  # Execute delayed queries immedietly after tasks finished
            ContentDbPlugin.content_db.processDelayed()


@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
    def parsePath(self, path):
        global access_log
        path_parts = super(UiRequestPlugin, self).parsePath(path)
        if path_parts:
            site_id = ContentDbPlugin.content_db.site_ids.get(path_parts["request_address"])
            if site_id:
                if ContentDbPlugin.content_db.isOptionalFile(site_id, path_parts["inner_path"]):
                    access_log[site_id][path_parts["inner_path"]] = 1
        return path_parts


@PluginManager.registerTo("FileRequest")
class FileRequestPlugin(object):
    def actionGetFile(self, params):
        stats = super(FileRequestPlugin, self).actionGetFile(params)
        self.recordFileRequest(params["site"], params["inner_path"], stats)
        return stats

    def actionStreamFile(self, params):
        stats = super(FileRequestPlugin, self).actionStreamFile(params)
        self.recordFileRequest(params["site"], params["inner_path"], stats)
        return stats

    def recordFileRequest(self, site_address, inner_path, stats):
        if not stats:
            # Only track the last request of files
            return False
        site_id = ContentDbPlugin.content_db.site_ids[site_address]
        if site_id and ContentDbPlugin.content_db.isOptionalFile(site_id, inner_path):
            request_log[site_id][inner_path] += stats["bytes_sent"]


@PluginManager.registerTo("Site")
class SitePlugin(object):
    def isDownloadable(self, inner_path):
        is_downloadable = super(SitePlugin, self).isDownloadable(inner_path)
        if is_downloadable:
            return is_downloadable

        for path in self.settings.get("optional_help", {}).keys():
            if inner_path.startswith(path):
                return True

        return False

    def fileForgot(self, inner_path):
        if "|" in inner_path and self.content_manager.isPinned(re.sub(r"\|.*", "", inner_path)):
            self.log.debug("File %s is pinned, no fileForgot" % inner_path)
            return False
        else:
            return super(SitePlugin, self).fileForgot(inner_path)

    def fileDone(self, inner_path):
        if "|" in inner_path and self.bad_files.get(inner_path, 0) > 5:  # Idle optional file done
            inner_path_file = re.sub(r"\|.*", "", inner_path)
            num_changed = 0
            for key, val in self.bad_files.items():
                if key.startswith(inner_path_file) and val > 1:
                    self.bad_files[key] = 1
                    num_changed += 1
            self.log.debug("Idle optional file piece done, changed retry number of %s pieces." % num_changed)
            if num_changed:
                gevent.spawn(self.retryBadFiles)

        return super(SitePlugin, self).fileDone(inner_path)


@PluginManager.registerTo("ConfigPlugin")
class ConfigPlugin(object):
    def createArguments(self):
        group = self.parser.add_argument_group("OptionalManager plugin")
        group.add_argument('--optional_limit', help='Limit total size of optional files', default="10%", metavar="GB or free space %")
        group.add_argument('--optional_limit_exclude_minsize', help='Exclude files larger than this limit from optional size limit calculation', default=20, metavar="MB", type=int)

        return super(ConfigPlugin, self).createArguments()
