#!/usr/bin/env python3
"""
Wuerth VPN – PyQt6 macOS client
Requires: pip3 install PyQt6 --break-system-packages

Uses vpn-slice for split tunneling — no manual routing needed.
"""

import os
import subprocess
import time
from typing import Optional

from PyQt6.QtCore import QThread, QObject, pyqtSignal
from PyQt6.QtGui import QTextCursor
from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QLabel, QLineEdit, QPushButton, QTextEdit, QFrame, QSizePolicy,
    QInputDialog, QMessageBox,
)

# ===== CONFIG =================================================================
DEFAULT_SERVER  = "loki.witglobal.net"
DEFAULT_USER    = "ex08802409"
PIDFILE         = "/tmp/openconnect-anyconnect.pid"
WINDOW_TITLE    = "WuerthVPN"

DEFAULT_HOSTS = """\
gitops.apps.ocp-dev01.wgn.wuerth.com
console-openshift-console.apps.ocp-dev01.wgn.wuerth.com
oauth-openshift.apps.ocp-dev01.wgn.wuerth.com
apm-dev.wgn.wuerth.com
odatang9.wgn.wuerth.com
fs.wgn.wuerth.com
fahrzeugeinrichtung.apps.ocp-dev01.wgn.wuerth.com
api.ocp-dev01.wgn.wuerth.com
console-openshift-console.apps.ocp-01.wgn.wuerth.com
gitops.apps.ocp-01.wgn.wuerth.com
oauth-openshift.apps.ocp-01.wgn.wuerth.com
api.ocp-01.wgn.wuerth.com
proxy.wgs.wuerth.com
odatang8.wgn.wuerth.com
fahrzeugeinrichtung.apps.ocp-01.wgn.wuerth.com"""
# =============================================================================

_password_cache: Optional[str] = None
_sudo_pw_cache: Optional[str] = None


def pw_load() -> Optional[str]:
    return _password_cache


def pw_save(pw: str):
    global _password_cache
    _password_cache = pw


def sudo_pw_load() -> Optional[str]:
    return _sudo_pw_cache


def sudo_pw_save(pw: str):
    global _sudo_pw_cache
    _sudo_pw_cache = pw


# ---------- PID / state -------------------------------------------------------
def _read_pidfile() -> Optional[int]:
    try:
        with open(PIDFILE) as f:
            return int(f.read().strip())
    except Exception:
        pass

    try:
        out = subprocess.check_output(["pgrep", "-x", "openconnect"], stderr=subprocess.DEVNULL)
        return int(out.decode().strip().splitlines()[0])
    except Exception:
        return None


def _pid_is_openconnect(pid: int) -> bool:
    try:
        out = subprocess.check_output(
            ["ps", "-p", str(pid), "-o", "comm="],
            stderr=subprocess.DEVNULL,
        )
        return b"openconnect" in out
    except Exception:
        return False


def is_connected() -> bool:
    pid = _read_pidfile()
    return bool(pid and _pid_is_openconnect(pid))


