#if canImport(Network)
import Foundation
import Network
import X509
import _CryptoExtras

/// The host-side MDM HTTPS listener (docs/MACOS_VM_MDM.md §4). Speaks the two MDM channels the guest
/// needs — `/checkin` (Authenticate / TokenUpdate / CheckOut) and `/server` (the Idle → command →
/// Acknowledged poll loop) — over TLS terminated with the CA-issued server identity. **No APNs:**
/// commands are drained by the device's own poll after `TokenUpdate` (Mode A) or after a vsock nudge
/// (Mode B). This whole file is Darwin-only (Network.framework); the rest of `NucleicMDM` compiles on
/// Linux without it.
///
/// The server is deliberately tiny and single-device (§2.5): one `MDMCommandQueue`, one issued device
/// identity, connection-per-request HTTP/1.1. It is reachable only over loopback + the vmnet gateway
/// address, and authenticates the device by the `Mdm-Signature` header chained to the local CA.
public actor NucleicMDMServer {
    public let config: MDMConfiguration
    private let ca: MDMCertificateAuthority
    private let deviceIdentity: MDMDeviceIdentity
    public let queue: MDMCommandQueue

    /// When true, a request whose `Mdm-Signature` fails CMS verification is rejected (403). Default
    /// false while the signature path is validated on hardware (§8): mdmclient always *sends* the
    /// header when `SignMessage=true`, but the detached-CMS shape is interop-sensitive, so we log a
    /// verification failure rather than brick enrollment before the spike confirms it.
    public var requireValidSignature: Bool

    private var listener: NWListener?
    private var boundPort: UInt16 = 0

    /// Observability hook — every protocol exchange (method, path, message type, queue action) is
    /// handed to this for the spike/log. Runs off-actor.
    public typealias LogSink = @Sendable (String) -> Void
    private let log: LogSink
    /// Fine-grained file trace (listener/connection/TLS/byte level). Separate from ``log``, which is
    /// the orchestration-facing summary: a failing TLS handshake produces no HTTP request at all, so
    /// it is invisible in ``log`` and only appears here.
    private let debug: MDMDebugLog?
    /// Monotonic per-connection id so interleaved connection lines can be followed.
    private var connectionCounter: UInt64 = 0

    public init(
        config: MDMConfiguration,
        ca: MDMCertificateAuthority,
        deviceIdentity: MDMDeviceIdentity,
        queue: MDMCommandQueue = MDMCommandQueue(),
        requireValidSignature: Bool = false,
        debug: MDMDebugLog? = nil,
        log: @escaping LogSink = { _ in }
    ) {
        self.config = config
        self.ca = ca
        self.deviceIdentity = deviceIdentity
        self.queue = queue
        self.requireValidSignature = requireValidSignature
        self.debug = debug
        self.log = log
    }

    private func nextConnectionID() -> UInt64 {
        connectionCounter &+= 1
        return connectionCounter
    }

    public enum ServerError: Error, CustomStringConvertible {
        case bindFailed(String)
        public var description: String {
            if case .bindFailed(let s) = self { return "MDM server bind failed: \(s)" }
            return ""
        }
    }

    /// Start listening on `host` (loopback or the vmnet gateway address) at `config.port`. Returns the
    /// bound port. The server TLS cert is issued on demand for `config.serverDNSName` (+ the host IP as
    /// an IP SAN when given), which the guest pins to the gateway in `/etc/hosts`.
    @discardableResult
    public func start(host: String = "127.0.0.1", ipSANs: [String] = []) async throws -> UInt16 {
        debug?.log(
            "listener",
            "start requested host=\(host) port=\(config.port) dnsName=\(config.serverDNSName) "
                + "ipSANs=\(ipSANs) topic=\(config.topic)")
        let server: (certificate: Certificate, privateKey: _RSA.Signing.PrivateKey)
        do {
            server = try ca.issueServerCertificate(
                dnsName: config.serverDNSName, ipSANs: ipSANs)
        } catch {
            debug?.log("tls", "FAILED to issue server certificate: \(error)")
            throw error
        }
        // The exact leaf the guest will be asked to validate. When the guest reports "the certificate
        // could not be verified", these are the fields to compare against its trust store. Each field
        // is logged on its own line: a single concatenated interpolation of this many X509 description
        // properties defeats the type-checker.
        if let debug {
            let leaf = server.certificate
            debug.log("tls", "server leaf subject: \(String(describing: leaf.subject))")
            debug.log("tls", "server leaf issuer: \(String(describing: leaf.issuer))")
            debug.log("tls", "server leaf serial: \(String(describing: leaf.serialNumber))")
            debug.log("tls", "server leaf notBefore: \(String(describing: leaf.notValidBefore))")
            debug.log("tls", "server leaf notAfter: \(String(describing: leaf.notValidAfter))")
            let root = ca.rootCertificate
            debug.log("tls", "CA root subject: \(String(describing: root.subject))")
            debug.log("tls", "CA root serial: \(String(describing: root.serialNumber))")
            debug.log("tls", "CA root notAfter: \(String(describing: root.notValidAfter))")
        }

        let identity: sec_identity_t
        do {
            identity = try MDMTLSIdentity.serverIdentity(
                certificate: server.certificate, privateKey: server.privateKey)
            debug?.log("tls", "sec_identity_t created (PKCS#12 round-trip through SecPKCS12Import ok)")
        } catch {
            debug?.log("tls", "FAILED to build sec_identity_t: \(error)")
            throw error
        }

        let tlsOptions = NWProtocolTLS.Options()
        sec_protocol_options_set_local_identity(tlsOptions.securityProtocolOptions, identity)
        // mdmclient speaks TLS 1.2+; leave the framework defaults (which negotiate 1.2/1.3).
        let params = NWParameters(tls: tlsOptions)
        params.allowLocalEndpointReuse = true
        params.requiredLocalEndpoint = NWEndpoint.hostPort(
            host: NWEndpoint.Host(host), port: NWEndpoint.Port(rawValue: UInt16(config.port))!)

        let listener: NWListener
        do { listener = try NWListener(using: params) } catch {
            debug?.log("listener", "BIND FAILED on \(host):\(config.port): \(error)")
            throw ServerError.bindFailed("\(error)")
        }
        let debug = self.debug
        listener.newConnectionHandler = { [weak self] connection in
            guard let self else {
                debug?.log("conn", "accepted after server teardown — cancelling")
                connection.cancel()
                return
            }
            Task {
                let id = await self.nextConnectionID()
                debug?.log(
                    "conn", "id=\(id) ACCEPTED from=\(connection.endpoint) — awaiting TLS handshake")
                // Connection state is where a TLS handshake failure actually surfaces: a guest that
                // distrusts our CA connects, fails the handshake, and goes straight to `.failed`
                // without ever sending an HTTP request. That transition used to be silent.
                connection.stateUpdateHandler = { state in
                    switch state {
                    case .ready:
                        debug?.log("conn", "id=\(id) READY (TLS handshake complete)")
                    case .preparing:
                        debug?.log("conn", "id=\(id) preparing")
                    case .waiting(let error):
                        debug?.log("conn", "id=\(id) WAITING: \(error)")
                    case .failed(let error):
                        debug?.log("conn", "id=\(id) FAILED: \(error) — likely TLS/certificate")
                    case .cancelled:
                        debug?.log("conn", "id=\(id) cancelled")
                    case .setup:
                        debug?.log("conn", "id=\(id) setup")
                    @unknown default:
                        debug?.log("conn", "id=\(id) unknown state")
                    }
                }
                connection.start(queue: .global(qos: .userInitiated))
                await self.serve(connection, id: id)
            }
        }

        try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
            let once = ResumeOnce()
            listener.stateUpdateHandler = { state in
                switch state {
                case .ready:
                    debug?.log("listener", "READY")
                    if once.claim() { cont.resume() }
                case .waiting(let e):
                    debug?.log("listener", "WAITING: \(e)")
                    if once.claim() { cont.resume(throwing: e) }
                case .failed(let e):
                    debug?.log("listener", "FAILED: \(e)")
                    if once.claim() { cont.resume(throwing: e) }
                case .cancelled:
                    debug?.log("listener", "cancelled")
                default:
                    debug?.log("listener", "state=\(state)")
                }
            }
            listener.start(queue: .global(qos: .userInitiated))
        }
        self.listener = listener
        self.boundPort = listener.port?.rawValue ?? UInt16(config.port)
        debug?.log(
            "listener",
            "LISTENING on \(host):\(boundPort) — ServerURL=\(config.serverURL) "
                + "CheckInURL=\(config.checkInURL); awaiting guest connections")
        log("MDM server listening on \(host):\(boundPort) (TLS, CN=\(config.serverDNSName))")
        return boundPort
    }

    public func stop() {
        listener?.cancel()
        listener = nil
    }

    // MARK: - Connection handling

    private func serve(_ connection: NWConnection, id: UInt64) async {
        var buffer = Data()
        var served = 0
        // HTTP/1.1 keep-alive: mdmclient reuses the connection across the whole poll loop.
        while true {
            guard let request = await nextRequest(connection, buffer: &buffer, id: id) else {
                debug?.log(
                    "conn",
                    "id=\(id) no further request (peer closed or parse gave up) after \(served) "
                        + "request(s); \(buffer.count) unconsumed byte(s) buffered")
                break
            }
            served += 1
            debug?.log(
                "http",
                "id=\(id) REQUEST #\(served) \(request.method) \(request.path) "
                    + "headers=\(request.headers.count) bodyBytes=\(request.body.count)")
            for (key, value) in request.headers.sorted(by: { $0.key < $1.key }) {
                // The signature header is long; record its size rather than the blob.
                let rendered = key == "mdm-signature" ? "<\(value.count) chars>" : value
                debug?.log("http", "id=\(id)   header \(key): \(rendered)")
            }
            debug?.logBytes("http", "id=\(id)   body", request.body)
            let response = await route(request, id: id)
            debug?.log(
                "http",
                "id=\(id) RESPONSE \(response.status) \(response.statusText) "
                    + "contentType=\(response.contentType ?? "-") bytes=\(response.body.count)")
            guard await send(response, on: connection) else {
                debug?.log("conn", "id=\(id) send failed — closing")
                break
            }
        }
        debug?.log("conn", "id=\(id) CLOSED after \(served) request(s)")
        connection.cancel()
    }

    private func route(_ request: HTTPRequest, id: UInt64) async -> HTTPResponse {
        let path = request.path.split(separator: "?").first.map(String.init) ?? request.path
        // Every request is logged before it is interpreted. Whether the guest reached the listener at
        // all is the first thing a failed enrollment needs to establish, and it is not recoverable
        // from a per-message log that only fires once parsing succeeds.
        log("\(request.method) \(path) (\(request.body.count) byte body)")
        // Verify the device signature (best-effort unless strict). PUT/POST bodies only.
        if !request.body.isEmpty {
            let hasHeader = request.headers["mdm-signature"] != nil
            let verified = await verifySignature(request)
            debug?.log(
                "sig",
                "id=\(id) Mdm-Signature present=\(hasHeader) verified=\(verified) "
                    + "strict=\(requireValidSignature)")
            if requireValidSignature && !verified {
                log("REJECT \(request.method) \(path): Mdm-Signature invalid")
                debug?.log("sig", "id=\(id) REJECTED 403 — strict verification failed")
                return .status(403, "Forbidden")
            }
        }
        switch path {
        case "/checkin": return await handleCheckIn(request, id: id)
        case "/server": return await handleCommand(request, id: id)
        default:
            log("404 \(request.method) \(path)")
            debug?.log("http", "id=\(id) 404 unroutable path=\(path)")
            return .status(404, "Not Found")
        }
    }

    private func handleCheckIn(_ request: HTTPRequest, id: UInt64) async -> HTTPResponse {
        let message: MDMCheckInMessage
        do { message = try MDMCheckInMessage.parse(request.body) } catch {
            // A 400 here is fatal to enrollment, so the body that caused it must be recoverable —
            // an unsupported MessageType and a malformed plist need opposite fixes.
            log("400 /checkin: \(error); body: \(Self.bodyPreview(request.body))")
            return .status(400, "Bad Request")
        }
        switch message {
        case .authenticate(let udid, let topic):
            log("check-in Authenticate udid=\(shortID(udid))")
            debug?.log(
                "checkin",
                "id=\(id) Authenticate udid=\(udid) topic=\(topic ?? "-") "
                    + "(expected topic=\(config.topic))")
        case .tokenUpdate(let udid, let pushMagic, let tokenHex):
            // The pivot: after acking TokenUpdate the device polls /server and drains the queue.
            let pending = await queue.pendingCount
            log("check-in TokenUpdate udid=\(shortID(udid)) — \(pending) command(s) queued to drain")
            debug?.log(
                "checkin",
                "id=\(id) TokenUpdate udid=\(udid) pushMagic=\(pushMagic ?? "-") "
                    + "tokenBytes=\((tokenHex?.count ?? 0) / 2) — THIS IS THE PIVOT: the device should "
                    + "now poll /server; \(pending) command(s) pending")
        case .checkOut(let udid):
            log("check-in CheckOut udid=\(shortID(udid))")
            debug?.log("checkin", "id=\(id) CheckOut udid=\(udid) — device is unenrolling")
        case .other(let type, let udid):
            // Acked, not rejected — a 400 here aborts enrollment (finding 8).
            log("check-in \(type) udid=\(shortID(udid)) — acked (unmodeled type)")
            debug?.log(
                "checkin",
                "id=\(id) \(type) udid=\(udid) — unmodeled MessageType, acked with empty 200")
        }
        // MDM check-in acks are an empty 200.
        return .empty
    }

    private func handleCommand(_ request: HTTPRequest, id: UInt64) async -> HTTPResponse {
        let report: MDMCommandStatusReport
        do { report = try MDMCommandStatusReport.parse(request.body) } catch {
            log("400 /server: \(error); body: \(Self.bodyPreview(request.body))")
            debug?.log("command", "id=\(id) 400 — unparseable status report: \(error)")
            return .status(400, "Bad Request")
        }
        if let summary = report.errorSummary {
            log("command \(report.status) uuid=\(report.commandUUID ?? "-"): \(summary)")
        } else {
            log("command \(report.status) uuid=\(report.commandUUID ?? "-")")
        }
        let pendingBefore = await queue.pendingCount
        debug?.log(
            "command",
            "id=\(id) report status=\(report.status) uuid=\(report.commandUUID ?? "-") "
                + "udid=\(report.udid) error=\(report.errorSummary ?? "-") "
                + "pendingBefore=\(pendingBefore)")
        guard let next = await queue.next(afterReport: report) else {
            // Empty 200 ends the poll loop.
            debug?.log(
                "queue",
                "id=\(id) nothing left to deliver — empty 200 ends the poll loop "
                    + "(drained=\(await queue.isDrained))")
            return .empty
        }
        guard let body = try? next.plist() else {
            debug?.log("command", "id=\(id) 500 — could not serialize \(next.requestType)")
            return .status(500, "Internal Server Error")
        }
        log("→ deliver \(next.requestType) uuid=\(next.uuid)")
        debug?.log(
            "queue",
            "id=\(id) DELIVER \(next.requestType) uuid=\(next.uuid) bytes=\(body.count) "
                + "pendingAfter=\(await queue.pendingCount)")
        return HTTPResponse(status: 200, statusText: "OK", contentType: "application/xml", body: body)
    }

    /// Verify the detached `Mdm-Signature` CMS over the request body against the local CA. Returns
    /// true when valid (or when there is no signature to check — the caller decides strictness).
    private func verifySignature(_ request: HTTPRequest) async -> Bool {
        guard let header = request.headers["mdm-signature"], let sig = Data(base64Encoded: header)
        else { return false }
        return await MDMSignatureVerifier.verify(
            body: request.body, detachedCMS: Array(sig), ca: ca)
    }

    // MARK: - Minimal HTTP/1.1

    struct HTTPRequest: Sendable {
        let method: String
        let path: String
        let headers: [String: String]  // lowercased keys
        let body: Data
    }

    struct HTTPResponse: Sendable {
        let status: Int
        let statusText: String
        let contentType: String?
        let body: Data
        static let empty = HTTPResponse(status: 200, statusText: "OK", contentType: nil, body: Data())
        static func status(_ code: Int, _ text: String) -> HTTPResponse {
            HTTPResponse(status: code, statusText: text, contentType: nil, body: Data())
        }
    }

    static let maxRequestBytes = 8 * 1024 * 1024

    private func nextRequest(
        _ conn: NWConnection, buffer: inout Data, id: UInt64
    ) async -> HTTPRequest? {
        while true {
            if let request = Self.parseRequest(from: &buffer) { return request }
            if buffer.count > Self.maxRequestBytes {
                debug?.log(
                    "http", "id=\(id) ABORT — buffered \(buffer.count) bytes without a complete request")
                return nil
            }
            guard let chunk = await receive(on: conn) else {
                debug?.log(
                    "conn",
                    "id=\(id) receive ended (EOF/error) with \(buffer.count) byte(s) buffered")
                if !buffer.isEmpty { debug?.logBytes("conn", "id=\(id)   partial", buffer) }
                return nil
            }
            if chunk.isEmpty {
                debug?.log("conn", "id=\(id) receive returned 0 bytes (keep-alive idle)")
            } else {
                debug?.log("conn", "id=\(id) received \(chunk.count) byte(s)")
                debug?.logBytes("conn", "id=\(id)   raw", chunk, limit: 256)
            }
            buffer.append(chunk)
        }
    }

    static func parseRequest(from buffer: inout Data) -> HTTPRequest? {
        guard let headerEnd = buffer.range(of: Data("\r\n\r\n".utf8)) else { return nil }
        let headerData = buffer.subdata(in: buffer.startIndex..<headerEnd.lowerBound)
        guard let headerText = String(data: headerData, encoding: .utf8) else { return nil }
        let lines = headerText.components(separatedBy: "\r\n")
        guard let requestLine = lines.first else { return nil }
        let parts = requestLine.split(separator: " ")
        guard parts.count >= 2 else { return nil }
        var headers: [String: String] = [:]
        for line in lines.dropFirst() {
            guard let colon = line.firstIndex(of: ":") else { continue }
            let key = line[..<colon].trimmingCharacters(in: .whitespaces).lowercased()
            headers[key] = line[line.index(after: colon)...].trimmingCharacters(in: .whitespaces)
        }
        let contentLength = headers["content-length"].flatMap(Int.init) ?? 0
        let bodyStart = headerEnd.upperBound
        guard buffer.distance(from: bodyStart, to: buffer.endIndex) >= contentLength else {
            return nil
        }
        let body = buffer.subdata(in: bodyStart..<buffer.index(bodyStart, offsetBy: contentLength))
        buffer.removeSubrange(buffer.startIndex..<buffer.index(bodyStart, offsetBy: contentLength))
        return HTTPRequest(
            method: String(parts[0]), path: String(parts[1]), headers: headers, body: body)
    }

    private func receive(on conn: NWConnection) async -> Data? {
        await withCheckedContinuation { (cont: CheckedContinuation<Data?, Never>) in
            conn.receive(minimumIncompleteLength: 1, maximumLength: 1 << 16) { data, _, isComplete, error in
                if let data, !data.isEmpty { cont.resume(returning: data) }
                else if isComplete || error != nil { cont.resume(returning: nil) }
                else { cont.resume(returning: Data()) }
            }
        }
    }

    private func send(_ response: HTTPResponse, on conn: NWConnection) async -> Bool {
        var head = "HTTP/1.1 \(response.status) \(response.statusText)\r\n"
        if let contentType = response.contentType { head += "Content-Type: \(contentType)\r\n" }
        head += "Content-Length: \(response.body.count)\r\nConnection: keep-alive\r\n\r\n"
        var payload = Data(head.utf8)
        payload.append(response.body)
        return await withCheckedContinuation { (cont: CheckedContinuation<Bool, Never>) in
            conn.send(content: payload, completion: .contentProcessed { error in
                cont.resume(returning: error == nil)
            })
        }
    }

    private func shortID(_ udid: String) -> String { String(udid.prefix(8)) }

    /// A single-line, bounded rendering of a request body for the failure log. XML plists (what
    /// `mdmclient` sends) read directly; anything else is hex so a wrong content type is still legible.
    /// Bounded because a body can be megabytes and this lands in a transcript kept beside the base.
    static func bodyPreview(_ body: Data, limit: Int = 400) -> String {
        guard !body.isEmpty else { return "<empty>" }
        let head = body.prefix(limit)
        if let text = String(data: head, encoding: .utf8) {
            let flattened = text.split(whereSeparator: \.isNewline)
                .map { $0.trimmingCharacters(in: .whitespaces) }
                .filter { !$0.isEmpty }
                .joined(separator: " ")
            if !flattened.isEmpty {
                return flattened + (body.count > limit ? "… (\(body.count) bytes)" : "")
            }
        }
        return head.map { String(format: "%02x", $0) }.joined()
            + (body.count > limit ? "… (\(body.count) bytes)" : "")
    }
}

/// Resume-once guard for the listener state callback.
private final class ResumeOnce: @unchecked Sendable {
    private let lock = NSLock()
    private var claimed = false
    func claim() -> Bool {
        lock.lock(); defer { lock.unlock() }
        if claimed { return false }
        claimed = true
        return true
    }
}
#endif
