import Foundation
import CryptoKit

final class Client {
    
    private let localRequestRoute = "message"
    
    private let urlMessageParameter = "m"
    
    init() {}
    
    func send(_ message: Message, to url: String, through route: TransmissionType, using keys: KeySet) async -> ServerResponse {
        let sentTime = Date.now
        let signedMessage = message.authenticate(using: keys.remote)
        let response: Message
        switch route {
        case .throughServer:
            response = await send(signedMessage, toServerUrl: url, authenticateWith: keys.server, verifyUsing: keys.device)
            
        case .overLocalWifi:
            response = await send(signedMessage, toLocalDeviceUrl: url, verifyUsing: keys.device)
        }
        let receivedTime = Date.now
        // Create best guess for creation of challenge.
        let roundTripTime = receivedTime.timeIntervalSince(sentTime)
        let serverChallenge = ServerChallenge(
            creationDate: sentTime.addingTimeInterval(roundTripTime / 2),
            message: response)
        
        // Validate message content
        guard response.result == .messageAccepted else {
            print("Failure: \(response)")
            return (response, nil)
        }
        
        guard response.clientChallenge == message.clientChallenge else {
            print("Invalid client challenge: \(response)")
            return (response.with(result: .invalidClientChallengeFromDevice), nil)
        }
        return (response, serverChallenge)
    }

    
    private func send(_ message: SignedMessage, toLocalDeviceUrl server: String, verifyUsing deviceKey: SymmetricKey) async -> Message {
        let data = message.encoded.hexEncoded
        guard let url = URL(string: server)?.appendingPathComponent("\(localRequestRoute)?\(urlMessageParameter)=\(data)") else {
            return message.message.with(result: .serverUrlInvalid)
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.timeoutInterval = 10
        return await perform(request, inResponseTo: message.message, verifyUsing: deviceKey)
    }
    
    private func send(_ message: SignedMessage, toServerUrl server: String, authenticateWith authToken: Data, verifyUsing deviceKey: SymmetricKey) async -> Message {
        guard let url = URL(string: server)?.appendingPathComponent(SesameRoute.postMessage.rawValue) else {
            return message.message.with(result: .serverUrlInvalid)
        }
        
        var request = URLRequest(url: url)
        request.httpBody = message.encoded
        request.httpMethod = "POST"
        request.timeoutInterval = 10
        request.addValue(authToken.hexEncoded, forHTTPHeaderField: SesameHeader.authenticationHeader)
        return await perform(request, inResponseTo: message.message, verifyUsing: deviceKey)
    }
    
    private func perform(_ request: URLRequest, inResponseTo message: Message, verifyUsing deviceKey: SymmetricKey) async -> Message {
        let (response, responseData) = await fulfill(request)
        guard response == .messageAccepted, let data = responseData else {
            return message.with(result: response)
        }
        guard data.count == SignedMessage.size else {
            print("[WARN] Received message with \(data.count) bytes (\(Array(data)))")
            return message.with(result: .invalidMessageSizeFromDevice)
        }
        let decodedMessage: SignedMessage
        do {
            decodedMessage = try SignedMessage(decodeFrom: data)
        } catch {
            return message.with(result: error as! MessageResult)
        }
        guard decodedMessage.isValid(using: deviceKey) else {
            return message.with(result: .invalidSignatureFromDevice)
        }
        return decodedMessage.message
    }
    
    private func fulfill(_ request: URLRequest) async -> (response: MessageResult, data: Data?) {
        do {
            let (data, response) = try await URLSession.shared.data(for: request)
            guard let code = (response as? HTTPURLResponse)?.statusCode else {
                return (.unexpectedUrlResponseType, nil)
            }
            return (.init(httpCode: code), data)
        } catch {
            print("Request failed: \(error)")
            return (.deviceTimedOut, nil)
        }
    }
}
