using DodoSSH.Client.Storage;

namespace DodoSSH.Client.Session;

/// <summary>
/// Which vaults this machine has been asked to leave off the screens that list their contents.
/// </summary>
/// <remarks>
/// <para>
/// <b>A reading preference, and only that.</b> Somebody in four teams does not want four teams' hosts in
/// front of them all day, and the answer is a switch per vault rather than four sign-ins. What this must
/// never become is an access control: a hidden vault still syncs, its key stays in the keyring, and a
/// shown host that authenticates with a key filed in it still connects. Hiding changes what is drawn and
/// nothing else.
/// </para>
/// <para>
/// <b>Deliberately unknown to <see cref="VaultSession"/>.</b> Nothing in that type takes one of these, and
/// nothing in it should: <see cref="VaultSession.ReadableVaults"/> is what the sync loop walks and what the
/// keyring is filled from, so a filter reaching it would be a preference that quietly stopped a team's
/// vault from syncing — and the user would find out weeks later, from a host that was never there. The
/// dependency runs one way, from here to the session's store, and this file is the only place the two meet.
/// </para>
/// <para>
/// Per machine, which is why it lives in the local cache rather than in the vault: the vault you set aside
/// on a work laptop is not the one you set aside on a phone. It is in the <em>encrypted</em> cache rather
/// than in <see cref="ClientSettings"/> because it is a list of vault ids, and that file is plaintext.
/// </para>
/// <para>
/// Not thread-safe, and not meant to be. Every caller is a view model on the UI thread, which is the same
/// reason the observable collections beside them are not either.
/// </para>
/// </remarks>
public sealed class VaultVisibility
{
    private readonly VaultStore store;
    private readonly HashSet<Guid> hidden;

    private VaultVisibility(VaultStore store, IEnumerable<Guid> hidden)
    {
        this.store = store;
        this.hidden = [.. hidden];
    }

    /// <summary>Reads this machine's preferences for an open session.</summary>
    /// <remarks>
    /// Read once, at unlock, rather than per list rebuild. The set is small and changes only when somebody
    /// presses a switch, and the lists that consult it are rebuilt on every background sync.
    /// </remarks>
    public static async Task<VaultVisibility> LoadAsync(
        VaultSession session,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(session);

        var vaults = session.Vault;

        var stored = await vaults.ListHiddenAsync(cancellationToken).ConfigureAwait(false);

        return new VaultVisibility(vaults, stored);
    }

    /// <summary>Whether this vault's items are kept off the screens.</summary>
    public bool IsHidden(Guid vaultId) => hidden.Contains(vaultId);

    /// <summary>Whether this vault's items are drawn.</summary>
    /// <remarks>
    /// A vault nobody has said anything about is shown. That is what makes this feature cost nothing to
    /// ignore, and it is also what a fresh cache, a new grant and a re-granted vault all land on.
    /// </remarks>
    public bool IsShown(Guid vaultId) => !hidden.Contains(vaultId);

    /// <summary>Records whether one vault's items are drawn.</summary>
    /// <remarks>
    /// The disk write happens first and the set is updated only if it returned. A preference that took
    /// effect on screen but never reached the cache would come back on the next launch, and a switch that
    /// silently forgets is worse than one that refuses.
    /// </remarks>
    public async Task SetHiddenAsync(Guid vaultId, bool value, CancellationToken cancellationToken)
    {
        await store.SetHiddenAsync(vaultId, value, cancellationToken).ConfigureAwait(false);

        if (value)
        {
            hidden.Add(vaultId);
        }
        else
        {
            hidden.Remove(vaultId);
        }
    }
}
