using Ablera.Serdica.Authentication.Models.Oidc;
using Ablera.Serdica.Authority.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OpenIddict.Abstractions;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static OpenIddict.Abstractions.OpenIddictConstants;

namespace Ablera.Serdica.Authority.Services;

/// <summary>
/// Synchronizes OpenIddict client registrations from configuration, performing upserts
/// and only applying changes when the descriptor differs from stored values.
/// </summary>
public class OidcClientSynchronizer(
        IOpenIddictApplicationManager manager,
        OidcJsonSettingsProvider settingsProvider,
        ILogger<OidcClientSynchronizer> logger)
{
    /// <summary>
    /// Reads configured clients and upserts them into OpenIddict, applying changes only when needed.
    /// </summary>
    public async Task SynchronizeAsync(CancellationToken cancellationToken = default)
    {
        // Iterate all clients from JSON settings
        foreach (var client in settingsProvider.Settings.RegisteredClients)
        {
            // Build the descriptor from config, injecting dynamic UI URLs when applicable
            var descriptor = BuildDescriptor(client);

            // Upsert the application
            await UpsertClientAsync(descriptor, cancellationToken);
        }
    }

    private OpenIddictApplicationDescriptor BuildDescriptor(
        RegisteredClient client)
    {
        var descriptor = new OpenIddictApplicationDescriptor
        {
            ClientId = client.ClientId,
            ClientType = client.ClientType switch
            {
                OpenIddictConstants.ClientTypes.Public => OpenIddictConstants.ClientTypes.Public,
                OpenIddictConstants.ClientTypes.Confidential => OpenIddictConstants.ClientTypes.Confidential,
                _ => throw new InvalidOperationException("Unknown client type")
            },
            DisplayName = client.DisplayName,
            // ClientSecret may be null for public clients
            ClientSecret = client.ClientSecret,
        };

        // non-UI clients: use static values
        foreach (var uri in client.RedirectUris ?? [])
            descriptor.RedirectUris.Add(new Uri(uri, UriKind.Absolute));
        foreach (var uri in client.PostLogoutRedirectUris ?? [])
            descriptor.PostLogoutRedirectUris.Add(new Uri(uri, UriKind.Absolute));

        // copy over any custom Properties
        foreach (var kv in client.Properties ?? [])
            descriptor.Properties[kv.Key] = kv.Value;

        // Copy permissions and requirements
        client.Permissions?.ToList()?.ForEach(x => descriptor.Permissions.Add(x));
        client.Requirements?.ToList()?.ForEach(x => descriptor.Requirements.Add(x));

        return descriptor;
    }

    private async Task UpsertClientAsync(
        OpenIddictApplicationDescriptor descriptor,
        CancellationToken cancellationToken)
    {
        var existing = await manager.FindByClientIdAsync(
            descriptor.ClientId ?? throw new ArgumentNullException(nameof(descriptor.ClientId)), 
            cancellationToken);
        if (existing is null)
        {
            try
            {
                await manager.CreateAsync(descriptor, cancellationToken);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Failed to create OIDC client '{ClientId}'", descriptor.ClientId);
                throw;
            }
            logger.LogInformation("Created OIDC client '{ClientId}'", descriptor.ClientId);
            return;
        }

        // Compare existing settings to the descriptor
        if (!await NeedsUpdateAsync(existing, descriptor, cancellationToken))
        {
            logger.LogDebug("No changes for client '{ClientId}', skipping update.", descriptor.ClientId);
            return;
        }

        // Perform update
        await manager.UpdateAsync(existing, descriptor, cancellationToken);
        logger.LogInformation("Updated OIDC client '{ClientId}'", descriptor.ClientId);
    }


    private async Task<bool> NeedsUpdateAsync(
        object existing,
        OpenIddictApplicationDescriptor descriptor,
        CancellationToken cancellationToken)
    {
        var existingRedirectUris = (await manager
            .GetRedirectUrisAsync(existing, cancellationToken))
            .ToHashSet(StringComparer.Ordinal);
        var descriptorRedirectUris = descriptor.RedirectUris
            .Select(u => u.OriginalString)
            .ToHashSet(StringComparer.Ordinal);
        if (!existingRedirectUris.SetEquals(descriptorRedirectUris)) return true;

        var existingPostLogoutRedirectUris = (await manager
            .GetPostLogoutRedirectUrisAsync(existing, cancellationToken))
            .ToHashSet(StringComparer.Ordinal);
        var descriptorPostLogoutRedirectUris = descriptor.PostLogoutRedirectUris
            .Select(u => u.OriginalString)
            .ToHashSet(StringComparer.Ordinal);
        if (!existingPostLogoutRedirectUris.SetEquals(descriptorPostLogoutRedirectUris)) return true;

        // Load permissions, requirements, client type, and check secret
        var existingPerms = (await manager.GetPermissionsAsync(existing, cancellationToken))
                            .ToHashSet(StringComparer.OrdinalIgnoreCase);
        if (!existingPerms.SetEquals(descriptor.Permissions)) return true;

        var existingReqs = (await manager.GetRequirementsAsync(existing, cancellationToken))
                           .ToHashSet(StringComparer.OrdinalIgnoreCase);
        if (!existingPerms.SetEquals(descriptor.Permissions)) return true;

        var existingType = await manager.GetClientTypeAsync(existing, cancellationToken);
        if (!string.Equals(existingType, descriptor.ClientType, StringComparison.OrdinalIgnoreCase)) return true;

        bool secretChanged = false;
        if (!string.IsNullOrWhiteSpace(descriptor.ClientSecret))
        {
            secretChanged = !await manager.ValidateClientSecretAsync(
                existing, descriptor.ClientSecret, cancellationToken);
        }
        if (secretChanged) return true;
        var existingProperties = (await manager.GetPropertiesAsync(existing, cancellationToken));
        
        if (!descriptor.Properties.DictionaryEquals(existingProperties, JsonElementEqualityComparer.Default)) return true;

        return false;
    }
}