import Foundation
import CryptoKit

/// Minimal XChaCha20-Poly1305 compatibility wrapper using CryptoKit's ChaChaPoly.
/// Implements HChaCha20 to derive a subkey and reduces the 24-byte nonce to a 12-byte nonce
/// as per XChaCha20 construction.
enum XChaCha20Poly1305Compat {

    /// Errors that can occur during XChaCha20-Poly1305 operations
    enum Error: Swift.Error {
        case invalidKeyLength(expected: Int, got: Int)
        case invalidNonceLength(expected: Int, got: Int)
    }

    struct SealBox {
        let ciphertext: Data
        let tag: Data
    }

    static func seal(plaintext: Data, key: Data, nonce24: Data, aad: Data? = nil) throws -> SealBox {
        guard key.count == 32 else {
            throw Error.invalidKeyLength(expected: 32, got: key.count)
        }
        guard nonce24.count == 24 else {
            throw Error.invalidNonceLength(expected: 24, got: nonce24.count)
        }

        let subkey = try hchacha20(key: key, nonce16: Data(nonce24.prefix(16)))
        let nonce12 = derive12ByteNonce(from24: nonce24)
        let chachaKey = SymmetricKey(data: subkey)
        let nonce = try ChaChaPoly.Nonce(data: nonce12)
        let sealed = try ChaChaPoly.seal(plaintext, using: chachaKey, nonce: nonce, authenticating: aad ?? Data())
        return SealBox(ciphertext: sealed.ciphertext, tag: sealed.tag)
    }

    static func open(ciphertext: Data, tag: Data, key: Data, nonce24: Data, aad: Data? = nil) throws -> Data {
        guard key.count == 32 else {
            throw Error.invalidKeyLength(expected: 32, got: key.count)
        }
        guard nonce24.count == 24 else {
            throw Error.invalidNonceLength(expected: 24, got: nonce24.count)
        }

        let subkey = try hchacha20(key: key, nonce16: Data(nonce24.prefix(16)))
        let nonce12 = derive12ByteNonce(from24: nonce24)
        let chachaKey = SymmetricKey(data: subkey)
        let box = try ChaChaPoly.SealedBox(nonce: ChaChaPoly.Nonce(data: nonce12), ciphertext: ciphertext, tag: tag)
        return try ChaChaPoly.open(box, using: chachaKey, authenticating: aad ?? Data())
    }

    // MARK: - Internals

    private static func derive12ByteNonce(from24 nonce24: Data) -> Data {
        // XChaCha20-Poly1305: 12-byte nonce = 4 zero bytes || last 8 bytes of the 24-byte nonce
        var out = Data(count: 12)
        out.replaceSubrange(0..<4, with: [0, 0, 0, 0])
        out.replaceSubrange(4..<12, with: nonce24.suffix(8))
        return out
    }

    private static func hchacha20(key: Data, nonce16: Data) throws -> Data {
        // HChaCha20 based on the original ChaCha20 core with a 16-byte nonce.
        guard key.count == 32 else {
            throw Error.invalidKeyLength(expected: 32, got: key.count)
        }
        guard nonce16.count == 16 else {
            throw Error.invalidNonceLength(expected: 16, got: nonce16.count)
        }

        // Constants "expand 32-byte k"
        var state: [UInt32] = [
            0x61707865, 0x3320646e, 0x79622d32, 0x6b206574,
            // key (8 words)
            key.loadLEWord(0), key.loadLEWord(4), key.loadLEWord(8), key.loadLEWord(12),
            key.loadLEWord(16), key.loadLEWord(20), key.loadLEWord(24), key.loadLEWord(28),
            // nonce (4 words)
            nonce16.loadLEWord(0), nonce16.loadLEWord(4), nonce16.loadLEWord(8), nonce16.loadLEWord(12)
        ]

        // 20 rounds (10 double rounds)
        for _ in 0..<10 {
            // Column rounds
            quarterRound(&state, 0, 4, 8, 12)
            quarterRound(&state, 1, 5, 9, 13)
            quarterRound(&state, 2, 6, 10, 14)
            quarterRound(&state, 3, 7, 11, 15)
            // Diagonal rounds
            quarterRound(&state, 0, 5, 10, 15)
            quarterRound(&state, 1, 6, 11, 12)
            quarterRound(&state, 2, 7, 8, 13)
            quarterRound(&state, 3, 4, 9, 14)
        }

        // Output subkey: state[0..3] and state[12..15]
        var out = Data(count: 32)
        out.storeLEWord(state[0], at: 0)
        out.storeLEWord(state[1], at: 4)
        out.storeLEWord(state[2], at: 8)
        out.storeLEWord(state[3], at: 12)
        out.storeLEWord(state[12], at: 16)
        out.storeLEWord(state[13], at: 20)
        out.storeLEWord(state[14], at: 24)
        out.storeLEWord(state[15], at: 28)
        return out
    }

    private static func quarterRound(_ s: inout [UInt32], _ a: Int, _ b: Int, _ c: Int, _ d: Int) {
        s[a] = s[a] &+ s[b]; s[d] ^= s[a]; s[d] = (s[d] << 16) | (s[d] >> 16)
        s[c] = s[c] &+ s[d]; s[b] ^= s[c]; s[b] = (s[b] << 12) | (s[b] >> 20)
        s[a] = s[a] &+ s[b]; s[d] ^= s[a]; s[d] = (s[d] << 8)  | (s[d] >> 24)
        s[c] = s[c] &+ s[d]; s[b] ^= s[c]; s[b] = (s[b] << 7)  | (s[b] >> 25)
    }
}

private extension Data {
    func loadLEWord(_ offset: Int) -> UInt32 {
        let range = offset..<(offset+4)
        let bytes = self[range]
        return bytes.withUnsafeBytes { ptr -> UInt32 in
            let b = ptr.bindMemory(to: UInt8.self)
            return UInt32(b[0]) | (UInt32(b[1]) << 8) | (UInt32(b[2]) << 16) | (UInt32(b[3]) << 24)
        }
    }

    mutating func storeLEWord(_ value: UInt32, at offset: Int) {
        let bytes: [UInt8] = [
            UInt8(value & 0xff),
            UInt8((value >> 8) & 0xff),
            UInt8((value >> 16) & 0xff),
            UInt8((value >> 24) & 0xff)
        ]
        replaceSubrange(offset..<(offset+4), with: bytes)
    }
}
