#!/usr/bin/env python3
"""Filetoss kontrolni prozor.

Qt6 i QML preko PyQt6. QML nosi cijeli izgled, a ovaj dio samo cita i pise
config, pali i gasi agenta i javlja stanje. Ako se kasnije prijede na cxx-qt,
QML ostaje isti.
"""
from __future__ import annotations

import json
import os
import shutil
import signal
import subprocess
import sys
from pathlib import Path

from PyQt6.QtCore import QObject, QTimer, QUrl, pyqtProperty, pyqtSignal, pyqtSlot
from PyQt6.QtGui import QGuiApplication, QIcon
from PyQt6.QtQml import QQmlApplicationEngine

import platforma  # noqa: E402

KONFIG_DIR = platforma.konfig_dir("filetoss")
CONFIG = KONFIG_DIR / "agent.json"
STANJE = KONFIG_DIR / "stanje.json"
NAREDBA = KONFIG_DIR / "naredba.json"
AUTOSTART = Path.home() / ".config" / "autostart" / "filetoss-tray.desktop"
APP_URL = "https://filetoss.humanitics.eu/app/"
SCREENTOSS_URL = "https://screentoss.humanitics.eu/"

# Prvo instalirani binarij, pa lokalni build za razvoj.
KANDIDATI = [
    Path("/usr/bin/filetoss"),
    Path(__file__).resolve().parent.parent / "agent-rs" / "target" / "release" / "filetoss",
]


def binarij() -> Path | None:
    if platforma.WINDOWS:
        return platforma.nadji("filetoss")
    for p in KANDIDATI:
        if p.exists() and os.access(p, os.X_OK):
            return p
    return None


def zadana_mapa() -> str:
    return str(Path.home() / "Downloads" / "Filetoss")


def otvori_lokalno_ili_online(nazivi: list[str], url: str) -> str:
    """Pokreni lokalnu aplikaciju, a na mrezu idi tek ako je nema.

    Web verzija je zamjena, ne odrediste: ako je program instaliran, korisnik
    ocekuje njega, s pristupom datotekama i bez prijave u pregledniku.
    """
    for naziv in nazivi:
        put = platforma.nadji(naziv)
        if put:
            platforma.pokreni_pozadinski([str(put)])
            return f"Otvaram {naziv}."
    # Zadnja postaja: web.
    platforma.otvori_url(url)
    return "Aplikacija nije instalirana, otvaram web."


