#!/usr/bin/env python3
import sys
import json
import os
import subprocess
import time
import re
import shutil
import math
from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMenu, QAction, QActionGroup
from PyQt5.QtGui import QIcon, QPixmap, QPainter, QColor, QBrush, QPen, QFont
from PyQt5.QtCore import QFileSystemWatcher, QTimer, Qt, QRectF

# ------------------------------------------------------------------------------
# I18N MODULE IMPORT
# ------------------------------------------------------------------------------
sys.path.insert(0, "/usr/libexec/fedora-update")
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lib"))
try:
    from fedora_update_i18n import i18n, _
except ImportError:
    def _(key, **kwargs):
        return key

# ------------------------------------------------------------------------------
# CONSTANTS & PATHS
# ------------------------------------------------------------------------------
STATUS_FILE = "/var/cache/fedora-update/status.json"
CONFIG_FILE = os.path.expanduser("~/.config/fedora-update/config.json")

DEV_ASSETS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "assets")
if os.path.exists(DEV_ASSETS):
    ASSETS_DIR = DEV_ASSETS
else:
    ASSETS_DIR = "/usr/share/fedora-update/assets"

ICON_OK = os.path.join(ASSETS_DIR, "fedora-update-ok.png")
ICON_PENDING = os.path.join(ASSETS_DIR, "fedora-update-pending.png")
ICON_CHECKING = os.path.join(ASSETS_DIR, "fedora-update-checking.png")

