import Foundation

// MARK: - Enums (tolerant decoding with `.unknown` fallback)

/// Device states for PON Controllers, OLTs, and ONUs (dashboard-level).
enum DeviceState: String, Decodable, CaseIterable {
    case online = "Online"
    case offline = "Offline"
    case unattached = "Unattached"
    case preProvisioned = "Pre-provisioned"
    case unknown

    init(from decoder: Decoder) throws {
        let raw = try decoder.singleValueContainer().decode(String.self)
        self = DeviceState(rawValue: raw) ?? .unknown
    }
}

/// ONU registration state. These are the buckets MCMS keeps on the **OLT-STATE**
/// document under `"ONU States"` — not a field on ONU-STATE (dev guide
/// Procedure 2). Raw values match the bucket names exactly.
enum ONULifecycleState: String, Decodable, CaseIterable {
    case registered      = "Registered"
    case unspecified     = "Unspecified"
    case unprovisioned   = "Unprovisioned"
    case deregistered    = "Deregistered"
    case dyingGasp       = "Dying Gasp"
    case disabled        = "Disabled"
    case disallowedAdmin = "Disallowed Admin"
    case disallowedError = "Disallowed Error"
    case disallowedRegID = "Disallowed Reg ID"
    case unknown

    init(from decoder: Decoder) throws {
        let raw = try decoder.singleValueContainer().decode(String.self)
        self = ONULifecycleState(rawValue: raw) ?? .unknown
    }

    /// Per dev guide Procedure 2: an ONU in `Registered` or `Unspecified` is online.
    var isOnline: Bool { self == .registered || self == .unspecified }
}

/// Automation roll-up states shown on the dashboard.
enum AutomationState: String, Decodable, CaseIterable {
    case done = "Done"
    case error = "Error"
    case waitingForInput = "Waiting For Input"
    case waitingForDevice = "Waiting For Device"
    case disabled = "Disabled"
    case unknown

    init(from decoder: Decoder) throws {
        let raw = try decoder.singleValueContainer().decode(String.self)
        self = AutomationState(rawValue: raw) ?? .unknown
    }
}

/// Syslog-style alarm/severity levels (lower rank = more severe).
enum AlarmSeverity: String, Decodable, CaseIterable, Comparable {
    case emergency, alert, critical, error, warning, notice, info, debug
    case unknown

    var rank: Int {
        switch self {
        case .emergency: return 0
        case .alert: return 1
        case .critical: return 2
        case .error: return 3
        case .warning: return 4
        case .notice: return 5
        case .info: return 6
        case .debug: return 7
        case .unknown: return 8
        }
    }

    init(from decoder: Decoder) throws {
        let raw = try decoder.singleValueContainer().decode(String.self)
        self = AlarmSeverity(rawValue: raw.lowercased()) ?? .unknown
    }

    static func < (lhs: AlarmSeverity, rhs: AlarmSeverity) -> Bool { lhs.rank < rhs.rank }

    /// Read a severity from a Mongo doc that may key it as `Severity` or `severity`.
    static func from(_ raw: JSONValue) -> AlarmSeverity {
        guard let s = raw["Severity"]?.stringValue ?? raw["severity"]?.stringValue else { return .unknown }
        return AlarmSeverity(rawValue: s.lowercased()) ?? .unknown
    }
}

// MARK: - Flexible documents
//
// Full MongoDB document schemas (ONU-CFG, ONU-STATE, OLT-CFG, CNTL-CFG, …) are
// large and version-specific. Rather than hand-transcribe every field (and risk
// getting it wrong), each document decodes its full body into `raw: JSONValue`
// and exposes a few convenience accessors. The accessor key paths below are
// best-effort guesses — RECONCILE THEM WITH `openapi.json` and a live response,
// then promote the fields you actually use into typed properties.

protocol MongoDocument: Decodable, Identifiable {
    var raw: JSONValue { get }
}

extension MongoDocument {
    /// The document's real Mongo `_id`, or nil when absent. Use THIS for the
    /// pagination cursor (`getAll`) — never the `id` fallback below.
    var documentId: String? { raw["_id"]?.stringValue }
}

