#!/usr/bin/env python3
import ipaddress
import json
import os
import subprocess
import locale
from locale import gettext as _
from typing import Optional, Tuple, Dict, Union

import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gio', '2.0')
from gi.repository import Gtk, Gio, GLib

DOMAIN = 'barium_pxeboot'
LOCALE_DIR = '/usr/share/locale'
locale.bindtextdomain(DOMAIN, LOCALE_DIR)
locale.textdomain(DOMAIN)

iconpath='/usr/share/icons'
if os.path.exists("barium_pxeboot.ui"):
    ui_file = "barium_pxeboot.ui"
    handler = "./barium_pxeboot.helper"
    iconpath = os.path.abspath('./')
else:
    ui_file = "/usr/share/barium_pxeboot/barium_pxeboot.ui"
    handler = "/usr/libexec/barium_pxeboot.helper"

ICON = os.path.join(iconpath, 'barium_pxeboot.svg')

cfgdir = os.environ['HOME'] + '/.config/barium_pxeboot'
json_conf = {}

mapping = {
    'ip_entry': 'IP',
    'prefix_entry': 'PREFIX',
    'subnet_entry': 'SUBNET',
    'mask_entry': 'NETMASK',
    'gateway_entry': 'GATEWAY',
    'mask_entry': 'NETMASK',
    'broadcast_entry': 'BROADCAST',
    'first_addr_entry': 'FIRSTADDRESS',
    'last_addr_entry': 'LASTADDRESS',
    'tftp_entry': 'TFTP',
    'dns_entry': 'DNS',
    'nfs_entry': 'NFS'
}

msg = {
    1  : _('IMPORTANT WARNINGS BEFORE APPLYING CHANGES'),
    2  : _('DHCP SERVER WARNING'),
    3  : _('Subnet Restriction:'),
    4  : _('There can be only one active DHCP server in a subnet to avoid IP address conflicts and network instability.'),
    5  : _('Action Required:'),
    6  : _('Do not enable the'),
    7  : _('"Use internal DHCPD"'),
    8  : _('Consequence:'),
    9  : _('Running multiple DHCP servers will cause network disruptions and client connectivity issues.'),
    10 : _('CONFIGURATION OVERWRITE WARNING'),
    11 : _('What happens when you click "Apply":'),
    12 : _('The program will completely overwrite the following configuration files with values from the GUI interface:'),
    13 : _('DHCP server configuration'),
    14 : _('TFTP server configuration files'),
    15 : _('NFS export settings'),
    16 : _('All PXE boot configuration and menu files'),
    17 : _('Network interface bindings and service parameters'),
    18 : _('Alternative Method Available:'),
    19 : _('Instead of using the GUI, you can:'),
    20 : _('Edit configuration files manually'),
    21 : _('(preserves your custom settings)'),
    22 : _('Control the PXE server using the systemd service:'),
    23 : _('# Start PXE server'),
    24 : _('# Stop PXE server'),
    25 : _('# Hot apply configs'),
    26 : _('# Check status'),
    27 : _('# Autostart service'),
    28 : _('# Disable autostart'),
    29 : _('Manual editing is recommended for advanced users who need to preserve custom configurations between GUI sessions.'),
    30 : _('checkbox if another DHCP server (router, dedicated server, etc.) already exists on your network.'),
    31 : _("FIREWALL WARNING"),
    32 : _("For PXE server operation, you need to allow incoming connections for the same subnet configured for DHCPD in firewall settings."),
    33 : _("In modular ROSA distributions, the easiest way is to uncomment IPTABLESTRUSTEDIP in ROSA.ini with the required subnet and reboot."),
    34 : _("Note:"),
    35 : _("Without proper firewall rules, PXE clients will not be able to connect to the TFTP server for boot files."),
    36 : _('Example iptables command:')
}

