import Foundation
import X509

/// The one object the host (provisioning / spike / Mode B) talks to. It owns the CA, the deployment
/// config, the baked device identity, and the command queue, and it produces the three artifacts the
/// guest needs staged over virtiofs (docs/MACOS_VM_MDM.md §5):
///
///   1. the **CA root** (`nucleic-mdm-ca.cer`) — trusted before enrollment so TLS/enrollment validate;
///   2. the **enrollment profile** (`nucleic-mdm-enroll.mobileconfig`) — the PKCS#12 identity + MDM;
///   3. the **enroll script** (`mdm-enroll.sh`) — pins `/etc/hosts`, trusts the CA, opens the profile.
///
/// The Darwin HTTPS server (`NucleicMDMServer`) is created via ``makeServer()`` behind
/// `#if canImport(Network)`; everything else is cross-platform so the artifact generation + the queue
/// are unit-testable on Linux CI.
public struct MDMServerController: Sendable {
    public let ca: MDMCertificateAuthority
    public let config: MDMConfiguration
    public let deviceIdentity: MDMDeviceIdentity
    /// Top-level policy identifiers last verified in the enrolled base. Empty before first success.
    public let profileIdentifiers: [String]
    public let queue: MDMCommandQueue

    /// Staged-artifact filenames (shared by the generator here and the enroll script).
    ///
    /// There is deliberately no standalone root-CA profile here. Installing one by hand cannot make
    /// the guest trust the CA — a manually installed profile's root has not been auto-trusted for TLS
    /// since macOS Ventura — so it was removed along with the flow that opened it. Trust is written to
    /// the admin domain during the minimal-agent boot instead.
    public enum Artifact {
        public static let caCert = "nucleic-mdm-ca.cer"
        public static let profile = "nucleic-mdm-enroll.mobileconfig"
        public static let enrollScript = "mdm-enroll.sh"
    }

    /// Create a controller with a fresh CA + identity (base build). Reuse ``init(ca:config:…)`` with a
    /// persisted CA for Mode B so a live clone's baked identity still verifies.
    public static func makeFresh(
        config: MDMConfiguration = .generate(), now: Date = Date()
    ) throws -> MDMServerController {
        let ca = try MDMCertificateAuthority.generate(now: now)
        let identity = try MDMDeviceIdentity.issue(from: ca, deviceName: config.deviceName, now: now)
        return MDMServerController(
            ca: ca, config: config, deviceIdentity: identity, queue: MDMCommandQueue())
    }

    public init(
        ca: MDMCertificateAuthority, config: MDMConfiguration,
        deviceIdentity: MDMDeviceIdentity, profileIdentifiers: [String] = [],
        queue: MDMCommandQueue = MDMCommandQueue()
    ) {
        self.ca = ca
        self.config = config
        self.deviceIdentity = deviceIdentity
        self.profileIdentifiers = profileIdentifiers
        self.queue = queue
    }

    // MARK: - Persistence

    /// Complete host-side state required to contact an enrolled base or one of its clones later.
    /// Versioning makes future migrations explicit instead of silently replacing a trusted identity.
    public struct Persisted: Codable, Sendable {
        public var version: Int
        public var ca: MDMCertificateAuthority.Persisted
        public var config: MDMConfiguration
        public var deviceIdentity: MDMDeviceIdentity.Persisted
        public var profileIdentifiers: [String]

        public init(
            version: Int = 1, ca: MDMCertificateAuthority.Persisted,
            config: MDMConfiguration, deviceIdentity: MDMDeviceIdentity.Persisted,
            profileIdentifiers: [String] = []
        ) {
            self.version = version
            self.ca = ca
            self.config = config
            self.deviceIdentity = deviceIdentity
            self.profileIdentifiers = profileIdentifiers
        }
    }

    public enum PersistenceError: Error, CustomStringConvertible {
        case unsupportedVersion(Int)

        public var description: String {
            switch self {
            case .unsupportedVersion(let version):
                return "unsupported MDM controller state version \(version)"
            }
        }
    }

    public func persisted() throws -> Persisted {
        try Persisted(
            ca: ca.persisted(), config: config, deviceIdentity: deviceIdentity.persisted(),
            profileIdentifiers: profileIdentifiers)
    }

    /// Return the same cryptographic identity and live queue with a newly verified policy inventory.
    public func withProfileIdentifiers(_ identifiers: [String]) -> MDMServerController {
        MDMServerController(
            ca: ca, config: config, deviceIdentity: deviceIdentity,
            profileIdentifiers: identifiers, queue: queue)
    }

    public init(persisted state: Persisted, queue: MDMCommandQueue = MDMCommandQueue()) throws {
        guard state.version == 1 else {
            throw PersistenceError.unsupportedVersion(state.version)
        }
        self.init(
            ca: try MDMCertificateAuthority(persisted: state.ca), config: state.config,
            deviceIdentity: try MDMDeviceIdentity(persisted: state.deviceIdentity),
            profileIdentifiers: state.profileIdentifiers, queue: queue)
    }