// NOTE on `id`: each document's `id` falls back to `raw.compactDescription` when
// `_id` is missing — a deterministic value derived from the content, NOT a fresh
// `UUID()`. `id` is a *computed* property, so a random UUID would change on every
// access and hand SwiftUI a new identity each render, tearing down and rebuilding
// List/ForEach rows (lost scroll position, broken animations). The content-derived
// key is stable for the same instance, so identity holds across re-renders.

/// nil when the string is nil or empty, else the string. Shared by the document
/// accessors that should treat "" as "absent".
private func nonEmpty(_ s: String?) -> String? { (s?.isEmpty == false) ? s : nil }

/// Single source of truth for ONU optical thresholds (dBm, user guide §7). Shared
/// by the dashboard abnormal-Rx filter and the detail-view tinting so the two
/// screens can't give the operator contradicting verdicts for the same reading.
enum OpticalThreshold {
    static let rxGreenFloor = -28.0   // at/above (and ≤ rxHigh) is in-spec / green
    static let rxRedFloor = -30.0     // below this is critical / red
    static let rxHigh = -10.0         // above this is abnormal (too hot)
    static let txGreenFloor = 3.0

    /// Whether an RX reading (dBm) is abnormal: too weak or too hot.
    static func rxAbnormal(_ dBm: Double) -> Bool { dBm < rxGreenFloor || dBm > rxHigh }
}

/// ONU state document (`ONU-STATE`).
struct ONUStateDoc: MongoDocument {
    let raw: JSONValue
    init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }

    var id: String { raw["_id"]?.stringValue ?? raw.compactDescription }

    var serialNumber: String? { raw["_id"]?.stringValue }
    /// Parent OLT MAC, from `OLT.MAC Address` (dev guide Procedure 2 §2).
    var parentOLT: String? { raw["OLT"]?["MAC Address"]?.stringValue }
    var lastUpdate: String? { raw["Time"]?.stringValue }

    /// Registration state is **not** carried on ONU-STATE — it lives in the
    /// OLT-STATE buckets. The list/dashboard view models resolve it via
    /// `OLTRepository.registrationMap()` and stamp it here. `nil` until resolved.
    var resolvedLifecycle: ONULifecycleState?
    var lifecycle: ONULifecycleState { resolvedLifecycle ?? .unknown }

    /// Operator-set label from ONU-CFG (`ONU.Name`); resolved + stamped by the
    /// list VM via `ONURepository.nameMap()`. `nil` until resolved / if unset.
    var resolvedName: String?
    /// Best label to show: the operator name if set, else the serial/MAC id.
    var displayName: String {
        if let name = resolvedName, !name.isEmpty { return name }
        return id
    }

    // Live optical + FEC. Current values live on ONU-STATE.STATS, not on the
    // /onus/stats/ time-series (field guide §3.12 / §7).
    private func ponStat(_ pon: String, _ key: String) -> Double? {
        raw["STATS"]?[pon]?[key]?.doubleValue
    }
    var rxOpticalDBm: Double? { ponStat("ONU-PON", "RX Optical Level") }
    var txOpticalDBm: Double? { ponStat("ONU-PON", "TX Optical Level") }
    var onuPreFEC: Double?    { ponStat("ONU-PON", "RX Pre-FEC BER") }
    var onuPostFEC: Double?   { ponStat("ONU-PON", "RX Post-FEC BER") }
    var oltPreFEC: Double?    { ponStat("OLT-PON", "RX Pre-FEC BER") }
    var oltPostFEC: Double?   { ponStat("OLT-PON", "RX Post-FEC BER") }
}

/// ONU config document (`ONU-CFG`).
struct ONUConfigDoc: MongoDocument {
    let raw: JSONValue
    init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
    var id: String { raw["_id"]?.stringValue ?? raw.compactDescription }
    /// Operator-set label (`ONU.Name`, §5.1).
    var name: String? { raw["ONU"]?["Name"]?.stringValue }
    var netconfName: String? { raw["Netconf"]?["Name"]?.stringValue }
}

