import Foundation

actor RelayAPI {
    static let shared = RelayAPI()

    func createPairing(displayName: String) async throws -> PairingCreateResponse {
        var request = URLRequest(url: endpoint(path: "pairing/create"))
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try JSONEncoder().encode(PairingRequest(displayName: displayName))

        return try await executeJSON(request)
    }

    func joinPairing(code: String, displayName: String) async throws -> PairingJoinResponse {
        var request = URLRequest(url: endpoint(path: "pairing/join"))
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try JSONEncoder().encode(
            PairingJoinRequest(code: code, displayName: displayName)
        )

        return try await executeJSON(request)
    }

    func uploadFile(url: URL, token: String) async throws -> FileUploadResponse {
        let boundary = "Boundary-\(UUID().uuidString)"
        let fileData: Data

        do {
            fileData = try Data(contentsOf: url)
        } catch {
            throw RelayAPIError.networkError(error)
        }

        var request = URLRequest(url: endpoint(path: "files"))
        request.httpMethod = "POST"
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        request.setValue(
            "multipart/form-data; boundary=\(boundary)",
            forHTTPHeaderField: "Content-Type"
        )
        request.httpBody = multipartBody(
            fileData: fileData,
            fileName: safeFileName(from: url),
            boundary: boundary
        )

        return try await executeJSON(request)
    }

    func downloadFile(id: String, token: String) async throws -> Data {
        var request = URLRequest(url: endpoint(path: "files/\(id)"))
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        return try await execute(request)
    }

    private let baseURL = URL(string: "https://fchaty.diyaa.de")!
    private let session = URLSession.shared

    private init() {}

    private func endpoint(path: String) -> URL {
        baseURL.appendingPathComponent(path)
    }

    private func executeJSON<Response: Decodable>(_ request: URLRequest) async throws -> Response {
        let data = try await execute(request)

        do {
            return try JSONDecoder().decode(Response.self, from: data)
        } catch {
            throw RelayAPIError.decodingError(error)
        }
    }

    private func execute(_ request: URLRequest) async throws -> Data {
        do {
            let (data, response) = try await session.data(for: request)

            guard let response = response as? HTTPURLResponse else {
                throw RelayAPIError.networkError(URLError(.badServerResponse))
            }

            guard (200 ... 299).contains(response.statusCode) else {
                throw RelayAPIError.serverError(response.statusCode)
            }

            return data
        } catch let error as RelayAPIError {
            throw error
        } catch {
            throw RelayAPIError.networkError(error)
        }
    }

    private func multipartBody(fileData: Data, fileName: String, boundary: String) -> Data {
        var body = Data()
        let escapedFileName = fileName
            .replacingOccurrences(of: "\\", with: "\\\\")
            .replacingOccurrences(of: "\"", with: "\\\"")
            .replacingOccurrences(of: "\r", with: "")
            .replacingOccurrences(of: "\n", with: "")

        append("--\(boundary)\r\n", to: &body)
        append(
            "Content-Disposition: form-data; name=\"file\"; filename=\"\(escapedFileName)\"\r\n",
            to: &body
        )
        append("Content-Type: application/octet-stream\r\n\r\n", to: &body)
        body.append(fileData)
        append("\r\n--\(boundary)--\r\n", to: &body)

        return body
    }

    private func safeFileName(from url: URL) -> String {
        let name = url.lastPathComponent
        return name.isEmpty ? "upload" : name
    }

    private func append(_ string: String, to data: inout Data) {
        data.append(Data(string.utf8))
    }
}

struct PairingCreateResponse: Codable, Equatable {
    let code: String
    let token: String
    let peerID: String
    let expiresAt: String
}

struct PairingJoinResponse: Codable, Equatable {
    let token: String
    let peerID: String
    let peer: PeerInfo
}

struct PeerInfo: Codable, Equatable {
    let id: String
    let displayName: String
}

struct FileUploadResponse: Codable, Equatable {
    let id: String
    let name: String
    let size: Int
}

enum RelayAPIError: Error {
    case networkError(Error)
    case serverError(Int)
    case decodingError(Error)
}

private struct PairingRequest: Encodable {
    let displayName: String
}

private struct PairingJoinRequest: Encodable {
    let code: String
    let displayName: String
}
