#!/usr/bin/env python3
#--------------------------------------------------------------------------------------------------------
# Name: Linux Lite - Lite Network Shares
# Architecture: amd64
# Author: Jerry Bezencon
# Website: https://www.linuxliteos.com
# Language: Python/GTK4
# Licence: GPLv2
#--------------------------------------------------------------------------------------------------------

import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, Gio, GLib, Gdk

import subprocess
import os
import sys
import pwd
import socket
import shutil

# Constants
APP_ID = "com.linuxlite.networkshares"
APP_NAME = "Lite Network Shares"
ICON_PATH = "/usr/share/icons/Papirus/24x24/apps/lite-networkshares.png"
SMB_CONF = "/etc/samba/smb.conf"
SMB_CONF_BACKUP = "/etc/samba/smb.conf.bak"


def get_hostname() -> str:
    """Get the system hostname."""
    return socket.gethostname()


def get_invoking_user() -> str:
    """Get the username of the user who launched the app (not root)."""
    uid = os.environ.get("PKEXEC_UID")
    if uid and uid.isdigit():
        try:
            return pwd.getpwuid(int(uid)).pw_name
        except Exception:
            pass
    user = os.environ.get("SUDO_USER")
    if user:
        return user
    try:
        import getpass
        return getpass.getuser()
    except Exception:
        return "linux"


def backup_smb_conf():
    """Backup smb.conf if backup doesn't exist."""
    if not os.path.exists(SMB_CONF_BACKUP) and os.path.exists(SMB_CONF):
        shutil.copy2(SMB_CONF, SMB_CONF_BACKUP)


def update_netbios_name():
    """Update netbios name in smb.conf to match hostname."""
    hostname = get_hostname()
    try:
        with open(SMB_CONF, 'r') as f:
            content = f.read()

        # Check if netbios name is already correct
        expected_line = f"netbios name = {hostname}"
        if expected_line in content:
            return False  # No change needed

        # Update netbios name
        import re
        new_content = re.sub(r'netbios name\s*=.*', f'netbios name = {hostname}', content)

        with open(SMB_CONF, 'w') as f:
            f.write(new_content)

        return True  # Changed
    except Exception as e:
        print(f"Error updating netbios name: {e}")
        return False


def setup_firewall():
    """Add firewall rules for Samba if not already configured."""
    if shutil.which('ufw'):
        # Check if Samba rule already exists
        result = subprocess.run(['ufw', 'status'], capture_output=True, text=True)
        if 'Samba' not in result.stdout:
            subprocess.run(['ufw', 'allow', 'Samba'], capture_output=True)

    if shutil.which('firewall-cmd'):
        subprocess.run(['firewall-cmd', '--permanent', '--zone=home', '--add-service=samba'], capture_output=True)
        subprocess.run(['firewall-cmd', '--permanent', '--zone=public', '--add-service=samba'], capture_output=True)
        subprocess.run(['firewall-cmd', '--reload'], capture_output=True)


def ensure_samba_unmasked():
    """Check if smbd/nmbd are running; if not, unmask and enable them."""
    for service in ['smbd', 'nmbd']:
        result = subprocess.run(
            ['systemctl', 'is-active', service],
            capture_output=True, text=True
        )
        if result.stdout.strip() != 'active':
            subprocess.run(['systemctl', 'unmask', service], capture_output=True)
            subprocess.run(['systemctl', 'enable', service], capture_output=True)
            subprocess.run(['systemctl', 'start', service], capture_output=True)


def enable_samba_services():
    """Enable and start Samba and discovery services."""
    ensure_samba_unmasked()
    subprocess.run(['systemctl', 'enable', 'smbd', 'nmbd'], capture_output=True)
    subprocess.run(['systemctl', 'start', 'smbd', 'nmbd'], capture_output=True)
    # Enable WS-Discovery for Windows 10/11 network browsing
    if shutil.which('wsdd'):
        subprocess.run(['systemctl', 'enable', 'wsdd'], capture_output=True)
        subprocess.run(['systemctl', 'start', 'wsdd'], capture_output=True)


