using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Configuration;
using StellaOps.Cryptography;

namespace StellaOps.Authority.Signing;

internal sealed class AuthoritySigningKeyManager
{
    private readonly object syncRoot = new();
    private readonly ICryptoProviderRegistry registry;
    private readonly IReadOnlyList<IAuthoritySigningKeySource> keySources;
    private readonly StellaOpsAuthorityOptions authorityOptions;
    private readonly string basePath;
    private readonly ILogger<AuthoritySigningKeyManager> logger;
    private RegisteredSigningKey? activeKey;
    private readonly Dictionary<string, RegisteredSigningKey> retiredKeys = new(StringComparer.OrdinalIgnoreCase);

    public AuthoritySigningKeyManager(
        ICryptoProviderRegistry registry,
        IEnumerable<IAuthoritySigningKeySource> keySources,
        IOptions<StellaOpsAuthorityOptions> authorityOptions,
        IHostEnvironment environment,
        ILogger<AuthoritySigningKeyManager> logger)
    {
        this.registry = registry ?? throw new ArgumentNullException(nameof(registry));
        if (keySources is null)
        {
            throw new ArgumentNullException(nameof(keySources));
        }

        this.keySources = keySources.ToArray();
        if (this.keySources.Count == 0)
        {
            throw new InvalidOperationException("At least one Authority signing key source must be registered.");
        }

        this.authorityOptions = authorityOptions?.Value ?? throw new ArgumentNullException(nameof(authorityOptions));
        basePath = environment?.ContentRootPath ?? throw new ArgumentNullException(nameof(environment));
        this.logger = logger ?? throw new ArgumentNullException(nameof(logger));

        LoadInitialKeys();
    }

    public SigningRotationResult Rotate(SigningRotationRequest request)
    {
        ArgumentNullException.ThrowIfNull(request);

        lock (syncRoot)
        {
            var signing = authorityOptions.Signing ?? throw new InvalidOperationException("Authority signing configuration is not available.");
            if (!signing.Enabled)
            {
                throw new InvalidOperationException("Signing is disabled. Enable signing before rotating keys.");
            }

            var keyId = (request.KeyId ?? string.Empty).Trim();
            if (string.IsNullOrWhiteSpace(keyId))
            {
                throw new InvalidOperationException("Rotation requires a keyId.");
            }

            var location = request.Location?.Trim();
            if (string.IsNullOrWhiteSpace(location))
            {
                throw new InvalidOperationException("Rotation requires a keyPath/location for the new signing key.");
            }

            var algorithm = NormaliseAlgorithm(string.IsNullOrWhiteSpace(request.Algorithm)
                ? signing.Algorithm
                : request.Algorithm);
            var source = NormaliseSource(string.IsNullOrWhiteSpace(request.Source)
                ? signing.KeySource
                : request.Source);
            var providerName = NormaliseProviderName(request.Provider ?? signing.Provider);

            IReadOnlyDictionary<string, string?>? metadata = null;
            if (request.Metadata is not null && request.Metadata.Count > 0)
            {
                metadata = new Dictionary<string, string?>(request.Metadata, StringComparer.OrdinalIgnoreCase);
            }

            var provider = ResolveProvider(providerName, algorithm);
            var loader = ResolveSource(source);
            var loadRequest = new AuthoritySigningKeyRequest(
                keyId,
                algorithm,
                source,
                location,
                AuthoritySigningKeyStatus.Active,
                basePath,
                provider.Name,
                additionalMetadata: metadata);
            var newKey = loader.Load(loadRequest);
            provider.UpsertSigningKey(newKey);

            if (retiredKeys.Remove(keyId))
            {
                logger.LogInformation("Promoted retired signing key {KeyId} to active status.", keyId);
            }

            string? previousKeyId = null;
            if (activeKey is not null)
            {
                previousKeyId = activeKey.Key.Reference.KeyId;
                if (!string.Equals(previousKeyId, keyId, StringComparison.OrdinalIgnoreCase))
                {
                    RetireCurrentActive();
                }
            }

            activeKey = new RegisteredSigningKey(newKey, provider.Name, source, location);
            signing.ActiveKeyId = keyId;
            signing.KeyPath = location;
            signing.KeySource = source;
            signing.Provider = provider.Name;

            RemoveAdditionalOption(keyId);

            logger.LogInformation("Authority signing key rotated. Active key is now {KeyId} via provider {Provider}.", keyId, provider.Name);

            return new SigningRotationResult(
                keyId,
                provider.Name,
                source,
                location,
                previousKeyId,
                retiredKeys.Keys.ToArray());
        }
    }

