import os
import logging
import shutil
import json
import webbrowser
from urllib2 import urlopen
from StringIO import StringIO
from zipfile import ZipFile

import pkg_resources
from twisted.internet import defer
from twisted.internet.task import LoopingCall

from lbrynet import conf
from lbrynet.lbrynet_daemon.Resources import NoCacheStaticFile
from lbrynet import __version__ as lbrynet_version
from lbryum.version import LBRYUM_VERSION as lbryum_version

log = logging.getLogger(__name__)


class UIManager(object):
    def __init__(self, root):
        self.ui_root = os.path.join(conf.settings.data_dir, "lbry-ui")
        self.active_dir = os.path.join(self.ui_root, "active")
        self.update_dir = os.path.join(self.ui_root, "update")

        if not os.path.isdir(self.ui_root):
            os.mkdir(self.ui_root)
        if not os.path.isdir(self.active_dir):
            os.mkdir(self.active_dir)
        if not os.path.isdir(self.update_dir):
            os.mkdir(self.update_dir)

        self.config = os.path.join(self.ui_root, "active.json")
        self.update_requires = os.path.join(self.update_dir, "requirements.txt")
        self.requirements = {}
        self.check_requirements = True
        self.ui_dir = self.active_dir
        self.git_version = None
        self.root = root
        self.branch = None
        self.update_checker = LoopingCall(self.setup)

        if not os.path.isfile(os.path.join(self.config)):
            self.loaded_git_version = None
            self.loaded_branch = None
            self.loaded_requirements = None
        else:
            try:
                f = open(self.config, "r")
                loaded_ui = json.loads(f.read())
                f.close()
                self.loaded_git_version = loaded_ui['commit']
                self.loaded_branch = loaded_ui['branch']
                self.loaded_requirements = loaded_ui['requirements']
            except:
                self.loaded_git_version = None
                self.loaded_branch = None
                self.loaded_requirements = None

    def setup(self, branch=None, check_requirements=None, user_specified=None):
        local_ui_path = user_specified or conf.settings.local_ui_path

        self.branch = branch or conf.settings.ui_branch
        self.check_requirements = (check_requirements if check_requirements is not None
                                   else conf.settings.check_ui_requirements)

        # Note that this currently overrides any manual setting of UI.
        # It might be worth considering changing that behavior but the expectation
        # is generally that any manual setting of the UI will happen during development
        # and not for folks checking out the QA / RC builds that bundle the UI.
        if self._check_for_bundled_ui():
            return defer.succeed(True)

        if local_ui_path:
            if os.path.isdir(local_ui_path):
                log.info("Checking user specified UI directory: " + str(local_ui_path))
                self.branch = "user-specified"
                self.loaded_git_version = "user-specified"
                d = self.migrate_ui(source=local_ui_path)
                d.addCallback(lambda _: self._load_ui())
                return d
            else:
                log.info("User specified UI directory doesn't exist, using " + self.branch)
        elif self.loaded_branch == "user-specified":
            log.info("Loading user provided UI")
            d = defer.maybeDeferred(self._load_ui)
            return d
        else:
            log.info("Checking for updates for UI branch: " + self.branch)
            self._git_url = "https://s3.amazonaws.com/lbry-ui/{}/data.json".format(self.branch)
            self._dist_url = "https://s3.amazonaws.com/lbry-ui/{}/dist.zip".format(self.branch)

        d = self._up_to_date()
        d.addCallback(lambda r: self._download_ui() if not r else self._load_ui())
        return d

    def _check_for_bundled_ui(self):
        """Try to load a bundled UI and return True if successful, False otherwise"""
        try:
            bundled_path = get_bundled_ui_path()
        except Exception:
            log.warning('Failed to get path for bundled UI', exc_info=True)
            return False
        else:
            bundle_manager = BundledUIManager(self.root, self.active_dir, bundled_path)
            loaded = bundle_manager.setup()
            if loaded:
                self.loaded_git_version = bundle_manager.version()
            return loaded

    def _up_to_date(self):
        def _get_git_info():
            try:
                # TODO: should this be switched to the non-blocking getPage?
                response = urlopen(self._git_url)
                return defer.succeed(read_sha(response))
            except Exception:
                return defer.fail()

        def _set_git(version):
            self.git_version = version.replace('\n', '')
            if self.git_version == self.loaded_git_version:
                log.info("UI is up to date")
                return defer.succeed(True)
            else:
                log.info("UI updates available, checking if installation meets requirements")
                return defer.succeed(False)

        def _use_existing():
            log.info("Failed to check for new ui version, trying to use cached ui")
            return defer.succeed(True)

        d = _get_git_info()
        d.addCallbacks(_set_git, lambda _: _use_existing)
        return d

    def migrate_ui(self, source=None):
        if not source:
            requires_file = self.update_requires
            source_dir = self.update_dir
            delete_source = True
        else:
            requires_file = os.path.join(source, "requirements.txt")
            source_dir = source
            delete_source = False

        def _skip_requirements():
            log.info("Skipping ui requirement check")
            return defer.succeed(True)

        def _check_requirements():
            if not os.path.isfile(requires_file):
                log.info("No requirements.txt file, rejecting request to migrate this UI")
                return defer.succeed(False)
            requirements = Requirements(requires_file)
            passed_requirements = requirements.check(lbrynet_version, lbryum_version)
            return defer.succeed(passed_requirements)

        def _disp_failure():
            log.info("Failed to satisfy requirements for branch '%s', update was not loaded",
                     self.branch)
            return defer.succeed(False)

        def _do_migrate():
            replace_dir(self.active_dir, source_dir)
            if delete_source:
                shutil.rmtree(source_dir)

            log.info("Loaded UI update")

            f = open(self.config, "w")
            loaded_ui = {
                'commit': self.git_version,
                'branch': self.branch,
                'requirements': self.requirements
            }
            f.write(json.dumps(loaded_ui))
            f.close()

            self.loaded_git_version = loaded_ui['commit']
            self.loaded_branch = loaded_ui['branch']
            self.loaded_requirements = loaded_ui['requirements']
            return defer.succeed(True)

        d = _check_requirements() if self.check_requirements else _skip_requirements()
        d.addCallback(lambda r: _do_migrate() if r else _disp_failure())
        return d

    def _download_ui(self):
        def _delete_update_dir():
            if os.path.isdir(self.update_dir):
                shutil.rmtree(self.update_dir)
            return defer.succeed(None)

        def _dl_ui():
            url = urlopen(self._dist_url)
            z = ZipFile(StringIO(url.read()))
            names = [i for i in z.namelist() if '.DS_exStore' not in i and '__MACOSX' not in i]
            z.extractall(self.update_dir, members=names)
            log.info("Downloaded files for UI commit " + str(self.git_version).replace("\n", ""))
            return self.branch

        d = _delete_update_dir()
        d.addCallback(lambda _: _dl_ui())
        d.addCallback(lambda _: self.migrate_ui())
        d.addCallback(lambda _: self._load_ui())
        return d

    def _load_ui(self):
        return load_ui(self.root, self.active_dir)

    def launch(self):
        webbrowser.open(conf.settings.UI_ADDRESS)


