using System.Security.Cryptography;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ROLAC.API.Data;
using ROLAC.API.DTOs.Shared;
using ROLAC.API.DTOs.Users;
using ROLAC.API.Entities;
using ROLAC.API.Entities.Logging;
using ROLAC.API.Services.Logging;

namespace ROLAC.API.Services;

public class UserManagementService : IUserManagementService
{
    private readonly UserManager<AppUser> _userManager;
    private readonly AppDbContext         _db;
    private readonly IAuditLogger         _audit;

    public UserManagementService(UserManager<AppUser> userManager, AppDbContext db, IAuditLogger audit)
    {
        _userManager = userManager;
        _db          = db;
        _audit       = audit;
    }

    // ── GetPaged ─────────────────────────────────────────────────────────────

    public async Task<PagedResult<UserListItemDto>> GetPagedAsync(
        int page, int pageSize, string? search)
    {
        var query = from u in _userManager.Users
                    join m in _db.Members.IgnoreQueryFilters()
                        on u.MemberId equals (int?)m.Id into mg
                    from m in mg.DefaultIfEmpty()
                    select new { u, m };

        if (!string.IsNullOrWhiteSpace(search))
        {
            var s = search.Trim().ToLower();
            query = query.Where(x =>
                x.u.Email!.ToLower().Contains(s) ||
                (x.m != null && (
                    x.m.FirstName_en.ToLower().Contains(s) ||
                    x.m.LastName_en.ToLower().Contains(s)  ||
                    (x.m.NickName != null && x.m.NickName.ToLower().Contains(s)))));
        }

        var total = await query.CountAsync();
        var rows  = await query
            .OrderBy(x => x.u.Email)
            .Skip((page - 1) * pageSize).Take(pageSize)
            .Select(x => new
            {
                x.u.Id, x.u.Email, x.u.MemberId, x.u.IsActive,
                x.u.LanguagePreference, x.u.LastLoginAt, x.u.CreatedAt,
                MemberDisplayName = x.m != null
                    ? (x.m.NickName ?? x.m.FirstName_en) + " " + x.m.LastName_en
                    : (string?)null,
            })
            .ToListAsync();

        // Batch-load roles
        var userIds  = rows.Select(r => r.Id).ToList();
        var roleMap  = await (
            from ur in _db.UserRoles
            join r  in _db.Roles on ur.RoleId equals r.Id
            where userIds.Contains(ur.UserId)
            select new { ur.UserId, r.Name }
        ).ToListAsync();

        var rolesByUser = roleMap
            .GroupBy(x => x.UserId)
            .ToDictionary(g => g.Key, g => g.Select(x => x.Name!).ToList());

        var items = rows.Select(r => new UserListItemDto
        {
            Id                 = r.Id,
            Email              = r.Email ?? "",
            MemberId           = r.MemberId,
            MemberDisplayName  = r.MemberDisplayName,
            IsActive           = r.IsActive,
            LanguagePreference = r.LanguagePreference,
            LastLoginAt        = r.LastLoginAt,
            CreatedAt          = r.CreatedAt,
            Roles              = rolesByUser.GetValueOrDefault(r.Id, []),
        }).ToList();

        return new PagedResult<UserListItemDto>
            { Items = items, TotalCount = total, Page = page, PageSize = pageSize };
    }

    // ── GetById ──────────────────────────────────────────────────────────────

    public async Task<UserDto?> GetByIdAsync(string id)
    {
        var user = await _userManager.FindByIdAsync(id);
        if (user is null) return null;

        var roles = await _userManager.GetRolesAsync(user);
        var memberName = user.MemberId.HasValue
            ? await _db.Members.IgnoreQueryFilters()
                .Where(m => m.Id == user.MemberId)
                .Select(m => (m.NickName ?? m.FirstName_en) + " " + m.LastName_en)
                .FirstOrDefaultAsync()
            : null;

        return new UserDto
        {
            Id                 = user.Id,
            Email              = user.Email ?? "",
            MemberId           = user.MemberId,
            MemberDisplayName  = memberName,
            Roles              = roles.ToList(),
            IsActive           = user.IsActive,
            LanguagePreference = user.LanguagePreference,
            LastLoginAt        = user.LastLoginAt,
            CreatedAt          = user.CreatedAt,
        };
    }

    // ── Create ───────────────────────────────────────────────────────────────

