// NTPProtocolAnalyzer.swift
//
// Per-packet NTPv4 analyzer. The host instantiates this class once per
// captured NTP packet on a background thread.

import Foundation
import CPAPluginSDK

final class NTPProtocolAnalyzer: NSObject, CPAPluginAnalyzerProtocol, @unchecked Sendable {

    private let payloadBytes: UInt16
    private let dissector = NTPDissector()

    @objc dynamic var ntpSize: NSNumber {
        NSNumber(value: payloadBytes)
    }

    @objc dynamic var ntpMode: String {
        guard let message = dissector.parseMessage(headerData) else { return "" }
        return message.mode.displayName
    }

    @objc dynamic var ntpStratum: NSNumber {
        guard let message = dissector.parseMessage(headerData) else { return 0 }
        return NSNumber(value: message.stratum)
    }

    @objc dynamic var ntpReference: String {
        guard let message = dissector.parseMessage(headerData) else { return "" }
        return message.referenceID.displayValue
    }

    @objc dynamic var ntpProtocolInfo: String {
        dissector.protocolInfo(headerData) ?? ""
    }

    private let headerData: Data

    /// The `AnalyzerToggleSpec` the principal class declares for
    /// `Settings ▸ Analyzers ▸ NTP Analyzer`, read back at dissection time.
    private static var usesLocalTimestamps: Bool {
        UserDefaults.standard.bool(forKey: NTPPlugInController.kLocalTimestamps)
    }

    private let utcFormatter: ISO8601DateFormatter = {
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        return formatter
    }()

