using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Crypto;
using ZB.MOM.WW.Secrets.MasterKey;
using ZB.MOM.WW.Secrets.Replicator.SqlServer;
using ZB.MOM.WW.Secrets.Sqlite;

namespace ZB.MOM.WW.Secrets.Cli.Interactive;

/// <summary>
/// Opens a <see cref="SecretsSession"/> for a target by constructing the same store stack the app's DI
/// extensions build — but directly, without a host, and (unlike the app) tolerant of a missing KEK:
/// resolution failure yields a DEGRADED session the shell can later upgrade with an operator-supplied
/// key, rather than throwing and leaving the operator unable to even list what is in the store.
/// </summary>
public sealed class SecretsSessionFactory
{
    /// <summary>Marker for an unresolved <c>${secret:}</c> reference embedded in a connection string.</summary>
    private const string SecretRefMarker = "${secret:";

    /// <summary>Builds the store stack for a target. KEK resolution failure yields a DEGRADED session, never a throw.</summary>
    /// <param name="target">The resolved store target.</param>
    /// <param name="overrideKek">
    /// An operator-supplied KEK that pre-empts the target's configured source, or <see langword="null"/>
    /// to use the configured provider.
    /// </param>
    /// <param name="ct">A token to cancel the schema migration.</param>
    /// <returns>An open session — full when a KEK resolves, degraded otherwise.</returns>
    public async Task<SecretsSession> OpenAsync(
        StoreTarget target, IMasterKeyProvider? overrideKek, CancellationToken ct)
    {
        ArgumentNullException.ThrowIfNull(target);

        List<string> warnings = [];

        (ISecretStore store, ISecretsStoreMigrator migrator, string storeKind) = BuildStore(target, warnings);
        (IMasterKeyProvider? masterKey, ISecretCipher? cipher) = ResolveKek(target, overrideKek, warnings);

        // Migrate regardless of KEK state: metadata/list ops must work in a degraded session.
        await migrator.MigrateAsync(ct).ConfigureAwait(false);

        return new SecretsSession
        {
            Target = target,
            Store = store,
            Migrator = migrator,
            MasterKey = masterKey,
            Cipher = cipher,
            StoreKind = storeKind,
            Warnings = warnings,
        };
    }

    // Mirrors the DI extensions' store selection: a configured SQL-Server hub wins, EXCEPT when its
    // connection string is an unresolved '${secret:}' reference (the app resolves that from its own
    // bootstrapped store at startup; the CLI has no such resolver), in which case fall back to SQLite.
    //
    // The CLI deliberately treats a configured Secrets:SqlServer section as the authoritative target
    // regardless of the app's replication topology: in hub-replication mode the SQL-Server hub is the
    // shared source of truth, so editing it here is correct — the change converges to each node's local
    // store via the sync sweep, rather than being written to one node's local SQLite and racing the hub.
    private static (ISecretStore Store, ISecretsStoreMigrator Migrator, string StoreKind) BuildStore(
        StoreTarget target, List<string> warnings)
    {
        if (target.SqlServer is { } sql && !string.IsNullOrEmpty(sql.ConnectionString))
        {
            if (sql.ConnectionString.Contains(SecretRefMarker, StringComparison.Ordinal))
            {
                warnings.Add(
                    "SQL-Server connection string is an unresolved '${secret:}' reference the console " +
                    "cannot resolve; falling back to the local SQLite store.");
            }
            else
            {
                var sqlFactory = new SecretsSqlServerConnectionFactory(sql);
                return (
                    new SqlServerSecretStore(sqlFactory),
                    new SqlServerSecretsStoreMigrator(sqlFactory),
                    "sqlserver");
            }
        }

        var sqliteFactory = new SecretsSqliteConnectionFactory(target.Secrets.SqlitePath);
        return (
            new SqliteSecretStore(sqliteFactory),
            new SqliteSecretsStoreMigrator(sqliteFactory),
            "sqlite");
    }

    // Resolves the KEK (override, else the configured provider) and forces material resolution now. A
    // documented resolution failure degrades the session here instead of throwing on the first seal.
    private static (IMasterKeyProvider? MasterKey, ISecretCipher? Cipher) ResolveKek(
        StoreTarget target, IMasterKeyProvider? overrideKek, List<string> warnings)
    {
        try
        {
            IMasterKeyProvider provider =
                overrideKek ?? MasterKeyProviderFactory.Create(target.Secrets.MasterKey);

            // GetMasterKey (not KekId) is the probe: KekId short-circuits when an explicit id is
            // configured (MasterKeyProviderBase.KekId) and so would not touch the key source, whereas
            // GetMasterKey always resolves and length-validates the material — exactly what must
            // succeed for the cipher to work.
            _ = provider.GetMasterKey();

            return (provider, new AesGcmEnvelopeCipher(provider));
        }
        catch (ArgumentOutOfRangeException ex)
        {
            // MasterKeyProviderFactory.Create throws this when MasterKeySource is out of range — a
            // malformed config value. This is operator-facing lockout tooling, so degrade rather than
            // crash; name the invalid value so the operator can see what to fix.
            warnings.Add(
                $"Master-key source '{target.Secrets.MasterKey.Source}' is not a recognized value — " +
                $"session is degraded (metadata only): {ex.Message}");
            return (null, null);
        }
        catch (MasterKeyUnavailableException ex)
        {
            // Thrown by every provider (Environment/File/Dpapi) and the base length check when the key
            // source is missing, empty, malformed, or the wrong length.
            warnings.Add($"Master key unavailable — session is degraded (metadata only): {ex.Message}");
            return (null, null);
        }
        catch (PlatformNotSupportedException ex)
        {
            // Thrown by DpapiMasterKeyProvider.ResolveKeyBytes off Windows.
            warnings.Add(
                $"Master key source is not supported on this platform — session is degraded (metadata only): {ex.Message}");
            return (null, null);
        }
    }
}
