using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using ROLAC.API.Data;
using ROLAC.API.DTOs.Auth;
using ROLAC.API.Entities;
using ROLAC.API.Entities.Logging;
using ROLAC.API.Services.Logging;

namespace ROLAC.API.Services;

public class AuthService : IAuthService
{
    private readonly UserManager<AppUser> _userManager;
    private readonly ITokenService        _tokenService;
    private readonly AppDbContext         _db;
    private readonly IPermissionService   _permissions;
    private readonly IAuditLogger         _audit;
    private readonly int                  _refreshTokenExpiryDays;

    public AuthService(
        UserManager<AppUser> userManager,
        ITokenService        tokenService,
        AppDbContext         db,
        IPermissionService   permissions,
        IAuditLogger         audit,
        IConfiguration       config)
    {
        _userManager            = userManager;
        _tokenService           = tokenService;
        _db                     = db;
        _permissions            = permissions;
        _audit                  = audit;
        _refreshTokenExpiryDays = int.Parse(config["Jwt:RefreshTokenExpiryDays"] ?? "30");
    }

    // -------------------------------------------------------------------------
    // Login
    // -------------------------------------------------------------------------

    public async Task<(LoginResponse Response, string RawRefreshToken)> LoginAsync(
        LoginRequest request, string? ipAddress = null, string? deviceInfo = null)
    {
        var user = await _userManager.FindByEmailAsync(request.Email);
        if (user is null)
        {
            AuditLoginFailed(request.Email, "Unknown email", ipAddress);
            throw new UnauthorizedAccessException("Invalid credentials.");
        }

        if (!await _userManager.CheckPasswordAsync(user, request.Password))
        {
            AuditLoginFailed(request.Email, "Wrong password", ipAddress, user.Id);
            throw new UnauthorizedAccessException("Invalid credentials.");
        }

        if (!user.IsActive)
        {
            AuditLoginFailed(request.Email, "Account inactive", ipAddress, user.Id);
            throw new UnauthorizedAccessException("Account is inactive.");
        }

        _audit.Write(
            AuditActions.Login, AuditCategories.Security, LogLevelEnum.Information,
            entityName: nameof(AppUser), entityId: user.Id,
            summary: $"Login succeeded: {user.Email}",
            userId: user.Id, userEmail: user.Email, ipAddress: ipAddress);

        return await IssueSessionAsync(user, ipAddress, deviceInfo);
    }

    // -------------------------------------------------------------------------
    // Issue session (shared by login and passwordless flows like invitations)
    // -------------------------------------------------------------------------

    public async Task<(LoginResponse Response, string RawRefreshToken)> IssueSessionAsync(
        AppUser user, string? ipAddress = null, string? deviceInfo = null)
    {
        var roles       = await _userManager.GetRolesAsync(user);
        var accessToken = _tokenService.GenerateAccessToken(user, roles);
        var rawRefresh  = _tokenService.GenerateRefreshToken();
        var tokenHash   = _tokenService.HashToken(rawRefresh);

        _db.RefreshTokens.Add(new RefreshToken
        {
            UserId     = user.Id,
            TokenHash  = tokenHash,
            ExpiresAt  = DateTime.UtcNow.AddDays(_refreshTokenExpiryDays),
            CreatedAt  = DateTime.UtcNow,
            IpAddress  = ipAddress,
            DeviceInfo = deviceInfo,
        });

        user.LastLoginAt = DateTime.UtcNow;
        await _userManager.UpdateAsync(user);
        await _db.SaveChangesAsync();

        return (await BuildResponseAsync(accessToken, user, roles), rawRefresh);
    }

    private void AuditLoginFailed(string email, string reason, string? ipAddress, string? userId = null)
        => _audit.Write(
            AuditActions.LoginFailed, AuditCategories.Security, LogLevelEnum.Warning,
            entityName: nameof(AppUser), entityId: userId,
            summary: $"Login failed ({reason}): {email}",
            userId: userId, userEmail: email, ipAddress: ipAddress);

    // -------------------------------------------------------------------------
    // Refresh
    // -------------------------------------------------------------------------

