import Foundation

/// Encodes any `Encodable` to deterministic CBOR bytes (SYNC_PROTOCOL §5). Mirrors the
/// `JSONEncoder` surface so call sites swap encoders without ceremony. Dates ride through
/// as their `Codable` default (a `Double` seconds-since-reference) — both peers use this
/// codec, so no date-strategy negotiation is needed.
public struct CBOREncoder: Sendable {
    public init() {}

    public func encode<T: Encodable>(_ value: T) throws -> Data {
        let encoder = _CBOREncoder(codingPath: [])
        try value.encode(to: encoder)
        return Data(encoder.finalValue().serialized())
    }
}

// MARK: - Building tree (reference-typed so nested containers mutate in place)

private final class RefArray { var items: [Node] = [] }
private final class RefMap { var order: [String] = []; var items: [String: Node] = [:] }

private enum Node {
    case value(CBORValue)
    case array(RefArray)
    case map(RefMap)

    func resolve() -> CBORValue {
        switch self {
        case .value(let v): return v
        case .array(let a): return .array(a.items.map { $0.resolve() })
        case .map(let m): return .map(m.order.map { (.text($0), m.items[$0]!.resolve()) })
        }
    }
}

private final class _CBOREncoder: Encoder {
    var codingPath: [CodingKey]
    let userInfo: [CodingUserInfoKey: Any] = [:]
    var root: Node?

    init(codingPath: [CodingKey]) { self.codingPath = codingPath }

    func finalValue() -> CBORValue { (root ?? .value(.null)).resolve() }

    func container<Key: CodingKey>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> {
        let map: RefMap
        if case .map(let existing) = root { map = existing }
        else { map = RefMap(); root = .map(map) }
        return KeyedEncodingContainer(CBORKeyedContainer<Key>(codingPath: codingPath, map: map))
    }

    func unkeyedContainer() -> UnkeyedEncodingContainer {
        let arr: RefArray
        if case .array(let existing) = root { arr = existing }
        else { arr = RefArray(); root = .array(arr) }
        return CBORUnkeyedContainer(codingPath: codingPath, array: arr)
    }

    func singleValueContainer() -> SingleValueEncodingContainer {
        CBORSingleValueContainer(codingPath: codingPath) { [weak self] node in
            self?.root = node
        }
    }
}

// Encode a nested Encodable into a fresh sub-encoder and return its built node.
private func encodeChild(_ value: Encodable, codingPath: [CodingKey]) throws -> Node {
    if let json = boxPrimitive(value) { return .value(json) }
    let sub = _CBOREncoder(codingPath: codingPath)
    try value.encode(to: sub)
    return sub.root ?? .value(.null)
}

// Fast-path primitives so we don't spin up a sub-encoder for leaves.
private func boxPrimitive(_ value: Encodable) -> CBORValue? {
    switch value {
    case let v as String: return .text(v)
    case let v as Bool: return .bool(v)
    case let v as Double: return .float(v)
    case let v as Float: return .float(Double(v))
    case let v as Int: return intNode(Int64(v))
    case let v as Int8: return intNode(Int64(v))
    case let v as Int16: return intNode(Int64(v))
    case let v as Int32: return intNode(Int64(v))
    case let v as Int64: return intNode(v)
    case let v as UInt: return .unsigned(UInt64(v))
    case let v as UInt8: return .unsigned(UInt64(v))
    case let v as UInt16: return .unsigned(UInt64(v))
    case let v as UInt32: return .unsigned(UInt64(v))
    case let v as UInt64: return .unsigned(v)
    default: return nil
    }
}

private func intNode(_ v: Int64) -> CBORValue {
    v >= 0 ? .unsigned(UInt64(v)) : .negative(UInt64(-1 - v))
}

// MARK: - Keyed

private struct CBORKeyedContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
    var codingPath: [CodingKey]
    let map: RefMap

    private func store(_ node: Node, _ key: Key) {
        if map.items[key.stringValue] == nil { map.order.append(key.stringValue) }
        map.items[key.stringValue] = node
    }

    mutating func encodeNil(forKey key: Key) throws { store(.value(.null), key) }
    mutating func encode(_ value: Bool, forKey key: Key) throws { store(.value(.bool(value)), key) }
    mutating func encode(_ value: String, forKey key: Key) throws { store(.value(.text(value)), key) }
    mutating func encode(_ value: Double, forKey key: Key) throws { store(.value(.float(value)), key) }
    mutating func encode(_ value: Float, forKey key: Key) throws { store(.value(.float(Double(value))), key) }
    mutating func encode(_ value: Int, forKey key: Key) throws { store(.value(intNode(Int64(value))), key) }
    mutating func encode(_ value: Int8, forKey key: Key) throws { store(.value(intNode(Int64(value))), key) }
    mutating func encode(_ value: Int16, forKey key: Key) throws { store(.value(intNode(Int64(value))), key) }
    mutating func encode(_ value: Int32, forKey key: Key) throws { store(.value(intNode(Int64(value))), key) }
    mutating func encode(_ value: Int64, forKey key: Key) throws { store(.value(intNode(value)), key) }
    mutating func encode(_ value: UInt, forKey key: Key) throws { store(.value(.unsigned(UInt64(value))), key) }
    mutating func encode(_ value: UInt8, forKey key: Key) throws { store(.value(.unsigned(UInt64(value))), key) }
    mutating func encode(_ value: UInt16, forKey key: Key) throws { store(.value(.unsigned(UInt64(value))), key) }
    mutating func encode(_ value: UInt32, forKey key: Key) throws { store(.value(.unsigned(UInt64(value))), key) }
    mutating func encode(_ value: UInt64, forKey key: Key) throws { store(.value(.unsigned(value)), key) }

    mutating func encode<T: Encodable>(_ value: T, forKey key: Key) throws {
        store(try encodeChild(value, codingPath: codingPath + [key]), key)
    }

    mutating func nestedContainer<NestedKey: CodingKey>(
        keyedBy keyType: NestedKey.Type, forKey key: Key
    ) -> KeyedEncodingContainer<NestedKey> {
        let child = RefMap()
        store(.map(child), key)
        return KeyedEncodingContainer(
            CBORKeyedContainer<NestedKey>(codingPath: codingPath + [key], map: child))
    }

    mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
        let child = RefArray()
        store(.array(child), key)
        return CBORUnkeyedContainer(codingPath: codingPath + [key], array: child)
    }

    mutating func superEncoder() -> Encoder { _CBOREncoder(codingPath: codingPath) }
    mutating func superEncoder(forKey key: Key) -> Encoder { _CBOREncoder(codingPath: codingPath + [key]) }
}

