// SPDX-License-Identifier: GPL-3.0-only

import Foundation
import HTPMobileCore

final class NativeEdge {
    private let condition = NSCondition()
    private var generation: UInt64 = 0
    private var handle: OpaquePointer?
    private var currentTransport = "unknown"
    private var activeOperations = 0
    private var destroying = false
    private var packetsSent: UInt64 = 0
    private var sendFailures: UInt64 = 0
    private var packetsReceived: UInt64 = 0
    private var packetsInjected: UInt64 = 0
    private var injectionFailures: UInt64 = 0
    private var peerResolutionAttempts = 0
    private var peerResolutionState = "pending"
    private var peerResolutionMs = 0
    private var currentAttemptIndex = -1

    var transport: String {
        condition.lock()
        defer { condition.unlock() }
        return currentTransport
    }

    var packetDiagnostics: String {
        condition.lock()
        defer { condition.unlock() }
        return "tx=\(packetsSent), txErrors=\(sendFailures), rx=\(packetsReceived), " +
            "injected=\(packetsInjected), injectErrors=\(injectionFailures), " +
            "arp=\(peerResolutionState), arpAttempts=\(peerResolutionAttempts), arpMs=\(peerResolutionMs)"
    }

    var attemptIndex: Int {
        condition.lock()
        defer { condition.unlock() }
        return currentAttemptIndex
    }

    var peerPath: String {
        guard let handle = acquireHandle() else { return "unknown" }
        defer { releaseHandle() }
        switch htp_n3n_get_peer_path(handle) {
        case HTP_N3N_PEER_PATH_P2P:
            return "p2p"
        case HTP_N3N_PEER_PATH_RELAY:
            return "relay"
        default:
            return "unknown"
        }
    }

    func markObservedRelay() {
        condition.lock()
        if currentTransport == "udp-p2p" || currentTransport == "udp-adaptive" {
            currentTransport = "udp-relay"
        }
        condition.unlock()
    }

    func connect(
        configuration: VpnConfiguration,
        supernodes: [String],
        startAt startIndex: Int
    ) async throws -> Int {
        let connectGeneration = beginConnection()
        let attempts = Self.transportAttempts(configuration: configuration, supernodes: supernodes)
        var registeredWithSupernode = false
        guard startIndex < attempts.count else {
            throw Self.unreachableError(registeredWithSupernode: false)
        }
        for index in max(0, startIndex)..<attempts.count {
            let attempt = attempts[index]
            try Task.checkCancellation()
            guard isCurrent(connectGeneration) else { throw CancellationError() }
            let candidate = create(attempt: attempt, configuration: configuration)
            guard let candidate, htp_n3n_start(candidate) == 0 else {
                if let candidate { htp_n3n_destroy(candidate) }
                continue
            }
            var adopted = false
            defer {
                if !adopted {
                    htp_n3n_stop(candidate)
                    htp_n3n_destroy(candidate)
                }
            }
            let timeout = attempt.mode == "tcp-relay" ? 12.0 : 8.0
            let deadline = Date().addingTimeInterval(timeout)
            while Date() < deadline {
                try Task.checkCancellation()
                guard isCurrent(connectGeneration) else { throw CancellationError() }
                let state = htp_n3n_get_state(candidate)
                if state == HTP_N3N_CONNECTED {
                    registeredWithSupernode = true
                    let started = Date()
                    let resolution = configuration.overlay.serverAddress.withCString {
                        htp_n3n_resolve_peer(
                            candidate,
                            $0,
                            (attempt.mode == "udp-p2p" || attempt.mode == "udp-adaptive") ? 3_000 : 5_000)
                    }
                    recordPeerResolution(
                        result: resolution,
                        elapsedMs: Int(Date().timeIntervalSince(started) * 1_000))
                    if resolution == 1 {
                        adopted = adopt(candidate, generation: connectGeneration,
                            transport: attempt.mode, attemptIndex: index)
                        if adopted { return index }
                        throw CancellationError()
                    }
                    break
                }
                if state == HTP_N3N_FAILED || state == HTP_N3N_STOPPED { break }
                try await Task.sleep(for: .milliseconds(200))
            }
        }
        guard isCurrent(connectGeneration) else { throw CancellationError() }
        throw Self.unreachableError(registeredWithSupernode: registeredWithSupernode)
    }

    static func attemptCount(configuration: VpnConfiguration, supernodes: [String]) -> Int {
        transportAttempts(configuration: configuration, supernodes: supernodes).count
    }

    static func firstAttemptIndex(
        configuration: VpnConfiguration,
        supernodes: [String],
        transport: String
    ) -> Int {
        transportAttempts(configuration: configuration, supernodes: supernodes)
            .firstIndex { $0.mode == transport } ?? 0
    }

    func send(_ packet: Data) {
        guard let handle = acquireHandle() else { return }
        defer { releaseHandle() }
        let result = packet.withUnsafeBytes { bytes in
            if let base = bytes.bindMemory(to: UInt8.self).baseAddress {
                return htp_n3n_send_ip_packet(handle, base, packet.count)
            }
            return -1
        }
        condition.lock()
        if result == 0 {
            packetsSent &+= 1
        } else {
            sendFailures &+= 1
        }
        condition.unlock()
    }

