// SPDX-License-Identifier: LicenseRef-Hive-Proprietary OR GPL-3.0-only

import Foundation

struct MobileVpnAPIError: LocalizedError {
    let statusCode: Int
    let code: String?
    let message: String
    var errorDescription: String? { message }
}

actor MobileVpnAPI {
    private let baseURL = URL(string: "https://rest.portal.hivetechnology.net/mobile-vpn/")!
    private let session: URLSession
    private let encoder = JSONEncoder()
    private let decoder = JSONDecoder()

    init(configuration: URLSessionConfiguration = .ephemeral) {
        configuration.timeoutIntervalForRequest = 20
        configuration.timeoutIntervalForResource = 30
        configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
        session = URLSession(configuration: configuration)
    }

    func redeem(ticket: String, appVersion: String, installationId: String,
                portalReturnUrl: String?) async throws -> (MobileSession, SessionCredentials) {
        struct Body: Encodable { let ticket, platform, appVersion, installationId: String }
        let response: RedeemResponse = try await request("handoffs/redeem", method: "POST",
            body: Body(ticket: ticket, platform: "ios", appVersion: appVersion, installationId: installationId))
        return (response.session, SessionCredentials(
            sessionId: response.session.id,
            token: response.sessionToken,
            websocketUrl: response.websocketUrl,
            deviceName: response.session.device?.name
                ?? L10n.text("generic.device", fallback: "Device"),
            portalReturnUrl: response.portalReturnUrl ?? portalReturnUrl))
    }

    func start(_ credentials: SessionCredentials) async throws -> MobileSession {
        let envelope: SessionEnvelope = try await request("sessions/\(credentials.sessionId)/start", method: "POST",
            body: EmptyBody(), credentials: credentials)
        return envelope.session
    }

    func configuration(_ credentials: SessionCredentials) async throws -> VpnConfiguration {
        try await request("sessions/\(credentials.sessionId)/config", body: Optional<EmptyBody>.none, credentials: credentials)
    }

    func session(_ credentials: SessionCredentials) async throws -> MobileSession {
        let envelope: SessionEnvelope = try await request("sessions/\(credentials.sessionId)",
            body: Optional<EmptyBody>.none, credentials: credentials)
        return envelope.session
    }

    func update(
        _ credentials: SessionCredentials,
        status: MobileVpnStatus,
        transport: String? = nil,
        quality: String? = nil,
        latencyMs: Int? = nil,
        errorCode: String? = nil,
        errorMessage: String? = nil
    ) async throws -> MobileSession {
        struct Body: Encodable {
            let status: MobileVpnStatus
            let transport, quality: String?
            let latencyMs: Int?
            let errorCode, errorMessage: String?
        }
        let envelope: SessionEnvelope = try await request("sessions/\(credentials.sessionId)/status", method: "PUT",
            body: Body(status: status, transport: transport, quality: quality, latencyMs: latencyMs,
                errorCode: errorCode, errorMessage: errorMessage), credentials: credentials)
        return envelope.session
    }

    func stop(_ credentials: SessionCredentials) async throws {
        let _: EmptyResponse = try await request("sessions/\(credentials.sessionId)", method: "DELETE",
            body: Optional<EmptyBody>.none, credentials: credentials, emptyResponse: true)
    }

    private func request<Response: Decodable, Body: Encodable>(
        _ path: String,
        method: String = "GET",
        body: Body? = nil,
        credentials: SessionCredentials? = nil,
        emptyResponse: Bool = false
    ) async throws -> Response {
        var request = URLRequest(url: baseURL.appending(path: path))
        request.httpMethod = method
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue("no-store", forHTTPHeaderField: "Cache-Control")
        if let credentials { request.setValue("Bearer \(credentials.token)", forHTTPHeaderField: "Authorization") }
        if let body {
            request.httpBody = try encoder.encode(body)
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        } else if method == "POST" {
            request.httpBody = Data("{}".utf8)
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        }
        let (data, response) = try await session.data(for: request)
        let http = response as! HTTPURLResponse
        guard (200..<300).contains(http.statusCode) else {
            let value = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
            throw MobileVpnAPIError(statusCode: http.statusCode, code: value?["code"] as? String,
                message: value?["message"] as? String ?? "HTTP \(http.statusCode)")
        }
        if emptyResponse || data.isEmpty { return EmptyResponse() as! Response }
        return try decoder.decode(Response.self, from: data)
    }
}

private struct EmptyBody: Encodable {}
private struct EmptyResponse: Decodable {}
