import Dynamic
import Foundation
import Virtualization
import VPhoneCore

/// Minimal VM for booting a vphone (virtual iPhone) in DFU mode.
@MainActor
class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
    let virtualMachine: VZVirtualMachine
    /// ECID hex string resolved from machineIdentifier (e.g. "0x0012345678ABCDEF").
    let ecidHex: String?
    /// Read handle for VM serial output.
    private var serialOutputReadHandle: FileHandle?
    /// Synthetic battery source for runtime charge/connectivity updates.
    private var batterySource: AnyObject?

    public enum Variant: String, Sendable {
        case less
        case regular
        case dev
        case jb
        case exp
    }
    
    struct Options {
        var configURL: URL
        var romURL: URL?
        var nvramURL: URL
        var diskURL: URL
        var cpuCount: Int = 8
        var memorySize: UInt64 = 8 * 1024 * 1024 * 1024
        var sepStorageURL: URL
        var sepRomURL: URL?
        var screenWidth: Int = 1290
        var screenHeight: Int = 2796
        var screenPPI: Int = 460
        var screenScale: Double = 3.0
        var kernelDebugPort: Int?
        var variant: Variant
        var noVphoned: Bool
    }

    private struct DeviceIdentity {
        let cpidHex: String
        let ecidHex: String
        let udid: String
    }

    init(options: Options) throws {
        // --- Hardware model (PV=3) ---
        let hwModel = try VPhoneHardware.createModel()
        print("[vphone] PV=3 hardware model: isSupported = true")

        // --- Load or create machineIdentifier from manifest ---
        let machineIdentifier: VZMacMachineIdentifier
        var manifest = try VPhoneVirtualMachineManifest.load(from: options.configURL)

        if manifest.machineIdentifier.isEmpty {
            // Create new machineIdentifier and save to manifest
            let newID = VZMacMachineIdentifier()
            machineIdentifier = newID

            // Update manifest with new machineIdentifier
            manifest = VPhoneVirtualMachineManifest(
                platformType: manifest.platformType,
                platformFusing: manifest.platformFusing,
                machineIdentifier: newID.dataRepresentation,
                cpuCount: manifest.cpuCount,
                memorySize: manifest.memorySize,
                screenConfig: manifest.screenConfig,
                networkConfig: manifest.networkConfig,
                diskImage: manifest.diskImage,
                nvramStorage: manifest.nvramStorage,
                romImages: manifest.romImages,
                sepStorage: manifest.sepStorage
            )
            try manifest.write(to: options.configURL)

            print("[vphone] Created new machineIdentifier -> saved to config.plist")
        } else if let savedID = VZMacMachineIdentifier(dataRepresentation: manifest.machineIdentifier) {
            machineIdentifier = savedID
            print("[vphone] Loaded machineIdentifier from config.plist (ECID stable)")
        } else {
            // Invalid data in manifest, create new
            let newID = VZMacMachineIdentifier()
            machineIdentifier = newID

            manifest = VPhoneVirtualMachineManifest(
                platformType: manifest.platformType,
                platformFusing: manifest.platformFusing,
                machineIdentifier: newID.dataRepresentation,
                cpuCount: manifest.cpuCount,
                memorySize: manifest.memorySize,
                screenConfig: manifest.screenConfig,
                networkConfig: manifest.networkConfig,
                diskImage: manifest.diskImage,
                nvramStorage: manifest.nvramStorage,
                romImages: manifest.romImages,
                sepStorage: manifest.sepStorage
            )
            try manifest.write(to: options.configURL)

            print("[vphone] Invalid machineIdentifier in config.plist, created new")
        }

        // --- Platform ---
        let platform = VZMacPlatformConfiguration()
        platform.machineIdentifier = machineIdentifier

        if let identity = Self.resolveDeviceIdentity(machineIdentifier: machineIdentifier) {
            ecidHex = identity.ecidHex
            print("[vphone] ECID: \(ecidHex!)")
            print("[vphone] Predicted UDID: \(identity.udid)")
            let outputURL = options.configURL.deletingLastPathComponent().appendingPathComponent(
                "udid-prediction.txt"
            )
            do {
                try Self.writeUDIDPrediction(identity: identity, to: outputURL)
                print("[vphone] Wrote UDID prediction: \(outputURL.path)")
            } catch {
                print("[vphone] Warning: failed to write udid-prediction.txt: \(error)")
            }
        } else {
            ecidHex = nil
            print("[vphone] Warning: failed to resolve ECID from machineIdentifier")
        }

        let auxStorage = try VZMacAuxiliaryStorage(
            creatingStorageAt: options.nvramURL,
            hardwareModel: hwModel,
            options: .allowOverwrite
        )
        platform.auxiliaryStorage = auxStorage
        platform.hardwareModel = hwModel

        // Set NVRAM boot-args to enable serial output
        let bootArgs = "serial=3 debug=0x104c04"
        if let bootArgsData = bootArgs.data(using: .utf8) {
            let ok =
                Dynamic(auxStorage)
                    ._setDataValue(bootArgsData, forNVRAMVariableNamed: "boot-args", error: nil)
                    .asBool ?? false
            if ok { print("[vphone] NVRAM boot-args: \(bootArgs)") }
        }

        // --- Boot loader with custom ROM ---
        let bootloader = VZMacOSBootLoader()
        if let romURL = options.romURL {
            Dynamic(bootloader)._setROMURL(romURL)
        }

        // --- VM Configuration ---
        let config = VZVirtualMachineConfiguration()
        config.bootLoader = bootloader
        config.platform = platform
        config.cpuCount = max(options.cpuCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount)
        config.memorySize = max(
            options.memorySize, VZVirtualMachineConfiguration.minimumAllowedMemorySize
        )

        // Display
        let gfx = VZMacGraphicsDeviceConfiguration()
        gfx.displays = [
            VZMacGraphicsDisplayConfiguration(
                widthInPixels: options.screenWidth, heightInPixels: options.screenHeight,
                pixelsPerInch: options.screenPPI
            ),
        ]
        config.graphicsDevices = [gfx]

        // Audio
        let afg = VZVirtioSoundDeviceConfiguration()
        let inputAudioStreamConfiguration = VZVirtioSoundDeviceInputStreamConfiguration()
        let outputAudioStreamConfiguration = VZVirtioSoundDeviceOutputStreamConfiguration()
        inputAudioStreamConfiguration.source = VZHostAudioInputStreamSource()
        outputAudioStreamConfiguration.sink = VZHostAudioOutputStreamSink()
        afg.streams = [inputAudioStreamConfiguration, outputAudioStreamConfiguration]
        config.audioDevices = [afg]

        // Storage
        guard FileManager.default.fileExists(atPath: options.diskURL.path) else {
            throw VPhoneError.diskNotFound(options.diskURL.path)
        }
        let attachment = try VZDiskImageStorageDeviceAttachment(url: options.diskURL, readOnly: false)
        config.storageDevices = [VZVirtioBlockDeviceConfiguration(attachment: attachment)]

        // Network (mode + MAC from the bundle manifest; nat/bridged/none)
        if let net = try VPhoneNetworking.makeNetworkDevice(manifest.networkConfig) {
            config.networkDevices = [net]
        } else {
            config.networkDevices = []
        }

        // Serial port (PL011 UART - pipes for input/output with boot detection)
        if let serialPort = Dynamic._VZPL011SerialPortConfiguration().asObject
            as? VZSerialPortConfiguration
        {
            let inputPipe = Pipe()
            let outputPipe = Pipe()

            serialPort.attachment = VZFileHandleSerialPortAttachment(
                fileHandleForReading: inputPipe.fileHandleForReading,
                fileHandleForWriting: outputPipe.fileHandleForWriting
            )

            // Forward host stdin -> VM serial input
            let writeHandle = inputPipe.fileHandleForWriting
            let stdinFD = FileHandle.standardInput.fileDescriptor
            DispatchQueue.global(qos: .userInteractive).async {
                var buf = [UInt8](repeating: 0, count: 4096)
                while true {
                    let n = read(stdinFD, &buf, buf.count)
                    if n <= 0 { break }
                    writeHandle.write(Data(buf[..<n]))
                }
            }

            serialOutputReadHandle = outputPipe.fileHandleForReading

            config.serialPorts = [serialPort]
            print("[vphone] PL011 serial port attached (interactive)")
        }
        
        if let obj1 = Dynamic._VZMacVideoToolboxDeviceConfiguration().asObject,
           let obj2 = Dynamic._VZMacNeuralEngineDeviceConfiguration().asObject,
           let obj3 = Dynamic._VZMacScalerAcceleratorDeviceConfiguration().asObject
        {
            Dynamic(config)._setAcceleratorDevices([obj1,obj2,obj3])
            print("[vphone] Accelerator devices configured")
        }

        // Multi-touch (USB touch screen)
        if let obj = Dynamic._VZUSBTouchScreenConfiguration().asObject {
            Dynamic(config)._setMultiTouchDevices([obj])
            print("[vphone] USB touch screen configured")
        }
        
        let obj = VZVirtioEntropyDeviceConfiguration()
        config.entropyDevices = [obj]
        print("[vphone] Entropy device configured")

        config.keyboards = [VZUSBKeyboardConfiguration()]

        if !options.noVphoned {
            // Vsock (host <-> guest control channel, no IP/TCP involved)
            config.socketDevices = [VZVirtioSocketDeviceConfiguration()]
        }

        // Power source (synthetic battery - guest sees full charge, charging)
        let source = Dynamic._VZMacSyntheticBatterySource()
        source.setCharge(100.0)
        source.setConnectivity(1) // 1=charging, 2=disconnected
        let batteryConfig = Dynamic._VZMacBatteryPowerSourceDeviceConfiguration()
        batteryConfig.setSource(source.asObject)
        if let batteryObj = batteryConfig.asObject {
            Dynamic(config)._setPowerSourceDevices([batteryObj])
            batterySource = source.asObject as AnyObject?
            print("[vphone] Synthetic battery configured (100%, charging)")
        }

        // Kernel GDB debug stub (auto-assigned by default; fixed when explicitly requested)
        if let kernelDebugPort = options.kernelDebugPort {
            guard (6000 ... 65535).contains(kernelDebugPort) else {
                throw VPhoneError.invalidKernelDebugPort(kernelDebugPort)
            }
            if let kernelDebugStub = Dynamic._VZGDBDebugStubConfiguration(port: kernelDebugPort)
                .asObject
            {
                Dynamic(config)._setDebugStub(kernelDebugStub)
                print("[vphone] Kernel GDB debug stub: tcp://127.0.0.1:\(kernelDebugPort)")
            } else {
                Dynamic(config)._setDebugStub(Dynamic._VZGDBDebugStubConfiguration().asObject)
                print("[vphone] Kernel GDB debug stub enabled (system-assigned port)")
            }
        } else {
            Dynamic(config)._setDebugStub(Dynamic._VZGDBDebugStubConfiguration().asObject)
            print("[vphone] Kernel GDB debug stub enabled (system-assigned port)")
        }

        // Coprocessors
        let sepConfig = Dynamic._VZSEPCoprocessorConfiguration(storageURL: options.sepStorageURL)
        if let sepRomURL = options.sepRomURL {
            sepConfig.setRomBinaryURL(sepRomURL)
        }
        sepConfig.setDebugStub(Dynamic._VZGDBDebugStubConfiguration().asObject)
        if let sepObj = sepConfig.asObject {
            Dynamic(config)._setCoprocessors([sepObj])
            print("[vphone] SEP coprocessor enabled (storage: \(options.sepStorageURL.path))")
        }

        // Validate
        try config.validate()
        print("[vphone] Configuration validated")

        virtualMachine = VZVirtualMachine(configuration: config)
        super.init()
        virtualMachine.delegate = self

        // Forward VM serial output -> host stdout
        if let readHandle = serialOutputReadHandle {
            readHandle.readabilityHandler = { handle in
                let data = handle.availableData
                if data.isEmpty { return }
                FileHandle.standardOutput.write(data)
            }
        }
    }

    private static func resolveDeviceIdentity(machineIdentifier: VZMacMachineIdentifier)
        -> DeviceIdentity?
    {
        let ecidValue: UInt64? = if let ecid = Dynamic(machineIdentifier)._ECID.asUInt64 {
            ecid
        } else if let ecidNumber = Dynamic(machineIdentifier)._ECID.asObject as? NSNumber {
            ecidNumber.uint64Value
        } else {
            nil
        }

        guard let ecidValue else { return nil }

        let cpidHex = String(format: "%08X", VPhoneHardware.udidChipID)
        let ecidHex = String(format: "%016llX", ecidValue)
        let udid = "\(cpidHex)-\(ecidHex)"
        return DeviceIdentity(cpidHex: cpidHex, ecidHex: ecidHex, udid: udid)
    }

    private static func writeUDIDPrediction(identity: DeviceIdentity, to outputURL: URL) throws {
        let content = """
        UDID=\(identity.udid)
        CPID=0x\(identity.cpidHex)
        ECID=0x\(identity.ecidHex)
        MACHINE_IDENTIFIER=config.plist
        """
        try content.write(to: outputURL, atomically: true, encoding: .utf8)
    }

    // MARK: - Battery

    /// Update the synthetic battery charge and connectivity at runtime.
    /// - Parameters:
    ///   - charge: Battery percentage (0.0–100.0).
    ///   - connectivity: 1 = charging, 2 = disconnected.
    func setBattery(charge: Double, connectivity: Int) {
        guard let source = batterySource else { return }
        Dynamic(source).setCharge(charge)
        Dynamic(source).setConnectivity(connectivity)
    }

    // MARK: - Start

    @MainActor
    func start(forceDFU: Bool) async throws {
        let opts = VZMacOSVirtualMachineStartOptions()
        Dynamic(opts)._setForceDFU(forceDFU)
        Dynamic(opts)._setStopInIBootStage1(false)
        Dynamic(opts)._setStopInIBootStage2(false)
        print("[vphone] Starting\(forceDFU ? " DFU" : "")...")
        nonisolated(unsafe) let vm = virtualMachine
        try await vm.start(options: opts)
        if forceDFU {
            print("[vphone] VM started in DFU mode - connect with irecovery")
        } else {
            print("[vphone] VM started - booting normally")
        }

        // Print auto-assigned debug stub port after VM starts (private API, macOS 26+ only)
        if ProcessInfo.processInfo.operatingSystemVersion.majorVersion >= 26 {
            if let debugStub = Dynamic(vm)._configuration._debugStub.asAnyObject {
                if let port = Dynamic(debugStub).port.asInt, port > 0 {
                    print("[vphone] Kernel GDB debug stub listening on tcp://127.0.0.1:\(port)")
                }
            }
        } else {
            print("[vphone] Kernel GDB debug stub port query requires macOS 26+, skipped")
        }
    }

    // MARK: - Delegate

    nonisolated func guestDidStop(_: VZVirtualMachine) {
        print("[vphone] Guest stopped")
        exit(EXIT_SUCCESS)
    }

    nonisolated func virtualMachine(_: VZVirtualMachine, didStopWithError error: Error) {
        print("[vphone] Stopped with error: \(error)")
        exit(EXIT_FAILURE)
    }

    nonisolated func virtualMachine(
        _: VZVirtualMachine, networkDevice _: VZNetworkDevice,
        attachmentWasDisconnectedWithError error: Error
    ) {
        print("[vphone] Network error: \(error)")
    }
}
