using Ablera.Serdica.Authentication.Extensions;
using Ablera.Serdica.Common.Tools.Extensions;
using Ablera.Serdica.DBModels.Serdica;
using Ablera.Serdica.Authority.Plugins.Base.Contracts;
using Ablera.Serdica.Authority.Plugins.Base.Models;
using Ablera.Serdica.Authority.Plugin.Standard.Models;
using Ablera.Serdica.Authority.Services;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using static Ablera.Serdica.Authority.Plugins.Base.Constants.ConstantsClass;

namespace Ablera.Serdica.Authority.Plugin.Standard;


/// <summary>
/// Concrete implementation that satisfies the refined <see cref="IUserManager{TUser,TKey}"/> façade and its
/// constituent smaller interfaces.  The DB schema is Serdica‑specific (Oracle) but all higher layers only see
/// the contracts defined in <c>Base.Contracts</c>.
/// </summary>
public sealed class UserAccountIdentityFacade(
    UserAccountSettingsProvider settingsProvider,
    IHttpContextAccessor httpContextAccessor,
    IWebHostEnvironment webHostEnvironment,
    SerdicaDbContext context) :
        IUserManagementFacade<UserAccountIdentityUser>
{
    #region IAuthService

    public async Task<AuthenticationResult> AuthenticateAsync(UserAccountIdentityUser identityUser,
                                                              string password,
                                                              bool lockoutOnFailure = false,
                                                              CancellationToken ct = default)
    {
        // already locked ?
        if (identityUser.UserAccount.LockAccount == YesKey)
        {
            await RecordAttemptAsync(identityUser.UserAccount.UserAccountId, false, ct);
            return AuthenticationResult.Fail(AuthenticationCode.AccountIsLocked.ToScreamingSnakeCase());
        }
        var pwd = await context.UserAccountPasswords
                             .AsNoTracking()
                             .FirstOrDefaultAsync(p => p.UserAccountId == identityUser.UserAccount.UserAccountId, ct);
        pwd ??= await UpsertDefaultDevelopmentEnvironmentPassword(identityUser.UserAccount, settingsProvider.Settings, ct);

        var success = pwd is not null && VerifyPassword(password, pwd.HashedPassword, pwd.Salt);
        await RecordAttemptAsync(identityUser.UserAccount.UserAccountId, success, ct);

        if (!success)
        {
            if (lockoutOnFailure)
                await EvaluateAndLockAsync(identityUser, settingsProvider.Settings, ct);
            return AuthenticationResult.Fail(AuthenticationCode.InvalidCredentials.ToScreamingSnakeCase());
        }

        // paranoia — make sure lock flag cleared if necessary
        if (identityUser.UserAccount.LockAccount == YesKey)
            await UnlockAsync(identityUser, ct);

        var claims = await GetBaseClaimsAsync(identityUser, ct);
        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(claims, typeof(UserAccountIdentityFacade).Namespace));
        return AuthenticationResult.Success(claimsPrincipal);
    }

    #endregion

    #region IUserStore

    public async Task<OperationResult> CreateAsync(UserAccountIdentityUser identityUser, string password, CancellationToken ct = default)
    {
        if (identityUser is null) return OperationResult.Fail("NULL_USER");
        if (string.IsNullOrWhiteSpace(password)) return OperationResult.Fail("EMPTY_PASSWORD");

        var user = new UserAccount
        {
            UserGuid = identityUser.Identity.Id,
            UserName = identityUser.Identity.UserName,
            UserEmail = identityUser.Identity.Email,
            LockAccount = NoKey,
        };
        await context.UserAccounts.AddAsync(user, ct);
        await context.SaveChangesAsync(ct);

        var salt = GenerateSalt(settingsProvider.Settings);
        var hashed = HashPassword(password, salt);

        await context.UserAccountPasswords.AddAsync(new UserAccountPassword
        {
            UserAccountId = user.UserAccountId,
            Salt = salt,
            HashedPassword = hashed,
            CreatedDate = DateTime.UtcNow
        }, ct);

        await context.SaveChangesAsync(ct);
        return OperationResult.Success();
    }

    public async Task<OperationResult> UpdateAsync(UserAccountIdentityUser identityUser, CancellationToken ct = default)
    {
        await context.UserAccounts.Where(x => x.UserGuid == identityUser.Identity.Id)
            .ExecuteUpdateAsync(q => q.SetProperty(x => x.UserName, identityUser.Identity.UserName)
                                      .SetProperty(x => x.LockAccount, identityUser.Identity.LockoutEnabled ? YesKey : NoKey)
                                      .SetProperty(x => x.UserEmail, identityUser.Identity.Email));
        await context.SaveChangesAsync(ct);
        return OperationResult.Success();
    }

    public async Task<UserAccountIdentityUser?> FindByIdAsync(string id, CancellationToken ct = default)
    {
        var userAccount = await context.UserAccounts
            .Include(u => u.UserRole1s)
            .Include(u => u.UserGroup1s)
            .Include(u => u.SrCust)
            .ThenInclude(u => u.CPerson)
            .FirstOrDefaultAsync(u => u.UserGuid == id, ct);

        if (userAccount == null)
            return null;

        return new UserAccountIdentityUser
        {
            Identity = new IdentityUser
            {
                Id = userAccount.UserGuid,
                UserName = userAccount.UserName,
                Email = userAccount.UserEmail,
                LockoutEnabled = userAccount.LockAccount == YesKey,
                EmailConfirmed = true,
                PhoneNumberConfirmed = true
            },
            UserAccount = userAccount
        };
    }

    public async Task<UserAccountIdentityUser?> FindByEmailAsync(string email, CancellationToken ct = default) 
    {

        if (string.IsNullOrWhiteSpace(email))
            return null;

        var userAccount = await context.UserAccounts
                  .Include(u => u.UserRole1s)
                  .Include(u => u.UserGroup1s)
                  .FirstOrDefaultAsync(u => u.UserEmail.ToLower() == email.ToLower(), ct);
        if (userAccount == null)
            return null;

        return new UserAccountIdentityUser
        {
            Identity = new IdentityUser
            {
                Id = userAccount.UserGuid,
                UserName = userAccount.UserName,
                Email = userAccount.UserEmail,
                LockoutEnabled = userAccount.LockAccount == YesKey,
                EmailConfirmed = true,
                PhoneNumberConfirmed = true
            },
            UserAccount = userAccount
        };
    }

    public async Task<UserAccountIdentityUser?> FindByNameAsync(string username, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(username))
            return null;

        var userAccount = await context.UserAccounts
                  .Include(u => u.UserRole1s)
                  .Include(u => u.UserGroup1s)
                  .FirstOrDefaultAsync(u => u.UserName.ToLower() == username.ToLower(), ct); 

        if (userAccount == null)
            return null;

        return new UserAccountIdentityUser
        {
            Identity = new IdentityUser
            {
                Id = userAccount.UserGuid,
                UserName = userAccount.UserName,
                Email = userAccount.UserEmail,
                LockoutEnabled = userAccount.LockAccount == YesKey,
                EmailConfirmed = true,
                PhoneNumberConfirmed = true
            },
            UserAccount = userAccount
        };
    }
    #endregion

    #region IClaimStore

    public Task<IReadOnlyCollection<Claim>> GetBaseClaimsAsync(UserAccountIdentityUser identityUser, CancellationToken ct = default)
        => Task.FromResult(
            identityUser.Identity.BuildClaims(identityUser.Identity.Email, identityUser.UserAccount?.SrCust?.CPerson?.Gname, identityUser.UserAccount?.SrCust?.CPerson?.Fname));

    public async Task<IReadOnlyCollection<Claim>?> GetRolesClaimsAsync(UserAccountIdentityUser identityUser, CancellationToken ct = default)
    {
        Claim[]? roleClaims = null;
        if (string.IsNullOrWhiteSpace(identityUser.Identity.Id) == false)
        {
            roleClaims = await context.UserAccounts
                .Include(x => x.UserRole1s)
                .Where(x => x.UserGuid == identityUser.Identity.Id)
                .SelectMany(x => x.UserRole1s.Select(y => y.Id))
                .Select(roleId => new Claim(ClaimTypes.Role, roleId))
                .ToArrayAsync(ct);
        }
        else if (string.IsNullOrWhiteSpace(identityUser.Identity.Email) == false)
        {
            roleClaims = await context.UserAccounts
                .Include(x => x.UserRole1s)
                .Where(x => x.UserEmail == identityUser.Identity.Email)
                .SelectMany(x => x.UserRole1s.Select(y => y.Id))
                .Select(roleId => new Claim(ClaimTypes.Role, roleId))
                .ToArrayAsync(ct);
        }
        else if (string.IsNullOrWhiteSpace(identityUser.Identity.UserName) == false)
        {
            roleClaims = await context.UserAccounts
                .Include(x => x.UserRole1s)
                .Where(x => x.UserName == identityUser.Identity.UserName)
                .SelectMany(x => x.UserRole1s.Select(y => y.Id))
                .Select(roleId => new Claim(ClaimTypes.Role, roleId))
                .ToArrayAsync(ct);
        }
        return roleClaims ?? [];
    }

    #endregion

    #region IPasswordManager

    public async Task<OperationResult> ChangePasswordAsync(UserAccountIdentityUser identityUser,
                                                           string currentPassword,
                                                           string newPassword,
                                                           CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(newPassword)) return OperationResult.Fail("EMPTY_NEW_PASSWORD");

        var pwd = await context.UserAccountPasswords.FirstOrDefaultAsync(p => p.UserAccount.UserGuid == identityUser.Identity.Id, ct);
        if (pwd is null) return OperationResult.Fail("PWD_ROW_NOT_FOUND");

        if (!VerifyPassword(currentPassword, pwd.HashedPassword, pwd.Salt))
            return OperationResult.Fail("INVALID_CURRENT_PASSWORD");

        pwd.Salt = GenerateSalt(settingsProvider.Settings);
        pwd.HashedPassword = HashPassword(newPassword, pwd.Salt);
        pwd.CreatedDate = DateTime.UtcNow;
        context.UserAccountPasswords.Update(pwd);
        await context.SaveChangesAsync(ct);
        return OperationResult.Success();
    }

    public async Task<OperationResult> ResetPasswordAsync(UserAccountIdentityUser identityUser,
                                                          string resetToken,
                                                          string newPassword,
                                                          CancellationToken ct = default)
    {
        // TODO: validate token (email/SMS/etc.)
        return await ChangePasswordAsync(identityUser, currentPassword: string.Empty, newPassword, ct);
    }

    #endregion

    #region IAccountLockManager

    public async Task<OperationResult> LockAsync(UserAccountIdentityUser identityUser, CancellationToken ct = default)
    {
        if (!settingsProvider.Settings.LockoutEnabled) return OperationResult.Success();

        var updated = await context.UserAccounts.Where(u => u.UserGuid == identityUser.Identity.Id)
                          .ExecuteUpdateAsync(q => q.SetProperty(x => x.LockAccount, YesKey)
                                                    .SetProperty(x => x.LockAccountDate, DateTime.UtcNow), ct);
        return updated > 0 ? OperationResult.Success() : OperationResult.Fail("LOCK_FAILED");
    }

    public async Task<OperationResult> UnlockAsync(UserAccountIdentityUser identityUser, CancellationToken ct = default)
    {
        var updated = await context.UserAccounts.Where(u => u.UserGuid == identityUser.Identity.Id)
                          .ExecuteUpdateAsync(q => q.SetProperty(x => x.LockAccount, NoKey)
                                                    .SetProperty(x => x.LockAccountDate, (DateTime?)null), ct);
        return updated > 0 ? OperationResult.Success() : OperationResult.Fail("UNLOCK_FAILED");
    }

    #endregion

    #region Helpers – attempts & lock evaluation
    private string? GetClientIp()
    {
        var http = httpContextAccessor.HttpContext;
        if (http is null) return null;              // background thread etc.

        // After UseForwardedHeaders this is usually all you need
        var ip = http.Connection.RemoteIpAddress?.ToString();

        // Fallback (e.g. you skipped the middleware or have multiple proxies)
        if (string.IsNullOrWhiteSpace(ip) &&
            http.Request.Headers.TryGetValue("X-Forwarded-For", out var h))
        {
            ip = h.ToString().Split(',')[0].Trim(); // first hop = client
        }

        return ip;
    }

    private async Task RecordAttemptAsync(decimal userAccountId, bool success, CancellationToken ct)
    {
        await context.UserLoginAttempts.AddAsync(new UserLoginAttempt
        {
            UserAccountId = userAccountId,
            AttemptDate = DateTime.UtcNow,
            Result = success ? YesKey : NoKey,
            IpAddress = GetClientIp()
        }, ct);
        await context.SaveChangesAsync(ct);
    }

    private async Task EvaluateAndLockAsync(UserAccountIdentityUser identityUser, UserAccountSettings settings, CancellationToken ct)
    {
        var windowStart = DateTime.UtcNow - settings.LockoutDuration;

        var last = await context.UserLoginAttempts
                              .Where(a => a.UserAccount.UserGuid == identityUser.Identity.Id && a.AttemptDate >= windowStart)
                              .OrderByDescending(a => a.AttemptDate)
                              .Take(settings.LockoutThreshold)
                              .Select(a => a.Result)
                              .ToListAsync(ct);

        if (last.Count == settings.LockoutThreshold && last.All(r => r == NoKey))
            await LockAsync(identityUser, ct);
    }

    private async Task<UserAccountPassword?> UpsertDefaultDevelopmentEnvironmentPassword(UserAccount userAccount, UserAccountSettings settings, CancellationToken ct)
    {
        if (webHostEnvironment.IsDevelopment() == false) return null;
        if (settings.DefaultCredentials?.Confirmation != "I acknowledge this is not safe!") return null;
        var defaultAccount = settings.DefaultCredentials?.Accounts?.FirstOrDefault(x => x.Username == userAccount.UserEmail);
        if (defaultAccount?.Password == null) return null;

        var salt = GenerateSalt(settings);
        var hashed = HashPassword(defaultAccount.Password!, salt);
        var userAccountPassword = new UserAccountPassword
        {
            UserAccountId = userAccount.UserAccountId,
            Salt = salt,
            HashedPassword = hashed,
            CreatedDate = DateTime.UtcNow
        };
        await context.UserAccountPasswords.AddAsync(userAccountPassword, ct);
        await context.SaveChangesAsync(ct);
        return userAccountPassword;
    }

    #endregion

    #region Crypto helpers

    private string GenerateSalt(UserAccountSettings settings)
    {
        var bytes = new byte[settings.SaltSize];
        RandomNumberGenerator.Fill(bytes);
        return Convert.ToHexString(bytes);
    }

    private static string HashPassword(string plain, string salt)
    {
        using var sha = SHA256.Create();
        var combined = salt + plain;
        return Convert.ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes(combined)));
    }

    private static bool VerifyPassword(string plain, string hashed, string salt) =>
        HashPassword(plain, salt) == hashed;


    #endregion
}
