import Foundation
import os

/// Клиент публичного API Rapira.
///
/// Транспортный слой без бизнес-логики: обработка статусов, декодирование
/// и таймауты. Что делать с ошибками — решают вызывающие стороны.
///
/// `nonisolated` — чтобы в app-таргете (MainActor по умолчанию) сетевые
/// вызовы из акторов не прыгали через главный поток.
nonisolated struct RapiraAPI: Sendable {
    enum APIError: Error {
        case invalidResponse
        case badStatus(Int)
    }

    static let shared = RapiraAPI()

    private let session: URLSession
    private static let logger = Logger(subsystem: AppGroup.id, category: "api")

    /// - Parameter session: подменяется в тестах; nil — сессия по умолчанию.
    init(session: URLSession? = nil) {
        self.session = session ?? Self.makeSession()
    }

    // MARK: - Методы

    /// Сводки по всем торговым парам.
    func symbolThumbs() async throws -> [SymbolThumb] {
        try await get([SymbolThumb].self, from: Endpoint.symbolThumbs.url)
    }

    /// Цены закрытия за окно `hours` часов с шагом `resolutionMinutes` минут.
    /// Ответ API — массив свечей `[ts, open, high, low, close, volume]`.
    func closes(
        for symbol: String,
        resolutionMinutes: Int = 15,
        hours: Int = 24,
        now: Date = .now
    ) async throws -> [Double] {
        let toMs = Int(now.timeIntervalSince1970 * 1000)
        let fromMs = toMs - hours * 3_600_000
        let endpoint = Endpoint.history(
            symbol: symbol, resolutionMinutes: resolutionMinutes, fromMs: fromMs, toMs: toMs
        )
        let candles = try await get([[Double]].self, from: endpoint.url)
        return candles.compactMap { $0.count > 4 ? $0[4] : nil }
    }

    /// SVG-иконка монеты с CDN.
    func iconData(for currency: String) async throws -> Data {
        try await data(from: Endpoint.icon(currency: currency).url)
    }

    // MARK: - Транспорт

    private func get<T: Decodable>(_ type: T.Type, from url: URL) async throws -> T {
        try JSONDecoder().decode(type, from: await data(from: url))
    }

    private func data(from url: URL) async throws -> Data {
        let (data, response) = try await session.data(from: url)
        guard let http = response as? HTTPURLResponse else {
            throw APIError.invalidResponse
        }
        guard (200..<300).contains(http.statusCode) else {
            Self.logger.error("HTTP \(http.statusCode) \(url.absoluteString, privacy: .public)")
            throw APIError.badStatus(http.statusCode)
        }
        return data
    }

    private static func makeSession() -> URLSession {
        // Виджет живёт секунды: URL-кэш не нужен (иконки кэшируются на диске),
        // а короткие таймауты не дают выйти за бюджет времени расширения.
        let configuration = URLSessionConfiguration.ephemeral
        configuration.timeoutIntervalForRequest = 10
        configuration.timeoutIntervalForResource = 20
        return URLSession(configuration: configuration)
    }
}

// MARK: - Эндпоинты

private enum Endpoint {
    case symbolThumbs
    case history(symbol: String, resolutionMinutes: Int, fromMs: Int, toMs: Int)
    case icon(currency: String)

    /// Адреса собираются из констант — force unwrap безопасен.
    var url: URL {
        switch self {
        case .symbolThumbs:
            URL(string: "https://api.rapira.net/market/symbol-thumb")!

        case let .history(symbol, resolution, fromMs, toMs):
            {
                var components = URLComponents(string: "https://api.rapira.net/market/history")!
                components.queryItems = [
                    URLQueryItem(name: "symbol", value: symbol),
                    URLQueryItem(name: "resolution", value: String(resolution)),
                    URLQueryItem(name: "from", value: String(fromMs)),
                    URLQueryItem(name: "to", value: String(toMs)),
                ]
                return components.url!
            }()

        case let .icon(currency):
            URL(string: "https://cdn.rapira.net/media/crypto/\(currency).svg")!
        }
    }
}