    func receive(timeoutMs: Int32 = 500) -> Data? {
        guard let handle = acquireHandle() else { return nil }
        defer { releaseHandle() }
        var buffer = [UInt8](repeating: 0, count: 65_535)
        let capacity = buffer.count
        var length = 0
        let result = buffer.withUnsafeMutableBytes { bytes in
            htp_n3n_receive_ip_packet(handle, bytes.bindMemory(to: UInt8.self).baseAddress,
                capacity, &length, timeoutMs)
        }
        guard result == 1 && length > 0 else { return nil }
        let packet = Data(buffer.prefix(length))
        condition.lock()
        packetsReceived &+= 1
        condition.unlock()
        return packet
    }

    func recordInjection(accepted: Bool) {
        condition.lock()
        if accepted { packetsInjected &+= 1 } else { injectionFailures &+= 1 }
        condition.unlock()
    }

    func stop() {
        condition.lock()
        generation &+= 1
        let current = handle
        self.handle = nil
        currentTransport = "unknown"
        currentAttemptIndex = -1
        packetsSent = 0
        sendFailures = 0
        packetsReceived = 0
        packetsInjected = 0
        injectionFailures = 0
        peerResolutionAttempts = 0
        peerResolutionState = "pending"
        peerResolutionMs = 0
        destroying = true
        while activeOperations > 0 { condition.wait() }
        if let current {
            htp_n3n_stop(current)
            htp_n3n_destroy(current)
        }
        destroying = false
        condition.broadcast()
        condition.unlock()
    }

    private func beginConnection() -> UInt64 {
        condition.lock()
        defer { condition.unlock() }
        while destroying { condition.wait() }
        generation &+= 1
        peerResolutionAttempts = 0
        peerResolutionState = "pending"
        peerResolutionMs = 0
        return generation
    }

    private func recordPeerResolution(result: Int32, elapsedMs: Int) {
        condition.lock()
        peerResolutionAttempts += 1
        peerResolutionState = result == 1 ? "resolved" : (result == 0 ? "timeout" : "error")
        peerResolutionMs = elapsedMs
        condition.unlock()
    }

    private func isCurrent(_ value: UInt64) -> Bool {
        condition.lock()
        defer { condition.unlock() }
        return generation == value
    }

    private func adopt(_ candidate: OpaquePointer, generation value: UInt64,
                       transport valueTransport: String, attemptIndex: Int) -> Bool {
        condition.lock()
        defer { condition.unlock() }
        guard generation == value, handle == nil, !destroying else { return false }
        handle = candidate
        currentTransport = valueTransport
        currentAttemptIndex = attemptIndex
        return true
    }

    private func acquireHandle() -> OpaquePointer? {
        condition.lock()
        defer { condition.unlock() }
        guard !destroying, let handle else { return nil }
        activeOperations += 1
        return handle
    }

    private func releaseHandle() {
        condition.lock()
        activeOperations -= 1
        if activeOperations == 0 { condition.broadcast() }
        condition.unlock()
    }

    private func create(attempt: EdgeTransportAttempt,
                        configuration: VpnConfiguration) -> OpaquePointer? {
        attempt.supernode.withCString { supernodePointer in
            configuration.n3n.community.withCString { communityPointer in
                configuration.n3n.key.withCString { keyPointer in
                    configuration.overlay.address.withCString { addressPointer in
                        configuration.overlay.serverAddress.withCString { peerAddressPointer in
                            var config = htp_n3n_config(
                                supernode: supernodePointer,
                                community: communityPointer,
                                key: keyPointer,
                                local_ipv4: addressPointer,
                                peer_ipv4: peerAddressPointer,
                                prefix_length: UInt8(configuration.overlay.prefixLength),
                                mtu: UInt16(configuration.overlay.mtu),
                                transport: attempt.nativeTransport)
                            return htp_n3n_create(&config, nil)
                        }
                    }
                }
            }
        }
    }

    private static func transportAttempts(
        configuration: VpnConfiguration,
        supernodes: [String]
    ) -> [EdgeTransportAttempt] {
        let supported = ["udp-adaptive", "udp-p2p", "udp-relay", "tcp-relay"]
        let configured = configuration.n3n.transportOrder ?? []
        let order = configured.reduce(into: [String]()) { result, mode in
            if supported.contains(mode), !result.contains(mode) { result.append(mode) }
        }
        let effectiveOrder = order.isEmpty ? ["udp-adaptive"] : order
        let uniqueSupernodes = supernodes.reduce(into: [String]()) { result, supernode in
            if !result.contains(supernode) { result.append(supernode) }
        }
        return effectiveOrder.flatMap { mode in
            uniqueSupernodes.map { EdgeTransportAttempt(supernode: $0, mode: mode) }
        }
    }

    private static func unreachableError(registeredWithSupernode: Bool) -> NSError {
        let message = registeredWithSupernode
            ? L10n.text(
                "error.supernode_runtime_unreachable",
                fallback: "The VPN supernode is responding, but the runtime cannot be reached on the n3n network"
            )
            : L10n.text(
                "error.supernode_unreachable",
                fallback: "No VPN supernode can be reached"
            )
        return NSError(domain: "HTPortalVPN", code: registeredWithSupernode ? 32 : 31,
            userInfo: [NSLocalizedDescriptionKey: message])
    }

    deinit { stop() }
}

private struct EdgeTransportAttempt {
    let supernode: String
    let mode: String

    var nativeTransport: htp_n3n_transport {
        switch mode {
        case "udp-relay":
            HTP_N3N_TRANSPORT_UDP_RELAY
        case "tcp-relay":
            HTP_N3N_TRANSPORT_TCP_RELAY
        default:
            HTP_N3N_TRANSPORT_UDP_P2P
        }
    }
}
