import importlib
import logging
import sys
import warnings
from os import makedirs, path
from typing import Callable, Optional

from errbot.backend_plugin_manager import BackendPluginManager
from errbot.core import ErrBot
from errbot.logs import format_logs
from errbot.plugin_manager import BotPluginManager
from errbot.repo_manager import BotRepoManager
from errbot.storage.base import StoragePluginBase
from errbot.utils import PLUGINS_SUBDIR

log = logging.getLogger(__name__)

HERE = path.dirname(path.abspath(__file__))
CORE_BACKENDS = path.join(HERE, "backends")
CORE_STORAGE = path.join(HERE, "storage")

PLUGIN_DEFAULT_INDEX = "https://errbot.io/repos.json"


def bot_config_defaults(config: object) -> None:
    if not hasattr(config, "ACCESS_CONTROLS_DEFAULT"):
        config.ACCESS_CONTROLS_DEFAULT = {}
    if not hasattr(config, "ACCESS_CONTROLS"):
        config.ACCESS_CONTROLS = {}
    if not hasattr(config, "HIDE_RESTRICTED_COMMANDS"):
        config.HIDE_RESTRICTED_COMMANDS = False
    if not hasattr(config, "HIDE_RESTRICTED_ACCESS"):
        config.HIDE_RESTRICTED_ACCESS = False
    if not hasattr(config, "BOT_PREFIX_OPTIONAL_ON_CHAT"):
        config.BOT_PREFIX_OPTIONAL_ON_CHAT = False
    if not hasattr(config, "BOT_PREFIX"):
        config.BOT_PREFIX = "!"
    if not hasattr(config, "BOT_ALT_PREFIXES"):
        config.BOT_ALT_PREFIXES = ()
    if not hasattr(config, "BOT_ALT_PREFIX_SEPARATORS"):
        config.BOT_ALT_PREFIX_SEPARATORS = ()
    if not hasattr(config, "BOT_ALT_PREFIX_CASEINSENSITIVE"):
        config.BOT_ALT_PREFIX_CASEINSENSITIVE = False
    if not hasattr(config, "DIVERT_TO_PRIVATE"):
        config.DIVERT_TO_PRIVATE = ()
    if not hasattr(config, "DIVERT_TO_THREAD"):
        config.DIVERT_TO_THREAD = ()
    if not hasattr(config, "MESSAGE_SIZE_LIMIT"):
        config.MESSAGE_SIZE_LIMIT = None  # No user limit declared.
    if not hasattr(config, "GROUPCHAT_NICK_PREFIXED"):
        config.GROUPCHAT_NICK_PREFIXED = False
    if not hasattr(config, "AUTOINSTALL_DEPS"):
        config.AUTOINSTALL_DEPS = True
    if not hasattr(config, "SUPPRESS_CMD_NOT_FOUND"):
        config.SUPPRESS_CMD_NOT_FOUND = False
    if not hasattr(config, "BOT_ASYNC"):
        config.BOT_ASYNC = True
    if not hasattr(config, "BOT_ASYNC_POOLSIZE"):
        config.BOT_ASYNC_POOLSIZE = 10
    if not hasattr(config, "CHATROOM_PRESENCE"):
        config.CHATROOM_PRESENCE = ()
    if not hasattr(config, "CHATROOM_RELAY"):
        config.CHATROOM_RELAY = ()
    if not hasattr(config, "REVERSE_CHATROOM_RELAY"):
        config.REVERSE_CHATROOM_RELAY = ()
    if not hasattr(config, "CHATROOM_FN"):
        config.CHATROOM_FN = "Errbot"
    if not hasattr(config, "TEXT_DEMO_MODE"):
        config.TEXT_DEMO_MODE = True
    if not hasattr(config, "BOT_ADMINS"):
        raise ValueError("BOT_ADMINS missing from config.py.")
    if not hasattr(config, "TEXT_COLOR_THEME"):
        config.TEXT_COLOR_THEME = "light"
    if not hasattr(config, "BOT_ADMINS_NOTIFICATIONS"):
        config.BOT_ADMINS_NOTIFICATIONS = config.BOT_ADMINS


