import Foundation

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

struct NetworkResponse {
    let statusCode: Int
    let headers: [AnyHashable: Any]
    let data: Data
}

enum NetworkError: Error {
    case invalidURL
    case unauthorized
    case network(Error)
    case noResponse
    case server(statusCode: Int, data: Data?)
}

final class NetworkClient {
    static let shared = NetworkClient()

    private let session: URLSession
    private let refreshQueue = DispatchQueue(label: "org.yobble.network.refresh")
    private var isRefreshing = false
    private var refreshCompletions: [(Bool) -> Void] = []

    init(session: URLSession = .shared) {
        self.session = session
    }

    func request(
        path: String,
        method: HTTPMethod = .get,
        query: [String: String?]? = nil,
        headers: [String: String] = [:],
        body: Data? = nil,
        contentType: String? = nil,
        requiresAuth: Bool = false,
        callbackQueue: DispatchQueue = .main,
        completion: @escaping (Result<NetworkResponse, NetworkError>) -> Void
    ) {
        performRequest(
            path: path,
            method: method,
            query: query,
            headers: headers,
            body: body,
            contentType: contentType,
            requiresAuth: requiresAuth,
            callbackQueue: callbackQueue,
            allowRefreshRetry: true,
            isRetry: false,
            completion: completion
        )
    }

    private func performRequest(
        path: String,
        method: HTTPMethod,
        query: [String: String?]?,
        headers: [String: String],
        body: Data?,
        contentType: String?,
        requiresAuth: Bool,
        callbackQueue: DispatchQueue,
        allowRefreshRetry: Bool,
        isRetry: Bool,
        completion: @escaping (Result<NetworkResponse, NetworkError>) -> Void
    ) {
        guard let url = buildURL(path: path, query: query) else {
            callbackQueue.async {
                completion(.failure(.invalidURL))
            }
            return
        }

        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue
        request.httpBody = body

        var allHeaders: [String: String] = [
            "User-Agent": AppConfig.USER_AGENT,
            "Accept": "application/json"
        ]

        if let contentType = contentType ?? (body != nil ? "application/json" : nil),
           headers["Content-Type"] == nil {
            allHeaders["Content-Type"] = contentType
        }

        headers.forEach { allHeaders[$0.key] = $0.value }

        if requiresAuth {
            guard let tokenInfo = currentTokenInfo() else {
                callbackQueue.async {
                    completion(.failure(.unauthorized))
                }
                return
            }
            allHeaders["Authorization"] = "Bearer \(tokenInfo.access)"
        }

        for (key, value) in allHeaders {
            request.setValue(value, forHTTPHeaderField: key)
        }

        let task = session.dataTask(with: request) { [weak self] data, response, error in
            guard let self else { return }

            if let error = error {
                callbackQueue.async {
                    completion(.failure(.network(error)))
                }
                return
            }

            guard let httpResponse = response as? HTTPURLResponse else {
                callbackQueue.async {
                    completion(.failure(.noResponse))
                }
                return
            }

            let responseData = data ?? Data()

            if (200...299).contains(httpResponse.statusCode) {
                let payload = NetworkResponse(
                    statusCode: httpResponse.statusCode,
                    headers: httpResponse.allHeaderFields,
                    data: responseData
                )
                callbackQueue.async {
                    completion(.success(payload))
                }
                return
            }

            if httpResponse.statusCode == 401,
               requiresAuth,
               allowRefreshRetry,
               !isRetry {
                self.refreshAccessToken { success in
                    if success {
                        self.performRequest(
                            path: path,
                            method: method,
                            query: query,
                            headers: headers,
                            body: body,
                            contentType: contentType,
                            requiresAuth: requiresAuth,
                            callbackQueue: callbackQueue,
                            allowRefreshRetry: false,
                            isRetry: true,
                            completion: completion
                        )
                    } else {
                        callbackQueue.async {
                            completion(.failure(.unauthorized))
                        }
                    }
                }
                return
            }

            callbackQueue.async {
                completion(.failure(.server(statusCode: httpResponse.statusCode, data: responseData)))
            }
        }

        task.resume()
    }

    private func buildURL(path: String, query: [String: String?]?) -> URL? {
        guard let baseURL = URL(string: AppConfig.API_SERVER) else {
            return nil
        }

        var cleanedPath = path
        if cleanedPath.hasPrefix("/") {
            cleanedPath.removeFirst()
        }

        let url = cleanedPath.isEmpty ? baseURL : baseURL.appendingPathComponent(cleanedPath)
        guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
            return nil
        }

        if let query {
            let items = query.compactMap { key, value -> URLQueryItem? in
                guard let value else { return nil }
                return URLQueryItem(name: key, value: value)
            }
            if !items.isEmpty {
                components.queryItems = items
            }
        }

        return components.url
    }

    private func currentTokenInfo() -> (login: String, access: String, refresh: String)? {
        guard
            let login = UserDefaults.standard.string(forKey: "currentUser"),
            let access = KeychainService.shared.get(forKey: "access_token", service: login),
            let refresh = KeychainService.shared.get(forKey: "refresh_token", service: login)
        else {
            return nil
        }
        return (login, access, refresh)
    }

    private func refreshAccessToken(completion: @escaping (Bool) -> Void) {
        refreshQueue.async {
            if self.isRefreshing {
                self.refreshCompletions.append(completion)
                return
            }

            guard let tokenInfo = self.currentTokenInfo() else {
                completion(false)
                return
            }

            self.isRefreshing = true
            self.refreshCompletions.append(completion)

            let payload = RefreshRequest(access_token: tokenInfo.access, refresh_token: tokenInfo.refresh)
            guard let body = try? JSONEncoder().encode(payload) else {
                self.completeRefresh(success: false)
                return
            }

            self.performRequest(
                path: "/v1/auth/token/refresh",
                method: .post,
                query: nil,
                headers: [:],
                body: body,
                contentType: "application/json",
                requiresAuth: false,
                callbackQueue: self.refreshQueue,
                allowRefreshRetry: false,
                isRetry: false
            ) { result in
                switch result {
                case .success(let response):
                    do {
                        let decoder = JSONDecoder()
                        let apiResponse = try decoder.decode(APIResponse<TokenPairPayload>.self, from: response.data)
                        guard apiResponse.status == "fine" else {
                            self.completeRefresh(success: false)
                            return
                        }

                        let data = apiResponse.data
                        KeychainService.shared.save(data.access_token, forKey: "access_token", service: tokenInfo.login)
                        KeychainService.shared.save(data.refresh_token, forKey: "refresh_token", service: tokenInfo.login)
                        if let userId = data.user_id {
                            KeychainService.shared.save(userId, forKey: "userId", service: tokenInfo.login)
                        }

                        NotificationCenter.default.post(name: .accessTokenDidChange, object: nil)

                        self.completeRefresh(success: true)
                    } catch {
                        self.completeRefresh(success: false)
                    }
                case .failure:
                    self.completeRefresh(success: false)
                }
            }
        }
    }

    private func completeRefresh(success: Bool) {
        let completions = refreshCompletions
        refreshCompletions.removeAll()
        isRefreshing = false
        completions.forEach { $0(success) }
    }
}

private struct RefreshRequest: Encodable {
    let access_token: String
    let refresh_token: String
}