    public async Task<(LoginResponse Response, string RawRefreshToken)> RefreshAsync(
        string rawRefreshToken, string? ipAddress = null)
    {
        var hash  = _tokenService.HashToken(rawRefreshToken);
        var token = await _db.RefreshTokens
            .FirstOrDefaultAsync(rt => rt.TokenHash == hash);

        if (token is null || !token.IsActive)
            throw new UnauthorizedAccessException("Invalid or expired refresh token.");

        var user = await _userManager.FindByIdAsync(token.UserId);
        if (user is null)
            throw new UnauthorizedAccessException("User not found.");

        var roles      = await _userManager.GetRolesAsync(user);
        var newAccess  = _tokenService.GenerateAccessToken(user, roles);
        var newRaw     = _tokenService.GenerateRefreshToken();
        var newHash    = _tokenService.HashToken(newRaw);

        // Rotate: mark old token replaced+revoked, create new token
        token.RevokedAt      = DateTime.UtcNow;
        token.ReplacedByHash = newHash;

        _db.RefreshTokens.Add(new RefreshToken
        {
            UserId     = user.Id,
            TokenHash  = newHash,
            ExpiresAt  = DateTime.UtcNow.AddDays(_refreshTokenExpiryDays),
            CreatedAt  = DateTime.UtcNow,
            IpAddress  = ipAddress,
            DeviceInfo = token.DeviceInfo,
        });

        await _db.SaveChangesAsync();

        return (await BuildResponseAsync(newAccess, user, roles), newRaw);
    }

    // -------------------------------------------------------------------------
    // Logout
    // -------------------------------------------------------------------------

    public async Task LogoutAsync(string rawRefreshToken)
    {
        var hash  = _tokenService.HashToken(rawRefreshToken);
        var token = await _db.RefreshTokens
            .FirstOrDefaultAsync(rt => rt.TokenHash == hash);

        if (token is not null && token.IsActive)
        {
            token.RevokedAt = DateTime.UtcNow;
            await _db.SaveChangesAsync();

            _audit.Write(
                AuditActions.Logout, AuditCategories.Security, LogLevelEnum.Information,
                entityName: nameof(AppUser), entityId: token.UserId,
                summary: "Logout", userId: token.UserId);
        }
    }

    // -------------------------------------------------------------------------
    // Change password
    // -------------------------------------------------------------------------

    public async Task<IdentityResult> ChangePasswordAsync(
        string userId, string currentPassword, string newPassword, string? currentRawRefreshToken)
    {
        var user = await _userManager.FindByIdAsync(userId);
        if (user is null)
            return IdentityResult.Failed(new IdentityError
            {
                Code        = "UserNotFound",
                Description = "User not found.",
            });

        var result = await _userManager.ChangePasswordAsync(user, currentPassword, newPassword);
        if (!result.Succeeded)
            return result;

        // Revoke the user's other active sessions; keep the current one alive.
        var currentHash = currentRawRefreshToken is null
            ? null
            : _tokenService.HashToken(currentRawRefreshToken);

        var otherTokens = await _db.RefreshTokens
            .Where(rt => rt.UserId == userId
                      && rt.RevokedAt == null
                      && (currentHash == null || rt.TokenHash != currentHash))
            .ToListAsync();

        foreach (var token in otherTokens)
            token.RevokedAt = DateTime.UtcNow;

        await _db.SaveChangesAsync();

        _audit.Write(
            AuditActions.PasswordChanged, AuditCategories.Security, LogLevelEnum.Information,
            entityName: nameof(AppUser), entityId: user.Id,
            summary: $"Password changed: {user.Email}",
            userId: user.Id, userEmail: user.Email);

        return result;
    }

    // -------------------------------------------------------------------------
    // Private helpers
    // -------------------------------------------------------------------------

    private async Task<LoginResponse> BuildResponseAsync(
        string accessToken, AppUser user, IList<string> roles)
        => new()
        {
            AccessToken = accessToken,
            ExpiresIn   = 15 * 60,
            User        = await BuildUserInfoAsync(user, roles),
        };

    /// <summary>Builds UserInfo including the effective permission map. Reused by /me.</summary>
    public async Task<UserInfo> BuildUserInfoAsync(AppUser user, IList<string> roles)
        => new()
        {
            Id                 = user.Id,
            Email              = user.Email!,
            Roles              = roles,
            LanguagePreference = user.LanguagePreference,
            Permissions        = await _permissions.GetEffectivePermissionsAsync(roles),
            MemberInfo         = await BuildMemberInfoAsync(user),
        };

    /// <summary>
    /// Loads the linked member's display fields, or null when the account has no
    /// MemberId or its member record was soft-deleted (excluded by query filter).
    /// </summary>
    private async Task<MemberInfo?> BuildMemberInfoAsync(AppUser user)
    {
        if (user.MemberId is not int memberId)
            return null;

        return await _db.Members
            .Where(member => member.Id == memberId)
            .Select(member => new MemberInfo
            {
                Id           = member.Id,
                NickName     = member.NickName,
                FirstName_en = member.FirstName_en,
                LastName_en  = member.LastName_en,
                FirstName_zh = member.FirstName_zh,
                LastName_zh  = member.LastName_zh,
            })
            .FirstOrDefaultAsync();
    }
}