    /// Atomically write the controller state and restrict it to the current host user. This file
    /// contains the CA and device private keys; it belongs beside the base, never in the guest share.
    public func writePersistedState(to url: URL) throws {
        let fm = FileManager.default
        try fm.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
        let encoder = JSONEncoder()
        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
        try encoder.encode(persisted()).write(to: url, options: .atomic)
        try fm.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
    }

    public static func loadPersistedState(
        from url: URL, queue: MDMCommandQueue = MDMCommandQueue()
    ) throws -> MDMServerController {
        let state = try JSONDecoder().decode(Persisted.self, from: Data(contentsOf: url))
        return try MDMServerController(persisted: state, queue: queue)
    }

    // MARK: - Artifacts

    /// The enrollment profile as a plain XML plist (unsigned) — the inspectable form.
    public func unsignedEnrollmentProfile(
        uuids: MDMEnrollmentProfile.UUIDs = MDMEnrollmentProfile.UUIDs()
    ) throws -> Data {
        try MDMEnrollmentProfile.make(
            config: config, identity: deviceIdentity,
            caCertificateDER: caCertificateDER(), uuids: uuids)
    }

    /// The enrollment `.mobileconfig` to stage — **unsigned**, deliberately.
    ///
    /// It used to be CMS-signed by a CA-issued leaf, so System Settings would show it "verified"
    /// rather than "Not Signed". A live run showed that is exactly backwards here: the install failed
    /// with *"Profile installation failed. The certificate could not be verified (authentication
    /// error)"* — and the server trace proves `mdmclient` never opened a connection, so the rejection
    /// happened **before any network contact** and cannot have been the check-in TLS. What macOS
    /// could not verify was the profile's own signing chain, which terminates at a CA the guest has
    /// no way to trust (five routes closed; docs/MACOS_VM_MDM_DEBUG_LOG.md runs 10–18).
    ///
    /// Unsigned is the ordinary case: macOS installs an unsigned profile after showing it as "Not
    /// Signed", which is how every self-service `.mobileconfig` works. Signing only helps when the
    /// signer chains to something the guest already trusts — and if that were available, none of this
    /// subsystem's difficulty would exist. The server's own authentication is unaffected: device
    /// messages still carry `Mdm-Signature`, verified host-side against our CA.
    public func enrollmentProfile(uuids: MDMEnrollmentProfile.UUIDs = MDMEnrollmentProfile.UUIDs())
        throws -> Data
    {
        try unsignedEnrollmentProfile(uuids: uuids)
    }

    /// The CA root certificate in DER (`.cer`).
    public func caCertificateDER() throws -> Data {
        Data(try ca.rootCertificateDER())
    }

    /// `/Library/Security/Trust Settings/Admin.plist` granting this CA blanket root trust — the
    /// fallback used when no supported API can establish that trust non-interactively
    /// (``MDMAdminTrustSettings``).
    public func adminTrustSettingsPlist(now: Date = Date()) throws -> Data {
        try MDMAdminTrustSettings.plist(trusting: ca.rootCertificate, now: now)
    }

