import Foundation
import UIKit

/// File-backed, category-tagged logger any client (URLSession,
/// WKWebView delegate, gate-sheet polling loop) can append to. The
/// goal is forensic: when something fails opaquely on a real device
/// (Turnstile won't pass, /api/token always 403, FlightAware schema
/// drift), the user can hit Settings → Tools → Diagnostics, run the
/// failing scenario, share the resulting log file, and we have the
/// exact request/response/cookie/JS-console trail to reason from.
///
/// Format: each line is `<ISO8601>\t[CATEGORY]\tEVENT\tk=v\tk=v...`
/// — TSV-shaped so a quick `grep` / `awk` / `cut` slices any field
/// without fragile regex.
///
/// One log file per app session: `Documents/Diagnostics/diag-<ts>.log`.
/// Documents is iCloud-backed and exposed via the Files app, so the
/// user can AirDrop it without needing a custom share button.
/// (We add one anyway in ``DiagnosticsView``.)
///
/// Writes go through a serial dispatch queue so callers can log from
/// any thread / actor without races.
final class DiagnosticLogger: @unchecked Sendable {
    static let shared = DiagnosticLogger()

    private let queue = DispatchQueue(label: "com.flights.diagnostic.logger", qos: .utility)
    private var fileHandle: FileHandle?
    let sessionID: String
    let logFileURL: URL?

    /// Master enable. Off by default in production so we don't spew
    /// to disk during normal use; the user flips it on from the
    /// Diagnostics screen before running a failing scenario.
    private(set) var isEnabled: Bool = true

    private init() {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyyMMdd-HHmmss"
        formatter.timeZone = TimeZone(identifier: "UTC")
        formatter.locale = Locale(identifier: "en_US_POSIX")
        self.sessionID = formatter.string(from: Date())

        guard let docs = FileManager.default.urls(
            for: .documentDirectory, in: .userDomainMask
        ).first else {
            self.logFileURL = nil
            return
        }
        let dir = docs.appendingPathComponent("Diagnostics", isDirectory: true)
        try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
        let url = dir.appendingPathComponent("diag-\(sessionID).log")
        if !FileManager.default.fileExists(atPath: url.path) {
            FileManager.default.createFile(atPath: url.path, contents: nil)
        }
        self.logFileURL = url
        self.fileHandle = try? FileHandle(forWritingTo: url)
        try? self.fileHandle?.seekToEnd()
        writeBootHeader()
    }

    // MARK: - Public API

    func setEnabled(_ enabled: Bool) {
        queue.async { [weak self] in
            self?.isEnabled = enabled
        }
    }

    /// Append a single event. `fields` becomes tab-separated `k=v`
    /// pairs. Values are flattened to `String(describing:)` then
    /// have tabs / newlines escaped so the line stays parseable.
    func log(_ category: String, _ event: String, _ fields: [String: Any] = [:]) {
        guard isEnabled else { return }
        let ts = Self.timestamp()
        var line = "\(ts)\t[\(category)]\t\(event)"
        // Sorted keys → deterministic order in the file.
        for k in fields.keys.sorted() {
            guard let v = fields[k] else { continue }
            line += "\t\(k)=\(Self.escape("\(v)"))"
        }
        line += "\n"
        guard let data = line.data(using: .utf8) else { return }
        queue.async { [weak self] in
            self?.fileHandle?.write(data)
        }
    }

    /// Flush and return all log files (newest first) so the
    /// diagnostics screen can list them.
    func allLogFiles() -> [URL] {
        guard let dir = logFileURL?.deletingLastPathComponent() else { return [] }
        guard let contents = try? FileManager.default.contentsOfDirectory(
            at: dir, includingPropertiesForKeys: [.contentModificationDateKey, .fileSizeKey]
        ) else { return [] }
        return contents
            .filter { $0.pathExtension == "log" }
            .sorted { (a, b) in
                let aDate = (try? a.resourceValues(forKeys: [.contentModificationDateKey])
                    .contentModificationDate) ?? .distantPast
                let bDate = (try? b.resourceValues(forKeys: [.contentModificationDateKey])
                    .contentModificationDate) ?? .distantPast
                return aDate > bDate
            }
    }

    /// Wipe all log files. Currently-open session continues into a
    /// fresh file at the same path. Used by the "Clear all" button.
    func clearAll() {
        let openURL = logFileURL
        for url in allLogFiles() where url != openURL {
            try? FileManager.default.removeItem(at: url)
        }
        // Truncate the current session's file too.
        if let url = openURL {
            queue.async { [weak self] in
                try? self?.fileHandle?.close()
                try? "".write(to: url, atomically: true, encoding: .utf8)
                self?.fileHandle = try? FileHandle(forWritingTo: url)
                try? self?.fileHandle?.seekToEnd()
                self?.writeBootHeader()
            }
        }
    }

    // MARK: - Boot header (device fingerprint)

    /// Writes a structured header with device + app context so the
    /// log is self-describing when shared.
    private func writeBootHeader() {
        let info = Bundle.main.infoDictionary ?? [:]
        let appVersion = info["CFBundleShortVersionString"] as? String ?? "?"
        let appBuild = info["CFBundleVersion"] as? String ?? "?"
        let device = UIDevice.current
        let screen = UIScreen.main
        let locale = Locale.current
        let tz = TimeZone.current.identifier
        let isSim: Bool = {
            #if targetEnvironment(simulator)
            return true
            #else
            return false
            #endif
        }()
        log("BOOT", "session", [
            "sessionID": sessionID,
            "isSimulator": isSim,
            "appVersion": appVersion,
            "appBuild": appBuild,
            "deviceModel": device.model,
            "systemName": device.systemName,
            "systemVersion": device.systemVersion,
            "name": device.name,
            "screen": "\(screen.bounds.width)x\(screen.bounds.height)@\(screen.scale)",
            "locale": locale.identifier,
            "tz": tz,
            "preferredLanguages": Locale.preferredLanguages.prefix(3).joined(separator: ","),
        ])
    }

    // MARK: - Helpers

    private static func timestamp() -> String {
        let f = ISO8601DateFormatter()
        f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        return f.string(from: Date())
    }

    /// Escape tabs/newlines so a value can't break the TSV shape.
    private static func escape(_ s: String) -> String {
        s.replacingOccurrences(of: "\t", with: " ")
         .replacingOccurrences(of: "\n", with: "\\n")
         .replacingOccurrences(of: "\r", with: "\\r")
    }
}
