import Foundation
import Observation

enum ONUDetailTab: String, CaseIterable, Identifiable {
    case stats = "Overview"
    case cpe = "CPE"
    case config = "Config"
    case alarms = "Alarms"
    case logs = "Logs"
    var id: String { rawValue }
}

@MainActor
@Observable
final class ONUDetailViewModel {
    let onuId: String
    private let connection: MCMSConnection
    /// Bumped per (re)load so a late/cancelled request — or one for a tab the user
    /// has since left — can't commit its results over the current tab's data.
    private var loadGeneration = 0

    var tab: ONUDetailTab = .stats
    var phase: LoadPhase = .idle

    var config: ONUConfigDoc?
    var liveState: ONUStateDoc?
    var stats: [StatSample] = []
    var alarms: [AlarmHistoryDoc] = []
    var logs: [LogEntry] = []
    var cpes: [CPEState] = []

    var firmwareFiles: [FirmwareFile] = []
    var firmwareLoadError: String?

    var isPerformingAction = false
    var actionMessage: String?

    init(connection: MCMSConnection, onuId: String) {
        self.connection = connection
        self.onuId = onuId
    }

    /// Tab switch / first load: blanks to a spinner while the new tab loads.
    func loadCurrentTab() async {
        phase = .loading
        await reloadCurrentTab()
    }

    /// Pull-to-refresh: reloads in place WITHOUT flipping to `.loading`, so the
    /// scrollable content (and the refresh task driving it) isn't torn down
    /// mid-request — which otherwise surfaces as a spurious "cancelled" error.
    func refresh() async {
        await reloadCurrentTab()
    }

    private func reloadCurrentTab() async {
        loadGeneration += 1
        let gen = loadGeneration
        let requested = tab
        // Commit only if this is still the latest load AND the user is still on the
        // tab we fetched for — otherwise drop the result.
        func current() -> Bool { gen == loadGeneration && requested == tab && !Task.isCancelled }
        do {
            switch requested {
            case .stats:
                // Live ONU-STATE carries the current optical/FEC levels (§3.12);
                // the /onus/stats/ time-series is a best-effort supplement.
                let state = try await connection.onu.state(id: onuId)
                let samples = (try? await connection.onu.stats(id: onuId, lastHours: 24)) ?? []
                guard current() else { return }
                liveState = state
                stats = samples
            case .config:
                let doc = try await connection.onu.config(id: onuId)
                guard current() else { return }
                config = doc
            case .cpe:
                let list = try await connection.onu.cpes(id: onuId)
                guard current() else { return }
                cpes = list
            case .alarms:
                let list = try await connection.onu.alarmHistories(query: queryForONU())
                    .sorted { $0.severity.rank < $1.severity.rank }
                guard current() else { return }
                alarms = list
            case .logs:
                let list = try await connection.onu.logs(id: onuId, lastHours: 24)
                guard current() else { return }
                logs = list
            }
            phase = .loaded
        } catch {
            guard current() else { return }
            phase = .failed((error as? APIError)?.localizedDescription ?? error.localizedDescription)
        }
    }

    /// Filter the fleet-wide alarm-history list to this ONU. ⚠️ The `_id` key is
    /// UNVERIFIED — if it's wrong the server ignores the filter and returns EVERY
    /// ONU's alarms into this detail screen (no error). Confirm the real key (or
    /// switch to the per-id `.onuAlarmHistory(id:)` endpoint) against openapi.json /
    /// a live response before trusting this tab.
    private func queryForONU() -> APIQuery {
        var q = APIQuery()
        q.query = ["_id": onuId]
        return q
    }

    func reset() async {
        isPerformingAction = true
        actionMessage = nil
        do {
            try await connection.onu.reset(id: onuId)
            actionMessage = "Reset requested."
        } catch {
            actionMessage = (error as? APIError)?.localizedDescription ?? error.localizedDescription
        }
        isPerformingAction = false
    }

    func loadFirmware() async {
        firmwareLoadError = nil
        do { firmwareFiles = try await connection.onu.firmwareFiles() }
        catch { firmwareLoadError = (error as? APIError)?.localizedDescription ?? error.localizedDescription }
    }

    /// Stage a firmware image to the ONU's inactive bank (Procedure 7). Service-affecting.
    func upgrade(to file: FirmwareFile) async {
        isPerformingAction = true
        actionMessage = nil
        do {
            let slot = try await connection.onu.upgradeFirmware(id: onuId, file: file)
            actionMessage = "Firmware \(file.version ?? "image") staged to bank \(slot). The ONU starts it after its next reboot."
        } catch {
            actionMessage = (error as? APIError)?.localizedDescription ?? error.localizedDescription
        }
        isPerformingAction = false
    }
}

extension StatSample {
    /// All numeric counters in the sample, sorted by key — for display/charting.
    var numericMetrics: [(key: String, value: Double)] {
        (raw.objectValue ?? [:]).compactMap { key, value in
            switch value {
            case .double(let d): return (key, d)
            case .int(let i): return (key, Double(i))
            default: return nil
            }
        }
        .sorted { $0.key < $1.key }
    }
}
