import Foundation
import CoreBluetooth
import ExternalAccessory
import Network

/**
 * Print Connection Protocol
 * Manages ephemeral connections for printing with retry capability
 */
protocol PrintConnection {
    /**
     * Check if connection is currently alive
     */
    func isAlive() -> Bool

    /**
     * Connect with retry mechanism
     */
    func connectWithRetry(maxRetries: Int, delayMs: Int) async throws -> Bool

    /**
     * Send data to printer
     */
    func send(data: Data) async throws

    /**
     * Close the connection
     */
    func close()

    /**
     * Get printer address
     */
    func getAddress() -> String
}

/**
 * Bluetooth MFi Print Connection
 */
class BluetoothPrintConnection: PrintConnection {
    private let identifier: String
    private var session: EASession?
    private var accessory: EAAccessory?

    init(identifier: String) {
        self.identifier = identifier
    }

    func isAlive() -> Bool {
        guard let session = session,
              let outputStream = session.outputStream else {
            return false
        }
        return outputStream.streamStatus == .open
    }

    func connectWithRetry(maxRetries: Int = 2, delayMs: Int = 500) async throws -> Bool {
        print("[Native:iOS] BluetoothPrintConnection.connect - START: \(identifier) with \(maxRetries) retries")

        for attempt in 0..<maxRetries {
            do {
                // Close any existing session
                close()

                // Find accessory
                let accessories = EAAccessoryManager.shared().connectedAccessories
                guard let accessory = accessories.first(where: {
                    $0.serialNumber == identifier ||
                    $0.name == identifier ||
                    "\($0.manufacturer)-\($0.modelNumber)" == identifier
                }) else {
                    throw PrintError.deviceNotFound("bt:\(identifier)")
                }

                self.accessory = accessory

                // Get supported protocol
                let supportedProtocols = ["com.printer.protocol", "jp.star-m.starpro"]
                guard let protocolString = accessory.protocolStrings.first(where: {
                    supportedProtocols.contains($0)
                }) else {
                    throw PrintError.bluetoothNotSupported()
                }

                // Create session
                guard let session = EASession(accessory: accessory, forProtocol: protocolString) else {
                    throw PrintError.connectionFailed("bt:\(identifier)", reason: "Cannot create session")
                }

                self.session = session

                // Open streams
                session.outputStream?.open()
                session.inputStream?.open()

                // Wait for stream to open
                var retries = 0
                while session.outputStream?.streamStatus != .open && retries < 20 {
                    try await Task.sleep(nanoseconds: 50_000_000) // 50ms
                    retries += 1
                }

                if session.outputStream?.streamStatus == .open {
                    print("[Native:iOS] BluetoothPrintConnection.connect - SUCCESS: Attempt \(attempt + 1)")
                    return true
                }

                throw PrintError.connectionFailed("bt:\(identifier)", reason: "Stream failed to open")

            } catch {
                print("[Native:iOS] BluetoothPrintConnection.connect - WARN: Attempt \(attempt + 1) failed: \(error)")
                if attempt < maxRetries - 1 {
                    try await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000)
                }
            }
        }

        print("[Native:iOS] BluetoothPrintConnection.connect - ERROR: Failed after \(maxRetries) attempts")
        return false
    }

    func send(data: Data) async throws {
        guard isAlive(),
              let outputStream = session?.outputStream else {
            throw PrintError.sendFailed(getAddress(), reason: "Not connected")
        }

        let bytesWritten = data.withUnsafeBytes {
            outputStream.write($0.bindMemory(to: UInt8.self).baseAddress!, maxLength: data.count)
        }

        if bytesWritten < 0 {
            close()
            throw PrintError.sendFailed(getAddress(), reason: "Write failed")
        }

        print("[Native:iOS] BluetoothPrintConnection.send - SUCCESS: Sent \(bytesWritten) bytes to \(identifier)")
    }

    func close() {
        session?.inputStream?.close()
        session?.outputStream?.close()
        session = nil
        print("[Native:iOS] BluetoothPrintConnection.close - SUCCESS: Closed connection to \(identifier)")
    }

    func getAddress() -> String {
        return "bt:\(identifier)"
    }
}

/**
 * LAN/TCP Print Connection
 */
class LANPrintConnection: PrintConnection {
    private let host: String
    private let port: Int
    private var connection: NWConnection?

    init(host: String, port: Int = 9100) {
        self.host = host
        self.port = port
    }

    func isAlive() -> Bool {
        guard let connection = connection else { return false }
        return connection.state == .ready
    }

