//
// PeerID.swift
// BitFoundation
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//

import struct Foundation.Data
import struct Foundation.CharacterSet

public struct PeerID: Equatable, Hashable, Sendable {
    enum Constants {
        /// 16
        static let nostrConvKeyPrefixLength = 16
        /// 8
        static let nostrShortKeyDisplayLength = 8
        /// 64
        fileprivate static let maxIDLength = 64
        /// 16
        fileprivate static let hexIDLength = 16 // 8 bytes = 16 hex chars
    }

    public enum Prefix: String, CaseIterable, Sendable {
        /// When no prefix is provided
        case empty = ""
        /// `"mesh:"`
        case mesh = "mesh:"
        /// `"name:"`
        case name = "name:"
        /// `"noise:"` (+ 64 characters hex)
        case noise = "noise:"
        /// `"nostr_"` (+ 16 characters hex)
        case geoDM = "nostr_"
        /// `"nostr:"` (+ 8 characters hex)
        case geoChat = "nostr:"
        /// `"group_"` (+ 32 characters hex) — virtual conversation ID for a
        /// private group (16-byte group ID). Never routed to a single peer.
        case group = "group_"
        /// `"bridge:"` (+ 16 characters hex) — a sender reached across a mesh
        /// bridge, identified by their rendezvous Nostr pubkey. Not a
        /// routable mesh peer.
        case bridge = "bridge:"
    }

    public let prefix: Prefix

    /// Returns the actual value without any prefix
    public let bare: String

    /// Returns the full `id` value by combining `(prefix + bare)`
    public var id: String { prefix.rawValue + bare }

    // Private so the callers have to go through a convenience init
    private init(prefix: Prefix, bare: any StringProtocol) {
        self.prefix = prefix
        self.bare = String(bare).lowercased()
    }
}

// MARK: - Convenience Inits

public extension PeerID {
    /// Convenience init to create GeoDM PeerID by appending `"nostr_"` to the first 16 characters of `pubKey`
    init(nostr_ pubKey: String) {
        self.init(prefix: .geoDM, bare: pubKey.prefix(Constants.nostrConvKeyPrefixLength))
    }

    /// Convenience init to create GeoChat PeerID by appending `"nostr:"` to the first 8 characters of `pubKey`
    init(nostr pubKey: String) {
        self.init(prefix: .geoChat, bare: pubKey.prefix(Constants.nostrShortKeyDisplayLength))
    }

    /// Convenience init to create a bridged-sender PeerID by appending
    /// `"bridge:"` to the first 16 characters of the rendezvous Nostr pubkey.
    init(bridge pubKey: String) {
        self.init(prefix: .bridge, bare: pubKey.prefix(Constants.nostrConvKeyPrefixLength))
    }

    /// Convenience init to create PeerID from String/Substring by splitting it into prefix and bare parts
    init(str: any StringProtocol) {
        if let prefix = Prefix.allCases.first(where: { $0 != .empty && str.hasPrefix($0.rawValue) }) {
            self.init(prefix: prefix, bare: String(str).dropFirst(prefix.rawValue.count))
        } else {
            self.init(prefix: .empty, bare: str)
        }
    }

    /// Convenience init to handle `Optional<String>`
    init?(str: (any StringProtocol)?) {
        guard let str else { return nil }
        self.init(str: str)
    }

    /// Convenience init to create PeerID by converting Data to String
    init?(data: Data) {
        self.init(str: String(data: data, encoding: .utf8))
    }

    /// Convenience init to "hide" hex-encoding implementation detail
    init(hexData: Data) {
        self.init(str: hexData.hexEncodedString())
    }

    /// Convenience init to "hide" hex-encoding implementation detail
    init?(hexData: Data?) {
        guard let hexData else { return nil }
        self.init(hexData: hexData)
    }
}

// MARK: - Group Conversation Helpers