def run_testparm() -> tuple[bool, str]:
    """Run testparm and return (success, errors)."""
    try:
        result = subprocess.run(
            ['testparm', '-s', SMB_CONF],
            capture_output=True,
            text=True
        )

        # Filter for errors/warnings
        errors = []
        for line in result.stderr.split('\n'):
            if any(x in line for x in ['Unknown', 'Ignoring', 'WARNING', 'NOTE:', 'set_variable_helper', 'Error loading services']):
                errors.append(line)

        error_text = '\n'.join(errors)

        # Check for critical errors
        has_critical = 'Error loading services' in error_text

        return (not has_critical, error_text)
    except FileNotFoundError:
        return (False, "testparm command not found. Is Samba installed?")
    except Exception as e:
        return (False, str(e))


def reload_samba():
    """Reload Samba configuration."""
    subprocess.run(['systemctl', 'reload', 'smbd'], capture_output=True)


def restart_samba() -> bool:
    """Restart Samba services."""
    result = subprocess.run(
        ['systemctl', 'restart', 'smbd', 'nmbd'],
        capture_output=True
    )
    return result.returncode == 0


class ProgressDialog(Adw.Window):
    """Progress dialog for operations."""

    def __init__(self, parent, title: str, text: str):
        super().__init__()
        self.set_title(title)
        self.set_modal(True)
        self.set_transient_for(parent)
        self.set_default_size(400, 120)
        self.set_deletable(False)

        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16)
        box.set_margin_top(24)
        box.set_margin_bottom(24)
        box.set_margin_start(24)
        box.set_margin_end(24)

        self.status_label = Gtk.Label(label=text)
        self.status_label.set_wrap(True)
        self.status_label.add_css_class("dark-text")
        box.append(self.status_label)

        self.progress_bar = Gtk.ProgressBar()
        box.append(self.progress_bar)

        self.set_content(box)

    def pulse(self):
        self.progress_bar.pulse()
        return True