/// OLT config document (`OLT-CFG`).
struct OLTConfigDoc: MongoDocument {
    let raw: JSONValue
    init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
    var id: String { raw["_id"]?.stringValue ?? raw.compactDescription }   // MAC address
    /// Operator label from `OLT.Name` (confirmed against a live OLT-CFG, e.g.
    /// "OLT69-RIN"). Note `NETCONF.Name` defaults to the MAC, so it's *not* the
    /// friendly name. Returns nil when unset.
    var name: String? {
        guard let value = raw["OLT"]?["Name"]?.stringValue, !value.isEmpty else { return nil }
        return value
    }
}

/// OLT state document (`OLT-STATE`). Carries the per-OLT ONU registration
/// buckets used to resolve each ONU's lifecycle (dev guide Procedure 2).
struct OLTStateDoc: MongoDocument {
    let raw: JSONValue
    init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
    var id: String { raw["_id"]?.stringValue ?? raw.compactDescription }   // OLT MAC address
    var lastUpdate: String? { raw["Time"]?.stringValue }

    /// Operator name from OLT-CFG (`OLT.Name` — NOT `Netconf.Name`, which defaults
    /// to the MAC); resolved + stamped by the list VM via `OLTRepository.nameMap()`.
    /// `nil` until resolved / if unset.
    var resolvedName: String?
    /// Best label to show: the operator name if set, else the MAC id.
    var displayName: String {
        if let name = resolvedName, !name.isEmpty { return name }
        return id
    }

    /// `"ONU States"` → bucket name → [ONU IDs].
    var onuStatesByBucket: [String: [String]] {
        guard let obj = raw["ONU States"]?.objectValue else { return [:] }
        return obj.mapValues { $0.arrayValue?.compactMap(\.stringValue) ?? [] }
    }

    var totalONUs: Int { onuStatesByBucket.values.reduce(0) { $0 + $1.count } }
    /// Online = Registered + Unspecified (dev guide Procedure 2).
    var onlineONUs: Int {
        onuStatesByBucket.reduce(0) { sum, entry in
            (ONULifecycleState(rawValue: entry.key)?.isOnline ?? false) ? sum + entry.value.count : sum
        }
    }

    /// Non-empty registration buckets, ordered by lifecycle severity. Carries the
    /// raw bucket `name` so callers key `ForEach` on it — keying on `state` would
    /// collide when two unrecognized bucket names both map to `.unknown`.
    var populatedBuckets: [(name: String, state: ONULifecycleState, ids: [String])] {
        onuStatesByBucket
            .filter { !$0.value.isEmpty }
            .map { (name: $0.key, state: ONULifecycleState(rawValue: $0.key) ?? .unknown, ids: $0.value.sorted()) }
            .sorted {
                let order = ONULifecycleState.allCases
                let a = order.firstIndex(of: $0.state) ?? .max
                let b = order.firstIndex(of: $1.state) ?? .max
                return (a, $0.name) < (b, $1.name)   // name tiebreak → stable order for unknowns
            }
    }
}

/// PON Controller config document (`CNTL-CFG`).
struct ControllerConfigDoc: MongoDocument {
    let raw: JSONValue
    init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
    var id: String { raw["_id"]?.stringValue ?? raw.compactDescription }   // MAC address
    var version: String? { raw["CNTL"]?["Version"]?.stringValue }
}

/// Alarm-history document (`*-ALARM-HIST-STATE`).
struct AlarmHistoryDoc: MongoDocument {
    let raw: JSONValue
    init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
    var id: String { raw["_id"]?.stringValue ?? raw.compactDescription }

    // TODO: confirm key paths.
    var severity: AlarmSeverity { AlarmSeverity.from(raw) }
    var message: String? { raw["Message"]?.stringValue ?? raw["message"]?.stringValue }
    var raisedAt: String? { raw["Time"]?.stringValue ?? raw["timestamp"]?.stringValue }
}

/// One statistics sample (`STATS-*`). Timestamp + arbitrary counters.
struct StatSample: MongoDocument {
    let raw: JSONValue
    init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
    var id: String { raw["_id"]?.stringValue ?? raw.compactDescription }
    var timestamp: String? { raw["Time"]?.stringValue ?? raw["timestamp"]?.stringValue }

    /// Pull a numeric counter by key for charting.
    func metric(_ key: String) -> Double? {
        switch raw[key] {
        case .some(.double(let d)): return d
        case .some(.int(let i)): return Double(i)
        default: return nil
        }
    }
}

