"""
Copyright (c) 2016-2020 Keith Sterling http://www.keithsterling.com

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions
of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""

import os
import logging.config
import yaml

from programy.utils.logging.ylogger import YLogger

from programy.config.file.factory import ConfigurationFactory
from programy.clients.args import CommandLineClientArguments
from programy.config.programy import ProgramyConfiguration
from programy.context import ClientContext
from programy.utils.license.keys import LicenseKeys
from programy.utils.substitutions.substitues import Substitutions
from programy.scheduling.scheduler import ProgramyScheduler
from programy.clients.render.text import TextRenderer
from programy.utils.classes.loader import ClassLoader
from programy.storage.factory import StorageFactory
from programy.utils.email.sender import EmailSender
from programy.triggers.manager import TriggerManager
from programy.triggers.system import SystemTriggers
from programy.clients.ping.responder import PingResponder
from programy.clients.botfactory import BotFactory
from programy.clients.response import ResponseLogger
from programy.utils.console.console import outputLog


class BotClient(ResponseLogger):

    def __init__(self, botid, argument_parser=None):
        self._id = botid
        self._license_keys = LicenseKeys()
        self._storage = None
        self._scheduler = None
        self._email = None
        self._trigger_mgr = None
        self._configuration = None
        self._ping_responder = None

        self._questions = 0

        self._arguments = self.parse_arguments(argument_parser=argument_parser)

        self.initiate_logging(self.arguments)

        self._subsitutions = Substitutions()
        if self.arguments.substitutions is not None:
            self._subsitutions.load_substitutions(self.arguments.substitutions)

        self.load_configuration(self.arguments)
        self.parse_configuration()

        self.load_storage()

        self._bot_factory = BotFactory(self, self.configuration.client_configuration)

        self.load_license_keys()
        self.get_license_keys()
        self._configuration.client_configuration.check_for_license_keys(self._license_keys)

        self.load_scheduler()

        self._renderer = None
        self.load_renderer()

        self.load_email()

        self.load_trigger_manager()

        self.load_ping_responder()

    @staticmethod
    def ylogger_type():
        return "client"

    @property
    def num_questions(self):
        return self._questions

    @property
    def configuration(self):
        return self._configuration

    @property
    def id(self):
        return self._id

    @property
    def arguments(self):
        return self._arguments

    @property
    def license_keys(self):
        return self._license_keys

    @property
    def scheduler(self):
        return self._scheduler

    @property
    def storage_factory(self):
        return self._storage

    @property
    def bot_factory(self):
        return self._bot_factory

    @property
    def renderer(self):
        return self._renderer

    @property
    def trigger_manager(self):
        return self._trigger_mgr

    @property
    def ping_responder(self):
        return self._ping_responder

    def get_description(self):
        if self.configuration is not None:
            if self.configuration.client_configuration is not None:
                return self.configuration.client_configuration.description
        return "Bot Client"

    def get_question_counts(self):
        return self.bot_factory.get_question_counts()

    def add_client_arguments(self, parser=None):
        del parser
        # Nothing to add
        return

    def parse_configuration(self):
        # Nothing to add
        return

    def parse_args(self, arguments, parsed_args):
        del arguments
        del parsed_args
        # Nothing to add
        return

    def parse_arguments(self, argument_parser):
        client_args = CommandLineClientArguments(self, parser=argument_parser)
        client_args.parse_args(self)
        return client_args

    def load_license_keys(self):
        if self.storage_factory.entity_storage_engine_available(StorageFactory.LICENSE_KEYS) is True:
            storage_engine = self.storage_factory.entity_storage_engine(StorageFactory.LICENSE_KEYS)
            if storage_engine is not None:
                keys_store = storage_engine.license_store()
                keys_store.load(self._license_keys)
            else:
                YLogger.error(self, "No storage engine available for license_keys")
        else:
            YLogger.warning(self, "No storage factory setting for license_keys")

    def get_license_keys(self):
        return

    def initiate_logging(self, arguments):
        if arguments.logging is not None:
            try:
                with open(arguments.logging, 'r+', encoding="utf-8") as yml_data_file:
                    logging_config = yaml.load(yml_data_file, Loader=yaml.FullLoader)
                    logging.config.dictConfig(logging_config)
                    YLogger.info(self, "Now logging under configuration")

            except Exception as excep:
                YLogger.exception(self, "Failed to open logging configuration [%s]", excep, arguments.logging)

        else:
            outputLog(self, "Warning. No logging configuration file defined, using defaults...")

    def get_client_configuration(self):
        """
        By overriding this class in you Configuration file, you can add new configurations
        and stil use the dynamic loader capabilities
        :return: Client configuration object
        """
        raise NotImplementedError("You must override this and return a subclassed client configuration")  # pragma: no cover

    def load_configuration(self, arguments, subs: Substitutions = None):
        if arguments.bot_root is None:
            if arguments.config_filename is not None:
                arguments.bot_root = os.path.dirname(arguments.config_filename)
            else:
                arguments.bot_root = "."
            outputLog(self, "No bot root argument set, defaulting to [%s]" % arguments.bot_root)

        if arguments.config_filename is not None:
            self._configuration = ConfigurationFactory.load_configuration_from_file(self.get_client_configuration(),
                                                                                    arguments.config_filename,
                                                                                    arguments.config_format,
                                                                                    arguments.bot_root,
                                                                                    subs)
        else:
            outputLog(self, "No configuration file specified, using defaults only !")
            self._configuration = ProgramyConfiguration(self.get_client_configuration())

    def load_scheduler(self):
        if self.configuration.client_configuration.scheduler is not None:
            YLogger.debug(None, "Loading Scheduler")
            self._scheduler = ProgramyScheduler(self, self.configuration.client_configuration.scheduler)
            self._scheduler.start()

    def load_email(self):
        if self._configuration.client_configuration.email is not None:
            YLogger.debug(None, "Loading Email Manager")
            self._email = EmailSender(self._configuration.client_configuration.email)

    def load_trigger_manager(self):
        if self._configuration.client_configuration.triggers is not None:
            YLogger.debug(None, "Loading Trigger Manager")
            self._trigger_mgr = TriggerManager.load_trigger_manager(self._configuration.client_configuration.triggers)

    def load_storage(self):
        self._storage = StorageFactory()
        if self.configuration.client_configuration.storage is not None:
            YLogger.debug(None, "Loading Storage Factory")
            self._storage.load_engines_from_config(self.configuration.client_configuration.storage)
        else:
            outputLog(self, "No storage defined!")

    def _render_callback(self):
        return False

    def load_renderer(self):
        callback = self._render_callback()
        try:
            if self._configuration.client_configuration.renderer is not None:
                YLogger.debug(None, "Loading Renderer")
                clazz = ClassLoader.instantiate_class(self._configuration.client_configuration.renderer)
                if callback is True:
                    self._renderer = clazz(self)
                else:
                    self._renderer = clazz(None)
                return

        except Exception as e:
            YLogger.exception(None, "Failed to load config specified renderer", e)

        self._renderer = self.get_default_renderer(callback)

    def get_default_renderer(self, callback=True):
        if callback is True:
            return TextRenderer(self)
        else:
            return TextRenderer(None)

    def create_client_context(self, userid):
        client_context = ClientContext(self, userid)
        client_context.bot = self._bot_factory.select_bot()
        client_context.brain = client_context.bot.brain_factory.select_brain()
        return client_context

    def load_ping_responder(self):

        self._ping_responder = PingResponder(self)

        self._ping_responder.register_with_healthchecker()

    def startup(self):
        if self._trigger_mgr is not None:
            self._trigger_mgr.trigger(event=SystemTriggers.SYSTEM_STARTUP)

        if self._ping_responder is not None:
            PingResponder.init_ping_response(self._ping_responder)

    def shutdown(self):
        if self._trigger_mgr is not None:
            self._trigger_mgr.trigger(event=SystemTriggers.SYSTEM_SHUTDOWN)

        if self._ping_responder is not None:
            self._ping_responder.shutdown_ping_service()

    def run(self, app=None):
        del app
        raise NotImplementedError("You must override this and implement the logic to run the client")  # pragma: no cover