# ---------- Worker thread -----------------------------------------------------
class VpnWorker(QObject):
    output = pyqtSignal(str)
    done   = pyqtSignal(bool, str)

    def __init__(self, action: str, server: str, user: str, vpn_pw: str, hosts: list[str], sudo_pw: str = ""):
        super().__init__()
        self.action = action
        self.server = server
        self.user   = user
        self.vpn_pw = vpn_pw
        self.hosts  = hosts
        self.sudo_pw = sudo_pw

    def log(self, text: str):
        self.output.emit(text)

    def run(self):
        if self.action == "connect":
            self._connect()
        else:
            self._disconnect()

    def _connect(self):
        subprocess.run(
            ["sudo", "-S", "rm", "-f", PIDFILE],
            input=(self.sudo_pw + "\n").encode(),
            stderr=subprocess.DEVNULL,
        )

        vpn_slice_arg = "vpn-slice " + " ".join(self.hosts)

        server = self.server
        if not server.startswith("https://"):
            server = "https://" + server

        cmd = [
            "sudo", "-kS", "openconnect",
            "--protocol=anyconnect",
            "--no-xmlpost",
            "--user", self.user,
            "--passwd-on-stdin",
            "--pid-file", PIDFILE,
            "--force-dpd", "0",
            "--script", vpn_slice_arg,
            server,
        ]

        self.log(f"$ openconnect --protocol=anyconnect --user {self.user} --script '{vpn_slice_arg}' {server}\n\n")

        p = subprocess.Popen(
            cmd,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            bufsize=0,
        )
        # wie die funktionierende printf-Pipe: sudo liest Zeile 1, openconnect
        # Zeile 2 (--passwd-on-stdin); stdin schließen, damit openconnect EOF sieht
        p.stdin.write(f"{self.sudo_pw}\n{self.vpn_pw}\n".encode())
        p.stdin.flush()
        p.stdin.close()

        start = time.time()
        results_seen = 0
        expected = len(self.hosts)

        while True:
            chunk = p.stdout.read(256)
            if chunk:
                text = chunk.decode(errors="replace")
                self.log(text)
                results_seen += text.count("Got results:")

            if results_seen >= expected:
                time.sleep(0.5)
                self.done.emit(True, "Connected")
                return

            if p.poll() is not None:
                self.done.emit(False, "Connect failed")
                return

            if time.time() - start > 60:
                self.done.emit(False, "Timeout")
                return

            time.sleep(0.05)

    def _disconnect(self):
        pid = _read_pidfile()
        if not pid:
            self.log("Not connected.\n")
            self.done.emit(False, "")
            return

        sudo_in = (self.sudo_pw + "\n").encode()

        self.log(f"Sending SIGINT to pid {pid}...\n")
        subprocess.run(
            ["sudo", "-S", "kill", "-INT", str(pid)],
            input=sudo_in,
            stderr=subprocess.DEVNULL,
        )

        start = time.time()
        while time.time() - start < 12:
            if not _pid_is_openconnect(pid):
                subprocess.run(
                    ["sudo", "-S", "rm", "-f", PIDFILE],
                    input=sudo_in,
                    stderr=subprocess.DEVNULL,
                )
                self.log("Disconnected.\n")
                self.done.emit(False, "Disconnected")
                return

            elapsed = time.time() - start
            if 3.0 < elapsed < 3.5:
                self.log("Escalating: SIGTERM\n")
                subprocess.run(
                    ["sudo", "-S", "kill", "-TERM", str(pid)],
                    input=sudo_in,
                    stderr=subprocess.DEVNULL,
                )
            if 8.0 < elapsed < 8.5:
                self.log("Escalating: SIGKILL\n")
                subprocess.run(
                    ["sudo", "-S", "kill", "-KILL", str(pid)],
                    input=sudo_in,
                    stderr=subprocess.DEVNULL,
                )
            time.sleep(0.2)

        self.done.emit(False, "Disconnect timeout")


# ---------- Stylesheet --------------------------------------------------------
STYLE = """
QMainWindow, QWidget#root { background: #1a1a1f; }

QFrame#card {
    background: #22222a;
    border: 1px solid #2e2e38;
    border-radius: 10px;
}
QLineEdit {
    background: #16161c;
    color: #e8e0d4;
    border: 1px solid #2e2e38;
    border-radius: 6px;
    padding: 6px 10px;
    font-size: 13px;
}
QLineEdit:focus    { border-color: #c8a96e; }
QLineEdit:disabled { color: #555560; }

QTextEdit#hosts {
    background: #16161c;
    color: #e8e0d4;
    border: 1px solid #2e2e38;
    border-radius: 6px;
    font-family: "Menlo", "Monaco", monospace;
    font-size: 12px;
    padding: 6px;
}
QTextEdit#hosts:focus { border-color: #c8a96e; }

QPushButton#btn_connect {
    background: #c8a96e; color: #1a1a1f;
    border: none; border-radius: 7px;
    padding: 8px 24px; font-size: 13px; font-weight: 600;
}
QPushButton#btn_connect:hover    { background: #d9bb82; }
QPushButton#btn_connect:pressed  { background: #b8994e; }
QPushButton#btn_connect:disabled { background: #3a3a44; color: #666; }

QPushButton#btn_disconnect {
    background: #3a2020; color: #e07070;
    border: 1px solid #5a3030; border-radius: 7px;
    padding: 8px 24px; font-size: 13px; font-weight: 600;
}
QPushButton#btn_disconnect:hover    { background: #4a2828; }
QPushButton#btn_disconnect:disabled { background: #2a1818; color: #555; }

QTextEdit#console {
    background: #0f0f14; color: #a8c0a0;
    border: 1px solid #2e2e38; border-radius: 8px;
    font-family: "Menlo", "Monaco", monospace;
    font-size: 11px; padding: 8px;
}
"""