    func connectWithRetry(maxRetries: Int = 2, delayMs: Int = 500) async throws -> Bool {
        print("[Native:iOS] LANPrintConnection.connect - START: \(host):\(port) with \(maxRetries) retries")

        for attempt in 0..<maxRetries {
            do {
                // Close any existing connection
                close()

                // Create new connection
                let newConnection = NWConnection(
                    host: NWEndpoint.Host(host),
                    port: NWEndpoint.Port(integerLiteral: NWEndpoint.Port.IntegerLiteralType(port)),
                    using: .tcp
                )

                // Setup connection with async/await
                let connected = await withCheckedContinuation { continuation in
                    var hasCompleted = false
                    let timeout = DispatchWorkItem {
                        if !hasCompleted {
                            hasCompleted = true
                            newConnection.cancel()
                            continuation.resume(returning: false)
                        }
                    }

                    DispatchQueue.global().asyncAfter(deadline: .now() + 5, execute: timeout)

                    newConnection.stateUpdateHandler = { state in
                        switch state {
                        case .ready:
                            if !hasCompleted {
                                hasCompleted = true
                                timeout.cancel()
                                continuation.resume(returning: true)
                            }
                        case .failed:
                            if !hasCompleted {
                                hasCompleted = true
                                timeout.cancel()
                                continuation.resume(returning: false)
                            }
                        default:
                            break
                        }
                    }

                    newConnection.start(queue: .global())
                }

                if connected {
                    self.connection = newConnection
                    print("[Native:iOS] LANPrintConnection.connect - SUCCESS: Attempt \(attempt + 1)")
                    return true
                }

                throw PrintError.connectionFailed("lan:\(host):\(port)")

            } catch {
                print("[Native:iOS] LANPrintConnection.connect - WARN: Attempt \(attempt + 1) failed: \(error)")
                if attempt < maxRetries - 1 {
                    try await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000)
                }
            }
        }

        print("[Native:iOS] LANPrintConnection.connect - ERROR: Failed after \(maxRetries) attempts")
        return false
    }

    func send(data: Data) async throws {
        guard let connection = connection, isAlive() else {
            throw PrintError.sendFailed(getAddress(), reason: "Not connected")
        }

        let sent = await withCheckedContinuation { continuation in
            connection.send(content: data, completion: .contentProcessed { error in
                continuation.resume(returning: error == nil)
            })
        }

        if !sent {
            close()
            throw PrintError.sendFailed(getAddress(), reason: "Send failed")
        }

        print("[Native:iOS] LANPrintConnection.send - SUCCESS: Sent \(data.count) bytes to \(host):\(port)")
    }

    func close() {
        connection?.cancel()
        connection = nil
        print("[Native:iOS] LANPrintConnection.close - SUCCESS: Closed connection to \(host):\(port)")
    }

    func getAddress() -> String {
        return "lan:\(host):\(port)"
    }
}

/**
 * BLE Print Connection
 * Handles BLE printer connections using CoreBluetooth
 */
class BLEPrintConnection: NSObject, PrintConnection {
    private let deviceUUID: String
    private var centralManager: CBCentralManager?
    private var peripheral: CBPeripheral?
    private var writeCharacteristic: CBCharacteristic?
    private var isConnected = false
    private var connectionContinuation: CheckedContinuation<Bool, Never>?
    private var writeContinuation: CheckedContinuation<Bool, Never>?

    // Common BLE printer service UUIDs
    private static let printerServiceUUIDs = [
        CBUUID(string: "49535343-FE7D-4AE5-8FA9-9FAFD205E455"), // Custom printer service
        CBUUID(string: "18F0"),                                  // Generic printer service
        CBUUID(string: "E7810A71-73AE-499D-8C15-FAA9AEF0C3F2")  // Another common printer UUID
    ]

    init(uuid: String) {
        self.deviceUUID = uuid
        super.init()
    }

    func isAlive() -> Bool {
        return isConnected && peripheral?.state == .connected
    }

