import Foundation

enum DevelopmentDiagnostics {
    static let logPath: String = {
        let baseURL = FileManager.default.homeDirectoryForCurrentUser
            .appendingPathComponent("Library", isDirectory: true)
            .appendingPathComponent("Logs", isDirectory: true)
            .appendingPathComponent("Aligner", isDirectory: true)
        return baseURL.appendingPathComponent("aligner-dev.log").path
    }()

    private static let queue = DispatchQueue(label: "com.ar.Aligner.development-diagnostics")
    private static let maxLogBytes: UInt64 = 10 * 1024 * 1024
    private static let runID = UUID().uuidString
    static func log(_ event: String, _ fields: [String: CustomStringConvertible?] = [:]) {
        guard ProcessInfo.processInfo.environment["ALIGNER_DEV_LOG_DISABLED"] != "1" else {
            return
        }

        let normalizedFields = fields.reduce(into: [String: String]()) { result, item in
            guard let value = item.value else { return }
            result[item.key] = String(describing: value)
        }

        queue.async {
            write(event: event, fields: normalizedFields)
        }
    }

    static func logSync(_ event: String, _ fields: [String: CustomStringConvertible?] = [:]) {
        guard ProcessInfo.processInfo.environment["ALIGNER_DEV_LOG_DISABLED"] != "1" else {
            return
        }

        let normalizedFields = fields.reduce(into: [String: String]()) { result, item in
            guard let value = item.value else { return }
            result[item.key] = String(describing: value)
        }

        queue.sync {
            write(event: event, fields: normalizedFields)
        }
    }

    static func argumentSummary(_ arguments: [String]) -> String {
        arguments
            .dropFirst()
            .map { argument in
                if argument.hasPrefix("--") {
                    return argument.split(separator: "=", maxSplits: 1).first.map(String.init) ?? "--unknown"
                }

                return "<argument>"
            }
            .joined(separator: ",")
    }

    static func stableFingerprint(_ value: String?) -> String {
        let text = value ?? ""
        var hash: UInt64 = 0xcbf29ce484222325
        for byte in text.utf8 {
            hash ^= UInt64(byte)
            hash = hash &* 0x100000001b3
        }
        return String(format: "%016llx", hash)
    }

    static func pathSummary(_ path: String?) -> [String: String] {
        guard let path, !path.isEmpty else {
            return [
                "present": "false",
                "locationKind": "missing",
                "basename": "",
                "fingerprint": stableFingerprint("")
            ]
        }

        return [
            "present": "true",
            "locationKind": locationKind(for: path),
            "basename": URL(fileURLWithPath: path).lastPathComponent,
            "fingerprint": stableFingerprint(path)
        ]
    }

    static var includesSensitiveFields: Bool {
        let value = ProcessInfo.processInfo.environment["ALIGNER_DIAGNOSTICS_INCLUDE_SENSITIVE"]?
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .lowercased()
        return value == "1" || value == "true" || value == "yes"
    }

    static func errorSummaryFields(_ error: Error, prefix: String = "error") -> [String: String] {
        let nsError = error as NSError
        var result: [String: String] = [
            "\(prefix)Domain": nsError.domain,
            "\(prefix)Code": String(nsError.code),
            "\(prefix)DescriptionHash": stableFingerprint(nsError.localizedDescription),
            "\(prefix)DescriptionLength": String(nsError.localizedDescription.count)
        ]

        if let filePath = nsError.userInfo[NSFilePathErrorKey] as? String {
            let summary = pathSummary(filePath)
            result["\(prefix)FilePathKind"] = summary["locationKind"]
            result["\(prefix)FilePathBasename"] = summary["basename"]
            result["\(prefix)FilePathHash"] = summary["fingerprint"]
        }

        if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError {
            result["\(prefix)UnderlyingDomain"] = underlyingError.domain
            result["\(prefix)UnderlyingCode"] = String(underlyingError.code)
        }

        return result
    }

    static func errorSummaryString(_ error: Error) -> String {
        let fields = errorSummaryFields(error)
        let domain = fields["errorDomain"] ?? "unknown"
        let code = fields["errorCode"] ?? "unknown"
        let descriptionHash = fields["errorDescriptionHash"] ?? stableFingerprint("")
        return "domain=\(domain) code=\(code) descriptionHash=\(descriptionHash)"
    }

    private static func locationKind(for path: String) -> String {
        let homePath = FileManager.default.homeDirectoryForCurrentUser.path
        let userApplicationsPath = homePath + "/Applications/"

        if path.hasPrefix(userApplicationsPath) {
            return "userApplications"
        }
        if path.hasPrefix("/Applications/") {
            return "systemApplications"
        }
        if path.hasPrefix("/Users/") && path.contains("/Projects/Aligner/03-O/C2.builds/") {
            return "alignerBuilds"
        }
        if path.hasPrefix("/Users/") && path.contains("/Projects/Aligner/03-O/C1.source/") {
            return "alignerSource"
        }
        if path.hasPrefix("/tmp/") || path.hasPrefix("/private/tmp/") {
            return "temporary"
        }
        if path.hasPrefix(homePath + "/") {
            return "userHome"
        }
        return "other"
    }

    private static func write(event: String, fields: [String: String]) {
        let fileURL = URL(fileURLWithPath: logPath)
        let directoryURL = fileURL.deletingLastPathComponent()

        do {
            try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true)
            rotateIfNeeded(fileURL: fileURL)

            var payload = fields
            payload["ts"] = timestampString()
            payload["event"] = event
            payload["pid"] = String(ProcessInfo.processInfo.processIdentifier)
            payload["runID"] = runID

            let line = payload
                .sorted { $0.key < $1.key }
                .map { "\($0.key)=\(escape($0.value))" }
                .joined(separator: " ")
                + "\n"

            if FileManager.default.fileExists(atPath: fileURL.path) {
                let handle = try FileHandle(forWritingTo: fileURL)
                try handle.seekToEnd()
                if let data = line.data(using: .utf8) {
                    try handle.write(contentsOf: data)
                }
                try handle.close()
            } else {
                try line.write(to: fileURL, atomically: true, encoding: .utf8)
            }
        } catch {
            fputs("Aligner development diagnostics failed: \(errorSummaryString(error))\n", stderr)
        }
    }

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

    private static func rotateIfNeeded(fileURL: URL) {
        guard
            let attributes = try? FileManager.default.attributesOfItem(atPath: fileURL.path),
            let size = attributes[.size] as? UInt64,
            size > maxLogBytes
        else {
            return
        }

        let archiveURL = fileURL
            .deletingLastPathComponent()
            .appendingPathComponent("aligner-dev.previous.log")
        try? FileManager.default.removeItem(at: archiveURL)
        try? FileManager.default.moveItem(at: fileURL, to: archiveURL)
    }

    private static func escape(_ value: String) -> String {
        if value.rangeOfCharacter(from: .whitespacesAndNewlines) == nil,
           !value.contains("=") {
            return value
        }

        return "\""
            + value
                .replacingOccurrences(of: "\\", with: "\\\\")
                .replacingOccurrences(of: "\"", with: "\\\"")
                .replacingOccurrences(of: "\n", with: "\\n")
            + "\""
    }
}
