from __future__ import with_statement

import logging
import sys
import threading
import time
import traceback

from functools import wraps

import abl.webconnector.infoprocessing as infoprocessing  # this import allows mocking

from abl.util import Bunch
from abl.vpath.base import URI

from .connector import DirectoryConnector
from .reporting import EventTypes as et
from .reporting import clean_directory, upload_event_logs
from .xmlrpcconnector import RPCConnector


logger = logging.getLogger(__name__)


POLL_TIMEOUT = 5.0


def wait_on_progress_or_live_termination(
    sentinel, live_api_connector, usage_reporter, event_type, **k
):
    connector = k.get("connector", None)
    timeout = k.get("timeout", POLL_TIMEOUT)

    waited = False
    while not sentinel and not live_api_connector.IsCurrentProcessOrphan():
        time.sleep(timeout)
        waited = True

        if connector and connector.live_ipc_channel:
            connector.live_ipc_channel.process()

    if not live_api_connector.IsCurrentProcessOrphan():
        if connector and connector.live_ipc_channel:
            connector.live_ipc_channel.process()

    if not sentinel and live_api_connector.IsCurrentProcessOrphan():
        usage_reporter.log(et.WC_INTERRUPTED)
        logger.info("%s interrupted by early Live termination", event_type)

    if sentinel and sentinel[0] is not None:
        raise sentinel[0]

    return waited


def run_async_with_sentinel(action, sentinel, synchronize=False):
    """
    Run a passed action in a daemonized background thread. When
    the action is finished, either normally or by exception, append
    True to the sentinel.

    If sychronize is True, we join the thread.
    This is mainly for testing.
    """

    def target():
        try:
            action()
        except:
            sentinel.append(sys.exc_info()[1])
            logger.exception("Exception in worker thread")
            raise
        else:
            sentinel.append(None)

    t = threading.Thread(target=target)
    t.setDaemon(True)
    t.start()
    # for testing.
    if synchronize:
        t.join()
    return t


VARIANT2TAG = dict(
    Suite="suite",
    Standard="standard",
    Lite="lite",
    Intro="intro",
    Trial="trial",
    Beta="beta",
)


def failsafe(usage_reporter):
    def _w(func):
        @wraps(func)
        def _d(*a, **k):
            try:
                return func(*a, **k)
            except:
                exc_str = traceback.format_exception(
                    sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2]
                )
                usage_reporter.log(et.EXCEPTION, stacktrace="\n".join(exc_str))
                logger.error("Cleaning usage directory failed: %s", sys.exc_info()[1])
                logger.error("%s", "\n".join(exc_str))

        return _d

    return _w


def tags_for_config(config):
    tags = []

    if config.platform == "win":
        tags.append("win64")
    elif config.platform == "mac":
        if config.architecture == "x86_64":
            tags.append("mac-intel")
        else:
            tags.append("mac-{}".format(config.architecture))
    else:
        tags.append(config.platform)

    if "live_variant" in config:
        tags.append(VARIANT2TAG[config.live_variant])

    return tags


def temp_old_live_path(live_path):
    return URI(str(live_path) + ".Old")


def safe_to_install_auto_update(connector, usage_reporter):
    old_location = temp_old_live_path(connector.app_base_path)
    if old_location.exists():
        usage_reporter.log(et.OLD_INSTALLATION_FOUND)
        logger.info(
            "Old Live installation found at %s; attempting to delete.", str(old_location)
        )

        failed = False
        try:
            old_location.remove(recursive=True)
        except:
            failed = True

        if failed or old_location.exists():
            usage_reporter.log(et.FAILED_TO_DELETE_OLD_INSTALLATION)
            logger.error(
                "Failed to delete old Live installation at %s", str(old_location)
            )
            return False

    return True


def get_event_log_base(config):
    event_log_base = URI(config.aip_usage_data_dir)
    if not event_log_base.exists() or not event_log_base.isdir():
        logger.error("Event log base %r is not proper.", event_log_base)
        sys.exit(1)
    return event_log_base


