import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

actor GatewayClient {
    private let ws: URLSessionWebSocketTask
    private let token: String
    private(set) var open = false
    var sequenceNum: Int? = nil

    init(gatewayURL: URL, token: String) {
        let queryItems = [URLQueryItem(name: "v", value: "10"), URLQueryItem(name: "encoding", value: "json")]
        ws = URLSession.shared.webSocketTask(with: gatewayURL.appending(queryItems: queryItems))
        self.token = token
    }

    func openConnection(intents: Intents) async throws {
        ws.resume()
        open = true
        guard case .hello(let helloMessage) = try await getMessage().d else { throw GatewayError.mismatchedOpcode }
        dump(helloMessage)

        let heartbeatTask = Task() {
            try await Task.sleep(for: .milliseconds(Int.random(in: 0...helloMessage.heartbeat_interval)))
            try await sendHeartbeat()

            while !Task.isCancelled {
                try await Task.sleep(for: .milliseconds(helloMessage.heartbeat_interval))
                try await sendHeartbeat()
            }
        }

        try await sendIdentify(intents: intents)
        _ = await heartbeatTask.result
    }

    func sendIdentify(intents: Intents) async throws {
        let payload = """
        {
          "op": 2,
          "d": {
            "token": "\(token)",
            "intents": \(intents.rawValue),
            "properties": {
              "os": "linux",
              "browser": "discordkit",
              "device": "discordkit"
            }
          }
        }
        """ // Im lazy
        try await ws.send(.string(payload))
    }

    func getMessage() async throws -> GatewayMessage {
        var strBuffer = ""
        var gwMessage: GatewayMessage? = nil
        let json = JSONDecoder()
        while gwMessage == nil {
            let wsMessage = try await ws.receive()
            guard case .string(let str) = wsMessage else { throw GatewayError.invalidMessage }
            strBuffer.append(str)
            do {
                gwMessage = try json.decode(GatewayMessage.self, from: Data(strBuffer.utf8))
            } catch DecodingError.dataCorrupted {
                continue
            }
        }
        guard let gwMessage = gwMessage else { throw GatewayError.invalidMessage }
        sequenceNum = gwMessage.s ?? sequenceNum
        return gwMessage
    }

    private func sendHeartbeat() async throws {
        let hbMessage = "{\"op\":1,\"d\":\(sequenceNum == nil ? "null" : String(sequenceNum!))}"
        try await ws.send(.string(hbMessage))
    }

    var events: AsyncStream<GatewayMessage> {
        AsyncStream { [self] in
            var event: GatewayMessage? = nil
            while event == nil {
                do {
                    while await !open {
                        try await Task.sleep(for: .seconds(1))
                    }
                    event = try await getMessage()
                    if event!.op == 1 { try await sendHeartbeat() }
                } catch {
                    print("Error listening to gateway: \(error)")
                }
            }
            return event!
        }
    }

}

public enum GatewayError: Error {
    case invalidMessage
    case invalidOpcode
    case mismatchedOpcode
}
