import Foundation
import NucleicProtocol

/// The persisted table of mesh-wide credential deletions this host knows (key deletion from
/// any device — the follow-up to remote agent sign-in). One row per kind, merged
/// `deletedAt`-monotonic (`CredentialTombstone.merged`) exactly like mesh roster tombstones,
/// so a replayed or stale revocation is a no-op and everyone converges on one stone per kind.
///
/// Backed by `UserDefaults` — the same persistence tier the runner already trusts for the Git
/// identity bundle, present on macOS and Linux alike — with an injectable suite so tests stay
/// hermetic. Consulted before this host ever *offers* a kind (manifest/provision) or *lands*
/// one (provision/update): a record stamped at-or-before the stone's `deletedAt` stays dead;
/// only a strictly newer login resurrects the kind.
public struct CredentialTombstoneStore: Sendable {
    public static let defaultsKey = "nucleic.credentialTombstones"

    private let suiteName: String?

    /// `suiteName` overrides the standard defaults domain (tests only).
    public init(suiteName: String? = nil) {
        self.suiteName = suiteName
    }

    private var defaults: UserDefaults {
        suiteName.flatMap(UserDefaults.init(suiteName:)) ?? .standard
    }

    /// Every tombstone on file, one per kind.
    public func all() -> [CredentialTombstone] {
        guard let data = defaults.data(forKey: Self.defaultsKey),
            let stones = try? JSONDecoder().decode([CredentialTombstone].self, from: data)
        else { return [] }
        return stones
    }

    /// Merge `incoming` into the table and return the subset that carried NEW information —
    /// stones strictly fresher than what was on file (the ones worth acting on and
    /// propagating). Replays and stale copies come back empty.
    @discardableResult
    public func record(_ incoming: [CredentialTombstone]) -> [CredentialTombstone] {
        var table = Dictionary(uniqueKeysWithValues: all().map { ($0.kind, $0) })
        var freshKinds: Set<CredentialKind> = []
        for stone in incoming {
            let winner = CredentialTombstone.merged(table[stone.kind], stone)
            if winner != table[stone.kind] {
                table[stone.kind] = winner
                freshKinds.insert(stone.kind)
            }
        }
        guard !freshKinds.isEmpty else { return [] }
        save(Array(table.values))
        return freshKinds.compactMap { table[$0] }
            .sorted { $0.kind.rawValue < $1.kind.rawValue }
    }

    /// The tombstone on file for `kind`, if any.
    public func tombstone(for kind: CredentialKind) -> CredentialTombstone? {
        all().first { $0.kind == kind }
    }

    /// Whether a credential revision (`kind`, stamped `updatedAt`) is suppressed by a recorded
    /// deletion — the gate every landing and every manifest/provision offer runs through.
    public func suppresses(kind: CredentialKind, updatedAt: Date) -> Bool {
        tombstone(for: kind)?.suppresses(recordUpdatedAt: updatedAt) ?? false
    }

    private func save(_ stones: [CredentialTombstone]) {
        let sorted = stones.sorted { $0.kind.rawValue < $1.kind.rawValue }
        if let data = try? JSONEncoder().encode(sorted) {
            defaults.set(data, forKey: Self.defaultsKey)
        }
    }
}
