using MemberCenter.Application.Abstractions;
using MemberCenter.Domain.Entities;
using MemberCenter.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using OpenIddict.Abstractions;

namespace MemberCenter.Infrastructure.Services;

public sealed class AuthResourceRegistryService : IAuthResourceRegistryService
{
    private const string MemberCenterResourceName = "member_center_api";

    private readonly MemberCenterDbContext _dbContext;
    private readonly IConfiguration _configuration;

    public AuthResourceRegistryService(MemberCenterDbContext dbContext, IConfiguration configuration)
    {
        _dbContext = dbContext;
        _configuration = configuration;
    }

    public async Task EnsureDefaultsAsync(CancellationToken cancellationToken = default)
    {
        var memberCenter = await EnsureResourceAsync(
            MemberCenterResourceName,
            ResolveAudience("MemberCenter", "Auth:MemberCenterAudience", "member_center_api"),
            "Member Center API",
            requireTenant: false,
            allowDelegatedToken: true,
            cancellationToken);

        var sendEngine = await EnsureResourceAsync(
            "send_engine_api",
            ResolveAudience("SendEngine", "Auth:SendEngineAudience", "send_engine_api"),
            "Send Engine API",
            requireTenant: true,
            allowDelegatedToken: false,
            cancellationToken);

        var fileAccess = await EnsureResourceAsync(
            "file_access_api",
            ResolveAudience("FileAccess", null, "file_access_api"),
            "File upload/download API",
            requireTenant: true,
            allowDelegatedToken: true,
            cancellationToken);

        await EnsureScopesAsync(memberCenter.Id, [
            OpenIddictConstants.Scopes.OpenId,
            OpenIddictConstants.Scopes.Email,
            OpenIddictConstants.Scopes.Profile,
            "profile:basic.read",
            "profile:basic.write",
            "profile:addresses.read",
            "profile:addresses.write",
            "profile:subscriptions.read",
            "profile:subscriptions.write",
            "newsletter:list.read",
            "newsletter:list.read.global",
            "newsletter:subscriptions.read",
            "newsletter:subscriptions.write",
            "newsletter:events.read",
            "newsletter:events.write",
            "newsletter:events.write.global"
        ], cancellationToken);

        await EnsureScopesAsync(sendEngine.Id, [
            "newsletter:send.write",
            "newsletter:send.read"
        ], cancellationToken);

        await EnsureScopesAsync(fileAccess.Id, [
            "files:upload.write",
            "files:download.read",
            "files:download.delegate",
            "files:metadata.read",
            "files:delete"
        ], cancellationToken);

        await EnsureUsagePermissionsAsync("web_login", [
            OpenIddictConstants.Scopes.OpenId,
            OpenIddictConstants.Scopes.Email,
            OpenIddictConstants.Scopes.Profile,
            "profile:basic.read",
            "profile:basic.write",
            "profile:addresses.read",
            "profile:addresses.write",
            "profile:subscriptions.read",
            "profile:subscriptions.write"
        ], cancellationToken);

        await EnsureUsagePermissionsAsync("webhook_outbound", [
            OpenIddictConstants.Scopes.OpenId,
            OpenIddictConstants.Scopes.Email,
            OpenIddictConstants.Scopes.Profile,
            "newsletter:events.write"
        ], cancellationToken);

        await EnsureUsagePermissionsAsync("tenant_api", [
            "newsletter:events.write",
            "newsletter:list.read",
            "newsletter:subscriptions.read",
            "newsletter:subscriptions.write",
            "profile:basic.read",
            "profile:basic.write",
            "profile:addresses.read",
            "profile:addresses.write",
            "profile:subscriptions.read",
            "profile:subscriptions.write"
        ], cancellationToken);

        await EnsureUsagePermissionsAsync("platform_service", [
            "newsletter:events.write.global",
            "newsletter:list.read.global",
            "profile:basic.read",
            "profile:basic.write",
            "profile:addresses.read",
            "profile:addresses.write",
            "profile:subscriptions.read",
            "profile:subscriptions.write"
        ], cancellationToken);

        await EnsureUsagePermissionsAsync("send_api", [
            "newsletter:send.write",
            "newsletter:send.read"
        ], cancellationToken);

        await EnsureUsagePermissionsAsync("file_api", [
            "files:upload.write",
            "files:download.read",
            "files:download.delegate",
            "files:metadata.read",
            "files:delete"
        ], cancellationToken);

        await _dbContext.SaveChangesAsync(cancellationToken);
    }

