import Foundation
import WatchConnectivity
import os

// In-memory ring buffer + os.Logger bridge for live diagnostics.
// Originally fed an in-app `DebugInfoView` overlay; that overlay was
// retired once `tail-logs.sh` made `watch-app.log` reliable as the
// off-device diagnostic surface. The `@Published` counters are kept
// so future programmatic surfaces (accessibility summary, settings-
// side health card) can subscribe without re-plumbing.
//
// `xcrun simctl spawn <watch_udid> log stream --predicate 'subsystem
// == "ht.halflig.app.watchkitapp"'` picks every event up off-device.
//
// MainActor-isolated so SwiftUI views can subscribe via @ObservedObject
// without bouncing through actor hops. Cross-actor callers (the
// WatchAPIClient actor, the WCSessionDelegate's private dispatch queue)
// reach in via `await MainActor.run { … }`.
//
// Privacy note: we ship instance, username, accountId tail, request URL
// host+path, response status, and error strings. Never the access
// token. Response bodies are not stored. All bridged interpolations are
// marked `.public` because the call sites have already passed the
// redaction discipline; without `.public` the same lines collapse to
// `<private>` under TestFlight signing.

private enum LogBridge {
    static let net = Logger(subsystem: "ht.halflig.app.watchkitapp", category: "net")
    static let wc = Logger(subsystem: "ht.halflig.app.watchkitapp", category: "wc")
    static let auth = Logger(subsystem: "ht.halflig.app.watchkitapp", category: "auth")
    static let ui = Logger(subsystem: "ht.halflig.app.watchkitapp", category: "ui")
    static let img = Logger(subsystem: "ht.halflig.app.watchkitapp", category: "img")
}

@MainActor
final class DebugRecorder: ObservableObject {
    // `nonisolated(unsafe)` on the singleton + `nonisolated` on init lets
    // any context (SwiftUI View struct construction off the main actor,
    // the WatchAPIClient actor, the WCSessionDelegate dispatch queue)
    // resolve the reference without an `await`. All *mutating* surface
    // stays MainActor-isolated by the class-level annotation, so callers
    // still bounce through `await MainActor.run { … }` to actually write.
    nonisolated(unsafe) static let shared = DebugRecorder()

    nonisolated private init() {}

    enum Kind: String {
        case net = "NET"
        case wc = "WC"
        case auth = "AUTH"
        case ui = "UI"
        case img = "IMG"

        fileprivate var logger: Logger {
            switch self {
            case .net: return LogBridge.net
            case .wc: return LogBridge.wc
            case .auth: return LogBridge.auth
            case .ui: return LogBridge.ui
            case .img: return LogBridge.img
            }
        }
    }

    struct Event: Identifiable {
        let id = UUID()
        let at: Date
        let kind: Kind
        let text: String
    }

    @Published private(set) var events: [Event] = []

    @Published private(set) var lastRequestURL: String?
    @Published private(set) var lastRequestMethod: String?
    @Published private(set) var lastRequestStartedAt: Date?
    @Published private(set) var lastRequestStatus: Int?
    @Published private(set) var lastRequestError: String?
    @Published private(set) var lastRequestDurationMs: Int?
    @Published private(set) var totalRequests: Int = 0
    @Published private(set) var totalRequestErrors: Int = 0

    @Published private(set) var lastContextAt: Date?
    @Published private(set) var lastContextKeys: [String]?
    @Published private(set) var lastMessageAt: Date?
    @Published private(set) var lastMessageType: String?

    @Published private(set) var sessionActivationStateRaw: Int?
    @Published private(set) var sessionActivationError: String?

    @Published private(set) var lastImageURL: String?
    @Published private(set) var lastImageStatus: Int?
    @Published private(set) var lastImageError: String?
    @Published private(set) var lastImageDurationMs: Int?
    @Published private(set) var pendingImages: Int = 0
    @Published private(set) var totalImageOk: Int = 0
    @Published private(set) var totalImageErr: Int = 0

    private let capacity = 60

    func note(_ kind: Kind, _ text: String) {
        kind.logger.notice("\(text, privacy: .public)")
        let e = Event(at: Date(), kind: kind, text: text)
        events.insert(e, at: 0)
        if events.count > capacity {
            events.removeLast(events.count - capacity)
        }
    }

    // MARK: - Network

    @discardableResult
    func requestStart(method: String, url: URL) -> Date {
        let now = Date()
        lastRequestURL = url.absoluteString
        lastRequestMethod = method
        lastRequestStartedAt = now
        lastRequestStatus = nil
        lastRequestError = nil
        lastRequestDurationMs = nil
        totalRequests += 1
        let host = url.host ?? "?"
        note(.net, "→ \(method) \(host)\(url.path)")
        return now
    }

    func requestSuccess(start: Date, status: Int) {
        let dur = Int(Date().timeIntervalSince(start) * 1000)
        lastRequestStatus = status
        lastRequestError = nil
        lastRequestDurationMs = dur
        note(.net, "← \(status) (\(dur)ms)")
    }

