import SwiftUI
import AppKit
import NucleicCore

/// A live view of the session's VM screen — a "monitor" hung off the chat so the user can watch
/// the GUI state of the guest the virtual-machine service booted for it (what the agent is doing in it,
/// a build running, a simulator, Setup Assistant, …). Works for either guest OS: a session runs at most
/// one macOS *or* Linux VM at a time, and the monitor tracks whichever is up.
///
/// It polls the VM's current framebuffer a couple of times a second over the same screen-capture path
/// computer-use uses (the native in-guest agent's ScreenCaptureKit when present, else SSH
/// `screencapture`) and renders the latest frame scaled to fit. It is **read-only**: it shows the
/// screen, it doesn't drive it. When no VM is running for the chat — or the guest can't be captured —
/// it shows a status placeholder rather than a stale image.
struct VMMonitorPanel: View {
    let session: Session?
    /// True for the copy hosted inside ``VMMonitorPiPController``'s floating window. The docked copy
    /// (false) idles its capture loop whenever that floating one is up, so the same guest is never
    /// captured twice.
    var isPictureInPicture = false
    @Environment(AppStore.self) private var store

    @State private var frame: NSImage?
    @State private var status: Status = .idle
    /// The guest is running a headless `mac_vm_exec` command with no computer-use action in flight — its
    /// screen is static but it's busy. Drives the translucent "Working in the background…" overlay so a
    /// frozen-looking frame doesn't read as a hang. Only meaningful while a live frame is showing.
    @State private var workingInBackground = false
    /// In-flight white flashes, one per click. This monitor is read-only, so a click that lands on the
    /// guest screen does nothing — flashing the whole screen tells the user they're tapping on glass and
    /// their input isn't reaching the VM. Each flash self-removes when its animation finishes.
    @State private var clickFlashes: [ClickFlash] = []
    /// What the monitor is currently showing / waiting on. Drives the placeholder when there's no frame.
    private enum Status: Equatable {
        case idle          // no chat open, or the host can't run macOS guests at all
        case notRunning    // no VM is booted for this session
        case booting       // VM is up but has no IP / hasn't produced a frame yet
        case noSignal      // VM is running but capture came back empty (unprovisioned guest / VZ bug)
        case live          // showing a frame
        case handedToPiP   // the floating PiP is showing this guest; this docked copy stopped capturing
    }

    /// The VM name(s) to track: the open chat's per-session guests — both the macOS (`nucleic-mac-<short>…`)
    /// and Linux (`nucleic-lin-<short>…`) siblings, since a session may run either. Empty with no open chat.
    private var candidateVMNames: [String] {
        guard let session else { return [] }
        return [MacVMManager.vmName(for: session.id), MacVMManager.linuxVMName(for: session.id)]
    }

