import Foundation
import Security

// MARK: - KeychainError

enum KeychainError: LocalizedError {
    case saveFailed(OSStatus)
    case loadFailed(OSStatus)
    case deleteFailed(OSStatus)
    case notFound
    case unexpectedData

    var errorDescription: String? {
        switch self {
        case .saveFailed(let status): return "Keychain save failed (OSStatus \(status))"
        case .loadFailed(let status): return "Keychain load failed (OSStatus \(status))"
        case .deleteFailed(let status): return "Keychain delete failed (OSStatus \(status))"
        case .notFound: return "Item not found in Keychain"
        case .unexpectedData: return "Unexpected data format in Keychain"
        }
    }
}

// MARK: - KeychainManager

/// Thread-safe iOS Keychain wrapper.
/// Stores data under `com.rosetta.messenger.<key>`.
final class KeychainManager: @unchecked Sendable {

    static let shared = KeychainManager()
    private init() {}

    private let service = "com.rosetta.messenger"

    // MARK: - CRUD

    func save(_ data: Data, forKey key: String) throws {
        let query = baseQuery(forKey: key).merging([
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
        ]) { $1 }

        // Delete existing entry first (update not atomic without SecItemUpdate)
        SecItemDelete(query as CFDictionary)

        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else { throw KeychainError.saveFailed(status) }
    }

    func load(forKey key: String) throws -> Data {
        let query = baseQuery(forKey: key).merging([
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne,
        ]) { $1 }

        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)

        switch status {
        case errSecSuccess:
            guard let data = result as? Data else { throw KeychainError.unexpectedData }
            return data
        case errSecItemNotFound:
            throw KeychainError.notFound
        default:
            throw KeychainError.loadFailed(status)
        }
    }

    func delete(forKey key: String) throws {
        let query = baseQuery(forKey: key)
        let status = SecItemDelete(query as CFDictionary)
        guard status == errSecSuccess || status == errSecItemNotFound else {
            throw KeychainError.deleteFailed(status)
        }
    }

    func contains(key: String) -> Bool {
        (try? load(forKey: key)) != nil
    }

    // MARK: - Private

    private func baseQuery(forKey key: String) -> [String: Any] {
        [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key,
        ]
    }
}

// MARK: - Convenience

extension KeychainManager {

    func saveString(_ string: String, forKey key: String) throws {
        guard let data = string.data(using: .utf8) else { return }
        try save(data, forKey: key)
    }

    func loadString(forKey key: String) throws -> String {
        let data = try load(forKey: key)
        guard let string = String(data: data, encoding: .utf8) else {
            throw KeychainError.unexpectedData
        }
        return string
    }

    func saveCodable<T: Encodable>(_ value: T, forKey key: String) throws {
        let data = try JSONEncoder().encode(value)
        try save(data, forKey: key)
    }

    func loadCodable<T: Decodable>(_ type: T.Type, forKey key: String) throws -> T {
        let data = try load(forKey: key)
        return try JSONDecoder().decode(type, from: data)
    }
}