def warning_text(IP, PREFIX):
    header_warning_text = f"""<span size="x-large" weight="bold" foreground="#2c3e50">
⚠ {msg[1]}
</span>"""

    first_warning_text = f"""<span size="large" weight="bold" foreground="#c0392b">
1. {msg[2]}
</span>

<span weight="bold">{msg[3]}</span>
{msg[4]}

<span weight="bold">{msg[5]}</span>
{msg[6]}<span style="italic">{msg[7]}</span>{msg[30]}

<span foreground="#7f8c8d" size="small">
⚠ <span weight="bold">{msg[8]}</span>
{msg[9]}
</span>"""

    second_warning_text = f"""<span size="large" weight="bold" foreground="#c0392b">
2. {msg[31]}
</span>

<span weight="bold">{msg[32]}</span>

    ( <span font_family="monospace">{IP}/{PREFIX}</span> )

{msg[33]}

<span weight="bold">{msg[36]}</span>
<tt>iptables -I INPUT -s {IP}/{PREFIX} -p tcp -m multiport --dports 80,2049 -j ACCEPT</tt>
<tt>iptables -I INPUT -s {IP}/{PREFIX} -p udp -m multiport --dports 67,68,69,2049 -j ACCEPT</tt>

<span foreground="#7f8c8d" size="small">
⚠ <span weight="bold">{msg[34]}</span>
{msg[35]}
</span>"""

    third_warning_text = f"""<span size="large" weight="bold" foreground="#c0392b">
3. {msg[10]}
</span>

<span weight="bold">{msg[11]}</span>
{msg[12]}

<span foreground="#2c3e50">
• {msg[13]} (<span font_family="monospace">dhcpd.conf</span>)
• {msg[14]}
• {msg[15]}
• {msg[16]}
• {msg[17]}
</span>

<span weight="bold">{msg[18]}</span>
{msg[19]}

1. <span weight="bold">{msg[20]}</span> 
   {msg[21]}

2. <span weight="bold">{msg[22]}</span>
   
<span font_family="monospace" background="#ecf0f1" foreground="#2c3e50">
  systemctl start    barium_pxeboot.service  {msg[23]}
  systemctl stop     barium_pxeboot.service  {msg[24]}  
  systemctl restart  barium_pxeboot.service  {msg[25]}
  systemctl status   barium_pxeboot.service  {msg[26]}
  systemctl enable   barium_pxeboot.service  {msg[27]}
  systemctl disable  barium_pxeboot.service  {msg[28]}
</span>
<span foreground="#7f8c8d" size="small">
💡 <span weight="bold">Tip:</span> 
{msg[29]}
</span>"""
    return (header_warning_text, first_warning_text, second_warning_text, third_warning_text)
    
def json_write():
    os.makedirs(cfgdir, exist_ok=True)
    with open(cfgdir + '/dump', 'w', encoding='utf-8') as f:
       json.dump(json_conf, f)

def get_wired_interface_nmcli() -> tuple[str, ...]:
    ''' Get all wired net interface names '''
    try:
        result = subprocess.run(
            ['nmcli', '-t', '-f', 'DEVICE,TYPE', 'device', 'status'],
            capture_output=True,
            text=True,
            timeout=5
        )
        if result.returncode != 0:
            return ('eth0',)
        
        wired_interfaces = []
        for line in result.stdout.strip().split('\n'):
            if not line:
                continue
            parts = line.split(':')
            if len(parts) >= 2 and parts[1] == 'ethernet':
                wired_interfaces.append(parts[0])
        
        # Если не найдено ни одного проводного интерфейса, возвращаем eth0, а вдруг )
        if not wired_interfaces:
            return ('eth0',)
        
        return tuple(wired_interfaces)
        
    except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
        return ('eth0',)