public extension PeerID {
    /// Convenience init to create a virtual group conversation PeerID from a
    /// 16-byte group ID ("group_" + 32 hex characters).
    init(groupID: Data) {
        self.init(str: Prefix.group.rawValue + groupID.hexEncodedString())
    }

    /// The 16-byte group ID behind a "group_" PeerID, if this is one.
    var groupIDData: Data? {
        guard isGroup, bare.count == 32 else { return nil }
        return Data(hexString: bare)
    }
}

// MARK: - Noise Public Key Helpers

public extension PeerID {
    /// Derive the stable 16-hex peer ID from a Noise static public key
    init(publicKey: Data) {
        self.init(str: publicKey.sha256Fingerprint().prefix(16))
    }

    /// Returns a 16-hex short peer ID derived from a 64-hex Noise public key if needed
    func toShort() -> PeerID {
        if let noiseKey {
            return PeerID(publicKey: noiseKey)
        }
        return self
    }
}

// MARK: - Codable

extension PeerID: Codable {
    public init(from decoder: any Decoder) throws {
        self.init(str: try decoder.singleValueContainer().decode(String.self))
    }

    public func encode(to encoder: any Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(id)
    }
}

// MARK: - Helpers

public extension PeerID {
    var isEmpty: Bool {
        id.isEmpty
    }

    /// Returns true if `id` starts with "`nostr:`"
    var isGeoChat: Bool {
        prefix == .geoChat
    }

    /// Returns true if `id` starts with "`nostr_`"
    var isGeoDM: Bool {
        prefix == .geoDM
    }

    /// Returns true if `id` starts with "`group_`"
    var isGroup: Bool {
        prefix == .group
    }

    /// Returns true if `id` starts with "`bridge:`"
    var isBridge: Bool {
        prefix == .bridge
    }

    func toPercentEncoded() -> String {
        id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? id
    }
}

public extension PeerID {
    var routingData: Data? {
        if let direct = Data(hexString: id), direct.count == 8 { return direct }
        if let bareData = Data(hexString: bare), bareData.count == 8 { return bareData }
        let short = toShort()
        return Data(hexString: short.id)
    }

    init?(routingData: Data) {
        guard routingData.count == 8 else { return nil }
        self.init(hexData: routingData)
    }
}

// MARK: - Validation

public extension PeerID {
    /// Validates a peer ID from any source (short 16-hex, full 64-hex, or internal alnum/-/_ up to 64)
    var isValid: Bool {
        if prefix != .empty {
            return PeerID(str: bare).isValid
        }

        // Accept short routing IDs (exact 16-hex) or Full Noise key hex (exact 64-hex)
        if isShort || isNoiseKeyHex {
            return true
        }

        // If length equals short or full but isn't valid hex, reject
        if id.count == Constants.hexIDLength || id.count == Constants.maxIDLength {
            return false
        }

        // Internal format: alphanumeric + dash/underscore up to 63 (not 16 or 64)
        let validCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
        return !id.isEmpty &&
                id.count < Constants.maxIDLength &&
                id.rangeOfCharacter(from: validCharset.inverted) == nil
    }

    /// Returns true if the `bare` id is all hex
    var isHex: Bool {
        bare.allSatisfy { $0.isHexDigit }
    }

    /// Short routing IDs (exact 16-hex)
    var isShort: Bool {
        bare.count == Constants.hexIDLength && isHex
    }

    /// Full Noise key hex (exact 64-hex)
    var isNoiseKeyHex: Bool {
        noiseKey != nil
    }

    /// Full Noise key (exact 64-hex) as Data
    var noiseKey: Data? {
        guard bare.count == Constants.maxIDLength else { return nil }
        return Data(hexString: bare)
    }
}

// MARK: - Comparable

extension PeerID: Comparable {
    public static func < (lhs: PeerID, rhs: PeerID) -> Bool {
        lhs.id < rhs.id
    }
}

// MARK: - CustomStringConvertible

extension PeerID: CustomStringConvertible {
    /// So it returns the actual `id` like before even inside another String
    public var description: String {
        id
    }
}
