from __future__ import annotations

import os
from datetime import datetime
import platform
import threading
import time
from pathlib import Path
from typing import Any, Optional

from PySide6 import QtCore, QtWidgets
from PySide6.QtCore import QSettings

from auditwalk.gui.history_panel import HistoryPanel
from auditwalk.gui.observer_server import ObserverServer
from auditwalk.gui.worker_client import WorkerClient
from auditwalk.storage.db import get_default_db_path

# src/auditwalk/gui/main_window.py -> repo root is parents[3]
_REPO_ROOT = Path(__file__).resolve().parents[3]
if not (_REPO_ROOT / "scripts").exists():
    raise RuntimeError(f"repo_root does not look valid: {_REPO_ROOT}")


class MainWindow(QtWidgets.QMainWindow):
    """
    Minimal GUI shell for AuditWalk.

    - History is a right-side dock.
    - cache_db_path is the single shared source for worker spawn and history reads.
    - caching is disabled by default (None).
    """

    def __init__(self) -> None:
        super().__init__()
        self.setWindowTitle("AuditWalk")

        # GUI-local persistent state (geometry + docks only)
        self._settings = QSettings("AdenMediaGroup", "AuditWalk")

        geom = self._settings.value("window/geometry")
        if geom:
            self.restoreGeometry(geom)

        state = self._settings.value("window/state")
        if state:
            self.restoreState(state)

        # Cache disabled by default (opt-in).
        self._cache_db_path: Optional[Path] = None
        self._worker_client: Optional[WorkerClient] = None
        self._scan_cancel_requested = False

        self._observer_lock = threading.Lock()
        self._observer_server: ObserverServer | None = None
        self._observer_state: dict[str, Any] = {
            "mode": "observer-read-only",
            "state": "IDLE",
            "platform": platform.system().lower(),
            "scan": {
                "phase": "idle",
                "progress_percent": 0,
                "files_scanned": 0,
                "files_total": 0,
                "throughput_files_per_sec": 0.0,
            },
            "findings": {
                "state": "CLEAR",
                "summary": "No anomalies reported",
                "drift": "none",
                "total": 0,
                "severity_counts": {"critical": 0, "high": 0, "medium": 0, "low": 0},
            },
            "preflight": {
                "summary": "not evaluated",
            },
            "last_trusted_state": "-",
            "channel": "Local Secure Session",
        }

        central = QtWidgets.QWidget(self)
        central_layout = QtWidgets.QVBoxLayout(central)

        self._status = QtWidgets.QLabel("Ready.")
        central_layout.addWidget(self._status)
        self._last_run = QtWidgets.QLabel("Last run: —")
        self._last_run.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
        central_layout.addWidget(self._last_run)

        observer_row = QtWidgets.QHBoxLayout()
        self._observer_url = QtWidgets.QLineEdit("")
        self._observer_url.setReadOnly(True)
        self._observer_url.setPlaceholderText("Observer pairing URL")
        self._btn_refresh_observer = QtWidgets.QPushButton("Refresh Observer Link")
        self._btn_copy_observer = QtWidgets.QPushButton("Copy Observer Link")
        observer_row.addWidget(self._observer_url, 1)
        observer_row.addWidget(self._btn_refresh_observer)
        observer_row.addWidget(self._btn_copy_observer)
        central_layout.addLayout(observer_row)

        btn_row = QtWidgets.QHBoxLayout()
        self._btn_enable_cache = QtWidgets.QPushButton("Enable cache")
        self._btn_disable_cache = QtWidgets.QPushButton("Disable cache")
        self._btn_spawn_worker = QtWidgets.QPushButton("Spawn worker (local)")
        self._btn_scan_demo = QtWidgets.QPushButton("Scan demo path")
        self._btn_scan_folder = QtWidgets.QPushButton("Scan folder...")
        self._btn_cancel_scan = QtWidgets.QPushButton("Cancel scan")
        self._btn_cancel_scan.setEnabled(False)
        btn_row.addWidget(self._btn_enable_cache)
        btn_row.addWidget(self._btn_disable_cache)
        btn_row.addWidget(self._btn_spawn_worker)
        btn_row.addWidget(self._btn_scan_demo)
        btn_row.addWidget(self._btn_scan_folder)
        btn_row.addWidget(self._btn_cancel_scan)
        btn_row.addStretch(1)
        central_layout.addLayout(btn_row)

        self.setCentralWidget(central)

        self._history_panel = HistoryPanel(db_path=self._cache_db_path)
        self._history_dock = QtWidgets.QDockWidget("History", self)
        self._history_dock.setObjectName("dock_history")
        self._history_dock.setWidget(self._history_panel)
        self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self._history_dock)
        self._history_dock.hide()

        self._btn_enable_cache.clicked.connect(self._enable_cache)
        self._btn_disable_cache.clicked.connect(self._disable_cache)
        self._btn_spawn_worker.clicked.connect(self._spawn_worker)
        self._btn_scan_demo.clicked.connect(self._scan_demo_path)
        self._btn_scan_folder.clicked.connect(self._scan_selected_path)
        self._btn_cancel_scan.clicked.connect(self._cancel_scan)
        self._btn_refresh_observer.clicked.connect(self._refresh_observer_link)
        self._btn_copy_observer.clicked.connect(self._copy_observer_link)

        self._sync_ui()
        self._start_observer_server()

    def _sync_ui(self) -> None:
        enabled = self._cache_db_path is not None
        self._btn_enable_cache.setEnabled(not enabled)
        self._btn_disable_cache.setEnabled(enabled)
        self._history_dock.setVisible(enabled)
        self._status.setText(f"Cache: {self._cache_db_path}" if enabled else "Cache: disabled")
    @QtCore.Slot()
    def _refresh_observer_link(self) -> None:
        if self._observer_server is None:
            self._observer_url.setText("")
            self._status.setText("Observer not running.")
            return
        urls = self._observer_server.pairing_urls()
        self._observer_url.setText(urls["observer_url"])
        self._status.setText(f"Observer link refreshed (expires {urls['expires_at']}).")
    @QtCore.Slot()
    def _copy_observer_link(self) -> None:
        link = self._observer_url.text().strip()
        if not link:
            self._refresh_observer_link()
            link = self._observer_url.text().strip()
        if not link:
            self._status.setText("No observer link available to copy.")
            return
        QtWidgets.QApplication.clipboard().setText(link)
        self._status.setText("Observer link copied to clipboard.")

    def _start_observer_server(self) -> None:
        enabled = os.environ.get("AUDITWALK_OBSERVER_ENABLED", "1").strip().lower() not in {
            "0",
            "false",
            "off",
            "no",
        }
        if not enabled:
            return
        host = os.environ.get("AUDITWALK_OBSERVER_HOST", "0.0.0.0").strip() or "0.0.0.0"
        try:
            port = int(os.environ.get("AUDITWALK_OBSERVER_PORT", "4582").strip() or "4582")
        except ValueError:
            port = 4582
        try:
            ttl_seconds = int(os.environ.get("AUDITWALK_OBSERVER_TOKEN_TTL_SECONDS", "900").strip() or "900")
        except ValueError:
            ttl_seconds = 900

        server = ObserverServer(
            status_provider=self._observer_status_payload,
            host=host,
            port=port,
            token_ttl_seconds=ttl_seconds,
        )
        try:
            server.start()
            self._observer_server = server
            urls = server.pairing_urls()
            self._observer_url.setText(urls["observer_url"])
            self._status.setText(f"Observer ready: {urls['observer_url']}")
        except OSError as exc:
            self._observer_server = None
            self._status.setText(f"Observer failed: {exc}")

    def _observer_status_payload(self) -> dict[str, Any]:
        with self._observer_lock:
            return {
                "mode": self._observer_state.get("mode", "observer-read-only"),
                "state": self._observer_state.get("state", "IDLE"),
                "platform": self._observer_state.get("platform", "desktop"),
                "scan": dict(self._observer_state.get("scan", {})),
                "findings": dict(self._observer_state.get("findings", {})),
                "preflight": dict(self._observer_state.get("preflight", {})),
                "last_trusted_state": self._observer_state.get("last_trusted_state", "-"),
                "channel": self._observer_state.get("channel", "Local Secure Session"),
            }

    def _set_observer_scan(
        self,
        *,
        state: str,
        phase: str,
        files_scanned: int = 0,
        files_total: int = 0,
        progress_percent: int = 0,
        throughput: float = 0.0,
    ) -> None:
        with self._observer_lock:
            self._observer_state["state"] = state
            scan = self._observer_state["scan"]
            scan["phase"] = phase
            scan["files_scanned"] = int(max(0, files_scanned))
            scan["files_total"] = int(max(0, files_total))
            scan["progress_percent"] = int(max(0, min(100, progress_percent)))
            scan["throughput_files_per_sec"] = float(max(0.0, throughput))

    def _set_observer_findings(self, *, state: str, summary: str, drift: str = "none", total: int = 0) -> None:
        with self._observer_lock:
            findings = self._observer_state["findings"]
            findings["state"] = state
            findings["summary"] = summary
            findings["drift"] = drift
            findings["total"] = int(max(0, total))

    @QtCore.Slot()
    def _enable_cache(self) -> None:
        self._cache_db_path = get_default_db_path()
        self._history_panel.set_db_path(self._cache_db_path)
        self._sync_ui()

    @QtCore.Slot()
    def _disable_cache(self) -> None:
        self._cache_db_path = None
        self._history_panel.set_db_path(None)
        self._sync_ui()

    @QtCore.Slot()
    def _spawn_worker(self) -> None:
        try:
            client = WorkerClient.spawn_local(
                repo_root=_REPO_ROOT,
                cache_db_path=self._cache_db_path,
            )
            client.handshake()
            self._worker_client = client
            self._status.setText("Worker spawned (handshake OK).")
        except Exception as exc:
            try:
                if "client" in locals():
                    client.close()
            except Exception:
                pass
            self._worker_client = None
            self._status.setText(f"Worker spawn failed: {exc}")

    def _check_worker_alive(self) -> bool:
        if self._worker_client is None:
            return False

        proc = self._worker_client.proc
        if proc.poll() is not None:
            self._worker_client = None
            self._status.setText("Worker exited unexpectedly.")
            return False

        return True

    @QtCore.Slot()
    def _cancel_scan(self) -> None:
        self._scan_cancel_requested = True
        self._btn_cancel_scan.setEnabled(False)
        try:
            if self._worker_client is not None:
                self._worker_client.close()
        except Exception:
            pass
        finally:
            self._worker_client = None

        self._set_observer_scan(state="CANCELLED", phase="cancelled")
        self._set_observer_findings(state="REVIEW", summary="Scan cancelled by operator", drift="unknown")
        self._status.setText("Scan cancelled.")
        QtWidgets.QApplication.processEvents()

    @QtCore.Slot()
    def _scan_demo_path(self) -> None:
        self._run_scan_for_root(_REPO_ROOT)

    @QtCore.Slot()
    def _scan_selected_path(self) -> None:
        selected = QtWidgets.QFileDialog.getExistingDirectory(self, "Select Folder To Scan", str(_REPO_ROOT))
        if not selected:
            return
        self._run_scan_for_root(Path(selected))

    def _run_scan_for_root(self, root_path: Path) -> None:
        if not self._check_worker_alive():
            self._spawn_worker()
            if self._worker_client is None:
                return

        self._scan_cancel_requested = False
        self._btn_scan_demo.setEnabled(False)
        self._btn_scan_folder.setEnabled(False)
        self._btn_cancel_scan.setEnabled(True)
        max_bytes_total = 50_000_000
        start_time = time.time()
        self._set_observer_scan(state="RUNNING", phase="precheck")
        self._set_observer_findings(state="CLEAR", summary="Scan in progress", drift="unknown")

        try:
            rid = self._worker_client.send(
                "scan",
                {
                    "roots": [str(root_path)],
                    "limits": {"max_files": 500, "max_bytes_total": max_bytes_total},
                    "checks": {"stat": {"enabled": True}},
                    "follow_symlinks": False,
                    "max_depth": 6,
                },
            )
            self._status.setText(f"Scanning... root={root_path}")
            QtWidgets.QApplication.processEvents()

            deadline = time.time() + 30.0
            while True:
                if self._scan_cancel_requested:
                    self._status.setText("Scan cancelled.")
                    return
                if time.time() > deadline:
                    self._set_observer_scan(state="ERROR", phase="timeout")
                    self._set_observer_findings(state="ATTENTION", summary="Scan timed out", drift="review")
                    self._status.setText("Scan timed out.")
                    return
                try:
                    msg = self._worker_client.recv()
                except EOFError:
                    if not self._check_worker_alive():
                        return
                    self._set_observer_scan(state="ERROR", phase="worker-closed")
                    self._set_observer_findings(state="ATTENTION", summary="Worker stream closed unexpectedly")
                    self._status.setText("Worker stream closed unexpectedly.")
                    return
                if self._scan_cancel_requested:
                    self._status.setText("Scan cancelled.")
                    return
                if msg.get("request_id") != rid:
                    continue

                msg_type = msg.get("type")
                payload = msg.get("payload") or {}
                if msg_type == "ack":
                    self._set_observer_scan(state="RUNNING", phase="accepted")
                    self._status.setText(f"Scan accepted... root={root_path}")
                    QtWidgets.QApplication.processEvents()
                    continue
                if msg_type == "progress":
                    files_seen = int(payload.get("files_seen", 0) or 0)
                    bytes_seen = int(payload.get("bytes_seen", 0) or 0)
                    elapsed = max(0.1, time.time() - start_time)
                    throughput = files_seen / elapsed
                    pct = int(min(100, max(0, (bytes_seen / max_bytes_total) * 100)))
                    self._set_observer_scan(
                        state="RUNNING",
                        phase="scanning",
                        files_scanned=files_seen,
                        files_total=500,
                        progress_percent=pct,
                        throughput=throughput,
                    )
                    self._status.setText(f"Scanning... root={root_path} files={files_seen} bytes={bytes_seen}")
                    QtWidgets.QApplication.processEvents()
                    continue
                if msg_type == "error":
                    error_msg = payload.get("message", "unknown error")
                    self._set_observer_scan(state="ERROR", phase="error")
                    self._set_observer_findings(state="ATTENTION", summary=f"Scan error: {error_msg}", drift="review")
                    self._status.setText(f"Scan error: {error_msg}")
                    return
                if msg_type == "done":
                    files_seen = int(payload.get("files_seen", 0) or 0)
                    bytes_seen = int(payload.get("bytes_seen", 0) or 0)
                    elapsed = max(0.1, time.time() - start_time)
                    throughput = files_seen / elapsed
                    pct = int(min(100, max(0, (bytes_seen / max_bytes_total) * 100)))
                    self._set_observer_scan(
                        state="COMPLETE",
                        phase="complete",
                        files_scanned=files_seen,
                        files_total=500,
                        progress_percent=pct,
                        throughput=throughput,
                    )
                    self._set_observer_findings(state="CLEAR", summary="No high-risk anomalies reported", drift="none")
                    with self._observer_lock:
                        self._observer_state["last_trusted_state"] = datetime.now().isoformat(timespec="seconds")
                    self._status.setText(f"Done. files={files_seen} bytes={bytes_seen}")
                    break

            if not self._scan_cancel_requested:
                self._history_panel.refresh_runs()
                summary = self._history_panel.latest_summary_text()
                self._last_run.setText(f"Last run: {summary}" if summary else "Last run: —")
        except Exception as exc:
            self._set_observer_scan(state="ERROR", phase="exception")
            self._set_observer_findings(state="ATTENTION", summary=f"Scan failed: {exc}", drift="review")
            self._status.setText(f"Scan failed: {exc}")
        finally:
            self._btn_cancel_scan.setEnabled(False)
            self._btn_scan_demo.setEnabled(True)
            self._btn_scan_folder.setEnabled(True)

    def closeEvent(self, event) -> None:
        self._settings.setValue("window/geometry", self.saveGeometry())
        self._settings.setValue("window/state", self.saveState())
        try:
            if self._worker_client is not None:
                self._worker_client.close()
        except Exception:
            pass
        if self._observer_server is not None:
            self._observer_server.stop()
            self._observer_server = None
        super().closeEvent(event)