    func connectWithRetry(maxRetries: Int = 2, delayMs: Int = 500) async throws -> Bool {
        print("[Native:iOS] BLEPrintConnection.connect - START: \(deviceUUID) with \(maxRetries) retries")

        for attempt in 0..<maxRetries {
            do {
                // Close any existing connection
                close()

                // Initialize central manager if needed
                if centralManager == nil {
                    centralManager = CBCentralManager(delegate: self, queue: nil)

                    // Wait for central manager to be ready
                    var retries = 0
                    while centralManager?.state != .poweredOn && retries < 20 {
                        try await Task.sleep(nanoseconds: 100_000_000) // 100ms
                        retries += 1
                    }

                    guard centralManager?.state == .poweredOn else {
                        throw PrintError.bluetoothDisabled()
                    }
                }

                // Try to find and connect to peripheral
                let connected = await withCheckedContinuation { continuation in
                    self.connectionContinuation = continuation

                    // Try to retrieve peripheral by UUID
                    if let uuid = UUID(uuidString: deviceUUID) {
                        let peripherals = centralManager?.retrievePeripherals(withIdentifiers: [uuid])
                        if let peripheral = peripherals?.first {
                            self.peripheral = peripheral
                            peripheral.delegate = self
                            centralManager?.connect(peripheral, options: nil)

                            // Set timeout for connection
                            Task {
                                try? await Task.sleep(nanoseconds: 5_000_000_000) // 5 seconds timeout
                                if self.connectionContinuation != nil {
                                    self.connectionContinuation?.resume(returning: false)
                                    self.connectionContinuation = nil
                                }
                            }
                            return
                        }
                    }

                    // If not found by UUID, scan for it
                    centralManager?.scanForPeripherals(
                        withServices: Self.printerServiceUUIDs,
                        options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
                    )

                    // Stop scan after timeout
                    Task {
                        try? await Task.sleep(nanoseconds: 5_000_000_000) // 5 seconds
                        self.centralManager?.stopScan()
                        if self.connectionContinuation != nil {
                            self.connectionContinuation?.resume(returning: false)
                            self.connectionContinuation = nil
                        }
                    }
                }

                if connected {
                    print("[Native:iOS] BLEPrintConnection.connect - SUCCESS: Attempt \(attempt + 1)")
                    return true
                }

                throw PrintError.connectionFailed("ble:\(deviceUUID)")

            } catch {
                print("[Native:iOS] BLEPrintConnection.connect - WARN: Attempt \(attempt + 1) failed: \(error)")
                if attempt < maxRetries - 1 {
                    try await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000)
                }
            }
        }

        print("[Native:iOS] BLEPrintConnection.connect - ERROR: Failed after \(maxRetries) attempts")
        return false
    }

    func send(data: Data) async throws {
        guard isAlive(),
              let peripheral = peripheral,
              let characteristic = writeCharacteristic else {
            throw PrintError.sendFailed(getAddress(), reason: "Not connected or characteristic not available")
        }

        // ========== SOLUTION 1 OPTIMIZATION ==========
        // Optimized chunking based on zywell-thermal-printer proven parameters
        // OLD: chunkSize=185, delay=50ms (slow: ~6.25s for 23KB)
        // NEW: chunkSize=150, delay=15ms (safe & fast: ~2.3s for 23KB, exact zywell match)
        // NOTE: 250 bytes was too large, printer didn't print (buffer overflow?)
        let chunkSize = 150  // Match zywell exactly (proven safe for all printers)
        let delayMs = 15     // Match zywell exactly (15-20ms range, using 15ms)

        var offset = 0
        let totalChunks = (data.count + chunkSize - 1) / chunkSize
        let startTime = Date()

        print("[Native:iOS] BLEPrintConnection.send - OPTIMIZED START: total=\(data.count) bytes, chunks=\(totalChunks), chunkSize=\(chunkSize), delay=\(delayMs)ms")

        while offset < data.count {
            let endIndex = min(offset + chunkSize, data.count)
            let chunk = data.subdata(in: offset..<endIndex)
            let chunkIndex = (offset / chunkSize) + 1

            // Determine write type based on characteristic properties
            let writeType: CBCharacteristicWriteType =
                characteristic.properties.contains(.writeWithoutResponse) ? .withoutResponse : .withResponse

            if writeType == .withResponse {
                // Wait for write confirmation
                let success = await withCheckedContinuation { continuation in
                    self.writeContinuation = continuation
                    peripheral.writeValue(chunk, for: characteristic, type: writeType)

                    // Set timeout for write
                    Task {
                        try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second timeout
                        if self.writeContinuation != nil {
                            self.writeContinuation?.resume(returning: false)
                            self.writeContinuation = nil
                        }
                    }
                }

                if !success {
                    throw PrintError.sendFailed(getAddress(), reason: "Failed to write BLE characteristic")
                }
            } else {
                // Write without response (preferred for speed)
                peripheral.writeValue(chunk, for: characteristic, type: writeType)
            }

            offset = endIndex

            // Log progress every 20 chunks
            if chunkIndex % 20 == 0 || chunkIndex == totalChunks {
                let elapsed = Date().timeIntervalSince(startTime)
                print("[Native:iOS] BLEPrintConnection.send - PROGRESS: chunk \(chunkIndex)/\(totalChunks), elapsed=\(String(format: "%.2f", elapsed))s")
            }

            // Optimized delay between chunks (skip delay for last chunk)
            if offset < data.count {
                try await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000) // 12ms
            }
        }

        let totalTime = Date().timeIntervalSince(startTime)
        print("[Native:iOS] BLEPrintConnection.send - OPTIMIZED SUCCESS: Sent \(data.count) bytes in \(String(format: "%.2f", totalTime))s (\(totalChunks) chunks) to \(deviceUUID)")
    }

    func close() {
        if let peripheral = peripheral {
            centralManager?.cancelPeripheralConnection(peripheral)
        }
        peripheral = nil
        writeCharacteristic = nil
        isConnected = false
        connectionContinuation = nil
        writeContinuation = nil
        print("[Native:iOS] BLEPrintConnection.close - SUCCESS: Closed connection to \(deviceUUID)")
    }

    func getAddress() -> String {
        return "ble:\(deviceUUID)"
    }
}

