import Foundation

enum HTTPMethod: String {
    case get = "GET"
    case post = "POST"
    case put = "PUT"
    case delete = "DELETE"
}

actor NetworkService {
    nonisolated(unsafe) static let shared = NetworkService()

    private let session: URLSession

    private init() {
        let configuration = URLSessionConfiguration.default
        configuration.timeoutIntervalForRequest = Constants.API.requestTimeout
        configuration.timeoutIntervalForResource = Constants.API.downloadTimeout
        configuration.waitsForConnectivity = true
        self.session = URLSession(configuration: configuration)
    }

    func request<T: Decodable>(
        url: URL,
        method: HTTPMethod = .get,
        headers: [String: String]? = nil,
        body: Data? = nil
    ) async throws -> T {
        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue
        request.httpBody = body

        if let headers = headers {
            for (key, value) in headers {
                request.setValue(value, forHTTPHeaderField: key)
            }
        }

        request.setValue("application/json", forHTTPHeaderField: "Accept")

        if body != nil {
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        }

        do {
            let (data, response) = try await session.data(for: request)

            guard let httpResponse = response as? HTTPURLResponse else {
                throw NetworkError.invalidResponse
            }

            guard (200...299).contains(httpResponse.statusCode) else {
                if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 {
                    throw NetworkError.authenticationFailed
                }
                if httpResponse.statusCode == 429 {
                    throw NetworkError.rateLimited
                }
                throw NetworkError.serverError(httpResponse.statusCode)
            }

            do {
                let decoder = JSONDecoder()
                decoder.keyDecodingStrategy = .convertFromSnakeCase
                let decoded = try decoder.decode(T.self, from: data)
                return decoded
            } catch {
                throw NetworkError.decodingError(error)
            }

        } catch let error as NetworkError {
            throw error
        } catch let urlError as URLError {
            if await urlError.isNetworkError {
                throw NetworkError.noInternetConnection
            } else if await urlError.isTimeoutError {
                throw NetworkError.timeout
            } else {
                throw NetworkError.unknown(urlError)
            }
        } catch {
            throw NetworkError.unknown(error)
        }
    }

    func downloadData(from url: URL) async throws -> Data {
        do {
            let (data, response) = try await session.data(from: url)

            guard let httpResponse = response as? HTTPURLResponse else {
                throw NetworkError.invalidResponse
            }

            guard (200...299).contains(httpResponse.statusCode) else {
                throw NetworkError.serverError(httpResponse.statusCode)
            }

            return data

        } catch let error as NetworkError {
            throw error
        } catch let urlError as URLError {
            if await urlError.isNetworkError {
                throw NetworkError.noInternetConnection
            } else if await urlError.isTimeoutError {
                throw NetworkError.timeout
            } else {
                throw NetworkError.unknown(urlError)
            }
        } catch {
            throw NetworkError.unknown(error)
        }
    }

    func downloadString(from url: URL) async throws -> String {
        let data = try await downloadData(from: url)
        guard let string = await data.utf8String else {
            throw NetworkError.decodingError(NSError(domain: "UTF8Decoding", code: -1))
        }
        return string
    }
}
