#!/usr/bin/env python3
"""
Dashboard Launcher - Application GTK3 + WebKit2
Affiche une interface web pour lancer des connexions SSH et autres services.
"""

import os
import sys
import yaml
import gi
import subprocess
import re
import threading

gi.require_version('Gtk', '3.0')
gi.require_version('WebKit2', '4.1')
gi.require_version('Gdk', '3.0')

from gi.repository import Gtk, WebKit2, Gio, Gdk, GLib
import cairo

PID_FILE = '/tmp/dashboard-launcher.pid'
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_DIR = os.path.dirname(SCRIPT_DIR)
CONFIG_FILE = os.path.join(PROJECT_DIR, 'config', 'equipements.yaml')
ICONS_DIR = os.path.join(PROJECT_DIR, 'icons')

# Cache pour les résultats de ping
ping_results = {}


def ping_host(ip, timeout=1):
    """Ping une IP et retourne True si accessible."""
    try:
        # Nettoyer l'IP (enlever les doubles points, etc.)
        clean_ip = ip.replace('..', '.')
        result = subprocess.run(
            ['ping', '-c', '1', '-W', str(timeout), clean_ip],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            timeout=timeout + 1
        )
        return result.returncode == 0
    except Exception:
        return False


def ping_all_hosts(hosts, callback):
    """Ping toutes les IPs en parallèle et appelle callback avec les résultats."""
    def ping_thread(ip):
        result = ping_host(ip)
        ping_results[ip] = result

    threads = []
    for ip in hosts:
        t = threading.Thread(target=ping_thread, args=(ip,))
        t.daemon = True
        t.start()
        threads.append(t)

    def wait_and_callback():
        for t in threads:
            t.join(timeout=2)
        GLib.idle_add(callback)

    threading.Thread(target=wait_and_callback, daemon=True).start()


def load_config():
    """Charge la configuration depuis le fichier YAML."""
    try:
        with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
            return yaml.safe_load(f) or {}
    except (FileNotFoundError, yaml.YAMLError) as e:
        print(f"Erreur config: {e}")
        return {}


def get_icon_html(icon_name, size=32):
    """Retourne le HTML pour une icône."""
    if not icon_name:
        return f'<span class="service-icon" style="font-size:{size}px">🔗</span>'

    # Chemin vers image dans icons/
    if icon_name.startswith('icons/'):
        icon_path = os.path.join(PROJECT_DIR, icon_name)
        if os.path.exists(icon_path):
            return f'<img src="file://{icon_path}" class="service-img" style="width:{size}px;height:{size}px" alt="">'

    # Emojis de fallback
    icons = {
        'utilities-terminal': '💻', 'terminal': '💻', 'web-browser': '🌐',
        'applications-system': '⚙️', 'server': '🖥️', 'network-server': '🖧',
        'preferences-desktop-remote-desktop': '🖥️', 'security-high': '🔒',
        'go-home': '🏠', 'system-file-manager': '📁', 'firefox': '🦊',
        'google-chrome': '🌐', 'code': '📝',
    }
    emoji = icons.get(icon_name, icon_name if len(icon_name) <= 2 else '🔗')
    return f'<span class="service-icon" style="font-size:{size}px">{emoji}</span>'


