// 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 Security

// MARK: - SyncStore

final class SyncStore {

    /// Returns the store for the currently active vault.
    /// SyncManager always calls this at the start of each operation, so vault
    /// switches are picked up automatically without restarting SyncManager.
    static var shared: SyncStore {
        let vaultID = VaultRegistry.shared.activeVault?.id
        return SyncStore(vaultID: vaultID)
    }

    private let defaults = UserDefaults.standard
    private let keychainService = "fr.tducray.mypwdtool.sync"

    /// nil = default vault (legacy unprefixed keys); non-nil = additional vault
    private let vaultID: UUID?

    init(vaultID: UUID?) {
        self.vaultID = vaultID
    }

    /// The registry vault ID this store is scoped to.
    /// Use to guard against vault-switch races: compare against VaultRegistry.shared.activeVaultID.
    var ownerVaultID: UUID? { vaultID }

    // MARK: - UserDefaults keys (vault-scoped)

    private func key(_ base: String) -> String {
        guard let id = vaultID else { return base }   // default vault: keep old keys
        return "\(base)_\(id.uuidString)"
    }

    // Computed key names — scoped by vault when non-default
    private var kIsEnabled:      String { key("sync_enabled") }
    private var kStreamId:       String { key("sync_stream_id") }
    private var kDeviceId:       String { key("sync_device_id") }
    private var kInboxId:        String { key("sync_inbox_id") }
    private var kDeviceName:     String { key("sync_device_name") }
    private var kPeers:          String { key("sync_peers") }
    private var kRevokedInboxIds: String { key("sync_revoked_inbox_ids") }
    private var kCursor:                   String { key("sync_cursor") }
    private var kSenderCounter:            String { key("sync_sender_counter") }
    private var kAppliedEvents:            String { key("sync_applied_events") }
    private var kLastSyncAt:               String { key("sync_last_sync_at") }
    private var kPendingTransferEntryIDs:  String { key("sync_pending_transfer_ids") }
    /// Exposed so callers with a specific vaultID can read the stored device name.
    var deviceNameKey: String { kDeviceName }

    private var keychainSuffix: String {
        guard let id = vaultID else { return "" }
        return "_\(id.uuidString)"
    }

    // MARK: - Non-sensitive (UserDefaults)

    var isEnabled: Bool {
        get { defaults.bool(forKey: kIsEnabled) }
        set { defaults.set(newValue, forKey: kIsEnabled) }
    }

    var serverURL: String {
        get {
#if DEVSERVER
            return "http://localhost:8080/relay/v3"
#else
            return "https://relay.tducray.fr/relay/v3"
#endif
        }
        set { }
    }

    var streamId: String {
        get { defaults.string(forKey: kStreamId) ?? "" }
        set { defaults.set(newValue, forKey: kStreamId) }
    }

    var deviceId: String {
        get { defaults.string(forKey: kDeviceId) ?? "" }
        set { defaults.set(newValue, forKey: kDeviceId) }
    }

    var inboxId: String {
        get { defaults.string(forKey: kInboxId) ?? "" }
        set { defaults.set(newValue, forKey: kInboxId) }
    }

    var deviceName: String {
        get { defaults.string(forKey: kDeviceName) ?? "" }
        set { defaults.set(newValue, forKey: kDeviceName) }
    }

    var peers: [SyncPeer] {
        get {
            guard let data = defaults.data(forKey: kPeers) else { return [] }
            return (try? JSONDecoder().decode([SyncPeer].self, from: data)) ?? []
        }
        set {
            let data = try? JSONEncoder().encode(newValue)
            defaults.set(data, forKey: kPeers)
        }
    }

    /// Tombstone of inbox IDs revoked from this group. Checked by `.deviceHello` handling
    /// (both direct greetings and mesh-relayed introductions) so a revoked device can't be
    /// silently re-added by a stale/late-arriving introduction message that another peer
    /// queued before it learned of the revocation — see the 2026-07-18 mesh-relay fix, which
    /// made `.deviceHello` gossip between peers and thereby introduced this replay risk.
    var revokedInboxIds: Set<String> {
        get {
            guard let arr = defaults.stringArray(forKey: kRevokedInboxIds) else { return [] }
            return Set(arr)
        }
        set { defaults.set(Array(newValue), forKey: kRevokedInboxIds) }
    }

    func markRevoked(inboxId: String) {
        var ids = revokedInboxIds
        ids.insert(inboxId)
        revokedInboxIds = ids
    }

    var cursor: String {
        get { defaults.string(forKey: kCursor) ?? "" }
        set { defaults.set(newValue, forKey: kCursor) }
    }

    var senderCounter: Int {
        get { defaults.integer(forKey: kSenderCounter) }
        set { defaults.set(newValue, forKey: kSenderCounter) }
    }

    var lastSyncAt: Date? {
        let interval = defaults.double(forKey: kLastSyncAt)
        guard interval > 0 else { return nil }
        return Date(timeIntervalSince1970: interval)
    }

    func setLastSyncAt(_ date: Date) {
        defaults.set(date.timeIntervalSince1970, forKey: kLastSyncAt)
    }

    // MARK: - Applied event IDs (anti-replay)

    private var _appliedEvents: [String]? = nil

    private func loadAppliedEvents() -> [String] {
        if let cached = _appliedEvents { return cached }
        guard let data = defaults.data(forKey: kAppliedEvents) else {
            _appliedEvents = []
            return []
        }
        let events = (try? JSONDecoder().decode([String].self, from: data)) ?? []
        _appliedEvents = events
        return events
    }