/// Syslog line (`SYSLOG-*`).
struct LogEntry: MongoDocument {
    let raw: JSONValue
    init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
    var id: String { raw["_id"]?.stringValue ?? raw.compactDescription }
    var severity: AlarmSeverity { AlarmSeverity.from(raw) }
    var message: String? { raw["Message"]?.stringValue ?? raw["message"]?.stringValue }
    var time: String? { raw["Time"]?.stringValue ?? raw["timestamp"]?.stringValue }
}

/// A CPE behind an ONU, with its DHCP lease — from the `/cpe/onu/<id>/` helper.
struct CPEState: MongoDocument {
    let raw: JSONValue
    init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
    init(raw: JSONValue) { self.raw = raw }
    var id: String { raw["_id"]?.stringValue ?? raw.compactDescription }
    var mac: String? { raw["_id"]?.stringValue }

    // IPv4 (DHCP)
    var dhcpState: String? { nonEmpty(raw["DHCP"]?["Client State"]?.stringValue) }
    var ipv4: String? { nonEmpty(raw["DHCP"]?["Client IP Addr"]?.stringValue) }
    var dhcpServer: String? { nonEmpty(raw["DHCP"]?["Server ID"]?.stringValue) }
    var dhcpLeaseSeconds: Int? { raw["DHCP"]?["Lease Time"]?.intValue }
    var circuitId: String? { nonEmpty(raw["DHCP"]?["Circuit ID"]?.stringValue) }
    var dhcpLastSuccess: String? { nonEmpty(raw["DHCP"]?["Last Success Time"]?.stringValue) }

    // IPv6 (DHCPv6)
    var dhcpv6State: String? { nonEmpty(raw["DHCPV6"]?["Client State"]?.stringValue) }
    var ipv6: String? { nonEmpty(raw["DHCPV6"]?["Client IP Addr"]?.stringValue) }
    var ipv6ValidLifetime: String? { nonEmpty(raw["DHCPV6"]?["Valid Lifetime"]?.stringValue) }
    var ipv6Prefix: String? {
        guard let entry = raw["DHCPV6"]?["Prefix Delegation"]?.arrayValue?.first,
              let prefixObj = entry["Prefixes"]?.arrayValue?.first,
              let prefix = prefixObj["Prefix"]?.stringValue,
              let len = prefixObj["Prefix Length"]?.intValue else { return nil }
        return "\(prefix)/\(len)"
    }

    var hasLease: Bool { ipv4 != nil || ipv6 != nil }
}

// MARK: - ONU-STATE detail accessors (status / firmware / UNI / traffic)

/// One UNI port parsed from a `UNI-ETH N` / `UNI-POTS N` entry on ONU-STATE.
struct UNIPort: Identifiable {
    let name: String
    let raw: JSONValue
    var id: String { name }
    var isEthernet: Bool { name.localizedCaseInsensitiveContains("ETH") }
    var enabled: Bool? { raw["Enable"]?.boolValue }
    var speed: String? { raw["Speed"]?.stringValue }
    var duplex: String? { raw["Duplex"]?.stringValue }
    /// Resolved link state string on ETH ports, e.g. "10G Ethernet Full Duplex".
    var state: String? { raw["State"]?.stringValue }
}

/// Per-service traffic parsed from an `OLT-PON Service N` entry on ONU-STATE.STATS.
struct PONServiceStats: Identifiable {
    let name: String
    let raw: JSONValue
    var id: String { name }
    var rxRateBps: Double? { raw["RX Rate bps"]?.doubleValue }
    var txRateBps: Double? { raw["TX Rate bps"]?.doubleValue }
    var rxTotalOctets: Double? { raw["RX Total Octets"]?.doubleValue }
    var txTotalOctets: Double? { raw["TX Total Octets"]?.doubleValue }
}

extension ONUStateDoc {
    private var onu: JSONValue? { raw["ONU"] }

    // Status / identity
    var registrationState: String? { onu?["Registration State"]?.stringValue }
    var equipmentId: String? { onu?["Equipment ID"]?.stringValue }
    var vendor: String? { onu?["Vendor"]?.stringValue }
    var ponMode: String? { onu?["PON Mode"]?.stringValue }
    var onlineTime: String? { onu?["Online Time"]?.stringValue }
    var temperatureC: Double? { raw["GPON"]?["Temperature"]?.doubleValue }