class NetworkSharesWindow(Adw.ApplicationWindow):
    """Main application window."""

    def __init__(self, app):
        super().__init__(application=app)
        self.set_icon_name("lite-networkshares")
        self.set_title(APP_NAME)
        self.set_default_size(500, 340)

        # Navigation view
        self.navigation_view = Adw.NavigationView()
        self.set_content(self.navigation_view)

        # Create main page
        self._create_main_page()

        # Initial setup
        GLib.idle_add(self._initial_setup)

    def _initial_setup(self):
        """Run initial setup after window is shown."""
        if os.geteuid() != 0:
            dialog = Adw.AlertDialog()
            dialog.set_heading("Root Privileges Required")
            dialog.set_body("This application requires root privileges to configure network shares.")
            dialog.add_response("quit", "Quit")
            dialog.connect("response", lambda d, r: self.get_application().quit())
            dialog.present(self)
            return False

        # Backup smb.conf
        backup_smb_conf()

        # Check if smb.conf exists
        if not os.path.exists(SMB_CONF):
            dialog = Adw.AlertDialog()
            dialog.set_heading("Samba Not Configured")
            dialog.set_body("The Samba configuration file was not found.\n\n"
                          "Please ensure Samba is installed.")
            dialog.add_response("quit", "Quit")
            dialog.connect("response", lambda d, r: self.get_application().quit())
            dialog.present(self)
            return False

        # Update netbios name if needed
        if update_netbios_name():
            self._show_progress("Updating Samba netbios name...")

        # Setup firewall and enable services
        setup_firewall()
        enable_samba_services()

        return False

    def _show_progress(self, text: str):
        """Show a brief progress dialog."""
        progress = ProgressDialog(self, APP_NAME, text)
        progress.present()

        pulse_id = GLib.timeout_add(100, progress.pulse)

        def close_progress():
            GLib.source_remove(pulse_id)
            progress.close()
            return False

        GLib.timeout_add(2000, close_progress)

    def _create_main_page(self):
        """Create the main information page."""
        page = Adw.NavigationPage()
        page.set_title(APP_NAME)

        toolbar_view = Adw.ToolbarView()
        header = Adw.HeaderBar()
        toolbar_view.add_top_bar(header)

        # Main content
        content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=24)
        content_box.set_margin_top(24)
        content_box.set_margin_bottom(24)
        content_box.set_margin_start(24)
        content_box.set_margin_end(24)
        content_box.set_valign(Gtk.Align.CENTER)
        content_box.set_vexpand(True)

        # Info section - centered
        title_label = Gtk.Label(label="About Network Sharing")
        title_label.add_css_class("title-2")
        title_label.add_css_class("dark-text")
        title_label.set_halign(Gtk.Align.CENTER)
        content_box.append(title_label)

        desc_label = Gtk.Label()
        desc_label.set_markup(
            "Samba is the software suite that provides seamless file and print services to Windows computers.\n\n"
            "Click on the Edit Share Settings button below to get started.\n\n"
            "<b>You must</b> select Set Samba Password if this is the first time you are configuring Lite Network Shares."
        )
        desc_label.set_wrap(True)
        desc_label.set_justify(Gtk.Justification.CENTER)
        desc_label.set_halign(Gtk.Align.CENTER)
        desc_label.add_css_class("dim-label")
        desc_label.add_css_class("dark-text")
        content_box.append(desc_label)

        # Buttons
        button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
        button_box.set_halign(Gtk.Align.CENTER)
        button_box.set_margin_top(24)

        edit_btn = Gtk.Button(label="Edit Share Settings")
        edit_btn.add_css_class("suggested-action")
        edit_btn.add_css_class("pill")
        edit_btn.set_size_request(200, 44)
        edit_btn.connect("clicked", self._on_edit_clicked)
        button_box.append(edit_btn)

        password_btn = Gtk.Button(label="Set Samba Password")
        password_btn.add_css_class("pill")
        password_btn.set_size_request(200, 44)
        password_btn.connect("clicked", self._on_set_password_clicked)
        button_box.append(password_btn)

        quit_btn = Gtk.Button(label="Quit")
        quit_btn.add_css_class("pill")
        quit_btn.set_size_request(120, 44)
        quit_btn.connect("clicked", lambda b: self.get_application().quit())
        button_box.append(quit_btn)

        content_box.append(button_box)

        toolbar_view.set_content(content_box)
        page.set_child(toolbar_view)

        self.navigation_view.add(page)

    def _on_edit_clicked(self, button):
        """Show the built-in editor for smb.conf."""
        # Load smb.conf content
        try:
            with open(SMB_CONF, 'r') as f:
                content = f.read()
        except Exception as e:
            self._show_error("Error", f"Could not read configuration file: {e}")
            return

        # Create editor page
        page = Adw.NavigationPage()
        page.set_title("Edit smb.conf")

        toolbar_view = Adw.ToolbarView()

        # Header bar
        header = Adw.HeaderBar()
        toolbar_view.add_top_bar(header)

        # Editor content
        editor_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)

        # Scrolled window for text view
        scrolled = Gtk.ScrolledWindow()
        scrolled.set_vexpand(True)
        scrolled.set_hexpand(True)

        # Text view for editing
        self.text_view = Gtk.TextView()
        self.text_view.set_monospace(True)
        self.text_view.set_left_margin(12)
        self.text_view.set_right_margin(12)
        self.text_view.set_top_margin(12)
        self.text_view.set_bottom_margin(12)
        self.text_view.set_wrap_mode(Gtk.WrapMode.NONE)

        # Set content
        self.text_buffer = self.text_view.get_buffer()
        self.text_buffer.set_text(content)

        scrolled.set_child(self.text_view)
        editor_box.append(scrolled)

        # Apply button at bottom center
        button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        button_box.set_halign(Gtk.Align.CENTER)
        button_box.set_margin_bottom(12)

        apply_btn = Gtk.Button(label="Apply Now")
        apply_btn.add_css_class("suggested-action")
        apply_btn.add_css_class("pill")
        apply_btn.set_size_request(150, 40)
        apply_btn.connect("clicked", self._on_apply_clicked)
        button_box.append(apply_btn)

        editor_box.append(button_box)

        toolbar_view.set_content(editor_box)
        page.set_child(toolbar_view)

        # Resize window for editor
        self.set_default_size(720, 640)

        self.navigation_view.push(page)

    def _on_apply_clicked(self, button):
        """Save changes and restart services."""
        # Get content from text buffer
        start_iter = self.text_buffer.get_start_iter()
        end_iter = self.text_buffer.get_end_iter()
        content = self.text_buffer.get_text(start_iter, end_iter, True)

        # Save to smb.conf
        try:
            with open(SMB_CONF, 'w') as f:
                f.write(content)
        except Exception as e:
            self._show_error("Error", f"Could not save configuration file: {e}")
            return

        # Run testparm
        success, errors = run_testparm()

        if errors:
            # Show errors
            self._show_config_errors(errors, success)
        else:
            # Automatically restart services
            self._restart_services()

    def _show_config_errors(self, errors: str, can_continue: bool):
        """Show configuration errors dialog."""
        dialog = Adw.AlertDialog()

        if not can_continue:
            dialog.set_heading("Critical Configuration Errors")
            dialog.set_body(f"The following errors prevent Samba from running:\n\n{errors}\n\n"
                          "Please correct the configuration.")
            dialog.add_response("edit", "Edit Configuration")
            dialog.set_response_appearance("edit", Adw.ResponseAppearance.SUGGESTED)
            dialog.connect("response", lambda d, r: self._on_edit_clicked(None))
        else:
            dialog.set_heading("Configuration Warnings")
            dialog.set_body(f"The following warnings were found:\n\n{errors}\n\n"
                          "You may not be able to access Samba shares until the configuration is corrected.")
            dialog.add_response("edit", "Review Configuration")
            dialog.add_response("continue", "Continue Anyway")
            dialog.set_response_appearance("continue", Adw.ResponseAppearance.SUGGESTED)
            dialog.connect("response", self._on_error_response)

        dialog.present(self)

    def _on_error_response(self, dialog, response):
        """Handle error dialog response."""
        if response == "edit":
            pass
        else:
            self._restart_services()

    def _on_set_password_clicked(self, button):
        """Show dialog to set a Samba password for a user."""
        invoking_user = get_invoking_user()

        dialog = Adw.Window()
        dialog.set_title("Set Samba Password")
        dialog.set_modal(True)
        dialog.set_transient_for(self)
        dialog.set_default_size(520, -1)

        toolbar_view = Adw.ToolbarView()
        header = Adw.HeaderBar()
        header.set_show_end_title_buttons(False)
        header.set_show_start_title_buttons(False)

        cancel_btn = Gtk.Button(label="Cancel")
        cancel_btn.connect("clicked", lambda b: dialog.close())
        header.pack_start(cancel_btn)

        apply_btn = Gtk.Button(label="Set Password")
        apply_btn.add_css_class("suggested-action")
        header.pack_end(apply_btn)

        toolbar_view.add_top_bar(header)

        content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16)
        content.set_margin_top(24)
        content.set_margin_bottom(24)
        content.set_margin_start(24)
        content.set_margin_end(24)

        desc_label = Gtk.Label(
            label=f"Set the Samba password for user '{invoking_user}'.\n\n"
                  "This password is used when connecting to shared folders from other computers.")
        desc_label.set_wrap(True)
        desc_label.set_halign(Gtk.Align.CENTER)
        desc_label.add_css_class("dim-label")
        content.append(desc_label)

        pass_entry = Gtk.PasswordEntry()
        pass_entry.set_show_peek_icon(True)
        pass_entry.set_property("placeholder-text", "New password")
        content.append(pass_entry)

        confirm_entry = Gtk.PasswordEntry()
        confirm_entry.set_show_peek_icon(True)
        confirm_entry.set_property("placeholder-text", "Confirm password")
        content.append(confirm_entry)

        toolbar_view.set_content(content)
        dialog.set_content(toolbar_view)

        def on_apply(b):
            password = pass_entry.get_text()
            confirm = confirm_entry.get_text()
            if not password:
                self._show_error("Error", "Password cannot be empty.")
                return
            if password != confirm:
                self._show_error("Error", "Passwords do not match.")
                return
            result = subprocess.run(
                ['smbpasswd', '-a', '-s', invoking_user],
                input=f"{password}\n{password}\n",
                capture_output=True, text=True
            )
            dialog.close()
            if result.returncode == 0:
                self._show_info("Password Set",
                              f"Samba password for '{invoking_user}' has been set successfully.\n\n"
                              "You can now connect to shared folders using this password.")
            else:
                self._show_error("Error", f"Failed to set Samba password:\n{result.stderr}")

        apply_btn.connect("clicked", on_apply)
        dialog.present()

    def _restart_services(self):
        """Restart Samba services with progress."""
        progress = ProgressDialog(self, APP_NAME, "Restarting smbd and nmbd services...")
        progress.present()

        pulse_id = GLib.timeout_add(100, progress.pulse)

        def do_restart():
            success = restart_samba()
            GLib.source_remove(pulse_id)
            progress.close()

            if success:
                self._show_info("Services Restarted",
                              "Samba services have been restarted successfully.\n\n"
                              "Your network share settings are now active.",
                              quit_after=True)
            else:
                self._show_error("Error", "Failed to restart Samba services.\n\n"
                               "Please check the system logs for more information.")

            return False

        GLib.timeout_add(1000, do_restart)

    def _show_error(self, title: str, message: str):
        """Show error dialog."""
        dialog = Adw.AlertDialog()
        dialog.set_heading(title)
        dialog.set_body(message)
        dialog.add_response("ok", "OK")
        dialog.present(self)

    def _show_info(self, title: str, message: str, quit_after: bool = False):
        """Show info dialog."""
        dialog = Adw.AlertDialog()
        dialog.set_heading(title)
        dialog.set_body(message)
        dialog.add_response("ok", "OK")
        if quit_after:
            dialog.connect("response", lambda d, r: self.get_application().quit())
        dialog.present(self)


