import Foundation
import NucleicCore
#if canImport(WinSDK)
import WinSDK
#endif

// The Windows headless host (docs/WINDOWS_PORT.md §11): owns the AppStore graph, SyncHost,
// git, storage, and the wslc broker; the WinUI 3 renderer connects over loopback as a sync
// client. Shares its bring-up with nucleicd via `NucleicHeadless`; everything below is the
// hostd personality — single-instance guard, broker/engine wiring, console-signal shutdown.
//
// Still to land here (per §11, with their work items): the local renderer loopback listener +
// rendezvous file with owner-only DACL (items 10–11 — needs the renderer protocol DLL's local
// pairing path), DNS-SD advertise over dnsapi (item 13), and the post-reattach
// `ContainerManager.reconcile` sweep.
@main
@MainActor
struct Hostd {
    static func main() async {
        let env = ProcessInfo.processInfo.environment
        NucleicHeadless.scrubBlankAPIKeys(environment: env)

        // Single instance per channel: second launches exit quietly so a renderer relaunching
        // the host can never fork a split-brain store (§2.4).
        guard acquireSingleInstanceMutex() else {
            FileHandle.standardError.write(
                Data("nucleic-hostd: another instance owns this channel — exiting\n".utf8))
            exit(0)
        }

        // Data root: %APPDATA%\Nucleic<channelSuffix> (§4.4) — channel-suffixed so all five
        // channels coexist, mirroring the container naming scheme. Resolved through the shared
        // `NucleicPaths` so the store, the secrets dir, the run dir and the logs all land under
        // the SAME root (including when `NUCLEIC_DATA_DIR` relocates it); before that helper
        // existed hostd's root governed only the store path it passed to `makeStore`.
        let support = NucleicPaths.dataRoot
        do {
            try FileManager.default.createDirectory(at: support, withIntermediateDirectories: true)
        } catch {
            fail("could not create data dir \(support.path): \(error)")
        }

        // The sandbox mechanism: brokerd (C#, owns every wslc call) behind the Swift client,
        // driving the `SandboxEngine` the shared ContainerManager policy layer runs on (§3).
        // Engine wiring: pull progress feeds the shared download-progress UI surface; a broker
        // reattach invalidates the cached gateway. (The post-reattach
        // `ContainerManager.reconcile` sweep needs the store, so it's installed further down.)
        let engineBox = EngineBox()
        let broker = WslcBrokerClient(
            onReattach: { await engineBox.engine?.brokerReattached() },
            onPullProgress: { ref, status, current, total in
                await engineBox.engine?.notePullProgress(
                    ref: ref, status: status, current: current, total: total)
            })
        let engine = WslcContainerEngine(client: broker)
        await engineBox.set(engine)
        // Boot the broker + wslc session early (§11 start order) so the first session's
        // container create doesn't pay the VM boot, and the gateway is known before any
        // backend binds an approval server. Both are best-effort here: a missing WSL stack
        // surfaces through onboarding (`components.missing`), not a dead host process.
        do {
            try await broker.start()
            _ = try? await engine.sessionHostGateway()
        } catch {
            log("wslc broker unavailable at boot (onboarding will drive install): \(error)")
        }

        let store: AppStore
        do {
            store = try NucleicHeadless.makeStore(support: support, sandboxEngine: engine)
        } catch {
            fail("could not open the host store at \(support.path): \(error)")
        }

        if let name = env["NUCLEIC_HOST_NAME"], !name.isEmpty {
            store.syncHostName = name
        } else if store.syncHostName == "Mac" {
            // The default host name must not claim to be a Mac. COMPUTERNAME is always set on
            // Windows; "PC" is the last-resort label.
            store.syncHostName = env["COMPUTERNAME"] ?? "PC"
        }

        await NucleicHeadless.loadPersistentState(store)

        // Unlike nucleicd's relay-only default, a desktop host serves its own LAN first (§11).
        if env["NUCLEIC_SYNC_TRANSPORTS"] == nil { setenv("NUCLEIC_SYNC_TRANSPORTS", "lan,relay", 1) }
        await store.startSyncServer()
        for (transport, failure) in store.syncTransportHealth.sorted(by: { $0.key.rawValue < $1.key.rawValue }) {
            log("transport \(transport.rawValue) failed: \(failure)")
        }
        guard store.syncRunning else {
            fail("sync server failed to start — no transport could listen")
        }

        log("nucleic-hostd up — host \(store.syncHostID ?? "?") (\(store.syncHostName))")
        log("listening on: \(store.syncActiveTransports.map(\.rawValue).sorted().joined(separator: ", "))")
        log("data dir: \(support.path)")

        // Console-signal shutdown (§2.4): CTRL_CLOSE/LOGOFF/SHUTDOWN route into the same
        // graceful path nucleicd drives from SIGTERM — transcripts flush, the relay socket
        // closes politely, the broker (and with it the wslc session's containers) is released.
        installConsoleHandler {
            Task { @MainActor in
                log("shutting down")
                PowerBlocker.release()
                await store.stopSyncServer()
                await broker.shutdown()
                exit(0)
            }
        }

        // The host's heartbeat. There is no SwiftUI `onChange` here, so the one thing that has to
        // track store state — the sleep assertion — is reconciled on a coarse poll instead
        // (§4.3): `SetThreadExecutionState` is idempotent and `reconcile` no-ops when the level
        // hasn't changed, so a 5-second tick costs nothing and can't drift by more than a tick.
        // Running it on the main actor also keeps every assertion call on the same thread, which
        // is what makes the state stick.
        while true {
            try? await Task.sleep(nanoseconds: 5_000_000_000)
            PowerBlocker.reconcile(turnsInFlight: store.localTurnInFlightCount)
        }
    }

    // MARK: - Platform bits

    /// `Local\nucleic-hostd-<channel>` named mutex; the first owner wins (§2.4).
    private static func acquireSingleInstanceMutex() -> Bool {
        #if canImport(WinSDK)
        let name = "Local\\nucleic-hostd" + ContainerManager.channelSuffix
        let handle = name.withCString(encodedAs: UTF16.self) { CreateMutexW(nil, true, $0) }
        if handle == nil { return false }
        return GetLastError() != DWORD(ERROR_ALREADY_EXISTS)
        #else
        return true
        #endif
    }

    private static func installConsoleHandler(_ shutdown: @escaping @Sendable () -> Void) {
        #if canImport(WinSDK)
        consoleShutdown = shutdown
        SetConsoleCtrlHandler(
            { _ in
                consoleShutdown?()
                // Report handled; Windows grants a grace window before force-terminating.
                return true
            }, true)
        #endif
    }

    private static func log(_ message: String) {
        FileHandle.standardError.write(Data("nucleic-hostd: \(message)\n".utf8))
    }

    private static func fail(_ message: String) -> Never {
        FileHandle.standardError.write(Data("nucleic-hostd: FATAL \(message)\n".utf8))
        exit(1)
    }
}

/// The engine reference the broker's event closures need before the engine exists (the client
/// is constructed first so the engine can hold it) — a tiny settable box breaks the cycle.
private actor EngineBox {
    private(set) var engine: WslcContainerEngine?
    func set(_ engine: WslcContainerEngine) { self.engine = engine }
}

/// C-convention console handler can't capture context; the shutdown hop lives in a global.
#if canImport(WinSDK)
nonisolated(unsafe) private var consoleShutdown: (@Sendable () -> Void)?
#endif
