__license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html"
__copyright__ = "Copyright (C) 2018 The OctoPrint Project - Released under terms of the AGPLv3 License"

import textwrap

import flask
import octoprint.plugin
from flask_babel import gettext
from octoprint.access import USER_GROUP
from octoprint.access.permissions import Permissions
from octoprint.events import Events
from octoprint.util import to_unicode
from octoprint.util.version import is_octoprint_compatible

from .checks import Severity
from .checks.firmware_broken import FirmwareBrokenChecks
from .checks.firmware_development import FirmwareDevelopmentChecks
from .checks.firmware_hostcommands import FirmwareHostcommandsChecks
from .checks.firmware_unsafe import FirmwareUnsafeChecks

TERMINAL_WARNING = """
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
{message}

Learn more at {url}
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

"""

TERMINAL_INFO = """
---------------------------------------------------------------------------
{message}

Learn more at {url}
---------------------------------------------------------------------------

"""

FIRMWARE_CHECKS = {
    "firmware-unsafe": FirmwareUnsafeChecks.as_dict(),
    "firmware-broken": FirmwareBrokenChecks.as_dict(),
    "firmware-development": FirmwareDevelopmentChecks.as_dict(),
    "firmware-hostcommands": FirmwareHostcommandsChecks.as_dict(),
}


class FirmwareCheckPlugin(
    octoprint.plugin.AssetPlugin,
    octoprint.plugin.EventHandlerPlugin,
    octoprint.plugin.SimpleApiPlugin,
    octoprint.plugin.TemplatePlugin,
    octoprint.plugin.SettingsPlugin,
):
    # noinspection PyMissingConstructor
    def __init__(self):
        self._warnings = {}
        self._scan_received = True

    ##~~ TemplatePlugin API

    def get_template_configs(self):
        return [
            {
                "type": "sidebar",
                "name": gettext("Attention!"),
                "data_bind": "visible: printerState.isOperational() && loginState.isAdmin() && warnings().length > 0",
                "icon": "exclamation-triangle",
                "styles_wrapper": ["display: none"],
                "template": "firmware_check_sidebar_warning.jinja2",
                "suffix": "_warning",
            },
            {
                "type": "sidebar",
                "name": gettext("Info"),
                "data_bind": "visible: printerState.isOperational() && loginState.isAdmin() && infos().length > 0",
                "icon": "info-circle",
                "styles_wrapper": ["display: none"],
                "template": "firmware_check_sidebar_info.jinja2",
                "suffix": "_info",
            },
            {
                "type": "settings",
                "name": gettext("Firmware Check"),
                "custom_bindings": False,
            },
        ]

    ##~~ AssetPlugin API

    def get_assets(self):
        return {
            "js": ("js/firmware_check.js",),
            "clientjs": ("clientjs/firmware_check.js",),
            "css": ("css/firmware_check.css",),
            "less": ("less/firmware_check.less",),
        }

    ##~~ EventHandlerPlugin API

    def on_event(self, event, payload):
        if event == Events.DISCONNECTED:
            self._reset_warnings()
            self._reset_state()
            self._reset_checks()

    ##~~ SimpleApiPlugin API

    def on_api_get(self, request):
        if not Permissions.PLUGIN_FIRMWARE_CHECK_DISPLAY.can():
            return flask.make_response("Insufficient rights", 403)
        return flask.jsonify(self._warnings)

    def is_api_protected(self):
        return True

    ##~~ SettingsPlugin API

    def get_settings_defaults(self):
        return {"ignore_infos": False}

    ##~~ GCODE received hook handler

    def on_gcode_received(self, comm_instance, line, *args, **kwargs):
        if self._scan_received:
            self._run_checks("received", to_unicode(line, errors="replace"))
        return line

    ##~~ Firmware info hook handler

    def on_firmware_info_received(self, comm_instance, firmware_name, firmware_data):
        self._run_checks(
            "m115",
            to_unicode(firmware_name, errors="replace"),
            {
                to_unicode(key, errors="replace"): to_unicode(value, errors="replace")
                for key, value in firmware_data.items()
            },
        )

    ##~~ Firmware capability hook handler

    def on_firmware_cap_received(self, comm_instance, cap, enabled, all_caps):
        self._run_checks("cap", to_unicode(cap, errors="replace"), enabled)

    ##~~ Additional permissions hook handler

    def get_additional_permissions(self):
        return [
            {
                "key": "DISPLAY",
                "name": "Display firmware check warnings",
                "description": gettext("Allows to see firmware check warnings"),
                "roles": ["display"],
                "default_groups": [USER_GROUP],
            }
        ]

    ##~~ Softwareupdate hook

    def get_update_information(self):
        return {
            "firmware_check": {
                "displayName": "Firmware Check Plugin",
                "displayVersion": self._plugin_version,
                # version check: github repository
                "type": "github_release",
                "user": "OctoPrint",
                "repo": "OctoPrint-FirmwareCheck",
                "current": self._plugin_version,
                "stable_branch": {
                    "name": "Stable",
                    "branch": "master",
                    "commitish": ["devel", "master"],
                },
                "prerelease_branches": [
                    {
                        "name": "Prerelease",
                        "branch": "devel",
                        "commitish": ["devel", "master"],
                    }
                ],
                # update method: pip
                "pip": "https://github.com/OctoPrint/OctoPrint-FirmwareCheck/archive/{target_version}.zip",
            }
        }

    ##~~ Helpers

    def _run_checks(self, check_type, *args, **kwargs):
        changes = False

        still_active = False
        for warning_type, check_data in FIRMWARE_CHECKS.items():
            checks = check_data.get("checks")
            message = check_data.get("message")
            severity = check_data.get("severity", Severity.CRITICAL)
            url = "https://faq.octoprint.org/warning-{warning_type}".format(
                warning_type=warning_type
            )
            if not checks or not message:
                continue

            for check in checks:
                if not check.active:
                    # skip non active checks
                    continue

                method = getattr(check, check_type, None)
                if not callable(method):
                    # skip uncallable checks
                    continue

                # execute method
                try:
                    method(*args, **kwargs)
                except Exception:
                    self._logger.exception(
                        "There was an error running method {} on check {!r}".format(
                            check_type, check
                        )
                    )
                    continue

                # check if now triggered
                if check.triggered:
                    if check.url is not None:
                        url = check.url

                    self._register_warning(
                        warning_type,
                        message.format(**check.placeholders),
                        severity,
                        url,
                    )

                    # noinspection PyUnresolvedReferences
                    self._event_bus.fire(
                        Events.PLUGIN_FIRMWARE_CHECK_WARNING,
                        {
                            "check_name": check.name,
                            "warning_type": warning_type,
                            "severity": severity,
                            "url": url,
                        },
                    )
                    changes = True
                    break

                check.evaluate_timeout()
                self._logger.debug(f"Check {check} active? {check.active}")

            still_active = any(check.active for check in checks) or still_active

        self._scan_received = still_active
        self._logger.debug(f"Scanning received lines enabled? {self._scan_received}")

        if changes:
            self._ping_clients()

    def _register_warning(self, warning_type, message, severity, url):
        output = TERMINAL_WARNING
        if severity == Severity.INFO:
            output = TERMINAL_INFO

        self._log_to_terminal(
            output.format(
                message="\n".join(textwrap.wrap(message, 75)),
                warning_type=warning_type,
                url=url,
            )
        )
        self._warnings[warning_type] = {
            "message": message,
            "severity": severity,
            "url": url,
        }

        logline = f"{message}. More information at {url}"
        if severity == Severity.INFO:
            self._logger.info(logline)
        else:
            self._logger.warning(logline)

    def _reset_warnings(self):
        self._warnings.clear()
        self._ping_clients()

    def _reset_state(self):
        self._scan_received = True

    def _reset_checks(self):
        for _, check_data in FIRMWARE_CHECKS.items():
            checks = check_data.get("checks")
            if not checks:
                continue

            for check in checks:
                check.reset()

    def _log_to_terminal(self, message):
        if self._printer:
            lines = message.split("\n")
            self._printer.log_lines(*lines)

    def _ping_clients(self):
        self._plugin_manager.send_plugin_message(self._identifier, {"type": "update"})


