// ── Field Encryption ────────────────────────────────────────
// AES-256-GCM field-level encryption compatible with @bytelyst/field-encrypt (TypeScript).
// Produces and consumes the same EncryptedField JSON structure across all platforms.
// Uses Apple CryptoKit — available on iOS 13+, macOS 10.15+, watchOS 6+.

import CryptoKit
import Foundation

// MARK: - EncryptedField Model

/// Encrypted field structure — wire-compatible with @bytelyst/field-encrypt (TypeScript).
/// All byte arrays are hex-encoded strings for JSON serialization.
public struct BLEncryptedField: Codable, Sendable, Equatable {
    /// Sentinel — always `true` for encrypted fields.
    public let __encrypted: Bool
    /// Schema version (currently 1).
    public let v: Int
    /// Algorithm identifier.
    public let alg: String
    /// Ciphertext (hex-encoded).
    public let ct: String
    /// Initialization vector (hex-encoded, 12 bytes = 24 hex chars).
    public let iv: String
    /// GCM authentication tag (hex-encoded, 16 bytes = 32 hex chars).
    public let tag: String
    /// DEK identifier — identifies which key was used for encryption.
    public let dekId: String

    public init(ct: String, iv: String, tag: String, dekId: String) {
        self.__encrypted = true
        self.v = 1
        self.alg = "aes-256-gcm"
        self.ct = ct
        self.iv = iv
        self.tag = tag
        self.dekId = dekId
    }
}

// MARK: - BLFieldEncrypt

/// AES-256-GCM field-level encryption.
///
/// Produces `BLEncryptedField` objects that are wire-compatible with the
/// TypeScript `@bytelyst/field-encrypt` package. Backends and native clients
/// can encrypt/decrypt the same fields interchangeably.
///
/// Usage:
/// ```swift
/// let key = BLFieldEncrypt.generateKey()
/// let encrypted = try BLFieldEncrypt.encrypt("sensitive data", key: key, dekId: "dek_user1_notes")
/// let decrypted = try BLFieldEncrypt.decrypt(encrypted, key: key)
/// ```
public enum BLFieldEncrypt {

    /// AES-256-GCM key size in bytes.
    public static let keySize = 32

    /// GCM nonce (IV) size in bytes.
    private static let nonceSize = 12

    // MARK: - Encrypt

    /// Encrypt a plaintext string with AES-256-GCM.
    ///
    /// - Parameters:
    ///   - plaintext: UTF-8 string to encrypt.
    ///   - key: 32-byte symmetric key.
    ///   - dekId: DEK identifier stored in the output for key lookup on decrypt.
    ///   - aad: Optional additional authenticated data (e.g., "userId:context").
    /// - Returns: `BLEncryptedField` with hex-encoded ciphertext, IV, and tag.
    /// - Throws: `BLFieldEncryptError` if key size is wrong or encryption fails.
    public static func encrypt(
        _ plaintext: String,
        key: SymmetricKey,
        dekId: String,
        aad: String? = nil
    ) throws -> BLEncryptedField {
        guard key.bitCount == keySize * 8 else {
            throw BLFieldEncryptError.invalidKeySize(expected: keySize, actual: key.bitCount / 8)
        }

        let plaintextData = Data(plaintext.utf8)
        let nonce = AES.GCM.Nonce()

        let sealedBox: AES.GCM.SealedBox
        if let aad = aad {
            sealedBox = try AES.GCM.seal(
                plaintextData,
                using: key,
                nonce: nonce,
                authenticating: Data(aad.utf8)
            )
        } else {
            sealedBox = try AES.GCM.seal(plaintextData, using: key, nonce: nonce)
        }

        return BLEncryptedField(
            ct: sealedBox.ciphertext.hexString,
            iv: Data(sealedBox.nonce).hexString,
            tag: sealedBox.tag.hexString,
            dekId: dekId
        )
    }

    // MARK: - Decrypt

    /// Decrypt a `BLEncryptedField` back to plaintext.
    ///
    /// - Parameters:
    ///   - field: Encrypted field object (from Cosmos DB or API response).
    ///   - key: 32-byte symmetric key (must match the key used to encrypt).
    ///   - aad: Optional AAD (must match the AAD used during encryption).
    /// - Returns: Decrypted UTF-8 string.
    /// - Throws: `BLFieldEncryptError` if decryption or authentication fails.
    public static func decrypt(
        _ field: BLEncryptedField,
        key: SymmetricKey,
        aad: String? = nil
    ) throws -> String {
        guard key.bitCount == keySize * 8 else {
            throw BLFieldEncryptError.invalidKeySize(expected: keySize, actual: key.bitCount / 8)
        }

        guard let ctData = Data(hexString: field.ct),
              let ivData = Data(hexString: field.iv),
              let tagData = Data(hexString: field.tag) else {
            throw BLFieldEncryptError.invalidHexEncoding
        }

        let nonce = try AES.GCM.Nonce(data: ivData)
        let sealedBox = try AES.GCM.SealedBox(nonce: nonce, ciphertext: ctData, tag: tagData)

        let decryptedData: Data
        if let aad = aad {
            decryptedData = try AES.GCM.open(sealedBox, using: key, authenticating: Data(aad.utf8))
        } else {
            decryptedData = try AES.GCM.open(sealedBox, using: key)
        }

        guard let plaintext = String(data: decryptedData, encoding: .utf8) else {
            throw BLFieldEncryptError.utf8DecodingFailed
        }

        return plaintext
    }