    public SigningKeySnapshot Snapshot
    {
        get
        {
            lock (syncRoot)
            {
                var active = activeKey;
                return new SigningKeySnapshot(
                    active?.Key.Reference.KeyId,
                    active?.ProviderName,
                    active?.Source,
                    active?.Location,
                    retiredKeys.Values
                        .Select(static registration => new SigningKeySnapshot.RetiredKey(
                            registration.Key.Reference.KeyId,
                            registration.ProviderName,
                            registration.Source,
                            registration.Location))
                        .ToArray());
            }
        }
    }

    private void LoadInitialKeys()
    {
        var signing = authorityOptions.Signing;
        if (signing is null || !signing.Enabled)
        {
            logger.LogInformation("Authority signing is disabled; JWKS will expose ephemeral keys until signing is enabled.");
            return;
        }

        var algorithm = NormaliseAlgorithm(signing.Algorithm);
        var source = NormaliseSource(signing.KeySource);
        var activeRequest = new AuthoritySigningKeyRequest(
            signing.ActiveKeyId,
            algorithm,
            source,
            signing.KeyPath,
            AuthoritySigningKeyStatus.Active,
            basePath,
            NormaliseProviderName(signing.Provider));
        activeKey = LoadAndRegister(activeRequest);
        signing.KeySource = source;
        signing.Provider = activeKey.ProviderName;

        foreach (var additional in signing.AdditionalKeys)
        {
            var keyId = (additional.KeyId ?? string.Empty).Trim();
            if (string.IsNullOrWhiteSpace(keyId))
            {
                logger.LogWarning("Skipped additional signing key with empty keyId.");
                continue;
            }

            if (string.Equals(keyId, activeKey.Key.Reference.KeyId, StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            var additionalLocation = additional.Path?.Trim();
            if (string.IsNullOrWhiteSpace(additionalLocation))
            {
                logger.LogWarning("Additional signing key {KeyId} is missing a path. Skipping.", keyId);
                continue;
            }

            var additionalSource = NormaliseSource(additional.Source ?? source);
            var request = new AuthoritySigningKeyRequest(
                keyId,
                algorithm,
                additionalSource,
                additionalLocation,
                AuthoritySigningKeyStatus.Retired,
                basePath,
                NormaliseProviderName(signing.Provider));

            try
            {
                var registration = LoadAndRegister(request);
                retiredKeys[registration.Key.Reference.KeyId] = registration;
                additional.Source = additionalSource;
            }
            catch (Exception ex)
            {
                logger.LogWarning(ex, "Failed to load retired signing key {KeyId}. It will be ignored for JWKS responses.", keyId);
            }
        }
    }

    private void RetireCurrentActive()
    {
        if (activeKey is null)
        {
            return;
        }

        var previous = activeKey;
        var metadata = new Dictionary<string, string?>(previous.Key.Metadata, StringComparer.OrdinalIgnoreCase)
        {
            ["status"] = AuthoritySigningKeyStatus.Retired
        };

        var privateParameters = previous.Key.PrivateParameters;
        var retiredKey = new CryptoSigningKey(
            previous.Key.Reference,
            previous.Key.AlgorithmId,
            in privateParameters,
            previous.Key.CreatedAt,
            previous.Key.ExpiresAt,
            metadata);

        var provider = ResolveProvider(previous.ProviderName, retiredKey.AlgorithmId);
        provider.UpsertSigningKey(retiredKey);

        var registration = new RegisteredSigningKey(retiredKey, provider.Name, previous.Source, previous.Location);
        retiredKeys[registration.Key.Reference.KeyId] = registration;
        UpsertAdditionalOption(registration);

        logger.LogInformation("Moved signing key {KeyId} to retired set (provider {Provider}).", registration.Key.Reference.KeyId, provider.Name);
    }

    private RegisteredSigningKey LoadAndRegister(AuthoritySigningKeyRequest request)
    {
        var source = ResolveSource(request.Source);
        var provider = ResolveProvider(request.Provider, request.Algorithm);
        var key = source.Load(request);
        provider.UpsertSigningKey(key);

        logger.LogDebug("Loaded signing key {KeyId} (status {Status}) via provider {Provider}.", key.Reference.KeyId, request.Status, provider.Name);

        return new RegisteredSigningKey(key, provider.Name, request.Source, request.Location);
    }

    private IAuthoritySigningKeySource ResolveSource(string source)
    {
        foreach (var loader in keySources)
        {
            if (loader.CanLoad(source))
            {
                return loader;
            }
        }

        throw new InvalidOperationException($"No signing key source registered for '{source}'.");
    }

    private ICryptoProvider ResolveProvider(string? preferredProvider, string algorithm)
    {
        var normalised = NormaliseProviderName(preferredProvider);
        if (!string.IsNullOrWhiteSpace(normalised) &&
            registry.TryResolve(normalised!, out var provider) &&
            provider.Supports(CryptoCapability.Signing, algorithm))
        {
            return provider;
        }

        return registry.ResolveOrThrow(CryptoCapability.Signing, algorithm);
    }

    private void UpsertAdditionalOption(RegisteredSigningKey registration)
    {
        var additional = authorityOptions.Signing.AdditionalKeys;
        for (var index = 0; index < additional.Count; index++)
        {
            var entry = additional[index];
            if (string.Equals(entry.KeyId, registration.Key.Reference.KeyId, StringComparison.OrdinalIgnoreCase))
            {
                entry.Path = registration.Location;
                entry.Source = registration.Source;
                return;
            }
        }

        additional.Add(new AuthoritySigningAdditionalKeyOptions
        {
            KeyId = registration.Key.Reference.KeyId,
            Path = registration.Location,
            Source = registration.Source
        });
    }

    private void RemoveAdditionalOption(string keyId)
    {
        var additional = authorityOptions.Signing.AdditionalKeys;
        for (var index = additional.Count - 1; index >= 0; index--)
        {
            if (string.Equals(additional[index].KeyId, keyId, StringComparison.OrdinalIgnoreCase))
            {
                additional.RemoveAt(index);
            }
        }
    }

    private static string NormaliseAlgorithm(string? algorithm)
    {
        return string.IsNullOrWhiteSpace(algorithm)
            ? SignatureAlgorithms.Es256
            : algorithm.Trim();
    }

    private static string NormaliseSource(string? source)
    {
        return string.IsNullOrWhiteSpace(source) ? "file" : source.Trim();
    }

    private static string? NormaliseProviderName(string? provider)
    {
        return string.IsNullOrWhiteSpace(provider) ? null : provider.Trim();
    }

    private sealed record RegisteredSigningKey(
        CryptoSigningKey Key,
        string ProviderName,
        string Source,
        string Location);
}

internal sealed record SigningRotationResult(
    string ActiveKeyId,
    string ActiveProvider,
    string ActiveSource,
    string ActiveLocation,
    string? PreviousKeyId,
    IReadOnlyCollection<string> RetiredKeyIds);

internal sealed class SigningKeySnapshot
{
    public SigningKeySnapshot(
        string? activeKeyId,
        string? activeProvider,
        string? activeSource,
        string? activeLocation,
        IReadOnlyCollection<RetiredKey> retired)
    {
        ActiveKeyId = activeKeyId;
        ActiveProvider = activeProvider;
        ActiveSource = activeSource;
        ActiveLocation = activeLocation;
        Retired = retired ?? Array.Empty<RetiredKey>();
    }

    public string? ActiveKeyId { get; }

    public string? ActiveProvider { get; }

    public string? ActiveSource { get; }

    public string? ActiveLocation { get; }

    public IReadOnlyCollection<RetiredKey> Retired { get; }

    public sealed record RetiredKey(
        string KeyId,
        string Provider,
        string Source,
        string Location);
}
