//
//  EncryptionService.swift
//  oAI
//
//  Secure encryption for sensitive data (API keys)
//  Uses CryptoKit with machine-specific key derivation
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Rune Olsen
//
// This file is part of oAI.
//
// oAI is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// oAI is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
// Public License for more details.
//
// You should have received a copy of the GNU Affero General Public
// License along with oAI. If not, see <https://www.gnu.org/licenses/>.


import Foundation
import CryptoKit
import IOKit

class EncryptionService {
    nonisolated static let shared = EncryptionService()

    private let salt = "oAI-secure-storage-v1" // App-specific salt
    private lazy var encryptionKey: SymmetricKey = {
        deriveEncryptionKey()
    }()

    private init() {}

    // 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 func deriveEncryptionKey() -> SymmetricKey {
        // Combine machine UUID + bundle ID + salt for key material
        let machineUUID = getMachineUUID()
        let bundleID = Bundle.main.bundleIdentifier ?? "com.oai.oAI"
        let keyMaterial = "\(machineUUID)-\(bundleID)-\(salt)"

        // Hash to create consistent 256-bit key
        let hash = SHA256.hash(data: Data(keyMaterial.utf8))
        return SymmetricKey(data: hash)
    }

    /// Get machine-specific UUID (IOPlatformUUID)
    private 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"
            }
        }
    }
}