def generate_html(config):
    """Génère le HTML avec layout: local en haut, distant à gauche, url à droite."""

    apparence = config.get('apparence', {})
    fenetre = config.get('fenetre', {})
    theme = apparence.get('theme', 'dark')
    font_size = apparence.get('police_taille', 14)
    icon_distant = apparence.get('icon_taille_distant', 48)
    icon_local = apparence.get('icon_taille_local', 64)
    icon_url = apparence.get('icon_taille_url', 64)
    icon_minitools = apparence.get('icon_taille_minitools', 32)
    show_local_labels = apparence.get('afficher_label_local', True)
    border_radius = apparence.get('border_radius', 12)
    espacement_local = apparence.get('espacement_local', 10)
    espacement_distant = apparence.get('espacement_distant', 6)
    espacement_url = apparence.get('espacement_url', 8)
    espacement_minitools = apparence.get('espacement_minitools', 8)
    icon_fermer = apparence.get('icon_taille_fermer', 22)
    icon_parametre = apparence.get('icon_taille_parametre', 22)
    icon_theme_width = apparence.get('icon_taille_theme', 60)
    icon_theme_height = int(icon_theme_width * 238 / 512)  # Ratio de l'image night-day.png
    border_radius_local = apparence.get('border_radius_local', 12)

    # Colonnes configurables
    col_url = fenetre.get('section_url', {}).get('colonne', 2)

    distant = config.get('distant', [])
    local = config.get('local', [])
    urls = config.get('url', [])

    # Section DISTANT avec status ping
    distant_html = ""
    for machine in distant:
        ip = machine.get('ip', '')
        nom = machine.get('nom', '')
        # Vérifier le statut ping (vert si up, rouge si down, gris si inconnu)
        is_up = ping_results.get(ip)
        if is_up is None:
            status_class = "status-unknown"
        elif is_up:
            status_class = "status-up"
        else:
            status_class = "status-down"

        services_html = ""
        for service in machine.get('services', []):
            icon_html = get_icon_html(service.get('icon', ''), icon_distant)
            services_html += f'''
                <a href="{service.get('url', '')}" class="item" title="{nom} - {service.get('nom', '')}">
                    {icon_html}
                    <span class="label">{service.get('nom', '')}</span>
                </a>'''
        distant_html += f'''
            <div class="ip-box">
                <div class="ip-header"><span class="status-dot {status_class}" data-ip="{ip}"></span>{nom}</div>
                <div class="ip-name">{ip}</div>
                <div class="items">{services_html}</div>
            </div>'''

    # Section LOCAL
    local_html = ""
    for app in local:
        icon_html = get_icon_html(app.get('icon', ''), icon_local)
        label_html = f'<span class="label">{app.get("nom", "")}</span>' if show_local_labels else ''
        local_html += f'''
            <a href="app://run/{app.get('command', '')}" class="item item-local" title="{app.get('nom', '')}">
                {icon_html}
                {label_html}
            </a>'''

    minitools = config.get('minitools', [])
    minitools_html = ""
    for tool in minitools:
        icon_html = get_icon_html(tool.get('icon', ''), icon_minitools)
        label_html = f'<span class="label">{tool.get("nom", "")}</span>' if show_local_labels else ''
        href = ''
        if tool.get('command'):
            href = f"app://run/{tool.get('command')}"
        elif tool.get('url'):
            href = tool.get('url')
        if not href:
            continue
        minitools_html += f'''
            <a href="{href}" class="item mini-item" title="{tool.get('nom', '')}">
                {icon_html}
                {label_html}
            </a>'''

    # Section URL
    url_html = ""
    for bookmark in urls:
        icon_html = get_icon_html(bookmark.get('icon', ''), icon_url)
        url_html += f'''
            <a href="{bookmark.get('url', '')}" class="item item-url" title="{bookmark.get('nom', '')}">
                {icon_html}
                <span class="label">{bookmark.get('nom', '')}</span>
            </a>'''

    # Couleurs thème Adwaita - box = fond équipement (clair), item = fond service (foncé)
    if theme == 'light':
        c = {'bg': '#fafafa', 'container': '#ffffff', 'header': '#ebebeb', 'border': '#d0d0d0',
             'text': '#2e2e2e', 'text2': '#5e5e5e', 'box': '#e8e8e8', 'item': '#f5f5f5',
             'hover': '#888b8f', 'status': '#2ec27e'}
    else:
        c = {'bg': '#1e1e1e', 'container': '#242424', 'header': '#303030', 'border': '#1a1a1a',
             'text': '#ffffff', 'text2': '#999999', 'box': '#3d3d3d', 'item': '#2d2d2d',
             'hover': '#888b8f', 'status': '#2ec27e'}

    # Surcharge avec les couleurs personnalisées si définies
    if apparence.get('couleur_fond'):
        c['container'] = apparence['couleur_fond']
    if apparence.get('couleur_header'):
        c['header'] = apparence['couleur_header']
    else:
        c['header'] = c['container']  # Par défaut, même couleur que le fond
    if apparence.get('couleur_item'):
        c['item'] = apparence['couleur_item']
    if apparence.get('couleur_bordure'):
        c['border'] = apparence['couleur_bordure']
    if apparence.get('couleur_hover'):
        c['hover'] = apparence['couleur_hover']

    # Couleurs séparées pour chaque section (local, distant, url)
    c['box_local'] = apparence.get('couleur_box_local') or c['box']
    c['box_distant'] = apparence.get('couleur_box_distant') or c['box']
    c['box_url'] = apparence.get('couleur_box_url') or c['box']
    c['box_minitools'] = apparence.get('couleur_box_minitools') or c['box_local']

    # Image night-day: 512x238 (ratio 2.15:1) - affichée entièrement comme toggle

    html = f'''<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
html, body {{ height: 100%; overflow: hidden; background: transparent; }}
body {{
    font-family: 'Cantarell', 'Segoe UI', sans-serif;
    font-size: {font_size}px;
    font-weight: 400;
    background: transparent;
    color: {c['text']};
}}
.container {{
    height: 100%;
    display: flex;
    flex-direction: column;
    background: {c['container']};
    border-radius: {border_radius}px;
    border: 1px solid {c['border']};
    overflow: hidden;
}}
/* Header avec LOCAL intégré */
.header {{
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    padding: 8px 14px;
    background: {c['header']};
    min-height: 80px;
    gap: 12px;
}}
.header-local {{
    display: flex;
    align-items: center;
    gap: {espacement_local}px;
    flex: 1;
    padding: 8px 12px;
    background: {c['box_local']};
    border-radius: {border_radius_local}px;
}}
.header-controls {{
    display: flex;
    gap: 4px;
    align-items: center;
    flex-shrink: 0;
}}
.header-btn {{
    width: 32px;
    height: 32px;
    background: transparent;
    border: none;
    cursor: pointer;
    border-radius: 6px;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s;
}}
.header-btn:hover {{
    background: {c['hover']};
}}
.header-btn.btn-parametre img {{
    width: {icon_parametre}px;
    height: {icon_parametre}px;
    object-fit: contain;
}}
.header-btn.btn-fermer img {{
    width: {icon_fermer}px;
    height: {icon_fermer}px;
    object-fit: contain;
}}
.theme-btn {{
    overflow: hidden;
    width: {icon_theme_width}px;
    height: {icon_theme_height}px;
    padding: 0;
}}
.theme-btn img {{
    width: 100%;
    height: 100%;
    object-fit: contain;
}}
.main {{
    flex: 1;
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 10px;
    padding: 12px;
    overflow: hidden;
}}
/* DISTANT à gauche - flex wrap pour ajuster la taille des boxes */
.section-distant {{
    display: flex;
    flex-wrap: wrap;
    gap: {espacement_distant}px;
    align-content: start;
    overflow-y: auto;
    padding-right: 6px;
}}
/* URL à droite */
.section-url {{
    display: grid;
    grid-template-columns: repeat({col_url}, 1fr);
    gap: {espacement_url}px;
    padding: 12px;
    background: {c['box_url']};
    border-radius: 10px;
    align-content: start;
    min-width: 180px;
}}
/* Barre de mini outils */
.section-minitools {{
    display: flex;
    gap: {espacement_minitools}px;
    padding: 10px 14px;
    background: {c['box_minitools']};
    border-top: 1px solid {c['border']};
    flex-wrap: wrap;
    align-items: center;
    overflow-x: auto;
}}
.section-minitools .mini-item {{
    flex-direction: row;
    padding: 6px 10px;
    min-width: 90px;
}}
.section-minitools .mini-item .label {{
    font-size: {font_size - 2}px;
}}
/* IP box - taille ajustée au contenu */
.ip-box {{
    background: {c['box_distant']};
    border-radius: 8px;
    padding: 10px;
    flex-shrink: 0;
}}
.ip-header {{
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: {font_size}px;
    font-weight: 500;
    color: {c['text']};
    margin-bottom: 2px;
}}
.status-dot {{
    width: 8px;
    height: 8px;
    border-radius: 50%;
    flex-shrink: 0;
}}
.status-up {{
    background: #2ec27e;
}}
.status-down {{
    background: #e01b24;
}}
.status-unknown {{
    background: #888888;
}}
.ip-name {{
    font-size: {font_size - 2}px;
    font-family: 'JetBrains Mono', 'Consolas', monospace;
    color: {c['text2']};
    margin-bottom: 8px;
    margin-left: 14px;
}}
.items {{
    display: flex;
    flex-wrap: wrap;
    gap: {espacement_distant}px;
}}
.item {{
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 8px;
    background: {c['item']};
    border-radius: 8px;
    text-decoration: none;
    color: {c['text']};
    cursor: pointer;
    transition: background 0.15s, transform 0.1s;
}}
.item:hover {{
    background: {c['hover']};
    transform: translateY(-2px);
    color: #fff;
}}
.item-local {{
    background: transparent;
    padding: 8px 12px;
}}
.item-local:hover {{
    background: {c['item']};
}}
.item-url {{
    background: transparent;
    padding: 10px;
}}
.item-url:hover {{
    background: {c['item']};
}}
.service-icon {{
    line-height: 1;
}}
.service-img {{
    object-fit: contain;
}}
.label {{
    font-size: {font_size - 2}px;
    font-weight: 400;
    text-align: center;
    color: {c['text2']};
    max-width: 70px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}}
.item:hover .label {{
    color: #fff;
}}
/* Scrollbar style */
.section-distant::-webkit-scrollbar {{
    width: 6px;
}}
.section-distant::-webkit-scrollbar-track {{
    background: transparent;
}}
.section-distant::-webkit-scrollbar-thumb {{
    background: {c['border']};
    border-radius: 3px;
}}
.section-distant::-webkit-scrollbar-thumb:hover {{
    background: {c['text2']};
}}
</style>
</head>
<body>
<div class="container">
    <div class="header">
        <div class="header-local">{local_html}</div>
        <div class="header-controls">
            <button class="header-btn theme-btn" onclick="location.href='app://toggle-theme'" title="Thème">
                <img src="file://{ICONS_DIR}/night-day.png" alt="">
            </button>
            <button class="header-btn btn-parametre" onclick="location.href='app://settings'" title="Paramètres">
                <img src="file://{ICONS_DIR}/parametre.png" alt="">
            </button>
            <button class="header-btn btn-fermer" onclick="location.href='app://close'" title="Fermer">
                <img src="file://{ICONS_DIR}/fermer.png" alt="">
            </button>
        </div>
    </div>
    <div class="main">
        <div class="section-distant">{distant_html}</div>
        <div class="section-url">{url_html}</div>
    </div>
    <div class="section-minitools">{minitools_html}</div>
</div>
</body></html>'''
    return html