// MARK: - CBCentralManagerDelegate
extension BLEPrintConnection: CBCentralManagerDelegate {
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        print("[Native:iOS] BLEPrintConnection.centralManagerDidUpdateState - PROCESS: State=\(central.state.rawValue)")
    }

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral,
                        advertisementData: [String: Any], rssi RSSI: NSNumber) {
        print("[Native:iOS] BLEPrintConnection.didDiscover - PROCESS: Found \(peripheral.identifier.uuidString)")

        // Check if this is the device we're looking for
        if peripheral.identifier.uuidString == deviceUUID {
            self.peripheral = peripheral
            peripheral.delegate = self
            central.stopScan()
            central.connect(peripheral, options: nil)
        }
    }

    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print("[Native:iOS] BLEPrintConnection.didConnect - PROCESS: Connected, discovering services")
        peripheral.discoverServices(Self.printerServiceUUIDs.isEmpty ? nil : Self.printerServiceUUIDs)
    }

    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        print("[Native:iOS] BLEPrintConnection.didFailToConnect - ERROR: \(error?.localizedDescription ?? "Unknown error")")
        isConnected = false
        connectionContinuation?.resume(returning: false)
        connectionContinuation = nil
    }

    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        print("[Native:iOS] BLEPrintConnection.didDisconnect - PROCESS: Disconnected")
        isConnected = false
        writeCharacteristic = nil
    }
}

// MARK: - CBPeripheralDelegate
extension BLEPrintConnection: CBPeripheralDelegate {
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard error == nil else {
            print("[Native:iOS] BLEPrintConnection.didDiscoverServices - ERROR: \(error!)")
            connectionContinuation?.resume(returning: false)
            connectionContinuation = nil
            return
        }

        // Look for write characteristic in discovered services
        for service in peripheral.services ?? [] {
            peripheral.discoverCharacteristics(nil, for: service)
        }
    }

    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        guard error == nil else {
            print("[Native:iOS] BLEPrintConnection.didDiscoverCharacteristics - ERROR: \(error!)")
            return
        }

        // Find write characteristic
        for characteristic in service.characteristics ?? [] {
            if characteristic.properties.contains(.write) ||
               characteristic.properties.contains(.writeWithoutResponse) {
                writeCharacteristic = characteristic
                isConnected = true
                print("[Native:iOS] BLEPrintConnection.didDiscoverCharacteristics - SUCCESS: Found write characteristic: \(characteristic.uuid)")
                connectionContinuation?.resume(returning: true)
                connectionContinuation = nil
                return
            }
        }
    }

    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
        if let error = error {
            print("[Native:iOS] BLEPrintConnection.didWriteValue - ERROR: \(error)")
            writeContinuation?.resume(returning: false)
        } else {
            writeContinuation?.resume(returning: true)
        }
        writeContinuation = nil
    }
}

/**
 * Factory to create appropriate PrintConnection
 */
class PrintConnectionFactory {
    static func create(address: String) throws -> PrintConnection {
        let parts = address.split(separator: ":", maxSplits: 1)
        guard parts.count == 2 else {
            throw PrintError.invalidAddress(address)
        }

        let type = String(parts[0])
        let target = String(parts[1])

        switch type {
        case "bt":
            return BluetoothPrintConnection(identifier: target)

        case "lan":
            let lanParts = target.split(separator: ":")
            let host = String(lanParts[0])
            let port = lanParts.count > 1 ? Int(lanParts[1]) ?? 9100 : 9100
            return LANPrintConnection(host: host, port: port)

        case "ble":
            return BLEPrintConnection(uuid: target)

        default:
            throw PrintError.invalidAddress(address)
        }
    }
}