import AppKit
import ArgumentParser
import Foundation
import MCP
import PreviewsCore
import PreviewsEngine
import PreviewsMacOS

struct ServeCommand: ParsableCommand {
    static let configuration = CommandConfiguration(
        commandName: "serve",
        abstract: "Expose preview tools over MCP",
        discussion: """
            Two modes:

              • Stdio (default) — reads MCP JSON-RPC from stdin, writes to stdout.
                Intended for MCP-compatible clients (Claude Code, Cursor) that
                launch `previewsmcp serve` as a subprocess. Example `.mcp.json`:

                  {
                    "mcpServers": {
                      "previews": {
                        "command": "/path/to/previewsmcp",
                        "args": ["serve"]
                      }
                    }
                  }

              • Daemon (`--daemon`) — listens on a Unix domain socket at
                `~/.previewsmcp/serve.sock`. Multiplexes many concurrent preview
                sessions. Used by the CLI (run, snapshot, etc.) and by any
                external MCP client capable of speaking over UDS.

            Both modes expose the same tools: preview_list, preview_start,
            preview_snapshot, preview_configure, preview_switch, preview_variants,
            preview_elements, preview_touch, preview_stop, simulator_list.
            """
    )

    @Flag(name: .long, help: "Run as a daemon on a Unix domain socket instead of stdio")
    var daemon: Bool = false

    /// Set by `PreviewsMCPApp.main()` before `run()` is called. This is
    /// the only handoff point between the entry point (which creates the
    /// PreviewHost) and the serve command (which passes it to the MCP
    /// server). ParsableCommand's `run()` can't take parameters, so a
    /// static is the minimal shared-state mechanism.
    @MainActor static var sharedHost: PreviewHost!

    mutating func run() throws {
        if daemon {
            runDaemon()
        } else {
            runStdio()
        }
    }

    private func runStdio() {
        installStdioSignalHandler()
        Task { @MainActor in
            let host = Self.sharedHost!
            do {
                // One iosManager / configCache / registry per stdio process.
                // Attach the registry here so its publish-on-mutation hooks
                // are wired before any tool call can create a session.
                try DaemonPaths.ensureDirectory()
                let iosManager = IOSSessionManager()
                let registry = SessionRegistry(registryDir: DaemonPaths.sessionsDirectory)
                await registry.attachTo(iosManager: iosManager, previewHost: host)
                let (server, _) = try await configureMCPServer(
                    host: host, iosManager: iosManager,
                    configCache: ConfigCache(), registry: registry
                )
                let startISO = ISO8601DateFormatter().string(from: ProcessStartup.time)
                Log.info(
                    "MCP server starting on stdio (pid \(ProcessInfo.processInfo.processIdentifier), started \(startISO))..."
                )
                let transport = StdioTransport()
                try await runMCPServer(server, transport: transport)
            } catch {
                Log.error("MCP server error: \(error)")
                await MainActor.run { NSApp.terminate(nil) }
            }
        }
    }

    /// Install a signal-safe SIGTERM handler for stdio mode. Distinct from
    /// `DaemonLifecycle.installSignalHandlers()` which uses
    /// `DispatchSource.makeSignalSource(... queue: .main)` — that path is
    /// coupled to NSApplication's runloop and can be slow under load
    /// (issue #156 investigation). Stdio mode has no on-disk state to
    /// clean up before exit (no PID file, no socket), so an `_exit(0)`
    /// from the signal-delivery thread is correct: the kernel reaps file
    /// descriptors and the process is gone in <1 ms regardless of what
    /// libdispatch or AppKit are doing.
    ///
    /// The single observable side effect is the stderr breadcrumb. If a
    /// future shutdown wedge recurs, the presence/absence of this line
    /// in the per-instance log partitions "daemon ignored signal" from
    /// "Foundation Process wedged on the test-host side."
    private func installStdioSignalHandler() {
        signal(SIGTERM) { _ in
            let msg: StaticString = "previewsmcp stdio: received SIGTERM, exiting\n"
            msg.withUTF8Buffer { buf in
                _ = write(STDERR_FILENO, buf.baseAddress, buf.count)
            }
            _exit(0)
        }
    }

    private func runDaemon() {
        Task { @MainActor in
            do {
                // Liveness check via `connect()` is authoritative — the PID file
                // alone can be stale (deleted while daemon is still running) or
                // missing during the brief window between bind and PID write.
                // Without this check, a second daemon would unlink the first's
                // socket file and attempt to rebind, corrupting the running
                // system.
                if DaemonProbe.canConnect() {
                    let pidDesc =
                        DaemonLifecycle.daemonRunningPID().map { "pid \($0)" }
                        ?? "pid unknown"
                    Log.error(
                        "daemon already running (\(pidDesc)); use `previewsmcp kill-daemon` first"
                    )
                    Darwin.exit(1)
                }

                // Detach before any client can observe us as "ready" via the
                // socket. Otherwise a SIGHUP during the tiny window between
                // socket bind and setsid could cascade through the shared
                // process group and kill the daemon.
                DaemonLifecycle.detachFromTerminal()

                let host = Self.sharedHost!
                _ = try await DaemonListener.start(host: host)
                try DaemonLifecycle.register()
                let startISO = ISO8601DateFormatter().string(from: ProcessStartup.time)
                Log.info(
                    "daemon ready (pid \(ProcessInfo.processInfo.processIdentifier), started \(startISO))"
                )
                // One-line breadcrumb so `serve.log` records when the
                // daemon was (re)spawned because of a version-mismatch
                // restart. Diagnoses repeated restart loops in bug
                // reports without any persistent state. See issue #142.
                if let reason = ProcessInfo.processInfo.environment[
                    "_PREVIEWSMCP_DAEMON_RESTART_REASON"], !reason.isEmpty
                {
                    Log.info("daemon: started after version-mismatch restart (\(reason))")
                }
            } catch {
                Log.error("daemon startup failed: \(error)")
                DaemonLifecycle.unregister()
                Darwin.exit(1)
            }
        }
    }
}
