# Application: PiHole-Panel
# Author: Dale Osm (https://github.com/daleosm/)
# GNU GENERAL PUBLIC LICENSE

import urllib.request
import urllib.error
import xml.etree.ElementTree as ET
import json
import gi
import sys
import os
import hashlib
from pathlib import Path
from urllib.request import urlopen

from gi.repository import Gtk, Gio
from gi.repository import GLib as glib

from gtk_assistant import AssistantApp
gi.require_version("Gtk", "3.0")

# AssistantApp window class
wc = AssistantApp()

# Configuration variables of the app
update_interval_seconds = 3  # Time interval between updates
version_number = "2.6"  # Change this on every release!
config_directory = str(Path.home()) + "/.config"  # Directory of config file
config_filename = "pihole_panel_configs.xml"  # Filename of config file


class GridWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self)
        self.assistant = Gtk.Assistant()
        grid = Gtk.Grid(margin=4)
        grid.set_column_homogeneous(True)
        self.add(grid)

        self.grid = grid

        self.status_label, self.status_button = self.draw_status_elements()
        self.statistics_frame = self.draw_statistics_frame()
        self.top_queries_frame = self.draw_top_queries_frame()
        self.top_ads_frame = self.draw_top_ads_frame()
        self.updates_frame = self.draw_updates_frame()
        self.header_bar = self.draw_header_bar()
        self.hosts_combo = self.draw_hosts_combo()
        # Initial data fetch-and-display
        self.fetch_data_and_update_display(
            base_url, web_password)

        # Create a timer --> self.on_timer will be called periodically
        glib.timeout_add_seconds(update_interval_seconds, self.on_timer)

    def on_timer(self):
        # This function is called periodically
        print("Timer running...")

        # Get actively selected host ID
        index = hosts_combo.get_active()
        model = hosts_combo.get_model()
        item = model[index]

        try:
            urlopen(item[1], timeout=5).read()

        except urllib.error.URLError as e:
            dialog = Gtk.MessageDialog(self.assistant, 0, Gtk.MessageType.ERROR,
                                       Gtk.ButtonsType.CANCEL, "Invalid combination of Pi Address and Password")

            dialog.connect("response", lambda *a: dialog.destroy())
            dialog.set_position(Gtk.WindowPosition.CENTER)
            dialog.run()
            restart_program()
            return False

        except urllib.error.HTTPError as e:
            dialog = Gtk.MessageDialog(self.assistant, 0, Gtk.MessageType.ERROR,
                                       Gtk.ButtonsType.CANCEL, "Invalid combination of Pi Address and Password")

            dialog.connect("response", lambda *a: dialog.destroy())
            dialog.set_position(Gtk.WindowPosition.CENTER)
            dialog.run()
            restart_program()
            return False

        else:
            # Decide what hostname has been selected
            if item[0] == 1:
                self.fetch_data_and_update_display(base_url, web_password)
            if item[0] == 2:
                self.fetch_data_and_update_display(
                    configs["two_ip_address"], configs["two_key_code"])

            return True

    def version_check(self):
        # Fetch version number from GitHub repo
        get_version = urlopen(
            "https://raw.githubusercontent.com/daleosm/PiHole-Panel/master/VERSION").read()
        version_decoded = get_version.decode("utf-8")
        latest_version = version_decoded.strip("\n")

        if latest_version > version_number:
            return True
        else:
            return False

    def fetch_data_and_update_display(self, host_url, web_password):
        # Fetch required data from the Pi-Hole API, and update the window elements using responses received
        status, statistics_dict = self.get_status_and_statistics(host_url)
        readable_statistics_dict = make_dictionary_keys_readable(
            statistics_dict)
        top_queries_dict, top_ads_dict = self.get_top_items(
            host_url, web_password)

        # Update frames
        self.update_status_elements(status)
        self.update_statistics_frame(readable_statistics_dict)
        self.update_top_queries_frame(top_queries_dict)
        self.update_top_ads_frame(top_ads_dict)

    # Following 6 functions draw the elements of the window
    def draw_status_elements(self):
        button1 = Gtk.Switch(halign=Gtk.Align.CENTER)
        button1.connect("notify::active", self.on_status_switch_activated)

        status_label = Gtk.Label(halign=Gtk.Align.END)
        status_label.set_markup("<b>%s</b>" % "Status:")

        box = Gtk.Box(spacing=3)
        box.pack_start(status_label, True, True, 4)
        box.pack_start(button1, True, True, 4)

        # To add space between elements
        empty_label_1 = Gtk.Label(label="", margin=1)
        self.grid.attach(empty_label_1, 2, 1, 1, 1)
        self.grid.attach(box, 2, 2, 1, 1)

        # To add space between elements
        empty_label_2 = Gtk.Label(label="", margin=1)
        self.grid.attach(empty_label_2, 2, 3, 1, 1)

        return status_label, button1

    def draw_sub_window(self, button):
        self.popup = Gtk.Window()
        self.popup.set_resizable(False)
        self.popup.set_title("Settings")
        self.popup.set_position(Gtk.WindowPosition.CENTER)
        page_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        self.popup.add(page_box)
        self.popup.set_size_request(100, 250)

        # Create IP Address box
        ip_address_box = Gtk.HBox(homogeneous=False, spacing=12)

        ip_address_label = Gtk.Label(label="Pi1 Address:")
        ip_address_label.set_justify(Gtk.Justification.LEFT)
        ip_address_box.pack_start(ip_address_label, True, False, 6)

        ip_address_entry = Gtk.Entry()
        ip_address_entry.set_text(configs["ip_address"])
        ip_address_box.pack_start(ip_address_entry, True, False, 6)

        # Pack IP Address box
        page_box.pack_start(ip_address_box, True, False, 6)

        # Create key code box
        key_code_box = Gtk.HBox(homogeneous=False, spacing=12)

        key_code_label = Gtk.Label(label="Pi1 Password:")
        key_code_label.set_justify(Gtk.Justification.LEFT)
        key_code_box.pack_start(key_code_label, True, False, 6)

        key_code_entry = Gtk.Entry()
        key_code_entry.set_visibility(False)

        key_code_entry.set_text(configs["key_code"])
        key_code_box.pack_start(key_code_entry, True, False, 6)

        # Pack key code box
        page_box.pack_start(key_code_box, True, False, 6)

        # Create 2IP Address box
        two_ip_address_box = Gtk.HBox(homogeneous=False, spacing=12)

        two_ip_address_label = Gtk.Label(label="Pi2 Address:  ")
        two_ip_address_label.set_justify(Gtk.Justification.LEFT)
        two_ip_address_box.pack_start(two_ip_address_label, True, False, 6)

        two_ip_address_entry = Gtk.Entry()

        if "two_ip_address" in configs:
            if configs["two_ip_address"] is not None:
                two_ip_address_entry.set_text(configs["two_ip_address"])

        two_ip_address_box.pack_start(two_ip_address_entry, False, False, 6)

        # Pack 2IP Address box
        page_box.pack_start(two_ip_address_box, True, False, 6)

        # Create key code box

        two_key_code_box = Gtk.HBox(homogeneous=False, spacing=6)

        two_key_code_label = Gtk.Label(label="Pi2 Password:")
        two_key_code_label.set_justify(Gtk.Justification.LEFT)
        two_key_code_box.pack_start(two_key_code_label, True, False, 6)

        two_key_code_entry = Gtk.Entry()
        two_key_code_entry.set_visibility(False)

        if "two_key_code" in configs:
            if configs["two_key_code"] is not None:
                two_key_code_entry.set_text(configs["two_key_code"])

        two_key_code_box.pack_start(two_key_code_entry, False, False, 6)

        # Pack key code box
        page_box.pack_start(two_key_code_box, True, False, 6)

        # Create save button box
        button_box = Gtk.HBox(homogeneous=False, spacing=12)
        button = Gtk.Button.new_with_label("Save")

        button.connect("clicked", self.on_settings_save,
                       ip_address_entry, key_code_entry, two_ip_address_entry, two_key_code_entry)
        button_box.pack_end(button, False, False, 4)

        # Pack save button box
        page_box.pack_start(button_box, True, False, 12)

        self.popup.show_all()

    def draw_header_bar(self):

        hb = Gtk.HeaderBar()
        hb.set_show_close_button(True)
        self.set_titlebar(hb)

        button = Gtk.Button()
        icon = Gio.ThemedIcon(name="open-menu-symbolic")
        image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON)
        button.add(image)
        hb.pack_end(button)

        button.connect("clicked", self.draw_sub_window)

        return button

    def draw_hosts_combo(self):

        name_store = Gtk.ListStore(int, str)
        name_store.append([1, configs["ip_address"]])

        if "two_ip_address" in configs:
            if configs["two_ip_address"] is not None:
                name_store.append([2, configs["two_ip_address"]])

        global hosts_combo  # This is bad and should be removed at some point

        hosts_combo = Gtk.ComboBox.new_with_model_and_entry(name_store)
        hosts_combo.set_entry_text_column(1)
        hosts_combo.set_active(0)

        hosts_combo.connect("changed", self.on_hosts_combo_changed)

        self.grid.attach(hosts_combo, 1, 2, 1, 1)
        return hosts_combo

    def draw_statistics_frame(self):
        frame_vert = Gtk.Frame(label="Statistics")
        frame_vert.set_border_width(10)
        frame_vert.table_box = None

        self.grid.attach(frame_vert, 0, 4, 1, 1)
        return frame_vert

    def draw_top_queries_frame(self):
        frame_vert = Gtk.Frame(label="Top Queries")
        frame_vert.set_border_width(10)
        frame_vert.table_box = None

        self.grid.attach(frame_vert, 1, 4, 1, 1)
        return frame_vert

    def draw_top_ads_frame(self):
        frame_vert = Gtk.Frame(label="Top Ads")
        frame_vert.set_border_width(10)
        frame_vert.table_box = None

        self.grid.attach(frame_vert, 2, 4, 1, 1)
        return frame_vert

    def draw_updates_frame(self):

        if self.version_check() is True:
            label = Gtk.Label()
            label.set_markup("There is a new version <a href=\"https://github.com/daleosm/PiHole-Panel\" "
                             "title=\"Click to find out more\">update available</a>.")
            label.set_line_wrap(True)
            label.set_justify(Gtk.Justification.FILL)

            self.grid.attach(label, 2, 5, 1, 1)
            return label

    # Following 4 functions updates the values of window elements with given (fetched) values
    def update_status_elements(self, status):
        # Activate/ deactivate the button so that it reflects the actual current status
        if status == "enabled":
            self.status_button.set_active(True)
        else:
            self.status_button.set_active(False)
        # Update the status label
        self.status_label.set_markup("<b>Status:</b> " + status)

    def update_statistics_frame(self, statistics_dict):
        if self.statistics_frame.table_box:
            # Destroy and remove current data table box
            self.statistics_frame.table_box.destroy()
        # Create new data table box with given values
        table_box = self.create_table_box(
            "Statistic", "Value", statistics_dict)
        # Save so that it can be destroyed later
        self.statistics_frame.table_box = table_box
        self.statistics_frame.add(table_box)

        table_box.show_all()    # Show the new data table box

    def update_top_queries_frame(self, top_queries_dict):
        if self.top_queries_frame.table_box:
            self.top_queries_frame.table_box.destroy()

        if top_queries_dict:
            table_box = self.create_table_box(
                "Domain", "Hits", top_queries_dict)
            # Save so that it can be destroyed later
            self.top_queries_frame.table_box = table_box
            self.top_queries_frame.add(table_box)
            table_box.show_all()

    def update_top_ads_frame(self, top_ads_dict):
        if self.top_ads_frame.table_box:
            self.top_ads_frame.table_box.destroy()

        if top_ads_dict:
            table_box = self.create_table_box("Domain", "Hits", top_ads_dict)
            # Save so that it can be destroyed later
            self.top_ads_frame.table_box = table_box
            self.top_ads_frame.add(table_box)
            table_box.show_all()

    # Following 3 functions send requests to Pi-Hole API and return the response received
    def get_status_and_statistics(self, base_url):

        url = base_url + "api.php?summary"
        result = urlopen(url, timeout=15).read()
        json_obj = json.loads(result.decode())

        status = str(json_obj["status"])
        del json_obj["status"]  # We only want the statistics

        if "gravity_last_updated" in json_obj:
            del json_obj["gravity_last_updated"]  # This needs more work

        if "dns_queries_all_types" in json_obj:
            del json_obj["dns_queries_all_types"]  # Useless

        if "reply_NODATA" in json_obj:
            del json_obj["reply_NODATA"]  # Useless

        if "reply_NXDOMAIN" in json_obj:
            del json_obj["reply_NXDOMAIN"]  # Useless

        if "reply_CNAME" in json_obj:
            del json_obj["reply_CNAME"]  # Useless

        if "reply_IP" in json_obj:
            del json_obj["reply_IP"]  # Useless

        return status, json_obj

    def get_top_items(self, base_url, web_password):
        url = base_url + "api.php?topItems&auth=" + web_password
        results = urlopen(url, timeout=15).read()
        json_obj = json.loads(results.decode())

        top_queries_dict = json_obj["top_queries"]
        top_ads_dict = json_obj["top_ads"]

        return top_queries_dict, top_ads_dict

    # Function that runs when the status button is clicked
    def on_status_switch_activated(self, switch, gparam):
        if switch.get_active():
            status = self.send_enable_request()
        else:
            status = self.send_disable_request()

        self.update_status_elements(status)

    # Following 2 functions sends requests to Pi-Hole API to enable/ disable it
    def send_enable_request(self):
        url = base_url + "api.php?enable&auth=" + web_password
        results = urlopen(url, timeout=15).read()
        json_obj = json.loads(results.decode())
        return json_obj["status"]

    def send_disable_request(self):
        url = base_url + "api.php?disable&auth=" + web_password
        results = urlopen(url, timeout=15).read()
        json_obj = json.loads(results.decode())
        return json_obj["status"]

    # This function creates a box that contains data in the "items_dict" arranged as a 2-column table
    # This could be useful in other projects
    def create_table_box(self, left_heading, right_heading, items_dict):
        # First column box
        first_column_box = Gtk.Box(
            orientation=Gtk.Orientation.VERTICAL, spacing=0)
        first_col_heading_label = Gtk.Label(margin=4, halign=Gtk.Align.START)
        first_col_heading_label.set_markup(
            "<u>" + left_heading + "</u>")   # Column heading label
        first_column_box.pack_start(first_col_heading_label, False, False, 4)

        # Second column box
        second_column_box = Gtk.Box(
            orientation=Gtk.Orientation.VERTICAL, spacing=0)
        second_col_label = Gtk.Label(margin=4, halign=Gtk.Align.END)
        second_col_label.set_markup(
            "<u>" + right_heading + "</u>")  # Column heading label
        second_column_box.pack_start(second_col_label, False, False, 4)

        # Add rows to the two two columns
        for first, second in items_dict.items():
            info = (first[:36] + "..") if len(first) > 36 else first
            first_col_label = Gtk.Label(
                label=str(info), margin=4, halign=Gtk.Align.START)
            first_column_box.pack_start(first_col_label, False, False, 0)

            second_col_label = Gtk.Label(
                label=str(second), margin=4, halign=Gtk.Align.END)
            second_column_box.pack_start(second_col_label, False, False, 0)

        # Include the two boxes in one wrapper box (table box)
        table_box = Gtk.Box(spacing=8)
        table_box.pack_start(first_column_box, True, True, 0)
        table_box.pack_start(second_column_box, True, True, 0)

        return table_box

    def on_settings_save(self, button, ip_address_entry, key_code_entry, two_ip_address_entry, two_key_code_entry):
        # Make sure config has new entries
        if "two_ip_address" not in configs:
            configs["two_ip_address"] = ""
            wc.save_configs(config_directory, config_filename, configs)

        if "two_key_code" not in configs:
            configs["two_key_code"] = ""
            wc.save_configs(config_directory, config_filename, configs)

        configs["ip_address"] = ip_address_entry.get_text()
        configs["two_ip_address"] = two_ip_address_entry.get_text()
        configs2 = {}
        configs3 = {}

        if key_code_entry.get_text() != configs["key_code"]:
            configs2["key_code"] = key_code_entry.get_text()

            configs["key_code"] = hashlib.sha256(
                configs2["key_code"].encode("utf-8")).hexdigest()
            configs["key_code"] = hashlib.sha256(
                configs["key_code"].encode("utf-8")).hexdigest()

        if two_key_code_entry.get_text() != configs["two_key_code"]:
            configs3["two_key_code"] = two_key_code_entry.get_text()
            configs["two_key_code"] = hashlib.sha256(
                configs3["two_key_code"].encode("utf-8")).hexdigest()
            configs["two_key_code"] = hashlib.sha256(
                configs["two_key_code"].encode("utf-8")).hexdigest()

        # Check updated settings for Pi1
        url = configs["ip_address"] + \
            "api.php?topItems&auth=" + configs["key_code"]

        try:
            urlopen(url, timeout=15).read()
        except urllib.error.URLError as e:
            dialog = Gtk.MessageDialog(self.assistant, 0, Gtk.MessageType.ERROR,
                                       Gtk.ButtonsType.CANCEL, "Invalid combination of Pi Address and Password")

            dialog.connect("response", lambda *a: dialog.destroy())
            dialog.set_position(Gtk.WindowPosition.CENTER)
            dialog.run()
        except urllib.error.HTTPError as e:
            dialog = Gtk.MessageDialog(self.assistant, 0, Gtk.MessageType.ERROR,
                                       Gtk.ButtonsType.CANCEL, "Invalid combination of Pi Address and Password")

            dialog.connect("response", lambda *a: dialog.destroy())
            dialog.set_position(Gtk.WindowPosition.CENTER)
            dialog.run()

        results = urlopen(url, timeout=15).read()
        json_obj = json.loads(results.decode())
        if "top_queries" not in json_obj:
            dialog = Gtk.MessageDialog(self.assistant, 0, Gtk.MessageType.ERROR,
                                       Gtk.ButtonsType.CANCEL, "Invalid combination of Pi Address and Password")

            dialog.connect("response", lambda *a: dialog.destroy())
            dialog.set_position(Gtk.WindowPosition.CENTER)
            dialog.run()

        # Check updated settings for Pi2
        if two_ip_address_entry.get_text():
            url = configs["two_ip_address"] + \
                "api.php?topItems&auth=" + configs["two_key_code"]

            try:
                urlopen(url, timeout=15).read()
            except urllib.error.URLError as e:
                dialog = Gtk.MessageDialog(self.assistant, 0, Gtk.MessageType.ERROR,
                                           Gtk.ButtonsType.CANCEL, "Invalid combination of Pi Address and Password")

                dialog.connect("response", lambda *a: dialog.destroy())
                dialog.set_position(Gtk.WindowPosition.CENTER)
                dialog.run()
            except urllib.error.HTTPError as e:
                dialog = Gtk.MessageDialog(self.assistant, 0, Gtk.MessageType.ERROR,
                                           Gtk.ButtonsType.CANCEL, "Invalid combination of Pi Address and Password")

                dialog.connect("response", lambda *a: dialog.destroy())
                dialog.set_position(Gtk.WindowPosition.CENTER)
                dialog.run()
            results = urlopen(url, timeout=15).read()
            json_obj = json.loads(results.decode())
            if "top_queries" not in json_obj:
                dialog = Gtk.MessageDialog(self.assistant, 0, Gtk.MessageType.ERROR,
                                           Gtk.ButtonsType.CANCEL, "Invalid combination of Pi Address and Password")

                dialog.connect("response", lambda *a: dialog.destroy())
                dialog.set_position(Gtk.WindowPosition.CENTER)
                dialog.run()
        else:
            if configs["two_ip_address"] == "":
                configs["two_key_code"] = None

        result = wc.validate_configs(configs)

        if result:
            wc.save_configs(config_directory, config_filename, configs)

            restart_program()

    def on_hosts_combo_changed(self, combo):
        text = combo.get_active()
        index = combo.get_active()
        model = combo.get_model()
        item = model[index]

        if text is not None:
            print("Selected: host=%s" % item[1])


def restart_program():
    """Restarts the current program.
    Note: this function does not return. Any cleanup action (like
    saving data) must be done before calling this function."""
    python = sys.executable
    os.execl(python, python, * sys.argv)


# This function makes the keys in the dictionary human-readable
def make_dictionary_keys_readable(dict):
    new_dict = {}
    for key, val in dict.items():
        # Replace underscores with spaces and convert to Title Case
        new_key = key.replace("_", " ").title()
        new_dict[new_key] = val
        # print("{} --> {}".format(key, new_key))

    return new_dict


if wc.is_config_file_exist(config_directory, config_filename):

    configs = wc.load_configs(config_directory, config_filename)

    base_url = configs["ip_address"]
    web_password = configs["key_code"]

    wc.validate_configs(configs)

    win = GridWindow()
    win.set_icon_from_file("/usr/lib/pihole-panel/pihole-panel.png")
    win.connect("destroy", Gtk.main_quit)
    win.set_wmclass("PiHole Panel", "PiHole Panel")
    win.set_title("PiHole Panel")
    win.set_position(Gtk.WindowPosition.CENTER)
    win.show_all()

Gtk.main()