// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Thibault Ducray
//
// This file is part of MyPwdTool's open-source sync/encryption core — see
// LICENSE-SYNC-CRYPTO.md at the repo root and https://tducray.fr/mypwdtool/open-source/.
// The rest of this application is proprietary and NOT covered by this license.

import Foundation
import CryptoKit
import CommonCrypto

// MARK: - CryptoError

enum CryptoError: LocalizedError {
    case keyDerivationFailed
    case encryptionFailed
    case decryptionFailed
    case invalidBase64
    case invalidData
    case tamperedData

    var errorDescription: String? {
        switch self {
        case .keyDerivationFailed: return String(localized: "Key derivation failed")
        case .encryptionFailed:    return String(localized: "Encryption failed")
        case .decryptionFailed:    return String(localized: "Decryption failed — incorrect master password?")
        case .invalidBase64:       return String(localized: "Invalid Base64 data")
        case .invalidData:         return String(localized: "Invalid data")
        case .tamperedData:        return String(localized: "Corrupted or tampered data")
        }
    }
}

// MARK: - CryptoManager

final class CryptoManager {

    private nonisolated static let v1Version: UInt8 = 0x01

    // MARK: - Key derivation

    /// Derives a 256-bit KEK from the master password via PBKDF2-SHA256.
    static func deriveKey(from password: String, salt: Data, iterations: Int = 400_000) throws -> SymmetricKey {
        guard let passwordData = password.data(using: .utf8) else {
            throw CryptoError.keyDerivationFailed
        }
        var derivedKey = Data(count: 32)
        let result = derivedKey.withUnsafeMutableBytes { dkBytes in
            salt.withUnsafeBytes { saltBytes in
                passwordData.withUnsafeBytes { pwdBytes in
                    CCKeyDerivationPBKDF(
                        CCPBKDFAlgorithm(kCCPBKDF2),
                        pwdBytes.baseAddress, passwordData.count,
                        saltBytes.baseAddress, salt.count,
                        CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
                        UInt32(iterations),
                        dkBytes.baseAddress, 32
                    )
                }
            }
        }
        guard result == kCCSuccess else { throw CryptoError.keyDerivationFailed }
        return SymmetricKey(data: derivedKey)
    }

    /// Generates a cryptographically random 32-byte salt.
    static func generateSalt() -> Data {
        var salt = Data(count: 32)
        _ = salt.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, 32, $0.baseAddress!) }
        return salt
    }

    // MARK: - DEK

    static func generateDEK() -> SymmetricKey {
        SymmetricKey(size: .bits256)
    }

    /// Wraps a DEK under a KEK using AES-GCM.
    static func wrapDEK(_ dek: SymmetricKey, withKEK kek: SymmetricKey) throws -> Data {
        let dekBytes = dek.withUnsafeBytes { Data($0) }
        do {
            let sealed = try AES.GCM.seal(dekBytes, using: kek)
            guard let combined = sealed.combined else { throw CryptoError.encryptionFailed }
            return combined
        } catch let e as CryptoError { throw e }
        catch { throw CryptoError.encryptionFailed }
    }

    /// Unwraps a DEK. Throws `decryptionFailed` if the KEK is wrong or data is tampered.
    static func unwrapDEK(_ wrappedDEK: Data, withKEK kek: SymmetricKey) throws -> SymmetricKey {
        do {
            let sealed = try AES.GCM.SealedBox(combined: wrappedDEK)
            let dekBytes = try AES.GCM.open(sealed, using: kek)
            return SymmetricKey(data: dekBytes)
        } catch let e as CryptoError { throw e }
        catch { throw CryptoError.decryptionFailed }
    }

    // MARK: - Field encryption (v1)

    /// Encrypts plaintext with AAD. Blob: [0x01][nonce:12][ciphertext][tag:16], base64-encoded.
    static func encrypt(_ plaintext: String, key: SymmetricKey, aad: Data) throws -> String {
        guard let data = plaintext.data(using: .utf8) else { throw CryptoError.invalidData }
        do {
            let sealed = try AES.GCM.seal(data, using: key, authenticating: aad)
            guard let combined = sealed.combined else { throw CryptoError.encryptionFailed }
            var blob = Data([v1Version])
            blob.append(combined)
            return blob.base64EncodedString()
        } catch let e as CryptoError { throw e }
        catch { throw CryptoError.encryptionFailed }
    }

    /// Decrypts a v1 blob produced by `encrypt(_:key:aad:)`.
    nonisolated static func decrypt(_ base64Blob: String, key: SymmetricKey, aad: Data) throws -> String {
        guard var blob = Data(base64Encoded: base64Blob) else { throw CryptoError.invalidBase64 }
        guard blob.count >= 1 + 12 + 16, blob[0] == v1Version else { throw CryptoError.invalidData }
        blob.removeFirst()
        do {
            let sealed = try AES.GCM.SealedBox(combined: blob)
            let plainData = try AES.GCM.open(sealed, using: key, authenticating: aad)
            guard let plaintext = String(data: plainData, encoding: .utf8) else { throw CryptoError.invalidData }
            return plaintext
        } catch let e as CryptoError { throw e }
        catch { throw CryptoError.tamperedData }
    }

    // MARK: - AAD

    nonisolated static func aad(vaultID: UUID, entryID: UUID, fieldName: String) -> Data {
        Data("\(vaultID.uuidString)|\(entryID.uuidString)|\(fieldName)".utf8)
    }
}