    /// The in-guest enroll script. Discovers the vmnet gateway and pins the server DNS name to it, so
    /// the enrollment profile's stable URL resolves. That is now its whole job.
    ///
    /// It runs **as the console (agent) user**, not under a single `sudo bash`: only the `/etc/hosts`
    /// pin needs privilege, via the agent account's passwordless sudo, and the host's subsequent
    /// `open` of the enrollment profile must register it in the *console user's* System Settings (run
    /// as root it registers for root, and the GUI shows nothing to approve — finding 1).
    ///
    /// It establishes no trust of its own, by design. `security add-trusted-cert` raises a
    /// SecurityAgent sheet owned by a root CLI with no GUI app: it never becomes key, and five
    /// consecutive automated attempts typed into nothing. Its replacement — delivering the CA as an
    /// ordinary configuration profile — turned out to be a dead end for a different reason: since
    /// macOS Ventura a root installed by a MANUALLY installed profile is no longer auto-trusted for
    /// TLS (auto-trust survives only for roots delivered *by* an MDM, which is what we are enrolling
    /// into). CA trust is therefore established in the preceding minimal-agent boot, in the admin
    /// trust domain, and verified by the host before this profile is opened. Nothing here prompts, so
    /// the script stays synchronous.
    public func enrollScript() -> String {
        """
        #!/bin/bash
        # Nucleic MDM enroll — staged + run by provisioning (docs/MACOS_VM_MDM.md §4). Console user,
        # SIP-on; only the /etc/hosts pin uses passwordless sudo. Deliberately not one `sudo bash`.
        set -uo pipefail
        SHARE="$(cd "$(dirname "$0")" && pwd)"
        HOSTNAME_PIN="\(config.serverDNSName)"

        # The console user must be the agent account for `open` to reach a real System Settings, and
        # for the surface to find the enrollment row. A mismatch means auto-login didn't take — report
        # it (this is the auto-login canary) but continue; the surface still has a chance.
        CONSOLE_USER="$(/usr/bin/stat -f%Su /dev/console 2>/dev/null || echo unknown)"
        echo "[mdm-enroll] console user: $CONSOLE_USER (running as $(/usr/bin/id -un))"

        echo "[mdm-enroll] discovering vmnet gateway…"
        GW="$(route -n get default 2>/dev/null | awk '/gateway:/{print $2}')"
        if [ -z "${GW:-}" ]; then
          echo "[mdm-enroll] ERROR: no default gateway (vmnet NAT down?)" >&2; exit 1
        fi
        # Pin the server DNS name to the gateway so the profile's stable URL resolves (the vmnet
        # subnet is not fixed across clones — /etc/hosts is root-writable with SIP on).
        sudo /usr/bin/sed -i '' "/[[:space:]]$HOSTNAME_PIN\\$/d" /etc/hosts 2>/dev/null || true
        printf '%s %s\\n' "$GW" "$HOSTNAME_PIN" | sudo /usr/bin/tee -a /etc/hosts >/dev/null
        if ! grep -q "[[:space:]]$HOSTNAME_PIN\\$" /etc/hosts; then
          echo "[mdm-enroll] ERROR: could not pin $HOSTNAME_PIN in /etc/hosts" >&2; exit 1
        fi
        echo "[mdm-enroll] pinned $HOSTNAME_PIN -> $GW"

        # Nothing else happens here. CA trust is established during the PRECEDING minimal-agent boot,
        # in the guest's admin trust domain, because neither route available from this boot works:
        # `security add-trusted-cert` raises a SecurityAgent sheet that a root CLI with no GUI app can
        # never focus, and a root delivered by a MANUALLY installed configuration profile has not been
        # auto-trusted for TLS since macOS Ventura (it installs with SSL trust "no value specified").
        # The host verifies that trust — with macOS's own verdict — before it opens the enrollment
        # profile (`preflightEnrollment`), so this script only has to make the name resolve.
        echo "MDM_ENROLL_STAGED"
        """
    }

    /// The guest command that queues the **enrollment** profile, run only once the CA root profile is
    /// installed and trusted. Kept beside the enroll script so the staged path is defined in one place.
    public func openEnrollmentProfileCommand(guestShareDirectory: String) -> String {
        let path = "\(guestShareDirectory)/\(Artifact.profile)"
        return "open '\(path)' && echo MDM_ENROLL_PROFILE_OPENED"
    }

    /// Write the staged artifacts into `directory` (the virtiofs share). Returns the enroll-script path.
    @discardableResult
    public func stageArtifacts(
        into directory: URL, uuids: MDMEnrollmentProfile.UUIDs = MDMEnrollmentProfile.UUIDs()
    ) throws -> URL {
        try FileManager.default.createDirectory(
            at: directory, withIntermediateDirectories: true)
        // The `.cer` is staged for the host's pre-flight (`security verify-cert -p ssl` in the guest),
        // not for installation — the guest was made to trust it during the minimal-agent boot. The
        // standalone root PROFILE is deliberately NOT staged: installing it manually cannot establish
        // TLS trust on macOS Ventura or later, so staging it would only invite that dead end again.
        try caCertificateDER().write(to: directory.appendingPathComponent(Artifact.caCert))
        try enrollmentProfile(uuids: uuids).write(
            to: directory.appendingPathComponent(Artifact.profile))
        let scriptURL = directory.appendingPathComponent(Artifact.enrollScript)
        try enrollScript().write(to: scriptURL, atomically: true, encoding: .utf8)
        return scriptURL
    }

    // MARK: - Queue helpers

    /// Queue `InstallProfile` for each payload and return the command UUIDs for exact verification.
    @discardableResult
    public func queueInstallProfiles(_ profiles: [Data]) async -> [String] {
        let commands = profiles.map { MDMCommand.installProfile($0) }
        await queue.enqueue(commands)
        return commands.map(\.uuid)
    }

    // MARK: - Server (Darwin only)

    #if canImport(Network)
    /// Build the HTTPS server bound to this controller's CA/identity/queue.
    public func makeServer(
        requireValidSignature: Bool = false,
        debug: MDMDebugLog? = nil,
        log: @escaping NucleicMDMServer.LogSink = { _ in }
    ) -> NucleicMDMServer {
        NucleicMDMServer(
            config: config, ca: ca, deviceIdentity: deviceIdentity, queue: queue,
            requireValidSignature: requireValidSignature, debug: debug, log: log)
    }
    #endif
}
