//
//  HTTPClient.swift
//  Yattee
//
//  Modern async/await networking layer using URLSession.
//

import Foundation

/// Actor-based HTTP client for making network requests.
actor HTTPClient {
    // MARK: - Properties

    private let session: URLSession
    private let decoder: JSONDecoder
    private var userAgent: String?
    private var randomizeUserAgentPerRequest: Bool = false

    /// Headers automatically applied to every request from this client.
    /// Per-call `customHeaders` override these on key conflict.
    /// Used to bake an HTTP Basic Auth `Authorization` header into a per-instance client
    /// when the instance sits behind a reverse proxy that requires basic auth.
    private var defaultHeaders: [String: String] = [:]

    // MARK: - Initialization

    init(session: URLSession = .shared, decoder: JSONDecoder = .init()) {
        self.session = session
        self.decoder = decoder

        // Configure decoder for common API patterns
        self.decoder.keyDecodingStrategy = .convertFromSnakeCase
        self.decoder.dateDecodingStrategy = .iso8601
    }

    // MARK: - Configuration

    /// Sets the User-Agent header to use for all requests.
    /// - Parameter userAgent: The User-Agent string to use.
    func setUserAgent(_ userAgent: String) {
        self.userAgent = userAgent
    }

    /// Sets whether to generate a new random User-Agent for each request.
    /// - Parameter enabled: If true, ignores the fixed userAgent and generates a new random one per request.
    func setRandomizeUserAgentPerRequest(_ enabled: Bool) {
        self.randomizeUserAgentPerRequest = enabled
    }

    /// Sets headers to be applied to every request from this client.
    /// Per-call `customHeaders` override these on key conflict.
    func setDefaultHeaders(_ headers: [String: String]) {
        self.defaultHeaders = headers
    }

    // MARK: - Public Methods

    /// Fetches and decodes a response from the given endpoint.
    /// - Parameters:
    ///   - endpoint: The endpoint to fetch from.
    ///   - baseURL: The base URL to use for the request.
    ///   - customHeaders: Optional custom headers to add to the request (e.g., API keys).
    /// - Returns: The decoded response.
    func fetch<T: Decodable & Sendable>(
        _ endpoint: Endpoint,
        baseURL: URL,
        customHeaders: [String: String]? = nil
    ) async throws -> T {
        let request = try endpoint.urlRequest(baseURL: baseURL)
        return try await perform(request, customHeaders: customHeaders)
    }

    /// Fetches raw data from the given endpoint.
    /// - Parameters:
    ///   - endpoint: The endpoint to fetch from.
    ///   - baseURL: The base URL to use for the request.
    ///   - customHeaders: Optional custom headers to add to the request (e.g., API keys).
    /// - Returns: The raw response data.
    func fetchData(
        _ endpoint: Endpoint,
        baseURL: URL,
        customHeaders: [String: String]? = nil
    ) async throws -> Data {
        let request = try endpoint.urlRequest(baseURL: baseURL)
        return try await performRaw(request, customHeaders: customHeaders)
    }

    /// Sends a request without expecting a response body.
    /// Used for POST/PUT/DELETE operations that return empty responses.
    /// - Parameters:
    ///   - endpoint: The endpoint to send the request to.
    ///   - baseURL: The base URL to use for the request.
    ///   - customHeaders: Optional custom headers to add to the request (e.g., cookies).
    func sendRequest(
        _ endpoint: Endpoint,
        baseURL: URL,
        customHeaders: [String: String]? = nil
    ) async throws {
        let request = try endpoint.urlRequest(baseURL: baseURL)
        _ = try await performRaw(request, customHeaders: customHeaders)
    }

    /// Performs a request and returns the raw data without decoding.
    /// - Parameters:
    ///   - request: The URLRequest to perform.
    ///   - customHeaders: Optional custom headers to add to the request (e.g., API keys).
    /// - Returns: The raw response data.
    func performRaw(_ request: URLRequest, customHeaders: [String: String]? = nil) async throws -> Data {
        var mutableRequest = request

        // Apply User-Agent header
        if randomizeUserAgentPerRequest {
            // Generate a fresh random User-Agent for this request
            mutableRequest.setValue(UserAgentGenerator.generateRandom(), forHTTPHeaderField: "User-Agent")
        } else if let userAgent {
            mutableRequest.setValue(userAgent, forHTTPHeaderField: "User-Agent")
        }

        // Apply default headers first (e.g., basic auth bound to a per-instance client)
        for (key, value) in defaultHeaders {
            mutableRequest.setValue(value, forHTTPHeaderField: key)
        }

        // Apply custom headers (e.g., X-API-Key for authenticated requests).
        // These override default headers on key conflict.
        if let customHeaders {
            for (key, value) in customHeaders {
                mutableRequest.setValue(value, forHTTPHeaderField: key)
            }
        }

        let method = mutableRequest.httpMethod ?? "GET"
        let requestURL = mutableRequest.url ?? URL(string: "unknown")!
        let finalRequest = mutableRequest

        await MainActor.run {
            LoggingService.shared.logAPIRequest(method, url: requestURL)
        }

        // Check if task was already cancelled before making the request
        if Task.isCancelled {
            await MainActor.run {
                LoggingService.shared.debug("Request cancelled before execution: \(requestURL)", category: .api)
            }
            throw APIError.cancelled
        }

        let startTime = Date()
        let data: Data
        let response: URLResponse

        do {
            (data, response) = try await session.data(for: finalRequest)
        } catch let error as URLError {
            let apiError = mapURLError(error)
            // Add more context for cancelled requests
            if error.code == .cancelled {
                await MainActor.run {
                    LoggingService.shared.debug(
                        "URLSession request cancelled - Task.isCancelled: \(Task.isCancelled)",
                        category: .api
                    )
                }
            }
            await MainActor.run {
                LoggingService.shared.logAPIError(requestURL, error: apiError)
            }
            throw apiError
        } catch {
            await MainActor.run {
                LoggingService.shared.logAPIError(requestURL, error: error)
            }
            throw APIError.unknown(error.localizedDescription)
        }

        let duration = Date().timeIntervalSince(startTime)

        do {
            try validateResponse(response, data: data)
        } catch {
            await MainActor.run {
                LoggingService.shared.logAPIError(requestURL, error: error)
            }
            throw error
        }

        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
        await MainActor.run {
            LoggingService.shared.logAPIResponse(requestURL, statusCode: statusCode, duration: duration)
        }

        return data
    }

    // MARK: - Private Methods

    private func perform<T: Decodable & Sendable>(
        _ request: URLRequest,
        customHeaders: [String: String]? = nil
    ) async throws -> T {
        let data = try await performRaw(request, customHeaders: customHeaders)

        do {
            return try decoder.decode(T.self, from: data)
        } catch let error as DecodingError {
            let errorDescription = describeDecodingError(error)
            Task { @MainActor in
                LoggingService.shared.error("API decoding error: \(errorDescription)", category: .api)
            }
            throw APIError.decodingError(errorDescription)
        }
    }

    private func validateResponse(_ response: URLResponse, data: Data) throws {
        guard let httpResponse = response as? HTTPURLResponse else {
            return
        }

        let statusCode = httpResponse.statusCode

        switch statusCode {
        case 200...299:
            return
        case 401:
            throw APIError.unauthorized
        case 404:
            let detail = parseErrorDetail(from: data)
            throw APIError.notFound(detail)
        case 429:
            let retryAfter = httpResponse.value(forHTTPHeaderField: "Retry-After")
                .flatMap { TimeInterval($0) }
            throw APIError.rateLimited(retryAfter: retryAfter)
        default:
            let detail = parseErrorDetail(from: data)
            throw APIError.httpError(statusCode: statusCode, message: detail)
        }
    }

    private func mapURLError(_ error: URLError) -> APIError {
        switch error.code {
        case .timedOut:
            return .timeout
        case .notConnectedToInternet, .networkConnectionLost:
            return .noConnection
        case .cancelled:
            return .cancelled
        default:
            return .unknown(error.localizedDescription)
        }
    }

    private func describeDecodingError(_ error: DecodingError) -> String {
        switch error {
        case .keyNotFound(let key, let context):
            return "Missing key '\(key.stringValue)' at \(context.codingPath.map(\.stringValue).joined(separator: "."))"
        case .typeMismatch(let type, let context):
            return "Type mismatch for \(type) at \(context.codingPath.map(\.stringValue).joined(separator: "."))"
        case .valueNotFound(let type, let context):
            return "Null value for \(type) at \(context.codingPath.map(\.stringValue).joined(separator: "."))"
        case .dataCorrupted(let context):
            return "Data corrupted at \(context.codingPath.map(\.stringValue).joined(separator: "."))"
        @unknown default:
            return error.localizedDescription
        }
    }

    /// Parses error detail from JSON response body.
    /// Supports common API error formats: {"detail": "..."}, {"error": "..."}, {"message": "..."}
    private func parseErrorDetail(from data: Data) -> String? {
        guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            return nil
        }

        // Try common error message fields
        if let detail = json["detail"] as? String {
            return detail
        }
        if let error = json["error"] as? String {
            return error
        }
        if let message = json["message"] as? String {
            return message
        }

        return nil
    }
}

