import SwiftUI
import AppKit
import Observation
import NucleicCore

/// Whether the floating PiP monitor is on screen right now, published so the *docked* ``VMMonitorPanel``
/// can idle its capture loop while the PiP is showing the very same guest — one screen, one poller.
@MainActor
@Observable
final class VMMonitorPiPState {
    static let shared = VMMonitorPiPState()
    private(set) var isVisible = false
    fileprivate func set(_ visible: Bool) { isVisible = visible }
    private init() {}
}

/// **Picture-in-Picture VM monitor.** A small always-on-top window that shows the open chat's live VM
/// screen so the user can keep watching the guest while working in — and over — any other window, even
/// another app. It hosts the same read-only ``VMMonitorPanel`` the in-chat monitor uses, so like every
/// other VM interface surface it is **look-only**: it merely polls and renders the guest framebuffer
/// (no `VZVirtualMachineView`, no key window, no synthesized HID), and therefore **never captures the
/// host's mouse or keyboard**. The floating window itself is a non-activating panel that can't become
/// key/main, so clicking or dragging it never steals focus or routes input at the guest.
///
/// Driven by `RootView.watchAutoVMMonitor`: while the "Float VM monitors in Picture in Picture" setting
/// is on (default) and the open chat has a VM running, the PiP follows the conversation — appearing for
/// that chat's guest and retracting when the chat has none. It also stands down whenever the docked
/// monitor is already showing that guest in the foreground (Nucleic active with a VM Monitor panel
/// placed), since a floating copy of a screen you're looking at is only clutter; the docked panel in turn
/// idles its capture loop while the PiP is up, so only ever one of the two polls the guest.
///
/// The window is deliberately chrome-less — no title bar, no border — so the guest's screen is the whole
/// of it. A manual close sticks until the user switches chats (or the VM goes away), so the driver
/// doesn't fight the red button.
@MainActor
final class VMMonitorPiPController: NSObject, NSWindowDelegate {
    static let shared = VMMonitorPiPController()

    private var panel: NSPanel?
    /// The session the PiP is currently rendering, so content is rebuilt only when the chat changes.
    private var shownSession: SessionID?
    /// The session whose PiP the user explicitly closed — kept closed until they switch chats (or its
    /// VM disappears), so the 2-second auto-driver doesn't immediately reopen it under them.
    private var dismissedSession: SessionID?

    private override init() { super.init() }

    /// Show (or update) the floating PiP for `session`'s VM. Idempotent: re-showing the same chat just
    /// brings it forward. Respects a manual close for that same chat.
    func show(session: Session, store: AppStore) {
        // The user closed the PiP for exactly this chat's VM — honor that until the chat changes.
        if dismissedSession == session.id { return }
        dismissedSession = nil

        if let panel {
            if shownSession != session.id {
                panel.contentView = hostingView(for: session, store: store)
                shownSession = session.id
            }
            if !panel.isVisible { panel.orderFrontRegardless() }
            VMMonitorPiPState.shared.set(true)
            return
        }

        // A chrome-less, non-activating panel: no title bar and no border, so the window *is* the guest
        // screen. Still resizable by its edges, and draggable anywhere (see `hostingView`).
        // `.nonactivatingPanel` keeps a click from activating the app, and `NonKeyPanel` refuses key/main
        // outright — belt-and-suspenders against ever capturing input.
        let panel = NonKeyPanel(
            contentRect: NSRect(x: 0, y: 0, width: 320, height: 200),
            styleMask: [.borderless, .resizable, .nonactivatingPanel],
            backing: .buffered, defer: false)
        panel.contentView = hostingView(for: session, store: store)
        panel.isReleasedWhenClosed = false
        panel.delegate = self
        // Nothing behind the content to show through: the rounded corners are clipped by the SwiftUI
        // content, so the window itself must not paint its own square backdrop behind them.
        panel.isOpaque = false
        panel.backgroundColor = .clear
        panel.hasShadow = true
        // Float above every other window — and every other app: a raised level, visible across Spaces
        // and over full-screen apps, and staying put when Nucleic isn't frontmost (that's the point of
        // a PiP). The guest screen is 16:10, so lock the frame to it (content letterboxes anyway).
        panel.level = .floating
        panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
        panel.hidesOnDeactivate = false
        panel.isFloatingPanel = true
        panel.isMovableByWindowBackground = true
        panel.aspectRatio = NSSize(width: 16, height: 10)
        Self.positionTopTrailing(panel)
        panel.orderFrontRegardless()
        self.panel = panel
        self.shownSession = session.id
        VMMonitorPiPState.shared.set(true)
    }

    /// Retract the PiP (the open chat has no VM, the docked monitor took over, or the setting was turned
    /// off). Keeps the panel instance for reuse and clears any manual-dismissal so a returning VM shows
    /// again.
    func hide() {
        panel?.orderOut(nil)
        shownSession = nil
        dismissedSession = nil
        VMMonitorPiPState.shared.set(false)
    }

    /// The ⌘W close sticks for the current chat's VM until the user switches chats.
    func windowWillClose(_ notification: Notification) {
        dismissedSession = shownSession
        VMMonitorPiPState.shared.set(false)
    }

    private func hostingView(for session: Session, store: AppStore) -> NSHostingView<some View> {
        let view = NSHostingView(
            rootView: VMMonitorPanel(session: session, isPictureInPicture: true)
                .environment(store)
                .frame(minWidth: 240, minHeight: 150)
                .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
                // With the title bar gone there's no handle to move the window by, and
                // `isMovableByWindowBackground` never fires because the hosting view swallows the drag.
                // This gives the whole monitor back as the drag handle.
                .gesture(WindowDragGesture()))
        view.layer?.masksToBounds = true
        return view
    }

    /// Park a freshly-created PiP in the top-trailing corner of the main screen — the conventional
    /// PiP home, out of the way of most window content.
    private static func positionTopTrailing(_ panel: NSPanel) {
        guard let screen = NSScreen.main else { panel.center(); return }
        let visible = screen.visibleFrame
        let size = panel.frame.size
        let margin: CGFloat = 16
        let origin = NSPoint(
            x: visible.maxX - size.width - margin,
            y: visible.maxY - size.height - margin)
        panel.setFrameOrigin(origin)
    }
}

/// A panel that refuses to become key or main, so it can never route the host's keyboard/mouse at the
/// guest — the read-only PiP monitor only ever displays a captured frame. Its close/resize/drag controls
/// still work (those are mouse-driven and need no key status).
private final class NonKeyPanel: NSPanel {
    override var canBecomeKey: Bool { false }
    override var canBecomeMain: Bool { false }
}