class CommanderInChief(object):
    @staticmethod
    def command_upload_info(config, usage_reporter):
        logger.info("Upload info.")
        usage_reporter.log(et.UPLOAD_INFO)
        event_log_base = get_event_log_base(config)
        try:
            infoprocessing.upload_info(config.get("info_endpoint"), event_log_base)
        except:
            pass

    @staticmethod
    def command_cleanup_event_logs(
        config, live_api_connector, usage_reporter, synchronize=False
    ):
        logger.info("Cleanup event logs.")
        usage_reporter.log(et.CLEANUP_EVENT_LOGS)

        event_log_base = get_event_log_base(config)

        if not event_log_base.exists() or not event_log_base.isdir():
            logger.error("Event log base %r is not proper.", event_log_base)
            sys.exit(1)

        # list, but actually boolean - however, python's scoping rules
        # don't allow that
        cleanup_event_logs_finished = []
        # start the processing of event-logs asynchronously so that we can
        # check for our parent still alive

        @failsafe(usage_reporter)
        def cleanup_event_logs():
            clean_directory(event_log_base)

        run_async_with_sentinel(
            cleanup_event_logs, cleanup_event_logs_finished, synchronize=synchronize
        )
        wait_on_progress_or_live_termination(
            cleanup_event_logs_finished,
            live_api_connector,
            usage_reporter,
            event_type="Cleanup Event Logs",
        )

    @staticmethod
    def command_upload_event_logs(
        connector, config, live_api_connector, usage_reporter, synchronize=False
    ):
        logger.info("Uploading event logs.")
        usage_reporter.log(et.UPLOAD_EVENT_LOGS)

        event_log_base = get_event_log_base(config)

        # list, but actually boolean - however, python's scoping rules
        # don't allow that
        upload_event_logs_finished = []

        # start the processing of event-logs asynchronously so that we can
        # check for our parent still alive

        @failsafe(usage_reporter)
        def process_event_logs():
            upload_event_logs(connector, event_log_base, usage_reporter)

        run_async_with_sentinel(
            process_event_logs, upload_event_logs_finished, synchronize=synchronize
        )
        wait_on_progress_or_live_termination(
            upload_event_logs_finished,
            live_api_connector,
            usage_reporter,
            event_type="Upload Events",
        )

    @staticmethod
    def command_check_for_auto_updates(
        connector, config, live_api_connector, usage_reporter, synchronize=False
    ):

        tags = tags_for_config(config)
        version = config.version

        check_for_auto_updates_finished = []

        def check_for_auto_updates():
            res = connector.check_and_process_auto_updates(tags, version, usage_reporter)

            try:
                for task in res:
                    task.perform(usage_reporter)
            except Exception:
                connector.notify_error(immediate=False)
                raise

        run_async_with_sentinel(
            check_for_auto_updates, check_for_auto_updates_finished, synchronize
        )
        wait_on_progress_or_live_termination(
            check_for_auto_updates_finished,
            live_api_connector,
            usage_reporter,
            event_type="Check for Updates",
            connector=connector,
            timeout=1.0,
        )

    @staticmethod
    def command_install_version(connector, config, live_api_connector, usage_reporter):

        if connector.pending_update_available():
            logger.info("Notifying Live that the update is ready to apply")

            if connector.live_ipc_channel:
                connector.live_ipc_channel.send(
                    u"update_ready_to_apply", u"", immediate=True
                )

            logger.info("Waiting for Live to terminate")
            usage_reporter.log(et.WAITING_FOR_LIVE)

            while not live_api_connector.IsCurrentProcessOrphan():
                time.sleep(1.0)

            usage_reporter.log(et.LIVE_TERMINATED)

            if "crash_detection_file" in config:
                crash_detection_file = URI(config.crash_detection_file)
                if crash_detection_file.isfile():
                    usage_reporter.log(et.CRASH_DETECTION_FILE_FOUND)
                    logger.error(
                        "Can't apply auto update because a Crash detection file exists at %s",
                        str(crash_detection_file),
                    )
                    return

            if connector.check_live_is_running():
                logger.info(
                    "Can't apply auto update because another Live instance is running"
                )
                usage_reporter.log(et.OTHER_LIVE_RUNNING)
                return

            connector.cleanup_auto_updates_base()
            connector.apply_available_auto_update(usage_reporter)

    @staticmethod
    def command_skip_version(connector, config, live_api_connector, usage_reporter):
        info = connector.load_update_info()
        to_version = config["commands"]["skip_version"]["version"]
        logger.info("Skipping version %s", to_version)
        current_version = config.version
        if current_version in info["versions"]:
            vi = info["versions"][current_version]
            if to_version in vi:
                vi = vi[to_version]
                vi["status"] = "skipped"
        connector.save_update_info(info)


class NOPUsageReporter(object):
    def log(self, *args, **kwargs):
        pass


def main(
    config,
    live_api_connector,
    commander=CommanderInChief,
    usage_reporter=NOPUsageReporter(),
):
    try:
        usage_reporter.log(et.START)
        logger.info("Start")
        config = Bunch(**config)

        connector_class = RPCConnector
        app_base_path = URI(config.app_base_path)

        connector_params = dict(config=config)

        if "directory_connection" in config:
            connector_class = DirectoryConnector
            connector_params.update(dict(base=URI(config.directory_connection)))
        else:
            connector_params.update(
                dict(
                    endpoint_prefix=config.services_endpoint, session_id=config.session_id
                )
            )

        connector = connector_class(**connector_params)

        logger.debug(repr(config))

        if not (
            live_api_connector.HasInternetConnection()
            or connector_class is DirectoryConnector
        ):
            logger.info(
                'No internet connection available, aborting. Connect to the internet or use the "directory_connection" configuration option to load local asu files'
            )
            usage_reporter.log(et.NO_INTERNET_CONNECTION)
            sys.exit()

        commands = config["commands"]

        # always cleanup
        commander.command_cleanup_event_logs(config, live_api_connector, usage_reporter)
        # always upload anonymised info files
        commander.command_upload_info(config, usage_reporter)

        if "upload_event_logs" in commands:
            commander.command_upload_event_logs(
                connector, config, live_api_connector, usage_reporter
            )

        if "check_for_auto_updates" in commands:
            if safe_to_install_auto_update(connector, usage_reporter):
                commander.command_check_for_auto_updates(
                    connector, config, live_api_connector, usage_reporter
                )
                commander.command_install_version(
                    connector, config, live_api_connector, usage_reporter
                )

        if "skip_version" in commands:
            commander.command_skip_version(
                connector, config, live_api_connector, usage_reporter
            )

        logger.info("End")
        usage_reporter.log(et.END)
    except:
        logger.error(sys.exc_info()[1])
        raise
