import Foundation
import Observation
import NucleicProtocol

// os.Logger on Darwin; the stderr shim in LinuxSupport.swift elsewhere.
#if canImport(os)
import os
#endif

/// Mesh/Covalence diagnostics tracing — Console.app category "mesh". Every event recorded in the
/// in-app `MeshDiagnosticsLog` is mirrored here, so the same trail is available via `log stream`
/// / sysdiagnose even when the viewer window was never opened.
let meshLog = Logger(subsystem: "com.nucleic", category: "mesh")

/// One structural mesh-diagnostics event (the Covalence Mesh Viewer's log rows).
///
/// **Redaction invariant** (OBSERVABILITY_AND_TESTING §A.1): events carry structural metadata
/// only — host names/ids, session ids/titles, channel names, counts, states, errors. Never a
/// prompt, a composer draft, a dispatch message body, or any other user content. Session titles
/// and host names are already broadcast mesh-wide (feed casts, presence cards), so they don't
/// widen what a log line can leak.
public struct MeshDiagnosticsEvent: Identifiable, Sendable, Equatable {
    /// What part of the mesh produced the event — the viewer's filter axis.
    public enum Category: String, CaseIterable, Sendable {
        /// Peer connections: connect/disconnect/transport changes, mesh service lifecycle.
        case peer
        /// `runner.presence` card folds — the dispatcher's candidate pool changing.
        case presence
        /// Cast traffic (publish/ingest) on the mesh channels.
        case cast
        /// User responses to approvals — approve/deny and question answers — as they ride the mesh.
        case approval
        /// Covalence dispatch: placement attempts, acks, parks, queue retries.
        case dispatch
        /// Between-turn Covalence rebalancing: sweep decisions, moves, declines.
        case rebalance
        /// Session transfers (2PC engine), both directions.
        case transfer

        /// Short human label for filter menus and row badges.
        public var label: String {
            switch self {
            case .peer: "Peers"
            case .presence: "Presence"
            case .cast: "Casts"
            case .approval: "Approvals"
            case .dispatch: "Dispatch"
            case .rebalance: "Rebalance"
            case .transfer: "Transfers"
            }
        }
    }

    public enum Level: String, Sendable {
        case info
        /// Degraded but self-healing (a peer dropped, a candidate declined, a retry parked).
        case warning
        /// Something failed and stayed failed until an external change (user action, reconnect).
        case error
    }

    /// Monotonic per-launch sequence — the stable row identity (`at` can collide within a burst).
    public let seq: UInt64
    public let at: Date
    public let category: Category
    public let level: Level
    /// The one-line event statement ("peer connected: Studio via LAN").
    public let message: String
    /// Optional secondary structural line (load ratios, ids, error text).
    public let detail: String?

    public var id: UInt64 { seq }

    public init(
        seq: UInt64, at: Date, category: Category, level: Level,
        message: String, detail: String? = nil
    ) {
        self.seq = seq
        self.at = at
        self.category = category
        self.level = level
        self.message = message
        self.detail = detail
    }
}

/// The in-memory mesh-diagnostics event log: a capped ring buffer of structural events the
/// Covalence Mesh Viewer renders live, fed by `AppStore`'s mesh choke points (`publishCast` /
/// `ingestPeerCasts` / dispatch / rebalance / transfer / peer-presence folds).
///
/// In-memory by design — this is a live debugging surface, not durable telemetry; the durable
/// analogues are the `cast_event` table and unified logging (every record here also lands on
/// the `com.nucleic`/`mesh` logger). `@MainActor` alongside `AppStore`, whose mesh paths all
/// run there already, so recording is a plain synchronous call with no ordering races.
@MainActor
@Observable
public final class MeshDiagnosticsLog {
    /// Oldest-first events, trimmed to `capacity` (the viewer renders newest at the bottom).
    public private(set) var events: [MeshDiagnosticsEvent] = []
    /// Lifetime counts per category, unaffected by ring-buffer trimming or `clear()` — the
    /// viewer's "N since launch" chips stay honest about what scrolled away.
    public private(set) var totalsByCategory: [MeshDiagnosticsEvent.Category: Int] = [:]
    /// Max retained events. 500 matches the activity channel's retention cap.
    public let capacity: Int

    private var nextSeq: UInt64 = 1

    public init(capacity: Int = 500) {
        self.capacity = max(1, capacity)
    }

    /// Append one event (and mirror it to the "mesh" unified-logging category). Structural
    /// metadata only — see the redaction invariant on `MeshDiagnosticsEvent`.
    @discardableResult
    public func record(
        _ category: MeshDiagnosticsEvent.Category,
        level: MeshDiagnosticsEvent.Level = .info,
        _ message: String, detail: String? = nil, at: Date = Date()
    ) -> MeshDiagnosticsEvent {
        let event = MeshDiagnosticsEvent(
            seq: nextSeq, at: at, category: category, level: level,
            message: message, detail: detail)
        nextSeq += 1
        events.append(event)
        if events.count > capacity {
            events.removeFirst(events.count - capacity)
        }
        totalsByCategory[category, default: 0] += 1
        let line = detail.map { "\(message) — \($0)" } ?? message
        switch level {
        case .info:
            meshLog.info("\(category.rawValue, privacy: .public): \(line, privacy: .public)")
        case .warning:
            meshLog.warning("\(category.rawValue, privacy: .public): \(line, privacy: .public)")
        case .error:
            meshLog.error("\(category.rawValue, privacy: .public): \(line, privacy: .public)")
        }
        // Debug mode (Settings ▸ Covalence ▸ Diagnostics): the same trail, durable on disk —
        // survives relaunches and ring trimming, and feeds the mesh-log export.
        MeshDebugLog.append("[\(level.rawValue)] \(category.rawValue): \(line)")
        return event
    }

    /// Drop the retained rows (lifetime totals stay — they count what happened, not what's shown).
    public func clear() {
        events.removeAll()
    }
}

/// One Covalence-managed session as the viewer shows it: the session plus this host's rebalance
/// bookkeeping (in-flight flag, cooldown), assembled by `AppStore.covalenceSessionDiagnostics()`.
public struct CovalenceSessionDiagnostic: Identifiable, Sendable, Equatable {
    public let sessionID: SessionID
    public let title: String
    public let projectName: String
    /// The device the job was issued from — excluded from every rebalance by the Covalence invariant.
    public let originDeviceID: String
    public let status: SessionStatus
    /// A rebalance transfer for this session is in flight right now.
    public let isRebalancing: Bool
    /// When this session was last evaluated for a move (nil = never this launch).
    public let lastRebalanceAttempt: Date?
    /// Seconds until the per-session rebalance cooldown lapses (0 = eligible now).
    public let cooldownRemaining: TimeInterval

    public var id: SessionID { sessionID }

    public init(
        sessionID: SessionID, title: String, projectName: String, originDeviceID: String,
        status: SessionStatus, isRebalancing: Bool, lastRebalanceAttempt: Date?,
        cooldownRemaining: TimeInterval
    ) {
        self.sessionID = sessionID
        self.title = title
        self.projectName = projectName
        self.originDeviceID = originDeviceID
        self.status = status
        self.isRebalancing = isRebalancing
        self.lastRebalanceAttempt = lastRebalanceAttempt
        self.cooldownRemaining = cooldownRemaining
    }
}