class DashboardLauncherWindow(Gtk.Window):
    def __init__(self):
        super().__init__(title="Dashboard Launcher")
        self.config = load_config()
        self.fenetre_config = self.config.get('fenetre', {})

        # Activer la transparence pour éviter le flash blanc
        screen = self.get_screen()
        visual = screen.get_rgba_visual()
        if visual:
            self.set_visual(visual)
        self.set_app_paintable(True)
        self.connect('draw', self.on_draw)

        self.set_type_hint(Gdk.WindowTypeHint.POPUP_MENU)
        self.set_skip_taskbar_hint(True)
        self.set_skip_pager_hint(True)
        self.set_decorated(False)  # Pas de décoration pour éviter le flash
        self.set_resizable(False)

        if self.fenetre_config.get('toujours_au_dessus', False):
            self.set_keep_above(True)

        largeur = self.fenetre_config.get('largeur', 800)
        hauteur = self.fenetre_config.get('hauteur', 450)
        self.set_default_size(largeur, hauteur)

        display = Gdk.Display.get_default()
        ecran_num = self.fenetre_config.get('ecran', -1)

        if ecran_num is None or ecran_num < 0:
            seat = display.get_default_seat()
            pointer = seat.get_pointer()
            _, mouse_x, mouse_y = pointer.get_position()
            monitor = display.get_monitor_at_point(mouse_x, mouse_y)
        else:
            n_monitors = display.get_n_monitors()
            monitor = display.get_monitor(ecran_num) if ecran_num < n_monitors else display.get_primary_monitor() or display.get_monitor(0)

        geom = monitor.get_geometry()
        centrer = self.fenetre_config.get('centrer', True)
        x = self.fenetre_config.get('x', 0)
        y = self.fenetre_config.get('y', 50)

        if centrer:
            self.move(geom.x + (geom.width - largeur) // 2, geom.y + y)
        else:
            self.move(geom.x + x, geom.y + y)

        self.connect('key-press-event', self.on_key_press)

        settings = WebKit2.Settings()
        settings.set_property('hardware-acceleration-policy', WebKit2.HardwareAccelerationPolicy.NEVER)

        self.webview = WebKit2.WebView()
        self.webview.set_settings(settings)
        self.webview.connect('decide-policy', self.on_decide_policy)

        # Fond transparent pour éviter le flash blanc
        bg_color = Gdk.RGBA()
        bg_color.red = 0
        bg_color.green = 0
        bg_color.blue = 0
        bg_color.alpha = 0  # Transparent
        self.webview.set_background_color(bg_color)

        self.reload_content()
        self.add(self.webview)
        self.connect('destroy', self.on_destroy)

        # Lancer le ping en arrière-plan
        self.ping_timer_id = None
        self.autohide_timer_id = None
        self.mouse_inside = True
        self.start_ping_check()

        # Configurer le ping périodique
        ping_interval = self.config.get('apparence', {}).get('ping_intervalle', 360)
        if ping_interval > 0:
            self.ping_timer_id = GLib.timeout_add_seconds(ping_interval, self.on_ping_timer)

        # Fermeture sur perte de focus (clic extérieur)
        if self.fenetre_config.get('fermer_sur_clic_exterieur', True):
            self.connect('focus-out-event', self.on_focus_out)

        # Autohide: fermer si souris hors fenêtre pendant X secondes
        autohide_delay = self.fenetre_config.get('autohide', 0)
        if autohide_delay > 0:
            self.autohide_delay = autohide_delay
            self.connect('enter-notify-event', self.on_mouse_enter)
            self.connect('leave-notify-event', self.on_mouse_leave)

    def on_draw(self, widget, cr):
        """Dessine un fond transparent."""
        cr.set_source_rgba(0, 0, 0, 0)
        cr.set_operator(cairo.OPERATOR_SOURCE)
        cr.paint()
        return False

    def reload_content(self):
        self.config = load_config()
        self.webview.load_html(generate_html(self.config), 'file://')

    def start_ping_check(self):
        """Lance le ping de toutes les IPs en arrière-plan."""
        distant = self.config.get('distant', [])
        hosts = [m.get('ip', '') for m in distant if m.get('ip')]
        ping_all_hosts(hosts, self.on_ping_complete)

    def on_ping_complete(self):
        """Callback appelé quand tous les pings sont terminés."""
        self.reload_content()

    def on_ping_timer(self):
        """Timer pour le ping périodique."""
        self.start_ping_check()
        return True  # Continuer le timer

    def on_destroy(self, widget):
        """Nettoyage à la fermeture."""
        if self.ping_timer_id:
            GLib.source_remove(self.ping_timer_id)
        if self.autohide_timer_id:
            GLib.source_remove(self.autohide_timer_id)
        Gtk.main_quit()

    def on_focus_out(self, widget, event):
        """Ferme la fenêtre quand elle perd le focus."""
        self.destroy()
        return True

    def on_mouse_enter(self, widget, event):
        """Souris entre dans la fenêtre - annuler le timer autohide."""
        self.mouse_inside = True
        if self.autohide_timer_id:
            GLib.source_remove(self.autohide_timer_id)
            self.autohide_timer_id = None
        return False

    def on_mouse_leave(self, widget, event):
        """Souris quitte la fenêtre - démarrer le timer autohide."""
        self.mouse_inside = False
        if self.autohide_timer_id:
            GLib.source_remove(self.autohide_timer_id)
        self.autohide_timer_id = GLib.timeout_add_seconds(
            self.autohide_delay, self.on_autohide_timeout
        )
        return False

    def on_autohide_timeout(self):
        """Timer autohide expiré - fermer si souris toujours hors fenêtre."""
        if not self.mouse_inside:
            self.destroy()
        return False  # Ne pas répéter

    def toggle_theme(self):
        current = self.config.get('apparence', {}).get('theme', 'dark')
        new_theme = 'light' if current == 'dark' else 'dark'
        try:
            with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
                content = f.read()
            # Regex précise: uniquement "theme:" en début de ligne (après espaces)
            content = re.sub(r'^(\s*)theme:\s*"?\w+"?', rf'\1theme: "{new_theme}"', content, flags=re.MULTILINE)
            with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
                f.write(content)
            self.reload_content()
        except Exception as e:
            print(f"Erreur thème: {e}")

    def on_key_press(self, widget, event):
        if event.keyval == Gdk.KEY_Escape:
            self.destroy()
            return True
        return False

    def on_decide_policy(self, webview, decision, decision_type):
        if decision_type == WebKit2.PolicyDecisionType.NAVIGATION_ACTION:
            uri = decision.get_navigation_action().get_request().get_uri()

            if uri == 'app://close':
                self.destroy()
            elif uri == 'app://toggle-theme':
                self.toggle_theme()
            elif uri == 'app://settings':
                subprocess.Popen(['xdg-open', CONFIG_FILE])
            elif uri and uri.startswith('app://run/'):
                subprocess.Popen(uri.replace('app://run/', '').split())
            elif uri and (uri.startswith('smb://') or uri.startswith('nfs://')):
                # SMB/NFS: monter avec gio et ouvrir dans Nautilus
                self.mount_and_open(uri)
            elif uri and not uri.startswith('file://') and not uri.startswith('about:'):
                try:
                    Gio.AppInfo.launch_default_for_uri(uri, None)
                except Exception as e:
                    print(f"Erreur URI: {e}")
            else:
                return False
            decision.ignore()
            return True
        return False

    def mount_and_open(self, uri):
        """Monte un partage SMB/NFS avec gio et l'ouvre dans Nautilus."""
        def open_nautilus():
            subprocess.Popen(['nautilus', uri])
            return False  # Ne pas répéter

        def mount_thread():
            try:
                # Monter le partage avec gio mount
                subprocess.run(
                    ['gio', 'mount', uri],
                    capture_output=True,
                    text=True,
                    timeout=30
                )
            except subprocess.TimeoutExpired:
                print(f"Timeout montage: {uri}")
            except Exception as e:
                print(f"Erreur montage {uri}: {e}")
            finally:
                # Ouvrir dans Nautilus une seule fois
                GLib.idle_add(open_nautilus)

        # Lancer le montage en arrière-plan pour ne pas bloquer l'UI
        threading.Thread(target=mount_thread, daemon=True).start()


def is_running():
    if os.path.exists(PID_FILE):
        try:
            with open(PID_FILE, 'r') as f:
                pid = int(f.read().strip())
            os.kill(pid, 0)
            return pid
        except (OSError, ValueError):
            pass
    return None


def main():
    if '--toggle' in sys.argv and is_running():
        import signal
        os.kill(is_running(), signal.SIGTERM)
        if os.path.exists(PID_FILE):
            os.remove(PID_FILE)
        sys.exit(0)

    if is_running():
        print(f"Dashboard déjà en cours")
        sys.exit(0)

    os.environ['WEBKIT_DISABLE_COMPOSITING_MODE'] = '1'

    with open(PID_FILE, 'w') as f:
        f.write(str(os.getpid()))

    import atexit
    atexit.register(lambda: os.path.exists(PID_FILE) and os.remove(PID_FILE))

    win = DashboardLauncherWindow()
    win.show_all()
    Gtk.main()


if __name__ == '__main__':
    main()