# ------------------------------------------------------------------------------
# HELPER FUNCTIONS
# ------------------------------------------------------------------------------
def get_time_since_last_check(data):
    if not data or "last_check_ts" not in data:
        if os.path.exists(STATUS_FILE):
            try:
                mtime = os.path.getmtime(STATUS_FILE)
                diff = max(0, time.time() - mtime)
            except Exception:
                return _("UNKNOWN")
        else:
            return _("NEVER")
    else:
        try:
            diff = max(0, time.time() - float(data["last_check_ts"]))
        except Exception:
            return _("UNKNOWN")

    if diff < 60:
        return _("JUST_NOW")
    mins = int(diff // 60)
    hours = mins // 60
    days = hours // 24

    if days > 0:
        return _("AGO_DAYS_HOURS", days=days, hours=hours % 24)
    elif hours > 0:
        return _("AGO_HOURS_MINS", hours=hours, mins=mins % 60)
    else:
        return _("AGO_MINS", mins=mins)

def get_next_check_in():
    try:
        out = subprocess.check_output(
            ["systemctl", "status", "fedora-update-check.timer"],
            stderr=subprocess.DEVNULL, text=True, timeout=3
        )
        for line in out.splitlines():
            if "Trigger:" in line and "left" in line:
                match = re.search(r';\s*(.*?)\s*left', line, re.IGNORECASE)
                if match:
                    raw = match.group(1).strip()
                    cleaned = re.sub(r'(\d+)\s*min', r'\1m', raw)
                    cleaned = re.sub(r'(\d+)\s*hours?', r'\1h', cleaned)
                    cleaned = re.sub(r'(\d+)\s*days?', r'\1d', cleaned)
                    return re.sub(r'(\d+)\s*sec', r'\1s', cleaned)
    except Exception:
        pass

    return _("UNKNOWN")

# ------------------------------------------------------------------------------
# TRAY APPLICATION CLASS
# ------------------------------------------------------------------------------
class FedoraUpdateTray(QSystemTrayIcon):
    def __init__(self, app):
        super().__init__()
        self.app = app
        self.last_notified_count = 0

        self.menu = QMenu()
        self.setContextMenu(self.menu)
        self.menu.aboutToShow.connect(self.build_menu)
        self.activated.connect(self.on_activated)

        self.pulse_timer = QTimer()
        self.pulse_timer.timeout.connect(self.animate_pulse_frame)
        self.pulse_ticks = 0
        self.pulse_scale = 1.0
        self.pulse_growing = False
        self.is_pulsing = False

        cache_dir = os.path.dirname(STATUS_FILE)
        os.makedirs(cache_dir, exist_ok=True)

        self.watcher = QFileSystemWatcher()
        if os.path.exists(cache_dir):
            self.watcher.addPath(cache_dir)
            self.watcher.directoryChanged.connect(self.update_status)
            self.watcher.fileChanged.connect(self.update_status)

        self.poll_timer = QTimer()
        self.poll_timer.timeout.connect(self.update_status)
        self.poll_timer.start(15000)

        # Sett initielt standardikon
        self.setIcon(QIcon(ICON_OK))
        self.update_status()

    def on_activated(self, reason):
        if reason == QSystemTrayIcon.Trigger:
            self.launch_upgrade()

    def set_language(self, lang_code):
        i18n.save_language_override(lang_code)

    def load_status(self):
        if not os.path.exists(STATUS_FILE):
            return None
        try:
            with open(STATUS_FILE, 'r', encoding='utf-8') as f:
                return json.load(f)
        except (json.JSONDecodeError, OSError):
            return None

    def should_show_kofi(self):
        cfg = {}
        if os.path.exists(CONFIG_FILE):
            try:
                with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
                    cfg = json.load(f)
            except Exception:
                cfg = {}

        now = time.time()
        if "first_installed_ts" not in cfg:
            cfg["first_installed_ts"] = now
            os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
            try:
                with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
                    json.dump(cfg, f, indent=2)
            except Exception:
                pass
            return True

        first_ts = float(cfg.get("first_installed_ts", now))
        elapsed = max(0, now - first_ts)

        if elapsed < 86400:
            return True

        cycle = 30 * 86400
        if (elapsed % cycle) < 86400:
            return True

        return False

    def get_current_base_icon(self):
        data = self.load_status()
        dnf_list = data.get("dnf_updates", []) if data else []
        flatpak_list = data.get("flatpak_updates", []) if data else []
        nvidia_mismatch = data.get("nvidia_gl_mismatch", False) if data else False
        if nvidia_mismatch or (len(dnf_list) + len(flatpak_list)) > 0:
            return ICON_PENDING
        return ICON_OK

    def create_scaled_icon(self, base_icon_path, scale=1.0):
        pixmap = QPixmap(base_icon_path)
        if pixmap.isNull():
            return QIcon(base_icon_path)

        if abs(scale - 1.0) < 0.001:
            return QIcon(pixmap)

        target_size = max(1, int(round(128 * scale)))
        scaled = pixmap.scaled(target_size, target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)

        canvas = QPixmap(128, 128)
        canvas.fill(Qt.transparent)
        painter = QPainter(canvas)
        painter.setRenderHint(QPainter.Antialiasing)
        painter.setRenderHint(QPainter.SmoothPixmapTransform)

        offset_x = (128 - target_size) / 2.0
        offset_y = (128 - target_size) / 2.0
        painter.drawPixmap(int(round(offset_x)), int(round(offset_y)), scaled)
        painter.end()

        return QIcon(canvas)

    def trigger_pulse_animation(self):
        if not self.is_pulsing:
            self.is_pulsing = True
            self.pulse_frame = 0
            self.pulse_total_frames = 360  # ~12 seconds at 30 FPS (3x duration)
            self.pulse_timer.start(33)

    def animate_pulse_frame(self):
        self.pulse_frame += 1
        if self.pulse_frame >= self.pulse_total_frames:
            self.pulse_timer.stop()
            self.is_pulsing = False
            self.setIcon(QIcon(self.get_current_base_icon()))
            return

        progress = self.pulse_frame / float(self.pulse_total_frames)
        # 6 complete smooth sine pulses (1.0 -> 0.75 -> 1.0 -> 0.75 -> 1.0)
        scale = 0.875 + 0.125 * math.cos(progress * 12.0 * math.pi)
        self.setIcon(self.create_scaled_icon(self.get_current_base_icon(), scale=scale))

    def update_status(self):
        data = self.load_status()

        dnf_list = data.get("dnf_updates", []) if data else []
        flatpak_list = data.get("flatpak_updates", []) if data else []
        reboot_req = data.get("reboot_required", False) if data else False
        nvidia_mismatch = data.get("nvidia_gl_mismatch", False) if data else False

        total_updates = len(dnf_list) + len(flatpak_list)

        if nvidia_mismatch:
            if not self.is_pulsing:
                self.trigger_pulse_animation()
            tooltip = _("NVIDIA_SYNC_FAIL")
            self.setToolTip(tooltip)
        elif total_updates > 0:
            if not self.is_pulsing:
                self.setIcon(QIcon(ICON_PENDING))
            tooltip = _("UPDATES_AVAILABLE", count=total_updates)
            details = []
            if dnf_list: details.append(f"{_('CAT_DNF')}: {len(dnf_list)}")
            if flatpak_list: details.append(f"{_('CAT_FLATPAK')}: {len(flatpak_list)}")
            if details:
                tooltip += "\n• " + "\n• ".join(details)
            if reboot_req: tooltip += "\n" + _("TOOLTIP_REBOOT")
            self.setToolTip(tooltip)
        else:
            if not self.is_pulsing:
                self.setIcon(QIcon(ICON_OK))
            self.setToolTip(_("SYSTEM_UP_TO_DATE"))

        if total_updates > 0 and total_updates != self.last_notified_count:
            self.trigger_pulse_animation()
            self.showMessage(_("FEDORA_UPDATE_TITLE"), _("UPDATES_AVAILABLE_COUNT", count=total_updates), self.icon(), 5000)
            self.last_notified_count = total_updates
        elif total_updates == 0:
            self.last_notified_count = 0

    def build_menu(self):
        data = self.load_status()
        self.menu.clear()

        dnf_list = data.get("dnf_updates", []) if data else []
        flatpak_list = data.get("flatpak_updates", []) if data else []

        total_updates = len(dnf_list) + len(flatpak_list)

        pending_menu = self.menu.addMenu(_("UPDATES_AVAILABLE", count=total_updates))

        if total_updates > 0:
            pipeline = [
                (_("CAT_DNF"), dnf_list),
                (_("CAT_FLATPAK"), flatpak_list)
            ]

            for title, pkgs in pipeline:
                if pkgs:
                    pending_menu.addSection(f"{title} ({len(pkgs)})")
                    for pkg in pkgs:
                        action = QAction(f"  {pkg}", pending_menu)
                        action.setEnabled(False)
                        pending_menu.addAction(action)
        else:
            no_up = QAction(_("ALL_PACKAGES_UP_TO_DATE"), pending_menu)
            no_up.setEnabled(False)
            pending_menu.addAction(no_up)

        time_since = get_time_since_last_check(data)
        next_in = get_next_check_in()

        last_action = QAction(_("TIME_SINCE_LAST_CHECK", time=time_since), self.menu)
        last_action.setEnabled(False)
        self.menu.addAction(last_action)

        next_action = QAction(_("NEXT_CHECK_IN", time=next_in), self.menu)
        next_action.setEnabled(False)
        self.menu.addAction(next_action)

        self.menu.addSeparator()

        check_action = QAction(_("CHECK_FOR_UPDATES"), self.menu)
        check_action.triggered.connect(self.trigger_check)
        self.menu.addAction(check_action)

        upgrade_action = QAction(_("UPDATE_NOW"), self.menu)
        upgrade_action.triggered.connect(self.launch_upgrade)
        self.menu.addAction(upgrade_action)

        self.menu.addSeparator()

        lang_menu = self.menu.addMenu(_("LANGUAGE_SELECTION"))
        group = QActionGroup(lang_menu)
        group.setExclusive(True)

        sys_action = QAction(_("SYSTEM_DEFAULT_LANG"), lang_menu)
        sys_action.setCheckable(True)
        if not i18n.manual_override:
            sys_action.setChecked(True)
        sys_action.triggered.connect(lambda: self.set_language("system"))
        group.addAction(sys_action)
        lang_menu.addAction(sys_action)
        lang_menu.addSeparator()

        avail_langs = i18n.get_available_languages()
        for code, name in avail_langs.items():
            l_action = QAction(name, lang_menu)
            l_action.setCheckable(True)
            if i18n.manual_override == code:
                l_action.setChecked(True)
            l_action.triggered.connect(lambda checked, c=code: self.set_language(c))
            group.addAction(l_action)
            lang_menu.addAction(l_action)

        if self.should_show_kofi():
            self.menu.addSeparator()
            kofi_header = QAction("Please consider supporting this project", self.menu)
            kofi_header.setEnabled(False)
            self.menu.addAction(kofi_header)

            kofi_action = QAction("  ☕ Support on Ko-fi (ko-fi.com/tuxofvalhalla)", self.menu)
            kofi_action.triggered.connect(lambda: subprocess.Popen(["xdg-open", "https://ko-fi.com/tuxofvalhalla"]))
            self.menu.addAction(kofi_action)

        self.menu.addSeparator()

        quit_action = QAction(_("QUIT"), self.menu)
        quit_action.triggered.connect(self.app.quit)
        self.menu.addAction(quit_action)

    def trigger_check(self):
        self.setIcon(QIcon(ICON_CHECKING))
        self.setToolTip(_("CHECKING_FOR_UPDATES"))
        self.app.processEvents()
        try:
            subprocess.Popen(["systemctl", "start", "--no-block", "fedora-update-check.service"])
        except Exception:
            pass

    def launch_upgrade(self):
        self.launch_in_terminal(["/usr/bin/fedora-update-cli"], "Fedora-Update")

    def launch_in_terminal(self, cmd, title):
        # 1. Standard FreeDesktop default terminal execution
        if shutil.which("xdg-terminal-exec"):
            try:
                subprocess.Popen(["xdg-terminal-exec"] + cmd)
                return
            except Exception:
                pass

        # 2. System standard virtual terminal wrapper
        if shutil.which("x-terminal-emulator"):
            try:
                subprocess.Popen(["x-terminal-emulator", "-e"] + cmd)
                return
            except Exception:
                pass

        # 3. User-defined environment variable
        term = os.environ.get("TERMINAL")
        if term and shutil.which(term):
            try:
                subprocess.Popen([term, "-e"] + cmd)
                return
            except Exception:
                pass

        # 4. Comprehensive terminal emulator lookup (Modern, Wayland, DE-specific, Power-user & Exotic)
        terminal_list = [
            "ghostty", "ptyxis", "kitty", "alacritty", "wezterm", "foot",
            "konsole", "gnome-terminal", "xfce4-terminal", "mate-terminal",
            "cosmic-term", "blackbox", "rio", "contour", "terminator", "tilix",
            "lxterminal", "qterminal", "deepin-terminal", "hyper", "tabby",
            "urxvt", "rxvt", "st", "tilda", "guake", "xterm"
        ]

        for candidate in terminal_list:
            if shutil.which(candidate):
                try:
                    subprocess.Popen([candidate, "-e"] + cmd)
                    return
                except Exception:
                    continue

# ------------------------------------------------------------------------------
# MAIN ENTRY POINT
# ------------------------------------------------------------------------------
if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setApplicationName("00_fedora_update")
    app.setOrganizationName("00_a")
    app.setQuitOnLastWindowClosed(False)

    tray = FedoraUpdateTray(app)
    tray.show()
    sys.exit(app.exec_())