    var body: some View {
        ZStack {
            // A black stage, like a physical monitor: the guest screen sits letterboxed in it, and the
            // placeholder reads as an "off" display when nothing's live.
            Color.black
            if let frame {
                Image(nsImage: frame)
                    .resizable()
                    .interpolation(.medium)
                    .aspectRatio(contentMode: .fit)
                // The guest is doing headless work over SSH, so its screen sits frozen: lay a translucent
                // scrim + label over it so the still frame reads as "busy in the background", not hung.
                if workingInBackground { backgroundWorkingOverlay }
            } else {
                placeholder
            }
            // White flash over the whole monitor on each click, layered above the frame. Purely visual
            // (hit-testing off) so they never swallow a subsequent click.
            ForEach(clickFlashes) { flash in
                ClickFlashView { removeClickFlash(flash.id) }
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .animation(.easeInOut(duration: 0.25), value: workingInBackground)
        // The monitor is look-only: a click never reaches the guest. Register taps ourselves purely to
        // flash the screen white, telling the user they're tapping on glass and their input isn't landing.
        // Only while a live frame is up — a placeholder ("no VM running") has nothing to mistake for an
        // interactive screen.
        .contentShape(Rectangle())
        .onTapGesture {
            guard frame != nil else { return }
            clickFlashes.append(ClickFlash())
        }
        // Restart polling when the open chat changes (a different chat means a different VM).
        .task(id: session?.id) { await poll() }
    }

    private func removeClickFlash(_ id: UUID) {
        clickFlashes.removeAll { $0.id == id }
    }

    private var placeholder: some View {
        VStack(spacing: 10) {
            Image(systemName: placeholderIcon)
                .font(.system(size: 34)).foregroundStyle(.tertiary)
            Text(placeholderMessage)
                .font(.callout).foregroundStyle(.secondary)
                .multilineTextAlignment(.center)
                .fixedSize(horizontal: false, vertical: true)
            if status == .booting {
                ProgressView().controlSize(.small)
            }
        }
        .padding(24)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    /// A partially-transparent layer over the live frame telling the user the guest is busy even though
    /// its screen isn't moving — shown while a `mac_vm_exec` command runs without any computer-use action
    /// driving the UI. Dims the frozen frame just enough to read as inactive without hiding it.
    private var backgroundWorkingOverlay: some View {
        ZStack {
            Color.black.opacity(0.45)
            HStack(spacing: 10) {
                ProgressView().controlSize(.small)
                Text("Working in the background…")
                    .font(.callout.weight(.medium))
                    .foregroundStyle(.white)
            }
            .padding(.horizontal, 16)
            .padding(.vertical, 10)
            .background(.ultraThinMaterial, in: Capsule())
            .environment(\.colorScheme, .dark)
        }
        .transition(.opacity)
        .allowsHitTesting(false)  // read-only monitor: never intercept clicks
    }

    /// Poll the VM's state + screen while the panel is on screen. Keeps the last good frame across a
    /// single failed capture (a transient SSH hiccup shouldn't blank the monitor); only a stopped VM
    /// clears it.
    private func poll() async {
        frame = nil
        workingInBackground = false
        let candidates = candidateVMNames
        guard !candidates.isEmpty, store.macVMSupported else {
            status = .idle
            return
        }
        while !Task.isCancelled {
            // The floating PiP is showing this same guest, and the two capture over the identical (and
            // not-free) screenshot path. Drop the frame and stand down until it retracts.
            if !isPictureInPicture, VMMonitorPiPState.shared.isVisible {
                frame = nil
                workingInBackground = false
                status = .handedToPiP
                try? await Task.sleep(for: .milliseconds(500))
                continue
            }
            let running = await store.runningMacVMs()
            // Match whichever of the session's guests (macOS or Linux) is actually up, and capture it
            // by its real name — the two never run at once for one session.
            if let entry = running.first(where: { candidates.contains($0.name) }) {
                if let base64 = await store.captureMacVMScreen(name: entry.name),
                    let image = Self.decode(base64) {
                    frame = image
                    status = .live
                } else if frame == nil {
                    // No frame yet: distinguish "still booting" (no IP) from "up but not capturable".
                    status = entry.ipAddress == nil ? .booting : .noSignal
                }
                // else: keep showing the last good frame despite this capture miss.
                // Only meaningful over a live frame; recompute each tick so it tracks the agent's work.
                workingInBackground = frame != nil
                    ? await store.macVMWorkingInBackground(name: entry.name)
                    : false
            } else {
                frame = nil
                status = .notRunning
                workingInBackground = false
            }
            // Refresh briskly while live; back off when there's nothing to show.
            try? await Task.sleep(for: .milliseconds(status == .live ? 1200 : 2000))
        }
    }

    private static func decode(_ base64: String) -> NSImage? {
        guard let data = Data(base64Encoded: base64) else { return nil }
        return NSImage(data: data)
    }

    private var placeholderIcon: String {
        switch status {
        case .idle, .notRunning, .noSignal: "power"
        case .booting: "hourglass"
        case .live: "display"
        case .handedToPiP: "pip"
        }
    }

    private var placeholderMessage: String {
        switch status {
        case .idle, .notRunning, .noSignal:
            "No VM is running for this chat. It appears here once the agent uses one."
        case .booting:
            "The VM is starting up…"
        case .live:
            ""
        case .handedToPiP:
            "This VM is showing in the floating Picture in Picture window."
        }
    }
}

/// One click's worth of white-flash feedback on the read-only monitor. Identifiable so several rapid
/// clicks each get their own independent flash.
private struct ClickFlash: Identifiable {
    let id = UUID()
}

/// A white flash across the whole monitor — the feedback that tells the user the monitor is look-only
/// (their click landed on glass, not on the guest). Self-drives its animation on appear and calls
/// `onFinished` once it's faded so the panel can drop it from the list.
private struct ClickFlashView: View {
    let onFinished: () -> Void
    @State private var faded = false

    var body: some View {
        Color.white
            .opacity(faded ? 0 : 0.3)
            .allowsHitTesting(false)  // pure feedback — never intercept the next click
            .onAppear { withAnimation(.easeOut(duration: 0.35)) { faded = true } }
            .task {
                // Outlive the 0.35 s animation, then ask the panel to remove this flash.
                try? await Task.sleep(for: .milliseconds(400))
                onFinished()
            }
    }
}
