//
//  ProfileStore.swift
//  SwanDesk — persistence for VPN profiles and their secrets.
//
//  Non-secret profile fields are stored as JSON in UserDefaults. Secrets (xauth
//  password + private key PEM) live in the login Keychain, keyed by profile id,
//  and are only ever read to hand to the daemon at connect time.
//

import Foundation
import Security
import SwiftUI

@MainActor
@Observable
final class ProfileStore {
    static let shared = ProfileStore()

    private let defaultsKey = "Profiles"
    private let keychainService = "fi.juvex.swandesk.secrets"

    private(set) var profiles: [VPNProfile] = []

    private init() {
        load()
    }

    // MARK: Profiles (UserDefaults)

    private func load() {
        guard let data = UserDefaults.standard.data(forKey: defaultsKey) else {
            profiles = []
            return
        }
        do {
            profiles = try JSONDecoder().decode([VPNProfile].self, from: data)
        } catch {
            // Don't just drop the user's profiles on the floor: a schema change or
            // on-disk corruption would otherwise silently wipe them the moment
            // persist() next runs. Stash the raw bytes under a side key so the data
            // is recoverable, and log loudly so this is diagnosable.
            NSLog("SwanDesk: failed to decode saved profiles (\(error)) — preserving raw data under '\(defaultsKey).corrupt'")
            UserDefaults.standard.set(data, forKey: "\(defaultsKey).corrupt")
            profiles = []
        }
    }

    private func persist() {
        if let data = try? JSONEncoder().encode(profiles) {
            UserDefaults.standard.set(data, forKey: defaultsKey)
        }
    }

    func add(_ profile: VPNProfile = VPNProfile()) -> VPNProfile {
        profiles.append(profile)
        persist()
        return profile
    }

    func update(_ profile: VPNProfile) {
        if let idx = profiles.firstIndex(where: { $0.id == profile.id }) {
            profiles[idx] = profile
        } else {
            profiles.append(profile)
        }
        persist()
    }

    func delete(_ profile: VPNProfile) {
        profiles.removeAll { $0.id == profile.id }
        deleteSecrets(for: profile.id)
        persist()
    }

    /// Reorder profiles (list position = priority among simultaneous tunnels,
    /// 0 = top = highest). The caller pushes the new order to the helper so
    /// already-connected tunnels re-resolve their route/DNS conflicts.
    func move(fromOffsets source: IndexSet, toOffset destination: Int) {
        profiles.move(fromOffsets: source, toOffset: destination)
        persist()
    }

    /// Add an imported profile (already carrying a fresh id) and its decrypted
    /// secrets. Returns the stored profile; `secretsSaved` is false if the
    /// Keychain write failed so the caller can warn the user.
    @discardableResult
    func importProfile(_ profile: VPNProfile, secrets: VPNSecrets) -> (profile: VPNProfile, secretsSaved: Bool) {
        profiles.append(profile)
        persist()
        let saved = setSecrets(secrets, for: profile.id)
        return (profile, saved)
    }

    // MARK: Secrets (Keychain)

    func secrets(for id: UUID) -> VPNSecrets {
        guard let data = keychainRead(account: id.uuidString),
              let decoded = try? JSONDecoder().decode(VPNSecrets.self, from: data) else {
            return VPNSecrets()
        }
        return decoded
    }

    /// Returns true on success. A failed write (e.g. a locked Keychain) must not
    /// be silent — the caller surfaces it so the user doesn't later connect with a
    /// stale/blank credential that looks like a server-side auth failure.
    @discardableResult
    func setSecrets(_ secrets: VPNSecrets, for id: UUID) -> Bool {
        guard let data = try? JSONEncoder().encode(secrets) else { return false }
        return keychainWrite(account: id.uuidString, data: data)
    }

    private func deleteSecrets(for id: UUID) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: keychainService,
            kSecAttrAccount as String: id.uuidString
        ]
        SecItemDelete(query as CFDictionary)
    }

    private func keychainRead(account: String) -> Data? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: keychainService,
            kSecAttrAccount as String: account,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        var item: CFTypeRef?
        guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess else { return nil }
        return item as? Data
    }

    @discardableResult
    private func keychainWrite(account: String, data: Data) -> Bool {
        // Delete-then-add is idempotent and avoids the SecItemUpdate-vs-Add branch
        // that silently dropped writes on any non-"not found" error. A real failure
        // (locked Keychain, denied prompt) now surfaces as a false return.
        let base: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: keychainService,
            kSecAttrAccount as String: account
        ]
        SecItemDelete(base as CFDictionary)
        var insert = base
        insert[kSecValueData as String] = data
        let status = SecItemAdd(insert as CFDictionary, nil)
        if status != errSecSuccess {
            NSLog("SwanDesk: Keychain write failed for \(account): OSStatus \(status)")
            return false
        }
        return true
    }
}
