//
//  EncryptionService.swift
//  Confab
//
//  Secure encryption for sensitive data (API keys)
//  Uses CryptoKit with machine-specific key derivation
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of Confab.
//
// Confab is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling Confab or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://confab.no>.


import Foundation
import CryptoKit
import IOKit

class EncryptionService {
    nonisolated static let shared = EncryptionService()

    private let encryptionKey: SymmetricKey

    private init() {
        self.encryptionKey = Self.deriveEncryptionKey()
    }

    // MARK: - Public Interface

    /// Encrypt a string value
    nonisolated func encrypt(_ value: String) throws -> String {
        guard let data = value.data(using: .utf8) else {
            throw EncryptionError.invalidInput
        }

        let sealedBox = try AES.GCM.seal(data, using: encryptionKey)
        guard let combined = sealedBox.combined else {
            throw EncryptionError.encryptionFailed
        }

        return combined.base64EncodedString()
    }

    /// Decrypt a string value
    nonisolated func decrypt(_ encryptedValue: String) throws -> String {
        guard let data = Data(base64Encoded: encryptedValue) else {
            throw EncryptionError.invalidInput
        }

        let sealedBox = try AES.GCM.SealedBox(combined: data)
        let decryptedData = try AES.GCM.open(sealedBox, using: encryptionKey)

        guard let decryptedString = String(data: decryptedData, encoding: .utf8) else {
            throw EncryptionError.decryptionFailed
        }

        return decryptedString
    }

    // MARK: - Key Derivation

    /// Derive encryption key from machine-specific data
    private static func deriveEncryptionKey() -> SymmetricKey {
        let machineUUID = getMachineUUID()
        // Pinned, not read from Bundle.main.bundleIdentifier: the app's bundle ID changed
        // (oAI -> Confab rename) but this key material must not, or every already-encrypted
        // setting (provider API keys, sync/email credentials) becomes undecryptable.
        let bundleID = "com.oai.oAI"
        let salt = "oAI-secure-storage-v1"
        let keyMaterial = "\(machineUUID)-\(bundleID)-\(salt)"
        let hash = SHA256.hash(data: Data(keyMaterial.utf8))
        return SymmetricKey(data: hash)
    }

    /// Get machine-specific UUID (IOPlatformUUID)
    private static func getMachineUUID() -> String {
        // Get IOPlatformUUID from IOKit
        let platformExpert = IOServiceGetMatchingService(
            kIOMainPortDefault,
            IOServiceMatching("IOPlatformExpertDevice")
        )

        guard platformExpert != 0 else {
            // Fallback to a stable identifier if IOKit unavailable
            return "oai-fallback-uuid"
        }

        defer { IOObjectRelease(platformExpert) }

        guard let uuidData = IORegistryEntryCreateCFProperty(
            platformExpert,
            "IOPlatformUUID" as CFString,
            kCFAllocatorDefault,
            0
        ) else {
            return "oai-fallback-uuid"
        }

        return (uuidData.takeRetainedValue() as? String) ?? "oai-fallback-uuid"
    }

    // MARK: - Errors

    enum EncryptionError: LocalizedError {
        case invalidInput
        case encryptionFailed
        case decryptionFailed

        var errorDescription: String? {
            switch self {
            case .invalidInput:
                return "Invalid input data for encryption/decryption"
            case .encryptionFailed:
                return "Failed to encrypt data"
            case .decryptionFailed:
                return "Failed to decrypt data"
            }
        }
    }
}
