import WidgetKit
import Foundation
import os

// MARK: - Модель отображения

/// Готовая к отрисовке пара: рыночные данные + график + иконка.
struct Quote: Identifiable, Sendable {
    let thumb: SymbolThumb
    /// Цены за 24ч; последняя точка — текущая цена, чтобы график был «живым».
    /// Пусто, если история недоступна.
    let series: [Double]
    let icon: Data?

    var id: String { thumb.id }
}

// MARK: - Entry

struct CryptoEntry: TimelineEntry, Sendable {
    let date: Date
    /// Пары в порядке, выбранном пользователем.
    let quotes: [Quote]
    /// true — не удалось получить данные ни из сети, ни из кэша.
    var dataUnavailable = false
}

// MARK: - Провайдер

struct CryptoProvider: TimelineProvider {
    private let api = RapiraAPI.shared
    private let store = PairSelectionStore.shared
    private let cache = MarketSnapshotCache()

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

    /// Плановое обновление и ускоренный повтор после ошибки.
    private static let refreshInterval: TimeInterval = 5 * 60
    private static let retryInterval: TimeInterval = 2 * 60

    // MARK: TimelineProvider

    func placeholder(in context: Context) -> CryptoEntry {
        PreviewData.entry
    }

    func getSnapshot(in context: Context, completion: @escaping (CryptoEntry) -> Void) {
        // Галерея виджетов ждать сеть не должна.
        guard !context.isPreview else {
            completion(PreviewData.entry)
            return
        }
        Task { completion(await makeEntry().entry) }
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<CryptoEntry>) -> Void) {
        Task {
            let (entry, isLive) = await makeEntry()
            let interval = isLive ? Self.refreshInterval : Self.retryInterval
            let timeline = Timeline(
                entries: [entry],
                policy: .after(Date.now.addingTimeInterval(interval))
            )
            completion(timeline)
        }
    }

    // MARK: - Сборка entry

    /// Живые данные → при ошибке последний удачный снимок → пустое состояние.
    private func makeEntry() async -> (entry: CryptoEntry, isLive: Bool) {
        let pairs = store.load()

        do {
            let thumbs = try await api.symbolThumbs()
            let selected = Self.select(pairs, from: thumbs)
            async let histories = loadHistories(for: selected.map(\.symbol))
            async let icons = IconStore.shared.icons(for: selected.map(\.quoteCurrency))
            let quotes = Self.makeQuotes(selected, histories: await histories, icons: await icons)

            cache.save(MarketSnapshot(date: .now, thumbs: thumbs, histories: await histories))
            return (CryptoEntry(date: .now, quotes: quotes), true)
        } catch {
            Self.logger.error("Рынок недоступен: \(String(describing: error), privacy: .public)")
            return (await fallbackEntry(pairs: pairs), false)
        }
    }

    /// Entry из дискового кэша; date — честное время тех данных.
    private func fallbackEntry(pairs: [String]) async -> CryptoEntry {
        guard let snapshot = cache.load() else {
            return CryptoEntry(date: .now, quotes: [], dataUnavailable: true)
        }
        let selected = Self.select(pairs, from: snapshot.thumbs)
        let icons = await IconStore.shared.icons(for: selected.map(\.quoteCurrency))
        let quotes = Self.makeQuotes(selected, histories: snapshot.histories, icons: icons)
        return CryptoEntry(date: snapshot.date, quotes: quotes)
    }

    /// История по каждой паре; сбой одной пары не валит остальные —
    /// пара просто остаётся без графика.
    private func loadHistories(for symbols: [String]) async -> [String: [Double]] {
        await withTaskGroup(of: (String, [Double]).self) { group in
            for symbol in symbols {
                group.addTask {
                    do {
                        return (symbol, try await api.closes(for: symbol))
                    } catch {
                        Self.logger.notice(
                            "Нет истории \(symbol, privacy: .public): \(String(describing: error), privacy: .public)"
                        )
                        return (symbol, [])
                    }
                }
            }

            var result: [String: [Double]] = [:]
            for await (symbol, closes) in group where closes.count > 1 {
                result[symbol] = closes
            }
            return result
        }
    }

    /// Отбирает выбранные пары, сохраняя порядок выбора.
    private static func select(_ pairs: [String], from thumbs: [SymbolThumb]) -> [SymbolThumb] {
        let bySymbol = Dictionary(thumbs.map { ($0.symbol, $0) }) { first, _ in first }
        return pairs.compactMap { bySymbol[$0] }
    }

    private static func makeQuotes(
        _ thumbs: [SymbolThumb],
        histories: [String: [Double]],
        icons: [String: Data]
    ) -> [Quote] {
        thumbs.map { thumb in
            var series = histories[thumb.symbol] ?? []
            if !series.isEmpty {
                series.append(thumb.close)
            }
            return Quote(thumb: thumb, series: series, icon: icons[thumb.quoteCurrency])
        }
    }
}