// MARK: - Unkeyed

private struct CBORUnkeyedContainer: UnkeyedEncodingContainer {
    var codingPath: [CodingKey]
    let array: RefArray
    var count: Int { array.items.count }

    mutating func encodeNil() throws { array.items.append(.value(.null)) }
    mutating func encode(_ value: Bool) throws { array.items.append(.value(.bool(value))) }
    mutating func encode(_ value: String) throws { array.items.append(.value(.text(value))) }
    mutating func encode(_ value: Double) throws { array.items.append(.value(.float(value))) }
    mutating func encode(_ value: Float) throws { array.items.append(.value(.float(Double(value)))) }
    mutating func encode(_ value: Int) throws { array.items.append(.value(intNode(Int64(value)))) }
    mutating func encode(_ value: Int8) throws { array.items.append(.value(intNode(Int64(value)))) }
    mutating func encode(_ value: Int16) throws { array.items.append(.value(intNode(Int64(value)))) }
    mutating func encode(_ value: Int32) throws { array.items.append(.value(intNode(Int64(value)))) }
    mutating func encode(_ value: Int64) throws { array.items.append(.value(intNode(value))) }
    mutating func encode(_ value: UInt) throws { array.items.append(.value(.unsigned(UInt64(value)))) }
    mutating func encode(_ value: UInt8) throws { array.items.append(.value(.unsigned(UInt64(value)))) }
    mutating func encode(_ value: UInt16) throws { array.items.append(.value(.unsigned(UInt64(value)))) }
    mutating func encode(_ value: UInt32) throws { array.items.append(.value(.unsigned(UInt64(value)))) }
    mutating func encode(_ value: UInt64) throws { array.items.append(.value(.unsigned(value))) }

    mutating func encode<T: Encodable>(_ value: T) throws {
        array.items.append(try encodeChild(value, codingPath: codingPath))
    }

    mutating func nestedContainer<NestedKey: CodingKey>(
        keyedBy keyType: NestedKey.Type
    ) -> KeyedEncodingContainer<NestedKey> {
        let child = RefMap()
        array.items.append(.map(child))
        return KeyedEncodingContainer(
            CBORKeyedContainer<NestedKey>(codingPath: codingPath, map: child))
    }

    mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
        let child = RefArray()
        array.items.append(.array(child))
        return CBORUnkeyedContainer(codingPath: codingPath, array: child)
    }

    mutating func superEncoder() -> Encoder { _CBOREncoder(codingPath: codingPath) }
}

// MARK: - Single value

private struct CBORSingleValueContainer: SingleValueEncodingContainer {
    var codingPath: [CodingKey]
    let sink: (Node) -> Void

    mutating func encodeNil() throws { sink(.value(.null)) }
    mutating func encode(_ value: Bool) throws { sink(.value(.bool(value))) }
    mutating func encode(_ value: String) throws { sink(.value(.text(value))) }
    mutating func encode(_ value: Double) throws { sink(.value(.float(value))) }
    mutating func encode(_ value: Float) throws { sink(.value(.float(Double(value)))) }
    mutating func encode(_ value: Int) throws { sink(.value(intNode(Int64(value)))) }
    mutating func encode(_ value: Int8) throws { sink(.value(intNode(Int64(value)))) }
    mutating func encode(_ value: Int16) throws { sink(.value(intNode(Int64(value)))) }
    mutating func encode(_ value: Int32) throws { sink(.value(intNode(Int64(value)))) }
    mutating func encode(_ value: Int64) throws { sink(.value(intNode(value))) }
    mutating func encode(_ value: UInt) throws { sink(.value(.unsigned(UInt64(value)))) }
    mutating func encode(_ value: UInt8) throws { sink(.value(.unsigned(UInt64(value)))) }
    mutating func encode(_ value: UInt16) throws { sink(.value(.unsigned(UInt64(value)))) }
    mutating func encode(_ value: UInt32) throws { sink(.value(.unsigned(UInt64(value)))) }
    mutating func encode(_ value: UInt64) throws { sink(.value(.unsigned(value))) }

    mutating func encode<T: Encodable>(_ value: T) throws {
        sink(try encodeChild(value, codingPath: codingPath))
    }
}
