import Foundation

/// User-facing diagnostics switches, persisted in `UserDefaults`. Currently just the
/// Smart Move log toggle (Settings → General → Diagnostics). Lives in `NucleicCore` so the
/// move pipeline (`AppStore`, `ProjectRelocator`) and the SwiftUI layer agree on the key.
public enum MoveDiagnostics {
    /// When on, the safe-move / Convert-to-Control pipeline writes a detailed trace to
    /// `logFileURL` so a slow or stuck move can be examined after the fact. Off by default —
    /// it's a diagnostic aid, not something to pay for on every move. See `MoveLog`.
    public static let loggingEnabledKey = "nucleic.move.loggingEnabled"

    /// Whether Smart Move logging is enabled app-wide.
    public static var loggingEnabled: Bool {
        UserDefaults.standard.bool(forKey: loggingEnabledKey)
    }

    /// `~/Library/Application Support/Nucleic/` — the same base the app store persists under
    /// (see `NucleicApp.supportDirectory`), recomputed here so `NucleicCore` needn't be told.
    public static var supportDirectory: URL {
        let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
        return base.appendingPathComponent("Nucleic", isDirectory: true)
    }

    /// The rolling Smart Move log file, created lazily by `MoveLog` when logging is on.
    public static var logFileURL: URL {
        supportDirectory.appendingPathComponent("SmartMove.log")
    }

    /// The previous log, kept across one rotation so a just-finished move isn't lost when the
    /// next one rolls a large file over.
    public static var previousLogFileURL: URL {
        supportDirectory.appendingPathComponent("SmartMove.previous.log")
    }
}

/// A thread-safe, append-only text logger for a single safe-move / Convert-to-Control run,
/// gated by `MoveDiagnostics.loggingEnabled`. When logging is off every method is a cheap
/// no-op, so the move pipeline can call it unconditionally.
///
/// Writes are serialized behind a lock because the pipeline logs from the main actor, a
/// background liveness `Task`, and detached copy / directory-size walks all at once. One shared
/// rolling file (`MoveDiagnostics.logFileURL`); each run brackets its lines with a header /
/// footer and tags every line with its short `label`, so interleaved moves of different projects
/// stay legible and finished runs remain on disk for comparison.
public final class MoveLog: @unchecked Sendable {
    private let label: String
    private let start: Date
    private let lock = NSLock()
    /// nil when logging is disabled or after `finish()`; guarded by `lock`.
    private var handle: FileHandle?

    /// The on-disk log file, or nil when logging is disabled / the file couldn't be opened.
    public let fileURL: URL?

    /// Whether this logger is actually writing (logging on and the file opened).
    public var isEnabled: Bool { fileURL != nil }

    /// Open (creating / rotating as needed) the shared move log and write a run header. Reads the
    /// global `MoveDiagnostics.loggingEnabled` toggle once — a move is short-lived, so a mid-move
    /// toggle change needn't take effect. `label` is a short human tag (e.g. the project name).
    public convenience init(label: String) {
        self.init(label: label,
                  directory: MoveDiagnostics.supportDirectory,
                  enabled: MoveDiagnostics.loggingEnabled)
    }

    /// Designated initializer with an explicit directory + enabled flag, so tests can write to a
    /// temp dir without touching real Application Support or the global toggle.
    init(label: String, directory: URL, enabled: Bool) {
        self.label = label
        self.start = Date()
        guard enabled else { handle = nil; fileURL = nil; return }

        let fm = FileManager.default
        let url = directory.appendingPathComponent("SmartMove.log")
        try? fm.createDirectory(at: directory, withIntermediateDirectories: true)
        // Rotate a large log so the file stays openable in a text editor.
        if let size = (try? fm.attributesOfItem(atPath: url.path)[.size]) as? Int, size > 2_000_000 {
            let rolled = directory.appendingPathComponent("SmartMove.previous.log")
            try? fm.removeItem(at: rolled)
            try? fm.moveItem(at: url, to: rolled)
        }
        if !fm.fileExists(atPath: url.path) { fm.createFile(atPath: url.path, contents: nil) }
        let opened = try? FileHandle(forWritingTo: url)
        _ = try? opened?.seekToEnd()
        self.handle = opened
        self.fileURL = opened == nil ? nil : url
        line("════════ move “\(label)” started ════════")
    }

    /// Append one timestamped, run-tagged line. Thread-safe; a no-op when logging is off.
    public func line(_ message: String) {
        let stamp = Self.stampFormatter.string(from: Date())
        let elapsed = String(format: "%7.1fs", Date().timeIntervalSince(start))
        let text = "[\(stamp)] [+\(elapsed)] [\(label)] \(message)\n"
        guard let data = text.data(using: .utf8) else { return }
        lock.lock(); defer { lock.unlock() }
        guard let handle else { return }
        try? handle.write(contentsOf: data)
    }

    /// Write a footer and close the file. Safe to call once; later `line` calls become no-ops
    /// (the handle is dropped under the lock, so a late async callback can't write to a closed fd).
    public func finish(_ summary: String) {
        line("──────── move “\(label)” finished: \(summary) ────────")
        lock.lock(); defer { lock.unlock() }
        try? handle?.close()
        handle = nil
    }

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