# Copyright (c) 2007, 2020, Oracle and/or its affiliates.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License, version 2.0,
# as published by the Free Software Foundation.
#
# This program is designed to work with certain software (including
# but not limited to OpenSSL) that is licensed under separate terms, as
# designated in a particular file or component or in included license
# documentation.  The authors of MySQL hereby grant you an additional
# permission to link the program and your derivative works with the
# separately licensed software that they have either included with
# the program or referenced in the documentation.
# This program is distributed in the hope that it will be useful,  but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
# the GNU General Public License, version 2.0, for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA


from workbench.log import log_info, log_error, log_warning, log_debug3

from workbench.utils import format_duration, Version
from workbench.db_utils import QueryError
import mforms
import time
import wb_admin_monitor

from workbench.notifications import NotificationCenter

def stradd(table, y, label, value):
    t = mforms.newLabel(label + ":")
    t.set_name(label)
    table.add(t, 0, 1, y, y+1, mforms.VFillFlag|mforms.HFillFlag)

    t = mforms.newLabel(value)
    t.set_style(mforms.BoldStyle)
    t.set_name(label + " Value")
    table.add(t, 1, 2, y, y+1, mforms.VFillFlag|mforms.HFillFlag)
    return t



class StateIcon(mforms.Box):
    on_icon = None
    off_icon = None

    def __init__(self):
        mforms.Box.__init__(self, True)
        self.set_release_on_add()
        self.set_managed()

        if not self.on_icon:
            self.on_icon = mforms.App.get().get_resource_path("mysql-status-on.png")
            self.off_icon = mforms.App.get().get_resource_path("mysql-status-off.png")

        self.set_spacing(8)
        self.image = mforms.newImageBox()
        self.image.set_name("Status Light")
        self.image.set_image(self.off_icon)
        self.add(self.image, False, True)

        self.label = mforms.newLabel("n/a")
        self.add(self.label, False, True)

        self.text = None


    def set_state(self, state):
        if state:
            self.image.set_image(self.on_icon)
            self.label.set_text("On")
        elif state is None:
            self.image.set_image(self.off_icon)
            self.label.set_text("n/a")
        else:
            self.image.set_image(self.off_icon)
            self.label.set_text("Off")


    def set_text(self, text):
        if not self.text:
            self.text = mforms.newLabel(text)
            self.text.set_style(mforms.BoldStyle)
            self.add(self.text, False, True)
        else:
            self.text.set_text(text)



class ConnectionInfo(mforms.Box):
    def __init__(self, owner):
        mforms.Box.__init__(self, True)
        self.set_release_on_add()
        self.set_managed()
        
        self.set_name("Connection Info Section")
        
        self.owner = owner

        self.set_spacing(35)

        self.icon = mforms.newImageBox()
        self.icon.set_image(mforms.App.get().get_resource_path("mysql-logo-00.png"))
        self.icon.set_name("Server Status Stamp")

        self.add(self.icon, False, True)

        vbox = mforms.newBox(False)
        self.vbox = vbox
        self.add(vbox, True, True)
        vbox.set_spacing(2)
        label = mforms.newLabel("Connection Name")
        label.set_name("Connection Name")
        vbox.add(label, False, True)

        self.connection_name = mforms.newLabel("?")
        self.connection_name.set_style(mforms.VeryBigStyle)
        self.connection_name.set_name("Connection Name Value")
        vbox.add(self.connection_name, False, True)

        self.info_table = None


    def update(self, ctrl_be):
        self.suspend_layout()
        self.connection_name.set_text(ctrl_be.server_profile.name)

        info = ctrl_be.server_variables
        status = ctrl_be.status_variables

        if self.info_table:
            self.vbox.remove(self.info_table)

        self.info_table = mforms.newTable()
        self.info_table.set_column_count(2)
        self.info_table.set_row_count(8)
        self.info_table.set_column_spacing(18)
        self.info_table.set_row_spacing(5)

        stradd(self.info_table, 0, "\nHost", "\n" + info.get("hostname", "n/a"))
        stradd(self.info_table, 1, "Socket", info.get("socket", "n/a"))
        stradd(self.info_table, 2, "Port", info.get("port", "n/a"))
        stradd(self.info_table, 3, "Version", "%s (%s)" % (info.get("version", "n/a"), info.get("version_comment", "")))
        stradd(self.info_table, 4, "Compiled For", "%s   (%s)" % (info.get("version_compile_os", "n/a"), info.get("version_compile_machine", "n/a")))

        stradd(self.info_table, 5, "Configuration File", ctrl_be.server_profile.config_file_path or "unknown")

        uptime = status.get("Uptime", None)
        if uptime:
            uptime = int(uptime)
            stradd(self.info_table, 6, "Running Since", "%s (%s)" % (time.ctime(ctrl_be.status_variables_time-uptime), format_duration(uptime, True)))
        else:
            stradd(self.info_table, 6, "Running Since", "n/a")
        self.vbox.add(self.info_table, True, True)

        box = mforms.newBox(True)
        refresh = mforms.newButton()
        refresh.set_text("Refresh")
        refresh.set_tooltip("Refresh server status information")
        refresh.add_clicked_callback(self.owner.refresh_status)
        box.add(refresh, False, False)
        self.info_table.add(box, 1, 2, 7, 8, mforms.VFillFlag)

        version = ctrl_be.target_version
        if version and info:
            icon = mforms.App.get().get_resource_path("mysql-logo-%i%i.png" % (version.majorNumber, version.minorNumber))
            if icon:
                self.icon.set_image(icon)
        self.resume_layout()