# ---------- Main Window -------------------------------------------------------
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(WINDOW_TITLE)
        self.resize(820, 640)
        self.setStyleSheet(STYLE)

        self._connected = False
        self._thread = None
        self._worker = None
        self._pending_close = False

        root = QWidget()
        root.setObjectName("root")
        self.setCentralWidget(root)

        layout = QVBoxLayout(root)
        layout.setContentsMargins(20, 20, 20, 20)
        layout.setSpacing(14)

        hdr = QHBoxLayout()
        self._dot = QLabel("●")
        self._dot.setStyleSheet("color: #555560; font-size: 18px;")
        hdr.addWidget(self._dot)

        lbl = QLabel(WINDOW_TITLE)
        lbl.setStyleSheet("color: #e8e0d4; font-size: 16px; font-weight: 600;")
        hdr.addWidget(lbl)

        hdr.addStretch()

        self._status_lbl = QLabel("")
        self._status_lbl.setStyleSheet("color: #8a8a9a; font-size: 12px;")
        hdr.addWidget(self._status_lbl)

        layout.addLayout(hdr)

        card = QFrame()
        card.setObjectName("card")
        cl = QVBoxLayout(card)
        cl.setContentsMargins(16, 14, 16, 14)
        cl.setSpacing(10)

        def field_row(label, widget):
            h = QHBoxLayout()
            lb = QLabel(label)
            lb.setStyleSheet("color: #8a8a9a; font-size: 12px;")
            lb.setFixedWidth(110)
            h.addWidget(lb)
            h.addWidget(widget)
            return h

        self.server_edit = QLineEdit(DEFAULT_SERVER)
        self.user_edit   = QLineEdit(DEFAULT_USER)

        self.pw_edit = QLineEdit()
        self.pw_edit.setEchoMode(QLineEdit.EchoMode.Password)
        self.pw_edit.setPlaceholderText("VPN password")

        self.sudo_pw_edit = QLineEdit()
        self.sudo_pw_edit.setEchoMode(QLineEdit.EchoMode.Password)
        self.sudo_pw_edit.setPlaceholderText("sudo password")

        self.hosts_edit = QTextEdit()
        self.hosts_edit.setObjectName("hosts")
        self.hosts_edit.setPlainText(DEFAULT_HOSTS)
        self.hosts_edit.setFixedHeight(80)
        self.hosts_edit.setPlaceholderText("one hostname per line")

        cl.addLayout(field_row("Server", self.server_edit))
        cl.addLayout(field_row("Username", self.user_edit))
        cl.addLayout(field_row("VPN Password", self.pw_edit))
        cl.addLayout(field_row("Sudo Password", self.sudo_pw_edit))

        from PyQt6.QtCore import Qt

        hosts_row = QHBoxLayout()
        hosts_lbl = QLabel("Hosts")
        hosts_lbl.setStyleSheet("color: #8a8a9a; font-size: 12px;")
        hosts_lbl.setFixedWidth(110)
        hosts_lbl.setAlignment(Qt.AlignmentFlag.AlignTop)
        hosts_row.addWidget(hosts_lbl)
        hosts_row.addWidget(self.hosts_edit)
        cl.addLayout(hosts_row)

        layout.addWidget(card)

        btn_row = QHBoxLayout()
        btn_row.addStretch()

        self.btn_connect = QPushButton("Connect")
        self.btn_connect.setObjectName("btn_connect")
        self.btn_connect.clicked.connect(self.on_connect)

        self.btn_disconnect = QPushButton("Disconnect")
        self.btn_disconnect.setObjectName("btn_disconnect")
        self.btn_disconnect.clicked.connect(self.on_disconnect)
        self.btn_disconnect.setEnabled(False)

        btn_row.addWidget(self.btn_connect)
        btn_row.addWidget(self.btn_disconnect)
        layout.addLayout(btn_row)

        self.console = QTextEdit()
        self.console.setObjectName("console")
        self.console.setReadOnly(True)
        self.console.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        layout.addWidget(self.console)

        if is_connected():
            self._set_connected(True)
            self._set_status("Connected", "#6db87a")
            self.log("Detected existing VPN connection.\n")

    def log(self, text: str):
        self.console.moveCursor(QTextCursor.MoveOperation.End)
        self.console.insertPlainText(text)
        self.console.moveCursor(QTextCursor.MoveOperation.End)

    def _set_status(self, text: str, color: str = "#8a8a9a"):
        self._status_lbl.setText(text)
        self._status_lbl.setStyleSheet(f"color: {color}; font-size: 12px;")

    def _set_connected(self, connected: bool):
        self._connected = connected
        self._dot.setStyleSheet(
            f"color: {'#6db87a' if connected else '#555560'}; font-size: 18px;"
        )
        self.btn_connect.setEnabled(not connected)
        self.btn_disconnect.setEnabled(connected)
        self.server_edit.setEnabled(not connected)
        self.user_edit.setEnabled(not connected)
        self.pw_edit.setEnabled(not connected)
        self.sudo_pw_edit.setEnabled(not connected)
        self.hosts_edit.setEnabled(not connected)

    def _get_password(self) -> Optional[str]:
        pw = self.pw_edit.text().strip()
        if pw:
            return pw

        pw = pw_load()
        if pw:
            self.pw_edit.setText(pw)
            return pw

        pw, ok = QInputDialog.getText(
            self,
            "VPN Password",
            "Enter VPN password:",
            QLineEdit.EchoMode.Password,
        )
        if ok and pw:
            pw_save(pw)
            self.pw_edit.setText(pw)
            return pw

        return None

    def _get_sudo_password(self) -> Optional[str]:
        pw = self.sudo_pw_edit.text().strip()
        if pw:
            sudo_pw_save(pw)
            return pw

        pw = sudo_pw_load()
        if pw:
            self.sudo_pw_edit.setText(pw)
            return pw

        pw, ok = QInputDialog.getText(
            self,
            "Sudo Password",
            "Enter sudo password:",
            QLineEdit.EchoMode.Password,
        )
        if ok and pw:
            sudo_pw_save(pw)
            self.sudo_pw_edit.setText(pw)
            return pw

        return None

    def _get_hosts(self) -> list[str]:
        lines = self.hosts_edit.toPlainText().strip().splitlines()
        return [line.strip() for line in lines if line.strip()]

    def on_connect(self):
        server = self.server_edit.text().strip()
        user   = self.user_edit.text().strip()
        pw     = self._get_password()
        sudo_pw = self._get_sudo_password()
        hosts  = self._get_hosts()

        if not (server and user and pw):
            self._set_status("Missing VPN credentials", "#e07070")
            return

        if not sudo_pw:
            self._set_status("Missing sudo password", "#e07070")
            return

        if not hosts:
            self._set_status("No hosts defined", "#e07070")
            return

        pw_save(pw)
        sudo_pw_save(sudo_pw)

        self.btn_connect.setEnabled(False)
        self._set_status("Connecting…", "#c8a96e")
        self._start_worker("connect", server, user, pw, hosts, sudo_pw)

    def on_disconnect(self):
        sudo_pw = self._get_sudo_password() or ""
        self.btn_disconnect.setEnabled(False)
        self._set_status("Disconnecting…", "#c8a96e")
        self._start_worker("disconnect", "", "", "", [], sudo_pw)

    def _start_worker(self, action, server, user, pw, hosts, sudo_pw=""):
        self._thread = QThread()
        self._worker = VpnWorker(action, server, user, pw, hosts, sudo_pw)
        self._worker.moveToThread(self._thread)
        self._thread.started.connect(self._worker.run)
        self._worker.output.connect(self.log)
        self._worker.done.connect(self._on_done)
        self._worker.done.connect(self._thread.quit)
        self._thread.start()

    def _on_done(self, connected: bool, message: str):
        self._set_connected(connected)

        if connected:
            self._set_status("Connected", "#6db87a")
        elif message == "Disconnected":
            self._set_status("Disconnected", "#8a8a9a")
        elif message:
            self._set_status(message, "#e07070")
        else:
            self._set_status("", "#8a8a9a")

        if self._pending_close and not connected:
            self._pending_close = False
            self.close()

    def closeEvent(self, event):
        if not (self._connected and is_connected()):
            event.accept()
            return

        resp = QMessageBox.question(
            self,
            "VPN aktiv",
            "Die VPN-Verbindung ist aktiv. Vor dem Beenden trennen?",
            QMessageBox.StandardButton.Yes
            | QMessageBox.StandardButton.No
            | QMessageBox.StandardButton.Cancel,
            QMessageBox.StandardButton.Yes,
        )

        if resp == QMessageBox.StandardButton.Cancel:
            event.ignore()
            return
        if resp == QMessageBox.StandardButton.No:
            event.accept()
            return

        sudo_pw = self._get_sudo_password()
        if not sudo_pw:
            event.ignore()
            return

        self._pending_close = True
        event.ignore()
        self.btn_disconnect.setEnabled(False)
        self._set_status("Disconnecting…", "#c8a96e")
        self._start_worker("disconnect", "", "", "", [], sudo_pw)


if __name__ == "__main__":
    import sys

    app = QApplication(sys.argv)
    app.setApplicationName(WINDOW_TITLE)
    app.setDesktopFileName("WuerthVPN")
    w = MainWindow()
    w.show()

    sys.exit(app.exec())