    func requestFailure(start: Date, error: String) {
        let dur = Int(Date().timeIntervalSince(start) * 1000)
        lastRequestStatus = nil
        lastRequestError = error
        lastRequestDurationMs = dur
        totalRequestErrors += 1
        note(.net, "← ERR \(prefix80(error))")
    }

    // MARK: - WCSession

    func wcActivation(state: WCSessionActivationState, error: Error?) {
        sessionActivationStateRaw = state.rawValue
        sessionActivationError = error?.localizedDescription
        let label: String
        switch state {
        case .notActivated: label = "notActivated"
        case .inactive: label = "inactive"
        case .activated: label = "activated"
        @unknown default: label = "?"
        }
        if let e = error {
            note(.wc, "activate=\(label) err=\(prefix80(e.localizedDescription))")
        } else {
            note(.wc, "activate=\(label)")
        }
    }

    func wcContext(_ ctx: [String: Any]) {
        lastContextAt = Date()
        lastContextKeys = ctx.keys.sorted()
        let type = ctx["type"] as? String ?? "?"
        note(.wc, "ctx type=\(type) keys=\(ctx.count)")
    }

    func wcMessage(_ msg: [String: Any]) {
        lastMessageAt = Date()
        let type = msg["type"] as? String ?? "?"
        lastMessageType = type
        note(.wc, "msg type=\(type)")
    }

    func wcUserInfo(_ info: [String: Any]) {
        lastMessageAt = Date()
        let type = info["type"] as? String ?? "?"
        lastMessageType = type
        note(.wc, "userInfo type=\(type)")
    }

    // MARK: - Auth

    func authApply(creds: AuthState.Credentials?, hint: Bool?) {
        if let c = creds {
            let h = hint.map { $0 ? "true" : "false" } ?? "-"
            note(.auth, "apply @\(c.username)@\(c.instance) hint=\(h)")
        } else {
            note(.auth, "apply nil")
        }
    }

    func authApplyFastPath() {
        note(.auth, "apply fast-path (no-op, ts only)")
    }

    func authCacheCleared() {
        note(.auth, "URLCache cleared (same-instance switch)")
    }

    func authCapabilityRefresh(supports: Bool) {
        note(.auth, "caps supportsTapestry=\(supports)")
    }

    func authClear() {
        note(.auth, "clear")
    }

    // MARK: - Image (AsyncImage probe — see MediaFeedView.MediaCell)

    @discardableResult
    func imgStart(url: URL) -> Date {
        let now = Date()
        lastImageURL = url.absoluteString
        lastImageStatus = nil
        lastImageError = nil
        lastImageDurationMs = nil
        pendingImages += 1
        let host = url.host ?? "?"
        note(.img, "→ \(host)\(prefix80(url.path))")
        return now
    }

    func imgSuccess(start: Date, status: Int, url: URL) {
        let dur = Int(Date().timeIntervalSince(start) * 1000)
        lastImageURL = url.absoluteString
        lastImageStatus = status
        lastImageError = nil
        lastImageDurationMs = dur
        pendingImages = max(0, pendingImages - 1)
        totalImageOk += 1
        note(.img, "← \(status) \(hostTail(url)) (\(dur)ms)")
    }

    /// Image fetch failed. `status` is set when the server returned a
    /// non-2xx response (so a 403 / 404 / 5xx is greppable as a failure
    /// next to transport errors); `nil` when the URLSession threw before
    /// any HTTP response arrived (TLS, timeout, no-route). `error` should
    /// carry the URLError code prefix on transport failures — the
    /// platform's `localizedDescription` collapses every transport flavour
    /// to "The operation couldn't be completed.…", which is content-free
    /// for remote triage.
    func imgFailure(start: Date, error: String, status: Int? = nil, url: URL) {
        let dur = Int(Date().timeIntervalSince(start) * 1000)
        lastImageURL = url.absoluteString
        lastImageStatus = status
        lastImageError = error
        lastImageDurationMs = dur
        pendingImages = max(0, pendingImages - 1)
        totalImageErr += 1
        let head = status.map { "\($0)" } ?? "ERR"
        note(.img, "← \(head) \(hostTail(url)) (\(dur)ms) \(prefix80(error))")
    }

    // MARK: - UI

    func uiTaskFired(_ name: String, accountId: String?) {
        let id = accountId.map { String($0.suffix(6)) } ?? "nil"
        note(.ui, "\(name) id=\(id)")
    }

    func uiNote(_ text: String) {
        note(.ui, text)
    }

    private func prefix80(_ s: String) -> String {
        s.count <= 80 ? s : String(s.prefix(80)) + "…"
    }

    /// Compact `host/…tail` rendering for log lines that already carry a
    /// timestamp + duration. Keeps every line groupable by host (which is
    /// what `watch-img.log` aggregation cares about) without exploding
    /// the line width with a full URL.
    private func hostTail(_ url: URL) -> String {
        let host = url.host ?? "?"
        let path = url.path
        let tail = path.count <= 40 ? path : "…" + String(path.suffix(40))
        return "\(host)\(tail)"
    }
}
