using System.Collections.Generic;
using System.Linq;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Authority.Storage.Mongo.Documents;
using StellaOps.Authority.Storage.Mongo.Stores;

namespace StellaOps.Authority.Plugin.Standard.Storage;

internal sealed class StandardClientProvisioningStore : IClientProvisioningStore
{
    private readonly string pluginName;
    private readonly IAuthorityClientStore clientStore;
    private readonly IAuthorityRevocationStore revocationStore;
    private readonly TimeProvider clock;

    public StandardClientProvisioningStore(
        string pluginName,
        IAuthorityClientStore clientStore,
        IAuthorityRevocationStore revocationStore,
        TimeProvider clock)
    {
        this.pluginName = pluginName ?? throw new ArgumentNullException(nameof(pluginName));
        this.clientStore = clientStore ?? throw new ArgumentNullException(nameof(clientStore));
        this.revocationStore = revocationStore ?? throw new ArgumentNullException(nameof(revocationStore));
        this.clock = clock ?? throw new ArgumentNullException(nameof(clock));
    }

    public async ValueTask<AuthorityPluginOperationResult<AuthorityClientDescriptor>> CreateOrUpdateAsync(
        AuthorityClientRegistration registration,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(registration);

        if (registration.Confidential && string.IsNullOrWhiteSpace(registration.ClientSecret))
        {
            return AuthorityPluginOperationResult<AuthorityClientDescriptor>.Failure("secret_required", "Confidential clients require a client secret.");
        }

        var document = await clientStore.FindByClientIdAsync(registration.ClientId, cancellationToken).ConfigureAwait(false)
            ?? new AuthorityClientDocument { ClientId = registration.ClientId, CreatedAt = clock.GetUtcNow() };

        document.Plugin = pluginName;
        document.ClientType = registration.Confidential ? "confidential" : "public";
        document.DisplayName = registration.DisplayName;
        document.SecretHash = registration.Confidential && registration.ClientSecret is not null
            ? AuthoritySecretHasher.ComputeHash(registration.ClientSecret)
            : null;
        document.UpdatedAt = clock.GetUtcNow();

        document.RedirectUris = registration.RedirectUris.Select(static uri => uri.ToString()).ToList();
        document.PostLogoutRedirectUris = registration.PostLogoutRedirectUris.Select(static uri => uri.ToString()).ToList();

        document.Properties[AuthorityClientMetadataKeys.AllowedGrantTypes] = string.Join(" ", registration.AllowedGrantTypes);
        document.Properties[AuthorityClientMetadataKeys.AllowedScopes] = string.Join(" ", registration.AllowedScopes);
        document.Properties[AuthorityClientMetadataKeys.RedirectUris] = string.Join(" ", document.RedirectUris);
        document.Properties[AuthorityClientMetadataKeys.PostLogoutRedirectUris] = string.Join(" ", document.PostLogoutRedirectUris);

        foreach (var (key, value) in registration.Properties)
        {
            document.Properties[key] = value;
        }

        await clientStore.UpsertAsync(document, cancellationToken).ConfigureAwait(false);
        await revocationStore.RemoveAsync("client", registration.ClientId, cancellationToken).ConfigureAwait(false);

        return AuthorityPluginOperationResult<AuthorityClientDescriptor>.Success(ToDescriptor(document));
    }

    public async ValueTask<AuthorityClientDescriptor?> FindByClientIdAsync(string clientId, CancellationToken cancellationToken)
    {
        var document = await clientStore.FindByClientIdAsync(clientId, cancellationToken).ConfigureAwait(false);
        return document is null ? null : ToDescriptor(document);
    }

    public async ValueTask<AuthorityPluginOperationResult> DeleteAsync(string clientId, CancellationToken cancellationToken)
    {
        var deleted = await clientStore.DeleteByClientIdAsync(clientId, cancellationToken).ConfigureAwait(false);
        if (!deleted)
        {
            return AuthorityPluginOperationResult.Failure("not_found", "Client was not found.");
        }

        var now = clock.GetUtcNow();
        var metadata = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
        {
            ["plugin"] = pluginName
        };

        var revocation = new AuthorityRevocationDocument
        {
            Category = "client",
            RevocationId = clientId,
            ClientId = clientId,
            Reason = "operator_request",
            ReasonDescription = $"Client '{clientId}' deleted via plugin '{pluginName}'.",
            RevokedAt = now,
            EffectiveAt = now,
            Metadata = metadata
        };

        try
        {
            await revocationStore.UpsertAsync(revocation, cancellationToken).ConfigureAwait(false);
        }
        catch
        {
            // Revocation export should proceed even if the metadata write fails.
        }

        return AuthorityPluginOperationResult.Success();
    }

    private static AuthorityClientDescriptor ToDescriptor(AuthorityClientDocument document)
    {
        var allowedGrantTypes = Split(document.Properties, AuthorityClientMetadataKeys.AllowedGrantTypes);
        var allowedScopes = Split(document.Properties, AuthorityClientMetadataKeys.AllowedScopes);

        var redirectUris = document.RedirectUris
            .Select(static value => Uri.TryCreate(value, UriKind.Absolute, out var uri) ? uri : null)
            .Where(static uri => uri is not null)
            .Cast<Uri>()
            .ToArray();

        var postLogoutUris = document.PostLogoutRedirectUris
            .Select(static value => Uri.TryCreate(value, UriKind.Absolute, out var uri) ? uri : null)
            .Where(static uri => uri is not null)
            .Cast<Uri>()
            .ToArray();

        return new AuthorityClientDescriptor(
            document.ClientId,
            document.DisplayName,
            string.Equals(document.ClientType, "confidential", StringComparison.OrdinalIgnoreCase),
            allowedGrantTypes,
            allowedScopes,
            redirectUris,
            postLogoutUris,
            document.Properties);
    }

    private static IReadOnlyCollection<string> Split(IReadOnlyDictionary<string, string?> properties, string key)
    {
        if (!properties.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value))
        {
            return Array.Empty<string>();
        }

        return value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
    }
}