def calculate_network_info(ip: str, prefix: Union[int, str]) -> Optional[Dict[str, str]]:
    """
    Расчет параметров сети
    """
    try:
        if isinstance(prefix, str):
            prefix = int(prefix.strip())
        
        if not (0 <= prefix <= 32):
            return None
        
        network = ipaddress.IPv4Network(f"{ip}/{prefix}", strict=False)
        
        subnet = str(network.network_address)
        netmask = str(network.netmask)
        broadcast = str(network.broadcast_address)
        
        # Чек selfip
        try:
            ip_obj = ipaddress.IPv4Address(ip)
            selfip = ip if ip_obj in network else subnet
        except:
            selfip = subnet
        
        selfip_obj = ipaddress.IPv4Address(selfip)
        network_addr = network.network_address
        broadcast_addr = network.broadcast_address
        
        is_valid_dhcp = prefix <= 30
        
        if not is_valid_dhcp:
            return {
                'SUBNET': subnet,
                'NETMASK': netmask,
                'BROADCAST': broadcast,
                'IP': ip,
                'SELFIP': selfip,
                'PREFIX': prefix,
                'CIDR': f"{ip}/{prefix}",
                'FIRSTADDRESS': '0.0.0.0',
                'LASTADDRESS': '0.0.0.0',
                'GATEWAY': '0.0.0.0',
                'DNS': '0.0.0.0',
                'NFS': '0.0.0.0',
                'TFTP': '0.0.0.0',
                'IS_VALID': False
            }
        
        all_hosts = list(network.hosts())
        first_addr = str(all_hosts[0]) if all_hosts else subnet
        last_addr = str(all_hosts[-1]) if all_hosts else subnet
        
        network_int = int(network_addr)
        broadcast_int = int(broadcast_addr)
        selfip_int = int(selfip_obj)
        
        # Считаем адреса в диапазонах до и после selfIP
        range1_count = max(0, selfip_int - network_int - 1)  # network+1 .. selfip-1
        range2_count = max(0, broadcast_int - selfip_int - 1)  # selfip+1 .. broadcast-1

        # Выбираем диапазон с большим количеством адресов
        if range1_count >= range2_count:
            start_ip = network_int + 1
            end_ip = selfip_int - 1
        else:
            start_ip = selfip_int + 1
            end_ip = broadcast_int - 1
       
        dhcp_start = str(ipaddress.IPv4Address(start_ip))
        dhcp_end = str(ipaddress.IPv4Address(end_ip))
    
        return {
            'SUBNET': subnet,
            'NETMASK': netmask,
            'BROADCAST': broadcast,
            'IP': ip,
            'SELFIP': selfip,
            'PREFIX': prefix,
            'CIDR': f"{ip}/{prefix}",
            'FIRSTADDRESS': dhcp_start,
            'LASTADDRESS': dhcp_end,
            'GATEWAY': selfip,
            'DNS': selfip,
            'NFS': selfip,
            'TFTP': selfip,
            'IS_VALID': True
        }
        
    except:
        return None

class ConfirmationDialog:
    def __init__(self, ui_file):
        self.builder = Gtk.Builder()
        self.builder.add_from_file(ui_file)
        # get elements
        self.dialog = self.builder.get_object("confirmation_dialog")
        self.ok_button = self.builder.get_object("ok_button")
        self.cancel_button = self.builder.get_object("cancel_button")
        self.checkbox = self.builder.get_object("dont_show_checkbox")
        self.header_label = self.builder.get_object("dialog_header_label")
        self.first_warning_label = self.builder.get_object("first_warning_label")
        self.second_warning_label = self.builder.get_object("second_warning_label")
        self.third_warning_label = self.builder.get_object("third_warning_label")
        # setup
        self.setup_buttons()
    
    def setup_buttons(self):
        self.ok_button.connect("clicked", 
                              lambda b: self.dialog.response(Gtk.ResponseType.OK))
        self.cancel_button.connect("clicked", 
                                  lambda b: self.dialog.response(Gtk.ResponseType.CANCEL))
        
    def run(self, parent_window=None):
        """Показывает диалог и возвращает результат"""
        global json_conf
        if json_conf['skip_confirmation']:
            return True
        
        if parent_window:
            self.dialog.set_transient_for(parent_window)
            self.dialog.set_modal(True)
        self.checkbox.set_active(False)
        response = self.dialog.run()
        
        # get result
        dont_show_again = self.checkbox.get_active()
        confirmed = (response == Gtk.ResponseType.OK)
        self.dialog.hide()
        
        if dont_show_again:
            json_conf['skip_confirmation'] = True
        
        return confirmed

    def reset_confirmation(self):
        """Сбрасывает настройку "не показывать" (можно вызвать из настроек программы)"""
        global json_conf
        json_conf['skip_confirmation'] = True

    def set_text(self, IP, PREFIX):
        warnings = warning_text(IP, PREFIX)
        self.header_label.set_markup(warnings[0])
        self.first_warning_label.set_markup(warnings[1])
        self.second_warning_label.set_markup(warnings[2])
        self.third_warning_label.set_markup(warnings[3])
        