def register_custom_events(*args, **kwargs):
    return [
        "warning",
    ]


__plugin_name__ = "Firmware Check"
__plugin_pythoncompat__ = ">=3.7,<4"
__plugin_disabling_discouraged__ = gettext(
    "Without this plugin OctoPrint will no longer be able to "
    "check if the printer it is connected to has a known safety "
    "issue or otherwise broken firmware and inform you about that fact."
)
__plugin_implementation__ = FirmwareCheckPlugin()
__plugin_hooks__ = {
    "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information,
    "octoprint.comm.protocol.gcode.received": (
        __plugin_implementation__.on_gcode_received,
        100,
    ),
    "octoprint.comm.protocol.firmware.info": (
        __plugin_implementation__.on_firmware_info_received,
        100,
    ),
    "octoprint.comm.protocol.firmware.capabilities": (
        __plugin_implementation__.on_firmware_cap_received,
        100,
    ),
    "octoprint.events.register_custom_events": register_custom_events,
    "octoprint.access.permissions": __plugin_implementation__.get_additional_permissions,
}

if is_octoprint_compatible("<1.6"):
    __plugin_settings_overlay__ = {
        "appearance": {
            "components": {
                "order": {
                    "sidebar": [
                        "plugin_firmware_check_warning",
                        "plugin_firmware_check_info",
                        "connection",
                        "state",
                        "files",
                    ]
                }
            }
        }
    }