class NetworkSharesApp(Adw.Application):
    """Main application class."""

    def __init__(self):
        super().__init__(
            application_id=APP_ID,
            flags=Gio.ApplicationFlags.FLAGS_NONE
        )

    def do_activate(self):
        self._apply_css()
        win = self.props.active_window
        if not win:
            win = NetworkSharesWindow(self)
        win.present()

    def _apply_css(self):
        css = b"""
        .dark-text {
            color: #1e1e1e;
        }
        .dim-label {
            opacity: 0.75;
        }
        """
        provider = Gtk.CssProvider()
        provider.load_from_data(css)
        Gtk.StyleContext.add_provider_for_display(
            Gdk.Display.get_default(), provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )


def main():
    # Check if running as root, if not, use pkexec to elevate
    if os.geteuid() != 0:
        try:
            # Allow root to access X display
            subprocess.run(['xhost', '+si:localuser:root'], capture_output=True)

            # Set up environment variables file for the elevated process
            env_file = '/tmp/.lite-networkshares-env'
            with open(env_file, 'w') as f:
                for var in ['DISPLAY', 'XAUTHORITY', 'WAYLAND_DISPLAY', 'XDG_RUNTIME_DIR']:
                    if var in os.environ:
                        f.write(f'{var}={os.environ[var]}\n')

            # Use pkexec to run the installed script directly
            os.execvp("pkexec", ["pkexec", "/usr/bin/lite-networkshares"] + sys.argv[1:])
        except Exception as e:
            print(f"Failed to elevate privileges: {e}")
            sys.exit(1)

    # Running as root - load environment from temp file if available
    env_file = '/tmp/.lite-networkshares-env'
    if os.path.exists(env_file):
        try:
            with open(env_file, 'r') as f:
                for line in f:
                    line = line.strip()
                    if '=' in line:
                        key, value = line.split('=', 1)
                        os.environ[key] = value
            os.unlink(env_file)
        except Exception:
            pass

    app = NetworkSharesApp()
    return app.run(sys.argv)


if __name__ == "__main__":
    sys.exit(main())
