using Ablera.Serdica.Common.Tools.Extensions;
using Ablera.Serdica.Authority.Contracts;
using Ablera.Serdica.Authority.Models;
using Ablera.Serdica.Authority.Plugins.Base.Contracts;
using Ablera.Serdica.Authority.Plugins.Base.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ablera.Serdica.DBModels.Serdica;
using Microsoft.Extensions.Logging;

namespace Ablera.Serdica.Authority.Services;

public class UserManagingDirector(
        SerdicaDbContext dbContext,
        ILogger<UserManagingDirector> logger,
        IEnumerable<IUserManagementFacade<IdentityUser<string>>> userManagers,
        IOptions<UserManagingDirectorConfig> options)
    : IUserManagingDirector<IdentityUser<string>>
{
    // --------------------------------------------------------------------
    //  Configuration taken from appsettings → injected via IOptions
    // --------------------------------------------------------------------
    private readonly UserManagingDirectorConfig _cfg = options.Value;

    // --------------------------------------------------------------------
    //  Priority table – bigger number = stronger / more important error
    // --------------------------------------------------------------------
    private static readonly IReadOnlyDictionary<string, int> ErrorRank =
        new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
        {
            [AuthenticationCode.AccountIsLocked.ToScreamingSnakeCase()] = 400,
            [AuthenticationCode.AccountIsNotActive.ToScreamingSnakeCase()] = 300,
            [AuthenticationCode.InvalidPassword.ToScreamingSnakeCase()] = 200,
            [AuthenticationCode.InvalidCredentials.ToScreamingSnakeCase()] = 200,
            [AuthenticationCode.AccountIsNotFound.ToScreamingSnakeCase()] = 100,
            [AuthenticationCode.NoAuthBackend.ToScreamingSnakeCase()] = 0
        };

    // --------------------------------------------------------------------
    //  Helpers that pick the “stronger” result
    // --------------------------------------------------------------------
    private static AuthenticationResult Pick(AuthenticationResult? a, AuthenticationResult? b)
    {
        if (a is null) return b!;
        if (b is null) return a;
        var ra = ErrorRank.GetValueOrDefault(a.ErrorCode ?? string.Empty, -1);
        var rb = ErrorRank.GetValueOrDefault(b.ErrorCode ?? string.Empty, -1);
        return rb > ra ? b : a;
    }

    private static OperationResult Pick(OperationResult? a, OperationResult? b)
    {
        if (a is null) return b!;
        if (b is null) return a;

        // Success beats any failure
        if (a.Succeeded && !b.Succeeded) return a;
        if (b.Succeeded && !a.Succeeded) return b;

        // Both success or both failure → use the ranking table
        var ra = ErrorRank.GetValueOrDefault(a.ErrorCode ?? string.Empty, -1);
        var rb = ErrorRank.GetValueOrDefault(b.ErrorCode ?? string.Empty, -1);
        return rb > ra ? b : a;
    }

    // ====================================================================
    //  1. Authentication
    // ====================================================================
    public async Task<AuthenticationResult> AuthenticateAsync(
        IdentityUser<string> user,
        string password,
        bool lockoutOnFailure = false,
        CancellationToken ct = default)
    {
        if (userManagers.Any() == false)
        {
            logger.LogWarning("No any backend authorization backend are found. Did you install any plugins?");
            return AuthenticationResult.Fail(AuthenticationCode.NoAuthBackend.ToScreamingSnakeCase());
        }
        AuthenticationResult? aggregate = null;

        foreach (var manager in userManagers)
        {
            var res = await manager.AuthenticateAsync(user, password, lockoutOnFailure, ct);

            if (res.Succeeded)                      // success wins instantly
                return res;

            aggregate = Pick(aggregate, res);       // remember strongest error

            if (!_cfg.LoginAnywhere)                // only first backend is allowed
                break;
        }

        return aggregate
            ?? AuthenticationResult.Fail(AuthenticationCode.NoAuthBackend.ToScreamingSnakeCase());
    }

    // ====================================================================
    //  2.  WRITE operations (propagation depends on UpdateEveryWhere flag)
    // ====================================================================

    public Task<OperationResult> CreateAsync(
        IdentityUser<string> user,
        string password,
        CancellationToken ct = default)
        => PropagateAsync(mgr => mgr.CreateAsync(user, password, ct));

    public Task<OperationResult> ChangePasswordAsync(
        IdentityUser<string> user,
        string currentPassword,
        string newPassword,
        CancellationToken ct = default)
        => PropagateAsync(mgr => mgr.ChangePasswordAsync(user, currentPassword, newPassword, ct));

    public Task<OperationResult> ResetPasswordAsync(
        IdentityUser<string> user,
        string token,
        string newPassword,
        CancellationToken ct = default)
        => PropagateAsync(mgr => mgr.ResetPasswordAsync(user, token, newPassword, ct));

    public Task<OperationResult> UpdateAsync(
        IdentityUser<string> user,
        CancellationToken ct = default)
        => PropagateAsync(mgr => mgr.UpdateAsync(user, ct));

    public Task<OperationResult> LockAsync(
        IdentityUser<string> user,
        CancellationToken ct = default)
        => PropagateAsync(mgr => mgr.LockAsync(user, ct));

    public Task<OperationResult> UnlockAsync(
        IdentityUser<string> user,
        CancellationToken ct = default)
        => PropagateAsync(mgr => mgr.UnlockAsync(user, ct));

    // --------------------------------------------------------------------
    //  Shared propagator for all write operations
    // --------------------------------------------------------------------
    private async Task<OperationResult> PropagateAsync(
        Func<IUserManagementFacade<IdentityUser<string>>, Task<OperationResult>> call)
    {
        OperationResult? aggregate = null;

        foreach (var mgr in userManagers)
        {
            var res = await call(mgr);
            aggregate = Pick(aggregate, res);

            if (!_cfg.UpdateEveryWhere)             // stop after first try
                break;

            if (!res.Succeeded)                     // stop propagation on first failure
                break;
        }

        return aggregate
            ?? OperationResult.Fail(AuthenticationCode.NoAuthBackend.ToScreamingSnakeCase());
    }

    // We seek on first mgr able to login and return the first non-null result.
    private async Task<IdentityUser<string>?> FindUserAsync(
        Func<IUserManagementFacade<IdentityUser<string>>,
             CancellationToken,
             Task<IdentityUser<string>?>> finder,
        CancellationToken ct)
    {
        foreach (var mgr in userManagers)
        {
            var user = await finder(mgr, ct);

            if (user is not null)
            {
                if (string.IsNullOrEmpty(user.Id)) // some backends may not have the Id populated, restore it from the DB
                {
                    if (string.IsNullOrEmpty(user.Email) == false)
                    {
                        user.Id = dbContext.UserAccounts.Where(x => x.UserEmail == user.Email).Select(x => x.UserGuid).FirstOrDefault();
                    } 
                    else if (string.IsNullOrEmpty(user.UserName) == false)
                    {
                        user.Id = dbContext.UserAccounts.Where(x => x.UserName == user.UserName).Select(x => x.UserGuid).FirstOrDefault();
                    }
                }

                if (string.IsNullOrWhiteSpace(user.Id) == false)
                {
                    return user; // found user with ID, return it
                }
            }

            if (!_cfg.LoginAnywhere) // stop after first backend → “not found”
                return null;
        }

        // searched all backends
        return null;
    }

    // ====================================================================
    //  3.  READ operations
    // ====================================================================
    public Task<IdentityUser<string>?> FindByEmailAsync(string email, CancellationToken ct = default)
        => FindUserAsync((mgr, token) => mgr.FindByEmailAsync(email, token), ct);

    public Task<IdentityUser<string>?> FindByNameAsync(string username, CancellationToken ct = default)
        => FindUserAsync((mgr, token) => mgr.FindByNameAsync(username, token), ct);

    public Task<IdentityUser<string>?> FindByIdAsync(string id, CancellationToken ct = default)
        => FindUserAsync((mgr, token) => mgr.FindByIdAsync(id, token), ct);

    // --------------------------------------------------------------------
    //  Claims aggregation – remove duplicates afterwards
    // --------------------------------------------------------------------
    public async Task<IReadOnlyCollection<Claim>> GetBaseClaimsAsync(
        IdentityUser<string> user,
        CancellationToken ct = default)
    {
        var bag = new List<Claim>();

        foreach (var mgr in userManagers)
        {
            var c = await mgr.GetBaseClaimsAsync(user, ct);
            if (c.Count > 0) bag.AddRange(c);
            if (!_cfg.LoginAnywhere) break;
        }

        return bag.Distinct(new ClaimComparer()).ToList().AsReadOnly();
    }

    public async Task<IReadOnlyCollection<Claim>?> GetRolesClaimsAsync(
        IdentityUser<string> user,
        CancellationToken ct = default)
    {
        var all = new List<Claim>();

        foreach (var mgr in userManagers)
        {
            var c = await mgr.GetRolesClaimsAsync(user, ct);
            if (c != null) all.AddRange(c);
            if (!_cfg.LoginAnywhere) break;
        }

        return all.Distinct(new ClaimComparer()).ToList().AsReadOnly();
    }

    // --------------------------------------------------------------------
    //  Claim structural equality helper
    // --------------------------------------------------------------------
    private sealed class ClaimComparer : IEqualityComparer<Claim>
    {
        public bool Equals(Claim? x, Claim? y)
            => x?.Type == y?.Type &&
               x?.Value == y?.Value &&
               x?.ValueType == y?.ValueType;

        public int GetHashCode(Claim obj)
            => HashCode.Combine(obj.Type, obj.Value, obj.ValueType);
    }
}
