/// UDP transport for EIS4 firmware communication.
/// Matches the desktop Cue protocol: raw SysEx frames over UDP port 5941.

import Foundation
import Network

@Observable
final class UDPManager: @unchecked Sendable {

    enum ConnectionState: String {
        case disconnected = "Disconnected"
        case connecting   = "Connecting..."
        case connected    = "Connected"
    }

    private static let defaultAddr = "192.168.4.1"
    private static let defaultPort: UInt16 = 5941
    private static let keepaliveInterval: TimeInterval = 5
    private static let timeout: TimeInterval = 10
    private static let addrKey = "eis4_udp_addr"

    var state: ConnectionState = .disconnected
    var address: String
    var port: UInt16

    /// Suppress keepalive timeout during blocking firmware operations (pH, clean, refs)
    var measuring: Bool = false

    private var connection: NWConnection?
    private let queue = DispatchQueue(label: "udp.eis4", qos: .userInitiated)
    private var onMessage: ((EisMessage) -> Void)?
    private var onDisconnect: (() -> Void)?
    private var keepaliveTimer: Timer?
    private var timeoutTimer: Timer?
    private var lastReceived: Date = .distantPast

    init() {
        let saved = UserDefaults.standard.string(forKey: Self.addrKey) ?? ""
        if saved.isEmpty {
            address = Self.defaultAddr
            port = Self.defaultPort
        } else {
            let parts = saved.split(separator: ":")
            address = String(parts[0])
            port = parts.count > 1 ? UInt16(parts[1]) ?? Self.defaultPort : Self.defaultPort
        }
    }

    func setMessageHandler(_ handler: @escaping (EisMessage) -> Void) {
        onMessage = handler
    }

    func setDisconnectHandler(_ handler: @escaping () -> Void) {
        onDisconnect = handler
    }

    // MARK: - Connection

    func connect() {
        disconnect()
        state = .connecting

        let addrStr = "\(address):\(port)"
        UserDefaults.standard.set(addrStr, forKey: Self.addrKey)

        let host = NWEndpoint.Host(address)
        let nwPort = NWEndpoint.Port(rawValue: port) ?? NWEndpoint.Port(rawValue: Self.defaultPort)!
        let params = NWParameters.udp
        params.allowLocalEndpointReuse = true

        let conn = NWConnection(host: host, port: nwPort, using: params)
        connection = conn

        conn.stateUpdateHandler = { [weak self] newState in
            DispatchQueue.main.async {
                self?.handleStateChange(newState)
            }
        }

        conn.start(queue: queue)
    }

    func disconnect() {
        stopTimers()
        connection?.cancel()
        connection = nil
        measuring = false
        state = .disconnected
        onDisconnect?()
    }

    func send(_ sysex: [UInt8]) {
        guard let conn = connection else { return }
        let data = Data(sysex)
        conn.send(content: data, completion: .contentProcessed({ _ in }))
    }

    // MARK: - State handling

    private func handleStateChange(_ newState: NWConnection.State) {
        switch newState {
        case .ready:
            state = .connected
            lastReceived = Date()
            send(buildSysexGetTemp())
            send(buildSysexGetConfig())
            send(buildSysexGetCellK())
            send(buildSysexGetClFactor())
            send(buildSysexGetPhCal())
            startTimers()
            receiveLoop()

        case .failed, .cancelled:
            state = .disconnected
            stopTimers()

        case .waiting:
            state = .connecting

        default:
            break
        }
    }

    // MARK: - Receive

    private func receiveLoop() {
        guard let conn = connection else { return }
        conn.receiveMessage { [weak self] content, _, _, error in
            guard let self else { return }
            if error != nil {
                DispatchQueue.main.async {
                    if self.state == .connected {
                        self.state = .disconnected
                    }
                }
                return
            }
            guard let data = content, !data.isEmpty else {
                self.receiveLoop()
                return
            }

            DispatchQueue.main.async {
                self.lastReceived = Date()
            }

            let frames = Self.extractSysExFrames(data)
            for frame in frames {
                if let msg = parseSysex(frame) {
                    DispatchQueue.main.async {
                        self.onMessage?(msg)
                    }
                }
            }

            self.receiveLoop()
        }
    }

    /// Extract SysEx payloads from a UDP datagram.
    /// Scans for F0..F7 boundaries. Returns inner bytes (between F0 and F7, exclusive).
    static func extractSysExFrames(_ data: Data) -> [[UInt8]] {
        let bytes = Array(data)
        var frames: [[UInt8]] = []
        var i = 0
        while i < bytes.count {
            if bytes[i] == 0xF0 {
                if let end = bytes[i...].firstIndex(of: 0xF7) {
                    let payload = Array(bytes[(i + 1)..<end])
                    if !payload.isEmpty {
                        frames.append(payload)
                    }
                    i = end + 1
                    continue
                }
            }
            i += 1
        }
        return frames
    }

    // MARK: - Timers

    private func startTimers() {
        keepaliveTimer = Timer.scheduledTimer(withTimeInterval: Self.keepaliveInterval, repeats: true) { [weak self] _ in
            self?.send(buildSysexGetTemp())
        }

        timeoutTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in
            guard let self, self.state == .connected, !self.measuring else { return }
            if Date().timeIntervalSince(self.lastReceived) > Self.timeout {
                self.state = .disconnected
                self.stopTimers()
                self.connection?.cancel()
                self.connection = nil
                self.onDisconnect?()
            }
        }
    }

    private func stopTimers() {
        keepaliveTimer?.invalidate()
        keepaliveTimer = nil
        timeoutTimer?.invalidate()
        timeoutTimer = nil
    }
}