    private let localFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS ZZZZZ"
        return formatter
    }()

    required init?(data: Data, packet: Any?) {
        guard data.count >= NTPDissector.headerSize else { return nil }
        payloadBytes = UInt16(clamping: data.count)
        headerData = data
        super.init()
    }

    // MARK: - CPAPluginAnalyzerProtocol

    func protocolSize() -> UInt16 { payloadBytes }
    func protocolName() -> String? { nil }
    func protocolInfo() -> String? { "" }
    func protocolInfo(_ data: Data) -> String? { dissector.protocolInfo(data) }
    func nextProtocol() -> String? { nil }
    func hasPayload() -> Bool { payloadBytes > NTPDissector.headerSize }
    func protocolNodes() -> [Any]? { [] }

    func protocolNodes(_ data: Data) -> [Any] {
        let bundle = Bundle(for: NTPPlugInController.self)
        guard let message = dissector.parseMessage(data) else {
            return [unparsableNode(bundle: bundle, dataLength: data.count)]
        }
        return [buildNodeTree(message: message, bundle: bundle, fullLength: data.count)]
    }

    func value(_ attributeKey: String, data: Data) -> Any? {
        dissector.protocolProperty[attributeKey]?(data)
    }

    // MARK: - Node tree

    private func buildNodeTree(message: NTPMessage, bundle: Bundle, fullLength: Int) -> CPANodeProtocol {
        let root = CPANode.nodeWithName(bundle.localized("NTP-Header"))
        root.headerLocation = 0
        root.headerLength = UInt16(NTPDissector.headerSize)
        root.toolTip = dissector.protocolInfo(headerData) ?? ""

        let flagsNode = CPANode.nodeWithName(bundle.localized("Flags"))
        flagsNode.headerLocation = 0
        flagsNode.headerLength = 1
        flagsNode.addChild(field("Leap Indicator", "\(message.leapIndicator.rawValue) — \(message.leapIndicator.displayName)", offset: 0, length: 1, bundle: bundle))
        flagsNode.addChild(field("Version", "\(message.versionNumber)", offset: 0, length: 1, bundle: bundle))
        flagsNode.addChild(field("Mode", "\(message.mode.rawValue) — \(message.mode.displayName)", offset: 0, length: 1, bundle: bundle))
        root.addChild(flagsNode)

        root.addChild(field("Stratum", stratumDescription(message.stratum), offset: 1, length: 1, bundle: bundle))
        root.addChild(field("Poll", "\(message.poll) (interval ≈ \(formatPower2Seconds(exponent: Int(message.poll))))", offset: 2, length: 1, bundle: bundle))
        root.addChild(field("Precision", "\(message.precision) (≈ \(formatPower2Seconds(exponent: Int(message.precision))))", offset: 3, length: 1, bundle: bundle))
        root.addChild(field("Root Delay", String(format: "%.6f s", message.rootDelaySeconds), offset: 4, length: 4, bundle: bundle))
        root.addChild(field("Root Dispersion", String(format: "%.6f s", message.rootDispersionSeconds), offset: 8, length: 4, bundle: bundle))
        root.addChild(field("Reference ID", message.referenceID.displayValue, offset: 12, length: 4, bundle: bundle))

        root.addChild(timestampNode("Reference Timestamp", message.referenceTimestamp, offset: 16, bundle: bundle))
        root.addChild(timestampNode("Origin Timestamp", message.originTimestamp, offset: 24, bundle: bundle))
        root.addChild(timestampNode("Receive Timestamp", message.receiveTimestamp, offset: 32, bundle: bundle))
        root.addChild(timestampNode("Transmit Timestamp", message.transmitTimestamp, offset: 40, bundle: bundle))

        if fullLength > NTPDissector.headerSize {
            let extra = fullLength - NTPDissector.headerSize
            let extNode = CPANode.nodeWithName(bundle.localized("Extension / Auth"), value: "\(extra) bytes")
            extNode.headerLocation = UInt16(NTPDissector.headerSize)
            extNode.headerLength = UInt16(clamping: extra)
            root.addChild(extNode)
        }

        return root
    }

    private func unparsableNode(bundle: Bundle, dataLength: Int) -> CPANodeProtocol {
        let node = CPANode.nodeWithName(bundle.localized("NTP-Header"), value: bundle.localized("Unparsable"))
        node.headerLocation = 0
        node.headerLength = UInt16(clamping: dataLength)
        return node
    }

    private func field(_ key: String, _ value: String, offset: UInt16, length: UInt16, bundle: Bundle) -> CPANodeProtocol {
        let node = CPANode.nodeWithName(bundle.localized(key), value: value)
        node.headerLocation = offset
        node.headerLength = length
        return node
    }

    /// Renders a timestamp as UTC ISO 8601, or in the user's time zone when the
    /// "Show NTP timestamps in local time" switch is on.
    ///
    /// Reading the switch here — per packet, off the main thread — is the whole
    /// point of a settings toggle: `UserDefaults` reads are thread-safe, and the
    /// host posts `kClearAnalyzingCaches` when the user flips it, so already
    /// analyzed packets are re-dissected with the new setting.
    private func timestampNode(_ key: String, _ timestamp: NTPTimestamp, offset: UInt16, bundle: Bundle) -> CPANodeProtocol {
        let displayValue: String
        if let date = timestamp.date {
            displayValue = NTPProtocolAnalyzer.usesLocalTimestamps
                ? localFormatter.string(from: date)
                : utcFormatter.string(from: date)
        } else {
            displayValue = timestamp.rawValue
        }
        let node = CPANode.nodeWithName(bundle.localized(key), value: displayValue)
        node.headerLocation = offset
        node.headerLength = 8
        return node
    }

    private func stratumDescription(_ stratum: UInt8) -> String {
        switch stratum {
        case 0:
            return "0 (kiss-of-death / unspecified)"
        case 1:
            return "1 (primary reference)"
        case 2...15:
            return "\(stratum) (secondary reference)"
        case 16:
            return "16 (unsynchronized)"
        default:
            return "\(stratum) (reserved)"
        }
    }

    private func formatPower2Seconds(exponent: Int) -> String {
        let value = pow(2.0, Double(exponent))
        if value >= 1.0 {
            return String(format: "%.0f s", value)
        }
        if value >= 0.001 {
            return String(format: "%.3f s", value)
        }
        return String(format: "%.6f s", value)
    }
}
