import Foundation

/// The Covalence-mesh debug trail (Settings ▸ Covalence ▸ Diagnostics): when the toggle is on,
/// every `MeshDiagnosticsLog` event is also appended to a rolling file, so a failing relay
/// or mesh can be diagnosed *after the fact* and across relaunches — the in-memory ring
/// only holds this launch's last 500 events, and unified logging is awkward to pull from a
/// sandboxed app. Same redaction invariant as the ring: structural metadata only.
///
/// Modeled on `MoveLog`: rolling file under `Application Support/Nucleic/`, one rotation
/// generation kept. Appends are serialized on a utility queue so the `@MainActor` recorder
/// never blocks on disk.
public enum MeshDebugLog {
    /// Settings toggle key (Settings ▸ Covalence ▸ Diagnostics ▸ "Mesh debug logging").
    public static let enabledKey = "nucleic.mesh.debugLoggingEnabled"

    public static var enabled: Bool {
        UserDefaults.standard.bool(forKey: enabledKey)
    }

    public static var logFileURL: URL {
        MoveDiagnostics.supportDirectory.appendingPathComponent("CovalenceMesh.log")
    }

    public static var previousLogFileURL: URL {
        MoveDiagnostics.supportDirectory.appendingPathComponent("CovalenceMesh.previous.log")
    }

    private static let queue = DispatchQueue(label: "com.nucleic.mesh-debug-log", qos: .utility)
    private static let rotateBytes = 2_000_000

    /// Append one already-formatted event line (timestamped here). Cheap no-op when the
    /// toggle is off; never throws — a full disk must not affect the mesh.
    public static func append(_ line: String) {
        guard enabled else { return }
        let stamp = stampFormatter.string(from: Date())
        queue.async { write("[\(stamp)] \(line)\n") }
    }

    /// The tail of the current log (and how much was skipped), for the export bundle.
    /// nil when no log exists yet.
    public static func recentText(maxBytes: Int = 512 * 1024) -> String? {
        queue.sync { tail(of: logFileURL, maxBytes: maxBytes) }
    }

    /// The tail of the rotated previous generation — the detailed export reaches back one
    /// rotation, so a problem that scrolled past the 2 MB mark is still recoverable.
    /// nil when nothing has rotated yet.
    public static func previousText(maxBytes: Int = 512 * 1024) -> String? {
        queue.sync { tail(of: previousLogFileURL, maxBytes: maxBytes) }
    }

    private static func tail(of url: URL, maxBytes: Int) -> String? {
        guard let data = try? Data(contentsOf: url), !data.isEmpty else { return nil }
        let bytes = data.count > maxBytes ? data.suffix(maxBytes) : data
        var text = String(decoding: bytes, as: UTF8.self)
        if data.count > maxBytes {
            text = "… (\(data.count - maxBytes) earlier bytes trimmed; full file at \(url.path))\n"
                + text
        }
        return text
    }

    private static func write(_ text: String) {
        let fm = FileManager.default
        let url = logFileURL
        try? fm.createDirectory(
            at: MoveDiagnostics.supportDirectory, withIntermediateDirectories: true)
        if let size = (try? fm.attributesOfItem(atPath: url.path)[.size]) as? Int,
            size > rotateBytes
        {
            try? fm.removeItem(at: previousLogFileURL)
            try? fm.moveItem(at: url, to: previousLogFileURL)
        }
        if !fm.fileExists(atPath: url.path) { fm.createFile(atPath: url.path, contents: nil) }
        guard let handle = try? FileHandle(forWritingTo: url) else { return }
        defer { try? handle.close() }
        _ = try? handle.seekToEnd()
        try? handle.write(contentsOf: Data(text.utf8))
    }

    private static let stampFormatter: DateFormatter = {
        let f = DateFormatter()
        f.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
        return f
    }()
}