def setup_bot(
    backend_name: str,
    logger: logging.Logger,
    config: object,
    restore: Optional[str] = None,
) -> ErrBot:
    # from here the environment is supposed to be set (daemon / non daemon,
    # config.py in the python path )

    bot_config_defaults(config)

    if hasattr(config, "BOT_LOG_FORMATTER"):
        format_logs(formatter=config.BOT_LOG_FORMATTER)
    else:
        format_logs(theme_color=config.TEXT_COLOR_THEME)

    if hasattr(config, "BOT_LOG_FILE") and config.BOT_LOG_FILE:
        hdlr = logging.FileHandler(config.BOT_LOG_FILE)
        hdlr.setFormatter(
            logging.Formatter("%(asctime)s %(levelname)-8s %(name)-25s %(message)s")
        )
        logger.addHandler(hdlr)

    if hasattr(config, "BOT_LOG_SENTRY") and config.BOT_LOG_SENTRY:
        sentry_integrations = []

        try:
            import sentry_sdk
            from sentry_sdk.integrations.logging import LoggingIntegration

        except ImportError:
            log.exception(
                "You have BOT_LOG_SENTRY enabled, but I couldn't import modules "
                "needed for Sentry integration. Did you install sentry-sdk? "
                "(See https://docs.sentry.io/platforms/python for installation instructions)"
            )
            exit(-1)

        sentry_logging = LoggingIntegration(
            level=config.SENTRY_LOGLEVEL, event_level=config.SENTRY_EVENTLEVEL
        )

        sentry_integrations.append(sentry_logging)

        if hasattr(config, "BOT_LOG_SENTRY_FLASK") and config.BOT_LOG_SENTRY_FLASK:
            try:
                from sentry_sdk.integrations.flask import FlaskIntegration
            except ImportError:
                log.exception(
                    "You have BOT_LOG_SENTRY enabled, but I couldn't import modules "
                    "needed for Sentry integration. Did you install sentry-sdk[flask]? "
                    "(See https://docs.sentry.io/platforms/python/flask for installation instructions)"
                )
                exit(-1)

            sentry_integrations.append(FlaskIntegration())

        sentry_options = getattr(config, "SENTRY_OPTIONS", {})
        if hasattr(config, "SENTRY_TRANSPORT") and isinstance(
            config.SENTRY_TRANSPORT, tuple
        ):
            warnings.warn(
                "SENTRY_TRANSPORT is deprecated. Please use SENTRY_OPTIONS instead.",
                DeprecationWarning,
            )
            try:
                mod = importlib.import_module(config.SENTRY_TRANSPORT[1])
                transport = getattr(mod, config.SENTRY_TRANSPORT[0])
                sentry_options["transport"] = transport
            except ImportError:
                log.exception(
                    f"Unable to import selected SENTRY_TRANSPORT - {config.SENTRY_TRANSPORT}"
                )
                exit(-1)
        # merge options dict with dedicated SENTRY_DSN setting
        sentry_kwargs = {
            **sentry_options,
            **{"dsn": config.SENTRY_DSN, "integrations": sentry_integrations},
        }
        sentry_sdk.init(**sentry_kwargs)

    logger.setLevel(config.BOT_LOG_LEVEL)

    storage_plugin = get_storage_plugin(config)

    # init the botplugin manager
    botplugins_dir = path.join(config.BOT_DATA_DIR, PLUGINS_SUBDIR)
    if not path.exists(botplugins_dir):
        makedirs(botplugins_dir, mode=0o755)

    plugin_indexes = getattr(config, "BOT_PLUGIN_INDEXES", (PLUGIN_DEFAULT_INDEX,))
    if isinstance(plugin_indexes, str):
        plugin_indexes = (plugin_indexes,)

    # Extra backend is expected to be a list type, convert string to list.
    extra_backend = getattr(config, "BOT_EXTRA_BACKEND_DIR", [])
    if isinstance(extra_backend, str):
        extra_backend = [extra_backend]

    backendpm = BackendPluginManager(
        config, "errbot.backends", backend_name, ErrBot, CORE_BACKENDS, extra_backend
    )

    log.info(f"Found Backend plugin: {backendpm.plugin_info.name}")

    repo_manager = BotRepoManager(storage_plugin, botplugins_dir, plugin_indexes)

    try:
        bot = backendpm.load_plugin()
        botpm = BotPluginManager(
            storage_plugin,
            config.BOT_EXTRA_PLUGIN_DIR,
            config.AUTOINSTALL_DEPS,
            getattr(config, "CORE_PLUGINS", None),
            lambda name, clazz: clazz(bot, name),
            getattr(config, "PLUGINS_CALLBACK_ORDER", (None,)),
        )
        bot.attach_storage_plugin(storage_plugin)
        bot.attach_repo_manager(repo_manager)
        bot.attach_plugin_manager(botpm)
        bot.initialize_backend_storage()

        # restore the bot from the restore script
        if restore:
            # Prepare the context for the restore script
            if "repos" in bot:
                log.fatal("You cannot restore onto a non empty bot.")
                sys.exit(-1)
            log.info(f"**** RESTORING the bot from {restore}")
            restore_bot_from_backup(restore, bot=bot, log=log)
            print("Restore complete. You can restart the bot normally")
            sys.exit(0)

        errors = bot.plugin_manager.update_plugin_places(
            repo_manager.get_all_repos_paths()
        )
        if errors:
            startup_errors = "\n".join(errors.values())
            log.error("Some plugins failed to load:\n%s", startup_errors)
            bot._plugin_errors_during_startup = startup_errors
        return bot
    except Exception:
        log.exception("Unable to load or configure the backend.")
        exit(-1)


def restore_bot_from_backup(backup_filename: str, *, bot, log: logging.Logger):
    """Restores the given bot by executing the 'backup' script.

    The backup file is a python script which manually execute a series of commands on the bot
    to restore it to its previous state.

    :param backup_filename: the full path to the backup script.
    :param bot: the bot instance to restore
    :param log: logger to use during the restoration process
    """
    with open(backup_filename) as f:
        exec(f.read(), {"log": log, "bot": bot})
    bot.close_storage()


def get_storage_plugin(config: object) -> Callable:
    """
    Find and load the storage plugin
    :param config: the bot configuration.
    :return: the storage plugin
    """
    storage_name = getattr(config, "STORAGE", "Shelf")
    extra_storage_plugins_dir = getattr(config, "BOT_EXTRA_STORAGE_PLUGINS_DIR", None)
    spm = BackendPluginManager(
        config,
        "errbot.storage",
        storage_name,
        StoragePluginBase,
        CORE_STORAGE,
        extra_storage_plugins_dir,
    )
    log.info(f"Found Storage plugin: {spm.plugin_info.name}.")
    return spm.load_plugin()


def bootstrap(
    bot_class, logger: logging.Logger, config: object, restore: Optional[str] = None
) -> None:
    """
    Main starting point of Errbot.

    :param bot_class: The backend class inheriting from Errbot you want to start.
    :param logger: The logger you want to use.
    :param config: The config.py module.
    :param restore: Start Errbot in restore mode (from a backup).
    """
    bot = setup_bot(bot_class, logger, config, restore)
    log.debug(f"Start serving commands from the {bot.mode} backend.")
    bot.serve_forever()
