#!/opt/synergyos/venv/bin/python3
"""SynergyOS Assistant — the command-center window.

Claude + SynergyMCP when configured and online; the local model otherwise.
Settings: API key, model, effort, which Synergy bundles to connect (OAuth login in the browser).
"""
import os, sys, threading
sys.path.insert(0, "/opt/synergyos/lib")
from PyQt5 import QtCore, QtGui, QtWidgets
import synergy_engine as E


class Signals(QtCore.QObject):
    line = QtCore.pyqtSignal(str, str)
    need_confirm = QtCore.pyqtSignal(str, str)
    done = QtCore.pyqtSignal()
    bundles = QtCore.pyqtSignal(str)


class SettingsDialog(QtWidgets.QDialog):
    def __init__(self, parent, cfg, mcp):
        super().__init__(parent)
        self.setWindowTitle("SynergyOS settings"); self.resize(560, 520)
        self.cfg, self.mcp = cfg, mcp
        f = QtWidgets.QFormLayout(self)
        self.key = QtWidgets.QLineEdit(cfg.get("anthropic_api_key", "")); self.key.setEchoMode(QtWidgets.QLineEdit.Password)
        self.key.setPlaceholderText("sk-ant-…  (stored in ~/.config/synergyos/config.json, mode 0600)")
        f.addRow("Anthropic API key", self.key)
        self.model = QtWidgets.QComboBox(); self.model.addItems(["claude-opus-5", "claude-sonnet-5", "claude-fable-5", "claude-haiku-4-5"])
        self.model.setCurrentText(cfg.get("model", E.DEFAULT_MODEL)); self.model.setEditable(True)
        f.addRow("Model", self.model)
        self.effort = QtWidgets.QComboBox(); self.effort.addItems(["low", "medium", "high", "xhigh", "max"])
        self.effort.setCurrentText(cfg.get("effort", "high")); f.addRow("Effort", self.effort)
        self.gateway = QtWidgets.QLineEdit(cfg.get("gateway", E.GATEWAY)); f.addRow("SynergyMCP gateway", self.gateway)
        f.addRow(QtWidgets.QLabel("<b>Synergy bundles</b> — tick to connect at startup; ✓ = signed in. "
                                  "Write access is a separate, explicit grant."))
        self.list = QtWidgets.QListWidget()
        for b in E.ALL_BUNDLES:
            it = QtWidgets.QListWidgetItem(b); it.setFlags(it.flags() | QtCore.Qt.ItemIsUserCheckable)
            it.setCheckState(QtCore.Qt.Checked if b in cfg.get("bundles", []) else QtCore.Qt.Unchecked)
            self.list.addItem(it)
        self._refresh_marks()
        f.addRow(self.list)
        h = QtWidgets.QHBoxLayout()
        b1 = QtWidgets.QPushButton("Sign in / connect selected"); b1.clicked.connect(lambda: self.connect_selected(False))
        b2 = QtWidgets.QPushButton("Grant WRITE on selected…"); b2.clicked.connect(lambda: self.connect_selected(True))
        b3 = QtWidgets.QPushButton("Forget selected"); b3.clicked.connect(self.forget_selected)
        h.addWidget(b1); h.addWidget(b2); h.addWidget(b3); f.addRow(h)
        self.status = QtWidgets.QLabel(""); f.addRow(self.status)
        bb = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Save | QtWidgets.QDialogButtonBox.Cancel)
        bb.accepted.connect(self.accept); bb.rejected.connect(self.reject); f.addRow(bb)

    def _refresh_marks(self):
        for i in range(self.list.count()):
            it = self.list.item(i); b = it.text().split(" ")[0]
            mark = "✓ " if self.mcp.has_credentials(b) else ""
            live = f"  ({len([t for t in self.mcp.tools if t['name'].startswith(b + '__')])} tools)" if b in self.mcp.sessions else ""
            it.setText(f"{b}{live}"); it.setData(QtCore.Qt.UserRole, b)
            it.setIcon(QtGui.QIcon.fromTheme("emblem-ok" if mark else "emblem-unavailable"))

    def selected(self):
        return [self.list.item(i).data(QtCore.Qt.UserRole) for i in range(self.list.count()) if self.list.item(i).isSelected()]

    def connect_selected(self, write):
        sel = self.selected()
        if not sel:
            self.status.setText("select one or more bundles first"); return
        if write and QtWidgets.QMessageBox.warning(self, "Grant write access",
                "Write scope lets the assistant change things on the fleet through these bundles (after your per-action "
                "approval). The gateway only grants it for bundles enabled server-side. Continue?",
                QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) != QtWidgets.QMessageBox.Yes:
            return
        self.cfg["gateway"] = self.gateway.text().strip()
        def work():
            for b in sel:
                try:
                    n = self.mcp.connect(b, write=write, interactive=True)
                    QtCore.QMetaObject.invokeMethod(self, "set_status", QtCore.Qt.QueuedConnection, QtCore.Q_ARG(str, f"{b}: connected, {n} tools"))
                except Exception as e:
                    QtCore.QMetaObject.invokeMethod(self, "set_status", QtCore.Qt.QueuedConnection, QtCore.Q_ARG(str, f"{b}: {e}"))
            QtCore.QMetaObject.invokeMethod(self, "refresh", QtCore.Qt.QueuedConnection)
        self.status.setText("a browser window will open for SynergyAuth sign-in…")
        threading.Thread(target=work, daemon=True).start()

    def forget_selected(self):
        for b in self.selected():
            try: self.mcp.disconnect(b, forget=True)
            except Exception: pass
        self._refresh_marks(); self.status.setText("forgotten")

    @QtCore.pyqtSlot(str)
    def set_status(self, s): self.status.setText(s)
    @QtCore.pyqtSlot()
    def refresh(self): self._refresh_marks()

    def result_cfg(self):
        self.cfg["anthropic_api_key"] = self.key.text().strip()
        self.cfg["model"] = self.model.currentText().strip()
        self.cfg["effort"] = self.effort.currentText()
        self.cfg["gateway"] = self.gateway.text().strip()
        self.cfg["bundles"] = [self.list.item(i).data(QtCore.Qt.UserRole) for i in range(self.list.count())
                               if self.list.item(i).checkState() == QtCore.Qt.Checked]
        return self.cfg