class PXEBootApp(Gtk.Application):
    def __init__(self):
        super().__init__(
            application_id="ru.rosa.pxeboot",
            flags=Gio.ApplicationFlags.FLAGS_NONE
        )
        self.checking_active = False
    
    def do_activate(self):
        global json_conf
        if not os.path.exists(ui_file):
            print(f"Ошибка: файл {ui_file} не найден")
            self.quit()
            return
        
        self.builder = Gtk.Builder()
        try:
            self.builder.add_from_file(ui_file)
        except GLib.Error as e:
            print(f"Ошибка загрузки UI: {e}")
            self.quit()
            return

        self.builder.connect_signals(self)

        # get window
        self.window = self.builder.get_object("window_main")
        if not self.window:
            print("Ошибка: не найден объект 'window_main' в UI файле")
            self.quit()
            return
        self.check_active()
        self.window.set_icon_from_file(ICON)

        # setup config
        if not json_conf:
            self.network_config = {}
            self.cidr_handler()
            json_conf['interface'] = get_wired_interface_nmcli()
            json_conf['skip_confirmation'] = False
            self.set_interfaces()
        else:
            self.builder.get_object("cidr_entry").set_text(json_conf['ip_cidr'])
            self.builder.get_object("cidr_entry_mask").set_text(json_conf['mask_cidr'])
            self.network_config = json_conf['network_config']
            self.update_all_entries()

            self.builder.get_object('autostart_flag').set_active(json_conf['autostart'])
            self.builder.get_object('start_flag').set_active(json_conf['start'])
            self.builder.get_object('dhcp_flag').set_active(json_conf['dhcpd'])

            self.builder.get_object('clear_mode_flag').set_active(json_conf['clear_mode'])
            self.builder.get_object('copy2ram_mode_flag').set_active(json_conf['copy2ram_mode'])
            self.builder.get_object('save2flash_mode_flag').set_active(json_conf['save2flash_mode'])
            
            self.set_interfaces(conf_iface=json_conf['interface'])
        

        # get elements
        self.btn_apply = self.builder.get_object("apply_button")
        self.btn_exit = self.builder.get_object("exit_button")

        self.window.set_application(self)
        self.window.show_all()
        self.on_details_flag_toggled(self)

        # setup dialog window
        self.dialog = ConfirmationDialog(ui_file)

        GLib.timeout_add_seconds(8, lambda: (self.check_active(), True)[1])

    def set_interfaces(self, conf_iface=None):
        iface_cbox = self.builder.get_object('interfaces_combobox')
        ifaces = get_wired_interface_nmcli()
        iface_cbox.remove_all()
        n = 0
        active = 0
        for a in ifaces:
            iface_cbox.append(a, a)
            if conf_iface and conf_iface == a:
                active = n
            n += 1
        iface_cbox.set_active(active)

    def check_active(self):
        if self.checking_active:
            return
        else:
            self.checking_active = True
        label = self.builder.get_object('started_label')
        smile = self.builder.get_object('smile_label')
        button = self.builder.get_object('started_button')
        ret = subprocess.run(['systemctl', 'is-active', 'barium_pxeboot.service' ], 
                            stdout=subprocess.DEVNULL,
                            stderr=subprocess.DEVNULL
                            ).returncode
        check = int(ret) == 0
        if check:
            label.set_text(_('PXE server is active'))
            smile.set_markup('<span font_family="monospace" size="large" \
                              weight="bold" foreground="#2ecc71">:)</span>')
        else:
            label.set_text(_('PXE server is not active'))
            smile.set_markup('<span font_family="monospace" size="large" \
                              weight="bold" foreground="#e74c3c">:(</span>')
        for obj in (label, smile, button):
            obj.set_sensitive(check)
            
        self.checking_active = False
    
    def update_all_entries(self):
        global mapping
        for widget_id, config_key in mapping.items():
            widget = self.builder.get_object(widget_id)
            if widget and config_key in self.network_config:
                widget.set_text(str(self.network_config[config_key]))

    # Signal handlers
    def on_started_button_clicked(self, widget):
        '''Stop barium-pxeboot srevice'''
        cmdline = ['pkexec', 'systemctl', 'stop', 'barium_pxeboot.service' ]
        exit_code = subprocess.run(cmdline, 
                          timeout=30,
                          stdout=subprocess.DEVNULL,
                          stderr=subprocess.DEVNULL).returncode
        self.check_active()
        
    def on_apply_button_clicked(self, widget):
        ''' Apply button handler '''
        global json_conf
        global mapping

        json_conf['interface'] = self.builder.get_object("interfaces_combobox").get_active_id()
        json_conf['ip_cidr'] = self.builder.get_object("cidr_entry").get_text()
        json_conf['mask_cidr']  =  self.builder.get_object("cidr_entry_mask").get_text()
        json_conf['network_config'] =  self.network_config
        for widget_id, config_key in mapping.items():
            widget = self.builder.get_object(widget_id)
            if widget:
                json_conf['network_config'][config_key] = widget.get_text()
        json_conf['autostart'] = self.builder.get_object('autostart_flag').get_active()
        json_conf['start'] = self.builder.get_object('start_flag').get_active()
        json_conf['dhcpd'] = self.builder.get_object('dhcp_flag').get_active()

        json_conf['clear_mode'] =  self.builder.get_object('clear_mode_flag').get_active()
        json_conf['copy2ram_mode'] = self.builder.get_object('copy2ram_mode_flag').get_active()
        json_conf['save2flash_mode'] = self.builder.get_object('save2flash_mode_flag').get_active()
        json_conf['additional_modes'] = self.builder.get_object('additional_modes_flag').get_active()

        self.dialog.set_text(json_conf['network_config']['SUBNET'], json_conf['network_config']['PREFIX'])

        if not self.dialog.run(parent_window=self.window):
            return

        json_write()
        cmdline = ['pkexec', handler, cfgdir + '/dump' ]
        try:
            result = subprocess.run(cmdline, 
                                  timeout=30,
                                  capture_output=True,
                                  text=True)
            ret = result.returncode
            if result.stdout:
                print(f"STDOUT: {result.stdout}")
            if result.stderr:
                print(f"STDERR: code: {ret}, {result.stderr}")
                
        except subprocess.TimeoutExpired:
            print("Timeout error")
        except Exception as e:
            print(f"Error: {e}")

        self.check_active()

    def on_exit_button_clicked(self, widget):
        """Обработчик кнопки Exit"""
        print("Exit button clicked")
        self.quit()
    
    def on_details_flag_toggled(self, button):
      if self.builder.get_object('details_flag').get_active():
          self.builder.get_object("details_scrolled").show()
      else:
          w, h = self.window.get_size()
          self.builder.get_object("details_scrolled").hide()
          self.window.resize(w, self.window.get_preferred_height()[1])
    
    #cidr handlers
    def on_cidr_entry_mask_activate(self, widget, *args):
        self.cidr_handler(self)
    def on_cidr_entry_mask_focus_out_event(self, widget, *args):
        self.cidr_handler(self)
    def on_cidr_entry_activate(self, widget, *args):
        self.cidr_handler(self)
    def on_cidr_entry_focus_out_event(self, widget, *args):
        self.cidr_handler(self)

    def cidr_handler(self, *args):
        ip_entry = self.builder.get_object("cidr_entry").get_text()
        prefix_entry = self.builder.get_object("cidr_entry_mask").get_text()
        result = calculate_network_info(ip_entry, prefix_entry)

        if result:
            self.network_config = result
            self.update_all_entries()
        else:
            self.builder.get_object("cidr_entry").set_text('Not valid cidr')
            return None

    def on_main_window_destroy(self, widget):
        print("Window destroyed")
        self.quit()

def main():
    global json_conf
    if os.path.exists(cfgdir + '/dump'):
        try:
            with open(cfgdir + '/dump', 'r', encoding='utf-8') as f:
                json_conf = json.load(f)
        except:
           json_conf = {}
    print(json_conf)
    app = PXEBootApp()
    return app.run()

if __name__ == "__main__":
    main()