class Agent(QObject):
    promjena = pyqtSignal()

    def __init__(self) -> None:
        super().__init__()
        self._cfg = self._ucitaj()
        self._poruka = "Spremno."
        self._greska = False
        self._zauzet = False
        self._pid: int | None = None
        self._stanje: dict = {}
        self._nadzor = QTimer(self)
        self._nadzor.timeout.connect(self._osvjezi)
        self._nadzor.start(1500)
        self._osvjezi()

    # ---------- config ----------

    def _ucitaj(self) -> dict:
        try:
            return json.loads(CONFIG.read_text())
        except Exception:
            return {}

    def _spremi(self) -> None:
        CONFIG.parent.mkdir(parents=True, exist_ok=True)
        CONFIG.write_text(json.dumps(self._cfg, indent=2))
        CONFIG.chmod(0o600)

    # ---------- stanje ----------

    def _nadi_pid(self) -> int | None:
        """Agent moze zivjeti sam ili unutar traya; gledamo oboje.

        Pravi dokaz je ipak stanje.json, jer ga pise sam agent, gdje god bio.
        """
        for naziv in ("filetoss", "filetoss-tray"):
            if platforma.WINDOWS:
                if platforma.radi(naziv):
                    # Windows tasklist ne daje pid u ovom obliku; broj nam sluzi
                    # samo kao dokaz da proces postoji.
                    return 1
                continue
            try:
                out = subprocess.run(
                    ["pgrep", "-x", naziv], capture_output=True, text=True, timeout=2
                )
                prvi = out.stdout.split()
                if prvi:
                    return int(prvi[0])
            except Exception:
                continue
        return None

    def _ucitaj_stanje(self) -> dict:
        try:
            s = json.loads(STANJE.read_text())
        except Exception:
            return {}
        # Ustajalo stanje znaci da agent vise ne radi.
        import time

        if time.time() - float(s.get("azurirano", 0)) > 8:
            return {}
        return s

    def _osvjezi(self) -> None:
        stari = (self._pid, self._cfg.get("user"), json.dumps(self._stanje, sort_keys=True))
        self._cfg = self._ucitaj() or self._cfg
        self._pid = self._nadi_pid()
        self._stanje = self._ucitaj_stanje()
        novi = (self._pid, self._cfg.get("user"), json.dumps(self._stanje, sort_keys=True))
        if novi != stari:
            self.promjena.emit()

    # ---------- svojstva za QML ----------

    @pyqtProperty(bool, notify=promjena)
    def prijavljen(self) -> bool:
        return bool(self._cfg.get("token"))

    @pyqtProperty(str, notify=promjena)
    def user(self) -> str:
        return self._cfg.get("user") or ""

    @pyqtProperty(str, notify=promjena)
    def ime(self) -> str:
        return self._cfg.get("name") or ""

    @pyqtProperty(str, notify=promjena)
    def mapa(self) -> str:
        return self._cfg.get("downloadDir") or zadana_mapa()

    @pyqtProperty(bool, notify=promjena)
    def autoAccept(self) -> bool:
        return bool(self._cfg.get("autoAccept", True))

    @pyqtProperty(bool, notify=promjena)
    def autostart(self) -> bool:
        return platforma.autostart_ukljucen("Filetoss", AUTOSTART)

    @pyqtProperty(bool, notify=promjena)
    def radi(self) -> bool:
        # Svjeze stanje znaci da agent radi, bez obzira u kojem je procesu.
        if self._stanje:
            return True
        return self._pid is not None

    @pyqtProperty(bool, notify=promjena)
    def povezan(self) -> bool:
        return bool(self._stanje.get("online"))

    @pyqtProperty(list, notify=promjena)
    def uredaji(self) -> list:
        return [
            {"uid": p.get("uid", ""), "name": p.get("name", "uređaj"), "via": p.get("via", "")}
            for p in self._stanje.get("peers", [])
        ]

    @pyqtProperty(bool, notify=promjena)
    def zauzet(self) -> bool:
        return self._zauzet

    @pyqtProperty(str, notify=promjena)
    def poruka(self) -> str:
        # Agentova poruka ima prednost: ona zna je li prijenos stvarno krenuo.
        agentova = (self._stanje or {}).get("poruka") or ""
        return agentova or self._poruka

    @pyqtProperty(bool, notify=promjena)
    def greska(self) -> bool:
        return self._greska

    def _javi(self, tekst: str, greska: bool = False) -> None:
        self._poruka = tekst
        self._greska = greska
        self.promjena.emit()

    # ---------- radnje ----------

    @pyqtSlot(str, str)
    def prijavi(self, user: str, lozinka: str) -> None:
        user = user.strip().lower()
        if len(user) < 3 or len(lozinka) < 4:
            self._javi("Korisnik i lozinka su prekratki.", True)
            return
        self._cfg["user"] = user
        import platform as _p

        self._cfg.setdefault("name", _p.node())
        self._cfg.setdefault("autoAccept", True)
        self._spremi()
        self._zauzet = True
        self._javi(f"Prijavljujem {user}…")
        self._pokreni(lozinka)
        QTimer.singleShot(3000, self._provjeri_prijavu)

    def _provjeri_prijavu(self) -> None:
        self._zauzet = False
        self._cfg = self._ucitaj()
        if self._cfg.get("token"):
            self._javi("Prijavljen.")
        else:
            self._javi("Prijava nije uspjela. Provjeri lozinku.", True)
        self.promjena.emit()

    def _pokreni(self, lozinka: str | None = None) -> None:
        bin_ = binarij()
        if not bin_:
            self._javi("Ne nalazim filetoss binarij.", True)
            return
        okolina = dict(os.environ)
        if lozinka:
            okolina["FILETOSS_PASS"] = lozinka
        # Tray je vlasnik agenta. Ako prozor pokrene jos jednog, na racunu se
        # pojave dva ista uredaja, a slanje zavrsi kod pogresnog.
        try:
            if platforma.WINDOWS:
                # Na Windowsu nema zasebnog tray procesa; ikonu nosi ovaj prozor.
                platforma.pokreni_pozadinski([str(bin_)], env=okolina)
                return
            tray = shutil.which("filetoss-tray")
            naredba = [tray, "--autostart"] if tray else [str(bin_)]
            subprocess.Popen(
                naredba,
                env=okolina,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                start_new_session=True,
            )
        except Exception as e:  # pragma: no cover
            self._javi(f"Pokretanje nije uspjelo: {e}", True)

    @pyqtSlot()
    def pokreni(self) -> None:
        self._pokreni()
        self._javi("Pokrećem…")
        QTimer.singleShot(1200, self._osvjezi)
        QTimer.singleShot(4000, self._provjeri_pokretanje)

    def _provjeri_pokretanje(self) -> None:
        self._osvjezi()
        if not self.radi:
            self._javi(
                "Agent se nije podigao. Provjeri je li instaliran paket filetoss.",
                True,
            )

    @pyqtSlot()
    def zaustavi(self) -> None:
        if self._pid and not platforma.WINDOWS:
            try:
                os.kill(self._pid, signal.SIGTERM)
            except Exception:
                pass
        platforma.ugasi("filetoss-tray", "filetoss")
        self._javi("Zaustavljeno.")
        QTimer.singleShot(800, self._osvjezi)

    @pyqtSlot()
    def odjavi(self) -> None:
        self.zaustavi()
        for k in ("token", "user"):
            self._cfg.pop(k, None)
        self._spremi()
        self._javi("Odjavljen.")

    @pyqtSlot(str)
    def postaviIme(self, ime: str) -> None:
        ime = ime.strip()[:48]
        if ime and ime != self._cfg.get("name"):
            self._cfg["name"] = ime
            self._spremi()
            self._javi("Naziv spremljen, vrijedi od sljedećeg pokretanja.")

    @pyqtSlot(str)
    def postaviMapu(self, url: str) -> None:
        put = QUrl(url).toLocalFile() if url.startswith("file:") else url
        if put:
            self._cfg["downloadDir"] = put
            self._spremi()
            self._javi(f"Sync mapa: {put}")

    @pyqtSlot(bool)
    def postaviAutoAccept(self, v: bool) -> None:
        self._cfg["autoAccept"] = bool(v)
        self._spremi()
        # Agent to primi odmah, bez ponovnog pokretanja.
        self._naredi({"t": "auto-accept", "enabled": bool(v)})
        self._javi("Primam bez pitanja." if v else "Svaki prijenos tražit će potvrdu.")

    @pyqtSlot(bool)
    def postaviAutostart(self, v: bool) -> None:
        if platforma.WINDOWS:
            # Registar, jer prečac u Startup mapi korisnik lako obriše, a onda
            # prekidač pokazuje jedno a sustav radi drugo.
            put = platforma.nadji("filetoss-gui") or binarij()
            platforma.autostart_postavi("Filetoss", f'"{put}"', AUTOSTART, "", bool(v))
            self._javi("Filetoss se diže s Windowsima." if v else "Automatsko pokretanje isključeno.")
            self.promjena.emit()
            return
        if v:
            AUTOSTART.parent.mkdir(parents=True, exist_ok=True)
            izvor = Path("/etc/xdg/autostart/filetoss-tray.desktop")
            if izvor.exists():
                AUTOSTART.write_text(izvor.read_text())
            else:
                AUTOSTART.write_text(
                    "[Desktop Entry]\nType=Application\nName=Filetoss\n"
                    f"Exec={binarij() or 'filetoss'}\nX-GNOME-Autostart-enabled=true\n"
                )
            self._javi("Filetoss se diže s prijavom.")
        else:
            AUTOSTART.unlink(missing_ok=True)
            self._javi("Automatsko pokretanje isključeno.")
        self.promjena.emit()

    @pyqtSlot()
    def otvoriMapu(self) -> None:
        Path(self.mapa).mkdir(parents=True, exist_ok=True)
        platforma.otvori_url(self.mapa)

    def _naredi(self, naredba: dict) -> bool:
        if not self.radi:
            self._javi("Filetoss nije pokrenut.", True)
            return False
        try:
            NAREDBA.parent.mkdir(parents=True, exist_ok=True)
            NAREDBA.write_text(json.dumps(naredba))
            return True
        except Exception as e:
            self._javi(f"Naredba nije zapisana: {e}", True)
            return False

    @pyqtSlot()
    def objasniPrazno(self) -> None:
        """Gumb ne smije biti mrtav; ako nema kome slati, reci zasto."""
        if not self._stanje:
            self._javi(
                "Agent ne javlja stanje. Vjerojatno je stara verzija; "
                "zaustavi ga i pokreni ponovo.",
                True,
            )
        else:
            self._javi(
                "Nema vidljivih uređaja. Otvori Filetoss na drugom uređaju, "
                "s istim računom.",
                True,
            )

    @pyqtSlot(str, list)
    def posalji(self, peer_uid: str, putanje: list) -> None:
        """Posalji lokalno preko agenta, bez preglednika."""
        datoteke = []
        for u in putanje:
            put = QUrl(u).toLocalFile() if str(u).startswith("file:") else str(u)
            if put:
                datoteke.append(put)
        if not datoteke:
            return
        if not peer_uid:
            self._javi("Odaberi uređaj kojem šalješ.", True)
            return
        # Popis se mijenja u hodu; uredaj je mogao nestati izmedu odabira i klika.
        if peer_uid not in {u["uid"] for u in self.uredaji}:
            self._javi("Taj uređaj više nije vidljiv. Osvježi popis.", True)
            return
        if self._naredi({"t": "send", "peerUid": peer_uid, "paths": datoteke}):
            koliko = len(datoteke)
            self._javi(f"Šaljem {koliko} {'datoteku' if koliko == 1 else 'datoteke'}…")

    @pyqtSlot()
    def otvoriApp(self) -> None:
        platforma.otvori_url(APP_URL)

    @pyqtSlot()
    def otvoriScreentoss(self) -> None:
        self._javi(
            otvori_lokalno_ili_online(
                ["screentoss-gui", "screentoss-tray", "screentoss"], SCREENTOSS_URL
            )
        )