    // MARK: - Key Generation

    /// Generate a random 32-byte AES-256 symmetric key.
    public static func generateKey() -> SymmetricKey {
        SymmetricKey(size: .bits256)
    }

    /// Create a `SymmetricKey` from a hex-encoded string (64 hex chars = 32 bytes).
    public static func keyFromHex(_ hex: String) throws -> SymmetricKey {
        guard let data = Data(hexString: hex), data.count == keySize else {
            throw BLFieldEncryptError.invalidKeySize(expected: keySize, actual: hex.count / 2)
        }
        return SymmetricKey(data: data)
    }

    // MARK: - Keychain Key Derivation

    /// Get or create a persistent encryption key in the Keychain.
    ///
    /// On first call, generates a random 32-byte AES-256 key and stores it
    /// as a hex string in the Keychain. On subsequent calls, loads the
    /// existing key. This provides a stable per-device DEK for client-side
    /// encryption without requiring the backend to provision keys.
    ///
    /// - Parameters:
    ///   - service: Keychain service identifier (typically the app's bundle ID).
    ///   - account: Keychain account key (e.g., `"field_encrypt_dek"`).
    /// - Returns: A 32-byte `SymmetricKey` backed by Keychain storage.
    /// - Throws: `BLFieldEncryptError.invalidHexEncoding` if stored key is corrupt.
    public static func getOrCreateKey(service: String, account: String = "field_encrypt_dek") throws -> SymmetricKey {
        if let existingHex = BLKeychain.read(service: service, key: account) {
            return try keyFromHex(existingHex)
        }

        let newKey = generateKey()
        let hex = newKey.withUnsafeBytes { Data($0).hexString }
        BLKeychain.save(service: service, key: account, value: hex)
        return newKey
    }

    /// Load an existing encryption key from the Keychain without creating one.
    ///
    /// - Parameters:
    ///   - service: Keychain service identifier.
    ///   - account: Keychain account key (e.g., `"field_encrypt_dek"`).
    /// - Returns: The stored `SymmetricKey`, or `nil` if none exists.
    public static func loadKey(service: String, account: String = "field_encrypt_dek") -> SymmetricKey? {
        guard let hex = BLKeychain.read(service: service, key: account) else { return nil }
        return try? keyFromHex(hex)
    }

    /// Delete the stored encryption key from the Keychain.
    ///
    /// - Parameters:
    ///   - service: Keychain service identifier.
    ///   - account: Keychain account key.
    /// - Returns: `true` if the key was deleted or didn't exist.
    @discardableResult
    public static func deleteKey(service: String, account: String = "field_encrypt_dek") -> Bool {
        BLKeychain.delete(service: service, key: account)
    }

    // MARK: - Type Guard

    /// Check if a JSON value represents an encrypted field.
    /// Compatible with the TypeScript `isEncryptedField()` type guard.
    public static func isEncrypted(_ value: Any?) -> Bool {
        if let dict = value as? [String: Any],
           let sentinel = dict["__encrypted"] as? Bool,
           sentinel == true,
           dict["v"] != nil,
           dict["alg"] != nil,
           dict["ct"] != nil,
           dict["iv"] != nil,
           dict["tag"] != nil,
           dict["dekId"] != nil {
            return true
        }
        return false
    }

    /// Check if a `Codable` value is a `BLEncryptedField`.
    public static func isEncrypted(_ field: BLEncryptedField?) -> Bool {
        field?.__encrypted == true
    }
}

// MARK: - Errors

public enum BLFieldEncryptError: LocalizedError {
    case invalidKeySize(expected: Int, actual: Int)
    case invalidHexEncoding
    case utf8DecodingFailed

    public var errorDescription: String? {
        switch self {
        case .invalidKeySize(let expected, let actual):
            return "AES-256-GCM requires a \(expected)-byte key, got \(actual)"
        case .invalidHexEncoding:
            return "Failed to decode hex-encoded field data"
        case .utf8DecodingFailed:
            return "Decrypted data is not valid UTF-8"
        }
    }
}

// MARK: - Hex Helpers

extension Data {
    /// Hex-encode data to a lowercase string.
    var hexString: String {
        map { String(format: "%02x", $0) }.joined()
    }

    /// Initialize Data from a hex-encoded string.
    init?(hexString: String) {
        let hex = hexString.lowercased()
        guard hex.count.isMultiple(of: 2) else { return nil }

        var data = Data(capacity: hex.count / 2)
        var index = hex.startIndex
        while index < hex.endIndex {
            let nextIndex = hex.index(index, offsetBy: 2)
            guard let byte = UInt8(hex[index..<nextIndex], radix: 16) else { return nil }
            data.append(byte)
            index = nextIndex
        }
        self = data
    }
}