    private func persistAppliedEvents(_ events: [String]) {
        _appliedEvents = events
        let data = try? JSONEncoder().encode(events)
        defaults.set(data, forKey: kAppliedEvents)
    }

    func hasApplied(eventId: String) -> Bool {
        loadAppliedEvents().contains(eventId)
    }

    func markApplied(eventId: String) {
        var events = loadAppliedEvents()
        guard !events.contains(eventId) else { return }
        events.append(eventId)
        // Cap at 10000
        if events.count > 10000 {
            events = Array(events.suffix(10000))
        }
        persistAppliedEvents(events)
    }

    // MARK: - Counter

    /// Increment and return the sender counter (monotonically increasing).
    func nextCounter() -> Int {
        let next = senderCounter + 1
        senderCounter = next
        return next
    }

    // MARK: - Keychain (sensitive, vault-scoped account names)

    private var kcSendToken: String { "send_token\(keychainSuffix)" }
    private var kcRecvToken: String { "recv_token\(keychainSuffix)" }
    private var kcSGK:       String { "sgk\(keychainSuffix)" }

    var sendToken: String? {
        get { keychainReadString(account: kcSendToken) }
        set { newValue != nil ? keychainWriteString(newValue!, account: kcSendToken) : keychainDelete(account: kcSendToken) }
    }

    var recvToken: String? {
        get { keychainReadString(account: kcRecvToken) }
        set { newValue != nil ? keychainWriteString(newValue!, account: kcRecvToken) : keychainDelete(account: kcRecvToken) }
    }

    var sgk: Data? {
        get { keychainReadData(account: kcSGK) }
        set { newValue != nil ? keychainWriteData(newValue!, account: kcSGK) : keychainDelete(account: kcSGK) }
    }

    // MARK: - Pending transfer entry IDs (written by VaultTransferService for cross-vault sync)

    /// Adds an entry ID to the pending-upsert list for this vault.
    /// Called by VaultTransferService when writing to a target vault that has sync configured.
    func addPendingTransferEntryID(_ id: UUID) {
        var ids = defaults.stringArray(forKey: kPendingTransferEntryIDs) ?? []
        let str = id.uuidString
        if !ids.contains(str) { ids.append(str) }
        defaults.set(ids, forKey: kPendingTransferEntryIDs)
    }

    /// Returns and clears all pending transfer entry IDs.
    func drainPendingTransferEntryIDs() -> [UUID] {
        guard let strs = defaults.stringArray(forKey: kPendingTransferEntryIDs), !strs.isEmpty else { return [] }
        defaults.removeObject(forKey: kPendingTransferEntryIDs)
        return strs.compactMap { UUID(uuidString: $0) }
    }

    // MARK: - Computed: isConfigured

    var isConfigured: Bool {
        isEnabled
            && !serverURL.isEmpty
            && !streamId.isEmpty
            && !deviceId.isEmpty
            && !inboxId.isEmpty
            && sendToken != nil
            && recvToken != nil
            && sgk != nil
    }

    // MARK: - Clear all sync data

    func clearAll() {
        isEnabled = false
        defaults.removeObject(forKey: kStreamId)
        defaults.removeObject(forKey: kDeviceId)
        defaults.removeObject(forKey: kInboxId)
        defaults.removeObject(forKey: kDeviceName)
        defaults.removeObject(forKey: kPeers)
        defaults.removeObject(forKey: kRevokedInboxIds)
        defaults.removeObject(forKey: kCursor)
        defaults.removeObject(forKey: kSenderCounter)
        defaults.removeObject(forKey: kAppliedEvents)
        defaults.removeObject(forKey: kLastSyncAt)
        _appliedEvents = nil
        sendToken = nil
        recvToken = nil
        sgk = nil
    }

    // MARK: - Keychain helpers

    private func keychainQuery(account: String) -> [String: Any] {
        let query: [String: Any] = [
            kSecClass as String:            kSecClassGenericPassword,
            kSecAttrService as String:      keychainService,
            kSecAttrAccount as String:      account,
            kSecAttrAccessGroup as String:  AppGroup.suiteName,
            kSecAttrAccessible as String:   kSecAttrAccessibleWhenUnlockedThisDeviceOnly
        ]
        return query
    }

    private func keychainReadData(account: String) -> Data? {
        var query = keychainQuery(account: account)
        query[kSecReturnData as String] = true
        query[kSecMatchLimit as String] = kSecMatchLimitOne

        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        guard status == errSecSuccess else { return nil }
        return result as? Data
    }

    private func keychainReadString(account: String) -> String? {
        guard let data = keychainReadData(account: account) else { return nil }
        return String(data: data, encoding: .utf8)
    }

    private func keychainWriteData(_ data: Data, account: String) {
        var query = keychainQuery(account: account)
        // Remove attributes not valid for update
        let updateAttributes: [String: Any] = [kSecValueData as String: data]

        if SecItemCopyMatching(query as CFDictionary, nil) == errSecSuccess {
            SecItemUpdate(query as CFDictionary, updateAttributes as CFDictionary)
        } else {
            query[kSecValueData as String] = data
            SecItemAdd(query as CFDictionary, nil)
        }
    }

    private func keychainWriteString(_ value: String, account: String) {
        guard let data = value.data(using: .utf8) else { return }
        keychainWriteData(data, account: account)
    }

    private func keychainDelete(account: String) {
        let query = keychainQuery(account: account)
        SecItemDelete(query as CFDictionary)
    }
}
