import Foundation
import Network

final class NetworkLayer {
    private let jid: JID
    private(set) var host: String = ""
    private(set) var port: Int = 0
    var received: (Data) -> Void
    var connected: (Bool, Bool) -> Void // result flag, tls flag
    var errorHappens: (ClientError) -> Void

    private var rawConnection: Connection?
    private var secureConnection: Connection?
    private var bridge: NWListener?
    private var bridgeConnection: Connection?
    private var queue: DispatchQueue
    private var bridgeSocketPath: URL

    init(forJid: JID) {
        jid = forJid
        queue = DispatchQueue(label: "snikket.network.queue.\(jid.uStr)")
        bridgeSocketPath = URL(fileURLWithPath: "/tmp/tls_bridge_listener\(jid.uStr).sock")
        try? FileManager.default.removeItem(at: bridgeSocketPath)
        received = { _ in }
        connected = { _, _ in }
        errorHappens = { _ in }
    }

    deinit {
        rawConnection?.cancel()
        secureConnection?.cancel()
        bridgeConnection?.cancel()
        bridge?.cancel()
    }

    func connectTo(host: String, port: Int) {
        self.host = host
        self.port = port
        rawConnection = Connection(
            type: .raw,
            queue: queue,
            onRecv: { [weak self] data in
                if let bConn = self?.bridgeConnection {
                    bConn.write(data: data)
                } else {
                    self?.received(data)
                }
            },
            onReady: { [weak self] ready, error in
                if let err = error {
                    self?.errorHappens(err)
                    return
                }
                self?.connected(ready, false)
            },
            host: host,
            port: port
        )
    }

    func write(data: Data) {
        if let secure = secureConnection {
            secure.write(data: data)
        } else {
            rawConnection?.write(data: data)
        }
    }

    func startTls() {
        do {
            try startBridge()
        } catch {
            secureConnection?.cancel()
            bridgeConnection?.cancel()
            bridge?.cancel()
            bridge = nil
            bridgeConnection = nil
            secureConnection = nil
            print(error.localizedDescription)
            errorHappens(.conInitError(error._code))
        }
    }
}

// MARK: Bridge
private extension NetworkLayer {
    func startBridge() throws {
        let params = NWParameters()
        params.defaultProtocolStack.transportProtocol = NWProtocolTCP.Options()
        params.requiredLocalEndpoint = NWEndpoint.unix(path: bridgeSocketPath.path)
        params.allowLocalEndpointReuse = false
        bridge = try NWListener(using: params)
        bridge?.newConnectionLimit = 1
        bridge?.stateUpdateHandler = { [weak self] state in
            guard let self else { return }
            switch state {
            case .ready:
                secureConnection = Connection(
                    type: .secure,
                    queue: self.queue,
                    onRecv: { data in
                        self.received(data)
                    },
                    onReady: { _, error in
                        if let err = error {
                            self.errorHappens(err)
                        } else {
                            self.connected(true, true)
                        }
                    },
                    socketPath: self.bridgeSocketPath,
                    domain: self.jid.domainPart
                )

            case .waiting(let error), .failed(let error):
                self.errorHappens(.conInitError(error._code))

            default:
                break
            }
        }
        bridge?.newConnectionHandler = { [weak self] connection in
            guard let self else { return }
            self.bridgeConnection = Connection(
                type: .bridge,
                queue: self.queue,
                onRecv: { data in
                    self.rawConnection?.write(data: data)
                },
                onReady: { _, _ in },
                nwConnection: connection
            )
        }
        bridge?.start(queue: queue)
    }
}