    // Firmware
    var fwVersion: String? { onu?["FW Version"]?.stringValue }
    var fwBankVersions: [String] { onu?["FW Bank Versions"]?.arrayValue?.compactMap(\.stringValue) ?? [] }
    var fwBankPtr: Int? { onu?["FW Bank Ptr"]?.intValue }
    /// Non-empty only while an upgrade is in progress / recorded.
    var fwUpgradeStatus: [String: JSONValue]? {
        let obj = onu?["FW Upgrade Status"]?.objectValue
        return (obj?.isEmpty == false) ? obj : nil
    }

    /// UNI ports, from the top-level `UNI-*` entries.
    var uniPorts: [UNIPort] {
        guard let obj = raw.objectValue else { return [] }
        return obj.keys.filter { $0.hasPrefix("UNI-") }.sorted()
            .compactMap { key in obj[key].map { UNIPort(name: key, raw: $0) } }
    }

    /// Per-service traffic, from `STATS["OLT-PON Service N"]`.
    var ponServices: [PONServiceStats] {
        guard let stats = raw["STATS"]?.objectValue else { return [] }
        return stats.keys.filter { $0.hasPrefix("OLT-PON Service ") }.sorted()
            .compactMap { key in stats[key].map { PONServiceStats(name: key, raw: $0) } }
    }
}

// MARK: - OLT-STATE detail accessors (uptime / traffic / environment / uplink)

extension OLTStateDoc {
    private var olt: JSONValue? { raw["OLT"] }
    private var ponStats: JSONValue? { raw["STATS"]?["OLT-PON"] }

    var onlineTime: String? { olt?["Online Time"]?.stringValue }
    var firmwareVersion: String? { olt?["FW Version"]?.stringValue }
    var serialNumber: String? { olt?["Serial Number"]?.stringValue }
    var model: String? { olt?["Model"]?.stringValue }
    var ponMode: String? { olt?["PON Mode"]?.stringValue }

    var laserShutdown: String? { olt?["Laser Shutdown"]?.stringValue }
    /// True when the OLT laser isn't "Laser ON" (shut down / inactive). Only
    /// meaningful when the field is present.
    var isLaserOff: Bool {
        guard let value = laserShutdown else { return false }
        return value.caseInsensitiveCompare("Laser ON") != .orderedSame
    }

    var statsTotalONUs: Int? { ponStats?["Total ONUs Count"]?.intValue }
    var statsOnlineONUs: Int? { ponStats?["Online ONUs Count"]?.intValue }
    var statsOfflineONUs: Int? { ponStats?["Offline ONUs Count"]?.intValue }
    var rxUtilPercent: Int? { ponStats?["RX BW Total Util"]?.intValue }
    var txUtilPercent: Int? { ponStats?["TX BW Total Util"]?.intValue }
    var rxRateBps: Double? { ponStats?["RX BW Ethernet Rate bps"]?.doubleValue }
    var txRateBps: Double? { ponStats?["TX BW Ethernet Rate bps"]?.doubleValue }

    var asicTempC: Int? { raw["STATS"]?["OLT-TEMP"]?["ASIC"]?.intValue }
    var voltage: Double? { raw["STATS"]?["OLT-ENV"]?["Voltage"]?.doubleValue }

    // Upstream switch (LLDP neighbour).
    var switchName: String? { nonEmpty(raw["Switch"]?["System Name"]?.stringValue) }
    var switchPort: String? { nonEmpty(raw["Switch"]?["Port ID"]?.stringValue) }
    var switchAddress: String? { nonEmpty(raw["Switch"]?["IPv4 Address"]?.stringValue) }
}

// MARK: - OLT action payloads

/// Body for `PUT /olts/<OLT>/disable-onu/<ONU>/` — sent WITHOUT the data wrapper.
struct DisableONURequest: Encodable {
    let disable: Bool
}

/// OLT action responses return `{ "Pending": true|false }`.
struct PendingStatus: Decodable {
    let pending: Bool
    enum CodingKeys: String, CodingKey { case pending = "Pending" }
}