    public async Task<IReadOnlyList<string>> ResolveAudiencesAsync(
        IEnumerable<string> scopes,
        CancellationToken cancellationToken = default)
    {
        var requestedScopes = scopes
            .Where(scope => !string.IsNullOrWhiteSpace(scope))
            .Select(scope => scope.Trim())
            .Distinct(StringComparer.Ordinal)
            .ToList();

        if (requestedScopes.Count == 0)
        {
            var defaultAudience = await _dbContext.AuthResources
                .Where(resource => resource.Name == MemberCenterResourceName && resource.IsEnabled)
                .Select(resource => resource.Audience)
                .SingleOrDefaultAsync(cancellationToken);

            return string.IsNullOrWhiteSpace(defaultAudience)
                ? ["member_center_api"]
                : [defaultAudience];
        }

        var resourceScopes = await _dbContext.AuthResourceScopes
            .AsNoTracking()
            .Include(resourceScope => resourceScope.Resource)
            .Where(resourceScope =>
                resourceScope.IsEnabled
                && requestedScopes.Contains(resourceScope.Scope)
                && resourceScope.Resource != null
                && resourceScope.Resource.IsEnabled)
            .ToListAsync(cancellationToken);

        var resolvedScopes = resourceScopes
            .Select(resourceScope => resourceScope.Scope)
            .ToHashSet(StringComparer.Ordinal);

        var unknownScopes = requestedScopes
            .Where(scope => !resolvedScopes.Contains(scope))
            .ToList();
        if (unknownScopes.Count > 0)
        {
            throw new InvalidOperationException($"Unknown auth scope(s): {string.Join(", ", unknownScopes)}");
        }

        return resourceScopes
            .Select(resourceScope => resourceScope.Resource!.Audience)
            .Distinct(StringComparer.Ordinal)
            .ToList();
    }

    public async Task<IReadOnlyList<string>> GetAllowedScopesForUsageAsync(
        string usage,
        CancellationToken cancellationToken = default)
    {
        var normalizedUsage = usage.Trim();
        return await _dbContext.AuthClientUsagePermissions
            .AsNoTracking()
            .Where(permission => permission.Usage == normalizedUsage && permission.IsEnabled)
            .OrderBy(permission => permission.Scope)
            .Select(permission => permission.Scope)
            .ToListAsync(cancellationToken);
    }

    private async Task<AuthResource> EnsureResourceAsync(
        string name,
        string audience,
        string description,
        bool requireTenant,
        bool allowDelegatedToken,
        CancellationToken cancellationToken)
    {
        var resource = await _dbContext.AuthResources
            .SingleOrDefaultAsync(item => item.Name == name, cancellationToken);

        if (resource is null)
        {
            resource = new AuthResource
            {
                Id = Guid.NewGuid(),
                Name = name,
                Audience = audience
            };
            _dbContext.AuthResources.Add(resource);
        }

        resource.Audience = audience;
        resource.Description = description;
        resource.RequireTenant = requireTenant;
        resource.AllowDelegatedToken = allowDelegatedToken;
        resource.IsEnabled = true;
        resource.UpdatedAt = DateTimeOffset.UtcNow;
        return resource;
    }

    private string ResolveAudience(string resourceKey, string? legacyKey, string defaultValue)
    {
        var configured = _configuration[$"Auth:Resources:{resourceKey}:Audience"];
        if (string.IsNullOrWhiteSpace(configured) && !string.IsNullOrWhiteSpace(legacyKey))
        {
            configured = _configuration[legacyKey];
        }

        return string.IsNullOrWhiteSpace(configured) ? defaultValue : configured;
    }

    private async Task EnsureScopesAsync(
        Guid resourceId,
        IEnumerable<string> scopes,
        CancellationToken cancellationToken)
    {
        var existingScopes = await _dbContext.AuthResourceScopes
            .Where(scope => scope.ResourceId == resourceId)
            .ToDictionaryAsync(scope => scope.Scope, StringComparer.Ordinal, cancellationToken);

        foreach (var scope in scopes.Distinct(StringComparer.Ordinal))
        {
            if (existingScopes.TryGetValue(scope, out var existing))
            {
                existing.IsEnabled = true;
                continue;
            }

            _dbContext.AuthResourceScopes.Add(new AuthResourceScope
            {
                Id = Guid.NewGuid(),
                ResourceId = resourceId,
                Scope = scope,
                IsEnabled = true
            });
        }
    }

    private async Task EnsureUsagePermissionsAsync(
        string usage,
        IEnumerable<string> scopes,
        CancellationToken cancellationToken)
    {
        var desiredScopes = scopes.ToHashSet(StringComparer.Ordinal);
        var existingPermissions = await _dbContext.AuthClientUsagePermissions
            .Where(permission => permission.Usage == usage)
            .ToDictionaryAsync(permission => permission.Scope, StringComparer.Ordinal, cancellationToken);

        foreach (var permission in existingPermissions.Values)
        {
            permission.IsEnabled = desiredScopes.Contains(permission.Scope);
        }

        foreach (var scope in desiredScopes)
        {
            if (existingPermissions.TryGetValue(scope, out var existing))
            {
                existing.IsEnabled = true;
                continue;
            }

            _dbContext.AuthClientUsagePermissions.Add(new AuthClientUsagePermission
            {
                Id = Guid.NewGuid(),
                Usage = usage,
                Scope = scope,
                IsEnabled = true
            });
        }
    }
}