class BundledUIManager(object):
    """Copies the UI bundled with lbrynet, if available.

    For the QA and nightly builds, we include a copy of the most
    recent checkout of the development UI. For production builds
    nothing is bundled.

    n.b: For QA and nightly builds the update check is skipped.
    """
    def __init__(self, root, active_dir, bundled_ui_path):
        self.root = root
        self.active_dir = active_dir
        self.bundled_ui_path = bundled_ui_path
        self.data_path = os.path.join(bundled_ui_path, 'data.json')
        self._version = None

    def version(self):
        if not self._version:
            self._version = open_and_read_sha(self.data_path)
        return self._version

    def bundle_is_available(self):
        return os.path.exists(self.data_path)

    def setup(self):
        """Load the bundled UI if possible and necessary

        Returns True if there is a bundled UI, False otherwise
        """
        if not self.bundle_is_available():
            log.debug('No bundled UI is available')
            return False
        if not self.is_active_already_bundled_ui():
            replace_dir(self.active_dir, self.bundled_ui_path)
        log.info('Loading the bundled UI')
        load_ui(self.root, self.active_dir)
        return True

    def is_active_already_bundled_ui(self):
        target_data_path = os.path.join(self.active_dir, 'data.json')
        if os.path.exists(target_data_path):
            target_version = open_and_read_sha(target_data_path)
            if self.version() == target_version:
                return True
        return False


def get_bundled_ui_path():
    return pkg_resources.resource_filename('lbrynet', 'resources/ui')


def are_same_version(data_a, data_b):
    """Compare two data files and return True if they are the same version"""
    with open(data_a) as a:
        with open(data_b) as b:
            return read_sha(a) == read_sha(b)


def open_and_read_sha(filename):
    with open(filename) as f:
        return read_sha(f)


def read_sha(filelike):
    data = json.load(filelike)
    return data['sha']


def replace_dir(active_dir, source_dir):
    if os.path.isdir(active_dir):
        shutil.rmtree(active_dir)
    shutil.copytree(source_dir, active_dir)


def load_ui(root, active_dir):
    for name in os.listdir(active_dir):
        entry = os.path.join(active_dir, name)
        if os.path.isdir(entry):
            root.putChild(os.path.basename(entry), NoCacheStaticFile(entry))


class Requirements(object):
    def __init__(self, requires_file):
        self.requires_file = requires_file

    def check(self, lbrynet_version, lbryum_version):
        requirements = self._read()
        expected = {'lbrynet': lbrynet_version, 'lbryum': lbryum_version}
        return check_requirements(requirements, expected)

    def _read(self):
        requirements = {}
        with open(self.requires_file, "r") as f:
            for requirement in [line for line in f.read().split('\n') if line]:
                t = requirement.split('=')
                if len(t) == 3:
                    requirements[t[0]] = {'version': t[1], 'operator': '=='}
                elif t[0][-1] == ">":
                    requirements[t[0][:-1]] = {'version': t[1], 'operator': '>='}
                elif t[0][-1] == "<":
                    requirements[t[0][:-1]] = {'version': t[1], 'operator': '<='}
        return requirements


def check_requirements(expected, actual):
    passed_requirements = True
    for name in expected:
        if name in actual:
            version = actual[name]
        else:
            continue
        log_msg = "Local version %s of %s does not meet UI requirement for version %s"
        if expected[name]['operator'] == '==':
            if not expected[name]['version'] == version:
                passed_requirements = False
                log.info(log_msg, version, name, expected[name]['version'])
            else:
                log.info("Local version of %s meets ui requirement" % name)
        if expected[name]['operator'] == '>=':
            if not expected[name]['version'] <= version:
                passed_requirements = False
                log.info(log_msg, version, name, expected[name]['version'])
            else:
                log.info("Local version of %s meets ui requirement" % name)
        if expected[name]['operator'] == '<=':
            if not expected[name]['version'] >= version:
                passed_requirements = False
                log.info(log_msg, version, name, expected[name]['version'])
            else:
                log.info("Local version of %s meets ui requirement" % name)
    return passed_requirements