def osiguraj_tray(naziv: str) -> None:
    """Podigni tray ako ne radi.

    Prozor i tray su zasebni procesi: zatvaranje prozora ne smije ostaviti
    korisnika bez ikone u traci, jer bi tada agent radio, a nista to ne bi
    pokazivalo.
    """
    if platforma.WINDOWS:
        # Windows nema StatusNotifierItem; ikonu nosi sam prozor.
        return
    if platforma.radi(naziv):
        return
    put = shutil.which(naziv)
    if put:
        # --autostart znaci samo ikona u traci, bez vlastitog prozora; inace bi
        # se uz ovaj prozor otvorio i stari tray prozor.
        subprocess.Popen(
            [put, "--autostart"],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            start_new_session=True,
        )


def ikona_u_traci(app, engine, agent, naslov: str):
    """Ikona u traci na Windowsu, s istim izborom kao Linux tray."""
    from PyQt6.QtWidgets import QMenu, QSystemTrayIcon

    ikona = Path(__file__).resolve().parent / "qml" / "logo.svg"
    traka = QSystemTrayIcon(QIcon(str(ikona)) if ikona.exists() else app.windowIcon(), app)
    traka.setToolTip(naslov)
    izbor = QMenu()

    def prikazi() -> None:
        for w in engine.rootObjects():
            w.setProperty("visible", True)
            if hasattr(w, "raise_"):
                w.raise_()
            if hasattr(w, "requestActivate"):
                w.requestActivate()

    izbor.addAction("Otvori prozor").triggered.connect(prikazi)
    izbor.addAction("Pokreni agenta").triggered.connect(agent.pokreni)
    izbor.addAction("Zaustavi agenta").triggered.connect(agent.zaustavi)
    izbor.addSeparator()

    def izlaz() -> None:
        agent.zaustavi()
        app.quit()

    izbor.addAction("Izlaz").triggered.connect(izlaz)
    traka.setContextMenu(izbor)
    traka.activated.connect(
        lambda razlog: prikazi() if razlog == QSystemTrayIcon.ActivationReason.Trigger else None
    )
    traka.show()
    app.setQuitOnLastWindowClosed(False)
    return traka