class Window(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("SynergyOS Assistant"); self.resize(820, 620)
        self.setWindowIcon(QtGui.QIcon.fromTheme("synergyos-assistant"))
        self.cfg = E.load_config()
        self.sig = Signals()
        self.mcp = E.MCPManager(self.cfg, open_url=lambda u: QtGui.QDesktopServices.openUrl(QtCore.QUrl(u)),
                                log=lambda s: self.sig.line.emit("status", s))
        self._confirm_ev = threading.Event(); self._confirm_ans = False
        self.brain = E.Brain(self.cfg, self.mcp, on=self._on, confirm=self._confirm)
        w = QtWidgets.QWidget(); self.setCentralWidget(w); v = QtWidgets.QVBoxLayout(w)
        top = QtWidgets.QHBoxLayout()
        self.mode = QtWidgets.QLabel(); top.addWidget(self.mode, 1)
        self.bundles_lbl = QtWidgets.QLabel(""); top.addWidget(self.bundles_lbl)
        gear = QtWidgets.QToolButton(); gear.setIcon(QtGui.QIcon.fromTheme("configure")); gear.setText("Settings")
        gear.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon); gear.clicked.connect(self.settings); top.addWidget(gear)
        QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+,"), self, activated=self.settings)
        v.addLayout(top)
        self.view = QtWidgets.QTextEdit(readOnly=True); self.view.setStyleSheet("font-size: 11pt;"); v.addWidget(self.view, 1)
        h = QtWidgets.QHBoxLayout()
        self.inp = QtWidgets.QLineEdit(placeholderText="Ask anything, or tell me what to do — on this laptop or across the fleet…")
        self.inp.returnPressed.connect(self.send); self.btn = QtWidgets.QPushButton("Send"); self.btn.clicked.connect(self.send)
        h.addWidget(self.inp, 1); h.addWidget(self.btn); v.addLayout(h)
        self.status = self.statusBar()
        self.sig.line.connect(self.append); self.sig.need_confirm.connect(self._confirm_dialog)
        self.sig.done.connect(lambda: self.set_busy(False)); self.sig.bundles.connect(self.bundles_lbl.setText)
        self.append("ai", "Hi — I'm the assistant built into SynergyOS. Ask me about this laptop, or about the fleet and apps "
                          "once bundles are connected (Settings). Anything that changes state is shown to you first.")
        self.refresh_mode()
        threading.Thread(target=self.autoconnect, daemon=True).start()

    # -- engine callbacks (worker thread)
    def _on(self, kind, text):
        if kind == "status":
            self.sig.line.emit("status", text)
        else:
            self.sig.line.emit(kind, text)
    def _confirm(self, title, detail):
        self._confirm_ev.clear(); self.sig.need_confirm.emit(title, detail); self._confirm_ev.wait(); return self._confirm_ans

    @QtCore.pyqtSlot(str, str)
    def _confirm_dialog(self, title, detail):
        box = QtWidgets.QMessageBox(self); box.setWindowTitle("SynergyOS wants to act")
        box.setText(f"<b>{title}</b>"); box.setInformativeText(f"<pre>{detail.replace('<','&lt;')}</pre>")
        box.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No); box.setDefaultButton(QtWidgets.QMessageBox.No)
        self._confirm_ans = box.exec_() == QtWidgets.QMessageBox.Yes; self._confirm_ev.set()

    # -- UI
    @QtCore.pyqtSlot()
    def refresh_mode(self):
        m = self.brain.mode()
        self.mode.setText(f"🧠 <b>{'Claude ' + self.cfg.get('model', E.DEFAULT_MODEL) if m == 'claude' else 'local model (offline / no key)'}</b>")
        self.sig.bundles.emit("🔗 " + (", ".join(self.mcp.connected()) or "no bundles connected"))

    def autoconnect(self):
        for b in self.cfg.get("bundles", []):
            if self.mcp.has_credentials(b):
                try: self.mcp.connect(b, interactive=False)
                except Exception as e: self.sig.line.emit("err", f"{b}: {e}")
        QtCore.QMetaObject.invokeMethod(self, "refresh_mode", QtCore.Qt.QueuedConnection)

    def settings(self):
        d = SettingsDialog(self, dict(self.cfg), self.mcp)
        if d.exec_() == QtWidgets.QDialog.Accepted:
            self.cfg.update(d.result_cfg()); E.save_config(self.cfg)
            self.brain.cfg = self.cfg; self.brain._client = None; self.mcp.cfg = self.cfg
            self.append("status", "settings saved")
        self.refresh_mode()

    def set_busy(self, b):
        self.inp.setEnabled(not b); self.btn.setEnabled(not b)
        if not b: self.inp.setFocus()

    @QtCore.pyqtSlot(str, str)
    def append(self, kind, text):
        if kind == "status":
            self.status.showMessage(text); return
        esc = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\n", "<br>")
        style = {"user": "color:#2e7d32;font-weight:bold", "ai": "", "text": "", "tool": "color:#888;font-family:monospace",
                 "out": "color:#555;font-family:monospace;font-size:9pt", "err": "color:#c62828"}[kind]
        prefix = {"user": "You: ", "ai": "SynergyOS: ", "text": "SynergyOS: ", "tool": "", "out": "", "err": "! "}[kind]
        self.view.append(f'<div style="{style}">{prefix}{esc}</div>')
        self.view.verticalScrollBar().setValue(self.view.verticalScrollBar().maximum())

    def send(self):
        t = self.inp.text().strip()
        if not t: return
        self.inp.clear(); self.append("user", t); self.set_busy(True)
        def work():
            self.brain.ask(t); self.sig.done.emit()
        threading.Thread(target=work, daemon=True).start()

    def closeEvent(self, e): e.ignore(); self.hide()


def main():
    app = QtWidgets.QApplication(sys.argv); app.setQuitOnLastWindowClosed(False)
    win = Window()
    tray = QtWidgets.QSystemTrayIcon(QtGui.QIcon.fromTheme("synergyos-assistant"))
    menu = QtWidgets.QMenu()
    show = lambda: (win.show(), win.raise_(), win.activateWindow())
    menu.addAction("Open Assistant", show); menu.addAction("Settings…", lambda: (show(), win.settings())); menu.addAction("Quit", app.quit)
    tray.setContextMenu(menu); tray.setToolTip("SynergyOS Assistant")
    tray.activated.connect(lambda r: show() if r == QtWidgets.QSystemTrayIcon.Trigger else None); tray.show()
    first_run = not os.path.exists(E.CONFIG_FILE)
    if "--hidden" not in sys.argv or first_run: win.show()
    if first_run: QtCore.QTimer.singleShot(800, win.settings)
    if "--settings" in sys.argv: QtCore.QTimer.singleShot(500, win.settings)
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()