    public async Task<CreateUserResult> CreateAsync(CreateUserRequest request)
    {
        // Validate member and uniqueness only when a MemberId is provided
        if (request.MemberId.HasValue)
        {
            var member = await _db.Members.FindAsync(request.MemberId.Value)
                ?? throw new InvalidOperationException(
                    $"Member {request.MemberId} does not exist.");

            if (_userManager.Users.Any(u => u.MemberId == request.MemberId))
                throw new InvalidOperationException(
                    "This member already has a user account.");
        }

        // Unique email
        if (await _userManager.FindByEmailAsync(request.Email) is not null)
            throw new InvalidOperationException(
                $"Email '{request.Email}' is already in use.");

        var tempPassword = GenerateTempPassword();
        var user = new AppUser
        {
            UserName           = request.Email,
            Email              = request.Email,
            EmailConfirmed     = true,
            MemberId           = request.MemberId,
            LanguagePreference = request.LanguagePreference,
            IsActive           = true,
            CreatedAt          = DateTime.UtcNow,
        };

        var result = await _userManager.CreateAsync(user, tempPassword);
        if (!result.Succeeded)
            throw new InvalidOperationException(
                string.Join("; ", result.Errors.Select(e => e.Description)));

        await _userManager.AddToRolesAsync(user, request.Roles);

        _audit.Write(
            AuditActions.RoleChanged, AuditCategories.Security, LogLevelEnum.Warning,
            entityName: nameof(AppUser), entityId: user.Id,
            summary: $"User created: {user.Email} with roles [{string.Join(", ", request.Roles)}]",
            after: new { user.Email, Roles = request.Roles });

        return new CreateUserResult { UserId = user.Id, TempPassword = tempPassword };
    }

    // ── Update ───────────────────────────────────────────────────────────────

    public async Task UpdateAsync(string id, UpdateUserRequest request)
    {
        var user = await _userManager.FindByIdAsync(id)
            ?? throw new KeyNotFoundException($"User {id} not found.");

        user.Email              = request.Email;
        user.UserName           = request.Email;
        user.NormalizedEmail    = request.Email.ToUpperInvariant();
        user.NormalizedUserName = request.Email.ToUpperInvariant();
        user.LanguagePreference = request.LanguagePreference;
        user.IsActive           = request.IsActive;
        user.LockoutEnd         = request.IsActive ? null : DateTimeOffset.MaxValue;

        var updateResult = await _userManager.UpdateAsync(user);
        if (!updateResult.Succeeded)
            throw new InvalidOperationException(
                string.Join("; ", updateResult.Errors.Select(e => e.Description)));

        var currentRoles = await _userManager.GetRolesAsync(user);
        var toRemove     = currentRoles.Except(request.Roles).ToList();
        var toAdd        = request.Roles.Except(currentRoles).ToList();
        if (toRemove.Count > 0) await _userManager.RemoveFromRolesAsync(user, toRemove);
        if (toAdd.Count > 0)    await _userManager.AddToRolesAsync(user, toAdd);

        if (toRemove.Count > 0 || toAdd.Count > 0)
            _audit.Write(
                AuditActions.RoleChanged, AuditCategories.Security, LogLevelEnum.Warning,
                entityName: nameof(AppUser), entityId: user.Id,
                summary: $"Roles changed for {user.Email}",
                before: new { Roles = currentRoles }, after: new { Roles = request.Roles });
    }

    // ── Deactivate ───────────────────────────────────────────────────────────

    public async Task DeactivateAsync(string id)
    {
        var user = await _userManager.FindByIdAsync(id)
            ?? throw new KeyNotFoundException($"User {id} not found.");
        user.IsActive   = false;
        user.LockoutEnd = DateTimeOffset.MaxValue;
        await _userManager.UpdateAsync(user);

        _audit.Write(
            AuditActions.UserDeactivated, AuditCategories.Security, LogLevelEnum.Warning,
            entityName: nameof(AppUser), entityId: user.Id,
            summary: $"User deactivated: {user.Email}");
    }

    // ── ResetPassword ────────────────────────────────────────────────────────

    public async Task<string> ResetPasswordAsync(string id)
    {
        var user = await _userManager.FindByIdAsync(id)
            ?? throw new KeyNotFoundException($"User {id} not found.");

        var tempPassword = GenerateTempPassword();
        var token        = await _userManager.GeneratePasswordResetTokenAsync(user);
        var result       = await _userManager.ResetPasswordAsync(user, token, tempPassword);

        if (!result.Succeeded)
            throw new InvalidOperationException(
                string.Join("; ", result.Errors.Select(e => e.Description)));

        return tempPassword;
    }

    // ── Helpers ──────────────────────────────────────────────────────────────

    private static string GenerateTempPassword()
    {
        const string upper   = "ABCDEFGHJKLMNPQRSTUVWXYZ";
        const string lower   = "abcdefghjkmnpqrstuvwxyz";
        const string digits  = "23456789";
        const string special = "!@#$%^";
        const string all     = upper + lower + digits + special;

        // Use cryptographically secure random number generator
        var pw = new char[12];
        pw[0] = upper[RandomNumberGenerator.GetInt32(upper.Length)];
        pw[1] = lower[RandomNumberGenerator.GetInt32(lower.Length)];
        pw[2] = digits[RandomNumberGenerator.GetInt32(digits.Length)];
        pw[3] = special[RandomNumberGenerator.GetInt32(special.Length)];
        for (var i = 4; i < 12; i++) pw[i] = all[RandomNumberGenerator.GetInt32(all.Length)];

        // Shuffle using Fisher-Yates with cryptographic randomness
        for (var i = pw.Length - 1; i > 0; i--)
        {
            var j = RandomNumberGenerator.GetInt32(i + 1);
            (pw[i], pw[j]) = (pw[j], pw[i]);
        }

        return new string(pw);
    }
}