def main() -> int:
    # Basic stil, jer sistemski (org.kde.desktop) vuce svijetlu paletu i onda
    # polja i prekidaci izgledaju kao tuda zakrpa na tamnoj kartici.
    os.environ["QT_QUICK_CONTROLS_STYLE"] = "Basic"
    if platforma.WINDOWS:
        from PyQt6.QtWidgets import QApplication

        app = QApplication(sys.argv)
    else:
        app = QGuiApplication(sys.argv)
    app.setApplicationName("Filetoss")
    app.setDesktopFileName("filetoss")
    ikona = Path(__file__).resolve().parent / "qml" / "logo.svg"
    if ikona.exists():
        app.setWindowIcon(QIcon(str(ikona)))

    osiguraj_tray("filetoss-tray")

    engine = QQmlApplicationEngine()
    agent = Agent()
    engine.rootContext().setContextProperty("agent", agent)
    qml = Path(__file__).resolve().parent / "qml" / "main.qml"
    engine.load(QUrl.fromLocalFile(str(qml)))
    if not engine.rootObjects():
        print("QML se nije ucitao", file=sys.stderr)
        return 1
    traka = ikona_u_traci(app, engine, agent, "Filetoss") if platforma.WINDOWS else None
    _ = traka
    return app.exec()


if __name__ == "__main__":
    raise SystemExit(main())