#===============================================================================
#
#===============================================================================
class WbAdminServerStatus(mforms.Box):
    status      = None
    connections = None
    _update_timeout = None

    @classmethod
    def wba_register(cls, admin_context):
        admin_context.register_page(cls, "Management", "Server Status", "Server Status", False)

    @classmethod
    def identifier(cls):
        return "admin_server_status"


    #---------------------------------------------------------------------------
    def __init__(self, ctrl_be, server_profile, main_view):
        mforms.Box.__init__(self, True)
        self.set_managed()
        self.set_release_on_add()
        
        self.set_name("Server Status Main")

        self.ui_created = False

        self.set_spacing(24)

        self.ctrl_be = ctrl_be
        self.server_profile = server_profile
        self.main_view = main_view

        lbox = mforms.newBox(False)
        lbox.set_name("Server Status Left Side")
        self.add(lbox, True, True)

        self.connection_info = ConnectionInfo(self)
        self.connection_info.set_padding(24)
        lbox.add(self.connection_info, False, True)

        self.scrollbox = mforms.newScrollPanel(mforms.ScrollPanelDrawBackground)
        self.scrollbox.set_padding(24)
        self.scrollbox.set_name("Extra Server Info Section")
        
        self.content = mforms.newBox(False)
        self.content.set_padding(20)
        self.content.set_spacing(4)
        self.scrollbox.add(self.content)
        lbox.add(self.scrollbox, True, True)

        image = mforms.newImageBox()
        image.set_name("Server Status Separator")
        if self.server_profile.host_os == "linux":
            image.set_image(mforms.App.get().get_resource_path("mysql-status-separator-linux.png"))
        else:
          image.set_image(mforms.App.get().get_resource_path("mysql-status-separator.png"))
        image.set_image_align(mforms.MiddleCenter)
        self.add(image, False, True)

        self.status = wb_admin_monitor.WbAdminMonitor(server_profile, self.ctrl_be)
        self.status.set_size(360, -1)
        self.status.set_padding(0, 24, 24, 24)
        self.add(self.status, False, True)

        self.controls = {}

        self.currently_started = None
        self.ctrl_be.add_me_for_event("server_started", self)
        self.ctrl_be.add_me_for_event("server_offline", self)
        self.ctrl_be.add_me_for_event("server_stopped", self)

        self.connection_info.update(self.ctrl_be)

    #---------------------------------------------------------------------------
    def server_started_event(self):
        if self.currently_started != True:
            self.refresh("started")
            self.currently_started = True
            if not self._update_timeout:
                self._update_timeout = mforms.Utilities.add_timeout(0.5, self.update)

    def server_offline_event(self):
        if self.currently_started != True:
            self.refresh("offline")
            self.currently_started = True
            if not self._update_timeout:
                self._update_timeout = mforms.Utilities.add_timeout(0.5, self.update)
    #---------------------------------------------------------------------------
    def server_stopped_event(self):
        if self.currently_started != False:
            self.refresh("stopped")
            self.currently_started = False
            if not self._update_timeout:
                self._update_timeout = mforms.Utilities.add_timeout(0.5, self.update)

    #---------------------------------------------------------------------------
    def refresh(self, status):
        self.status.refresh_status(status)


    #---------------------------------------------------------------------------
    def refresh_status(self):
        if not self._update_timeout:
            status = self.ctrl_be.force_check_server_state()
            if (status == "running" or not status  or status == "offline" ) and self.currently_started:
                self.ctrl_be.query_server_info()

            self._update_timeout = mforms.Utilities.add_timeout(0.5, self.update)


    #---------------------------------------------------------------------------
    def page_activated(self):
        self.suspend_layout()
        try:
            if not self.ui_created:
                self.create_info_sections()
                self.ui_created = True

                if not self._update_timeout:
                    self._update_timeout = mforms.Utilities.add_timeout(0.5, self.update)
        finally:
            self.resume_layout()

        if self.currently_started is None:
            if self.ctrl_be.is_server_running() == "running":
                self.server_started_event()
            elif self.ctrl_be.is_server_running() == "offline":
                self.server_offline_event()
            else:
                self.server_stopped_event()
        else:
            self.ctrl_be.query_server_info()

        self.refresh(self.ctrl_be.is_server_running())

    def update(self):
        self._update_timeout = None
        self.connection_info.update(self.ctrl_be)
        self.status.refresh_status(self.ctrl_be.is_server_running(verbose=False))
        info = self.ctrl_be.server_variables
        status = self.ctrl_be.status_variables
        plugins = dict(self.ctrl_be.server_active_plugins) # plugin -> type

        repl_error = None
        res = None
        try:
            res = self.ctrl_be.exec_query("SHOW SLAVE STATUS")
        except QueryError as e:
            if e.error == 1227:
                repl_error = "Insufficient privileges to view replica status"
            else:
                repl_error = "Error querying status: %s" % str(e)
        repl = {}
        if res and res.nextRow():
            for field in ["Slave_IO_State", "Master_Host"]:
                repl[field] = res.stringByName(field)

        disk_space = "unable to retrieve"
        if self.ctrl_be.server_control and info.get("datadir"):
            disk_space = self.ctrl_be.server_helper.get_available_space(info.get("datadir"))

        # Update the controls in the UI
        self.suspend_layout()
        self.controls["Disk Space in Data Dir"][0].set_text(disk_space)

        table = self.controls["Replica"][0]

        if repl:
            table.remove(self.controls[""][0])
            self.setup_info_table(table,
                                  [("Replica IO State", repl.get("Slave_IO_State")),
                                   ("Source Host", repl.get("Master_Host")),
                                   ("GTID Mode", info.get("gtid_mode"))],
                                  plugins)
        else:
            self.controls[""][0].set_text(repl_error or "this server is not a replica in a replication setup")
        table.relayout()

        for key, (control, value_source) in list(self.controls.items()):
            if callable(value_source):
                if isinstance(control, mforms.Label):
                    resp = value_source(info, plugins, status)
                    control.set_text(resp if resp else "n/a")
                else:
                    value = value_source(info, plugins, status)
                    if type(value) is tuple:
                        control.set_state(value[0])
                        if value[0] and value[1]:
                            control.set_text(value[1])
                    else:
                        control.set_state(value)

        self.resume_layout()

        mforms.Utilities.driver_shutdown()
        return False

    def create_info_sections(self):
        info = self.ctrl_be.server_variables
        status = self.ctrl_be.status_variables
        plugins = dict(self.ctrl_be.server_active_plugins) # plugin -> type

        repl = {}
        disk_space = "checking..."

        def tristate(value, true_value = None):
            if true_value is not None and value == true_value:
                return True
            if value == "OFF" or value == "NO":
                return False
            elif value and true_value is None:
                return True
            return None

        semi_sync_master = tristate(info.get("rpl_semi_sync_master_enabled"))
        semi_sync_slave = tristate(info.get("rpl_semi_sync_slave_enabled"))
        semi_sync_status = (semi_sync_master or semi_sync_slave, "(%s)"% ", ".join([x for x in [semi_sync_master and "master", semi_sync_slave and "slave"] if x]))
        memcached_status = True if 'daemon_memcached' in plugins else None
        
        if not repl:
            if semi_sync_master:
                semi_sync_master = False
            if semi_sync_slave:
                semi_sync_slave = False

        # the params to be passed to the lambdas
        params = (info, plugins, status)

        self.add_info_section_2("Available Server Features",
                              [("Performance Schema", lambda info, plugins, status: tristate(info.get("performance_schema"))),
                               ("Thread Pool", lambda info, plugins, status: tristate(info.get("thread_handling"), "loaded-dynamically")),
                               ("Memcached Plugin", lambda info, plugins, status: memcached_status),
                               ("Semisync Replication Plugin", lambda info, plugins, status: semi_sync_status),
                               ("SSL Availability", lambda info, plugins, status: info.get("have_openssl") == "YES" or info.get("have_ssl") == "YES"),
                               ("Windows Authentication", lambda info, plugins, status: "authentication_windows" in plugins) if self.server_profile.target_is_windows else ("PAM Authentication", lambda info, plugins, status: "authentication_pam" in plugins),
                               ("Password Validation", lambda info, plugins, status: (tristate(info.get("validate_password_policy")), "(Policy: %s)" % info.get("validate_password_policy"))),
                               ("Audit Log", lambda info, plugins, status: (tristate(info.get("audit_log_policy")), "(Log Policy: %s)" % info.get("audit_log_policy"))),
                               ("Firewall", lambda info, plugins, status: tristate(info.get("mysql_firewall_mode"))),
                               ("Firewall Trace", lambda info, plugins, status: tristate(info.get("mysql_firewall_trace")))],
                                params)

        log_output = info.get("log_output", "FILE")

        self.add_info_section("Server Directories",
                              [("Base Directory", lambda info, plugins, status: info.get("basedir")),
                               ("Data Directory", lambda info, plugins, status: info.get("datadir")),
                               ("Disk Space in Data Dir", disk_space),
                               ("InnoDB Data Directory", lambda info, plugins, status: info.get("innodb_data_home_dir")) if info.get("innodb_data_home_dir") else None,
                               ("Plugins Directory", lambda info, plugins, status: info.get("plugin_dir")),
                               ("Tmp Directory", lambda info, plugins, status: info.get("tmpdir")),
                               ("Error Log", lambda info, plugins, status: (info.get("log_error") and info.get("log_error")!="OFF", info.get("log_error"))),
                               ("General Log", lambda info, plugins, status: (info.get("general_log")!="OFF" and log_output != "NONE", info.get("general_log_file") if "FILE" in log_output else "[Stored in database]")),
                               ("Slow Query Log", lambda info, plugins, status: (info.get("slow_query_log")!="OFF" and log_output != "NONE", info.get("slow_query_log_file") if "FILE" in log_output else "[Stored in database]"))],
                              params)

        self.add_info_section("Replica",
                              [("", "checking...")],
                              params)

        self.add_info_section("Authentication",
                              [("SHA256 Password Private Key", lambda info, plugins, status: info.get("sha256_password_private_key_path")),
                               ("SHA256 Password Public Key", lambda info, plugins, status: info.get("sha256_password_public_key_path"))],
                              params)

        self.add_info_section("SSL",
                              [("SSL CA", lambda info, plugins, status: info.get("ssl_ca") or "n/a"),
                               ("SSL CA Path", lambda info, plugins, status: info.get("ssl_capath") or "n/a"),
                               ("SSL Cert", lambda info, plugins, status: info.get("ssl_cert") or "n/a"),
                               ("SSL Cipher", lambda info, plugins, status: info.get("ssl_cipher") or "n/a"),
                               ("SSL CRL", lambda info, plugins, status: info.get("ssl_crl") or "n/a"),
                               ("SSL CRL Path", lambda info, plugins, status: info.get("ssl_crlpath") or "n/a"),
                               ("SSL Key", lambda info, plugins, status: info.get("ssl_key") or "n/a")],
                              params)

        log_debug3("mysql_firewall_trace: %s\n" % info.get("mysql_firewall_trace"))
        log_debug3("Firewall_access_denied: %s\n" % status.get("Firewall_access_denied"))
        log_debug3("Firewall_access_granted: %s\n" % status.get("Firewall_access_granted"))
        log_debug3("Firewall_cached_entries: %s\n" % status.get("Firewall_cached_entries"))

        if info.get("mysql_firewall_mode") == "ON":
            self.add_info_section("Firewall",
                                  [("Access Denied", lambda info, plugins, status: str(status.get("Firewall_access_denied")) or "n/a"),
                                  ("Access Granted", lambda info, plugins, status: str(status.get("Firewall_access_granted")) or "n/a"),
                                  ("Access Suspicious", lambda info, plugins, status: str(status.get("Firewall_access_suspicious")) or "n/a"),
                                  ("Cached Entries", lambda info, plugins, status: str(status.get("Firewall_cached_entries")) or "n/a")],
                                  params)

    def add_info_section_2(self, title, info, params):
        label = mforms.newLabel(title)
        label.set_style(mforms.BigBoldStyle)
        self.content.add(label, False, True)
        sep = mforms.newBox(False)
        sep.set_back_color("#b2b2b2")
        sep.set_size(-1, 1)
        self.content.add(sep, False, True)

        hbox = mforms.newBox(True)

        info_table = self.make_info_table(info[:len(info)//2], params)
        hbox.add(info_table, True, True)
        info_table = self.make_info_table(info[len(info)//2:], params)
        hbox.add(info_table, True, True)

        self.content.add(hbox, False, True)
        self.content.get_parent().relayout()


    def add_info_section(self, title, info, params):
        label = mforms.newLabel(title)
        label.set_style(mforms.BigBoldStyle)
        self.content.add(label, False, True)
        sep = mforms.newBox(False)
        sep.set_back_color("#b2b2b2")
        sep.set_size(-1, 1)
        self.content.add(sep, False, True)

        info_table = self.make_info_table([x for x in info if x], params)
        self.content.add(info_table, False, True)
        self.controls[title] = (info_table, None)
        self.content.get_parent().relayout()


    def make_info_table(self, info, params):
        info_table = mforms.newTable()
        info_table.set_column_spacing(8)
        info_table.set_row_spacing(6)
        info_table.set_column_count(2)
        return self.setup_info_table(info_table, info, params)


    def setup_info_table(self, info_table, info, params):
        info_table.set_row_count(len(info)+1)
        for i, item in enumerate(info):
            (label, value_source) = item
            if callable(value_source):
                value = value_source(*params)
            else:
                value = value_source

            if label in self.controls:
                info_table.remove(self.controls[label][0])
            else:
                l = mforms.newLabel(label + ":")
                l.set_name(label)
                info_table.add(l, 0, 1, i, i+1, mforms.VFillFlag|mforms.HFillFlag)
            is_gtid_mode_setable = label == 'GTID Mode:' and self.ctrl_be.target_version >= Version(5, 7, 6)
            if type(value) is bool or value is None:
                b = StateIcon()
                if label and label != '':
                    b.set_name(label + " Value")
                b.set_state(value)
                info_table.add(b, 1, 2, i, i+1, mforms.HFillFlag | mforms.HExpandFlag | mforms.VFillFlag)
                self.controls[label] = (b, value_source)
            elif type(value) is tuple:
                b = StateIcon()
                if label and label != '':
                    b.set_name(label + " Value")
                b.set_state(value[0])
                if value[0] and value[1]:
                    b.set_text(value[1])
                info_table.add(b, 1, 2, i, i+1, mforms.HFillFlag | mforms.HExpandFlag | mforms.VFillFlag)
                self.controls[label] = (b, value_source)
            else:
                if is_gtid_mode_setable:
                    self.gtid_mode_selector = mforms.newSelector()
                    if label and label != '':
                        self.gtid_mode_selector.set_name(label + " Value")
                    self.gtid_mode_selector.add_items(["OFF", "UPGRADE_STEP_1", "UPGRADE_STEP_1", "ON"])
                    self.gtid_mode_selector.set_selected(self.gtid_mode_selector.index_of_item_with_title(value_source))
                    self.gtid_mode_selector.add_changed_callback(self._gtid_mode_changed)
                    info_table.add(self.gtid_mode_selector, 1, 2, i, i + 1, mforms.HFillFlag | mforms.HExpandFlag | mforms.VFillFlag)
                    self.controls[label] = (self.gtid_mode_selector, value_source)
                else:
                    l2 = mforms.newLabel(value or "")
                    if label and label != '':
                        l2.set_name(label + " Value")
                    l2.set_style(mforms.BoldStyle)
                    info_table.add(l2, 1, 2, i, i + 1, mforms.HFillFlag | mforms.HExpandFlag | mforms.VFillFlag)
                    self.controls[label] = (l2, value_source)
        info_table.add(mforms.newLabel(""), 0, 1, len(info), len(info)+1, mforms.HFillFlag) # blank space
        return info_table


    #---------------------------------------------------------------------------
    def page_deactivated(self):
        pass
    
    #---------------------------------------------------------------------------
    def shutdown(self):
        if self._update_timeout:
            mforms.Utilities.cancel_timeout(self._update_timeout)
            self._update_timeout = None
        self.status.stop()

    #---------------------------------------------------------------------------
    def _gtid_mode_changed(self):
        new_value = self.gtid_mode_selector.get_string_value()
        try:
            self.ctrl_be.exec_query("SET @@GLOBAL.GTID_MODE = %s;" % new_value)
        except QueryError as e:
            log_error("Error update GTID mode: %s" % str(e))

