using MealPlan.Api.Data;
using MealPlan.Api.DTOs.Auth;
using MealPlan.Api.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace MealPlan.Api.Services;

/// <summary>
/// Read-only authentication against the existing "AspNetUsers" table.
/// Password verification uses ASP.NET Core Identity's <see cref="IPasswordHasher{TUser}"/> so
/// it is compatible with hashes produced by ASP.NET Core Identity.
/// </summary>
public class AuthService : IAuthService
{
    private readonly AppDbContext _db;
    private readonly ITokenService _tokenService;
    private readonly IRefreshTokenStore _refreshTokenStore;
    private readonly IPasswordHasher<ApplicationUser> _passwordHasher;
    private readonly ILogger<AuthService> _logger;

    public AuthService(
        AppDbContext db,
        ITokenService tokenService,
        IRefreshTokenStore refreshTokenStore,
        IPasswordHasher<ApplicationUser> passwordHasher,
        ILogger<AuthService> logger)
    {
        _db = db;
        _tokenService = tokenService;
        _refreshTokenStore = refreshTokenStore;
        _passwordHasher = passwordHasher;
        _logger = logger;
    }

    public async Task<AuthResult> RegisterAsync(RegisterRequestDto request, CancellationToken ct = default)
    {
        var normalizedEmail = request.Email.Trim().ToLowerInvariant();

        var emailTaken = await _db.Users
            .AsNoTracking()
            .AnyAsync(u => u.Email != null && u.Email.ToLower() == normalizedEmail, ct);

        if (emailTaken)
        {
            return AuthResult.Fail("An account with this email already exists.");
        }

        var user = new ApplicationUser
        {
            Id = Guid.NewGuid(),
            Email = request.Email.Trim(),
            UserName = string.IsNullOrWhiteSpace(request.UserName)
                ? request.Email.Trim()
                : request.UserName.Trim(),
            CreatedAt = DateTime.UtcNow,
            IsActive = true,
        };

        user.PasswordHash = _passwordHasher.HashPassword(user, request.Password);

        _db.Users.Add(user);
        await _db.SaveChangesAsync(ct);

        _logger.LogInformation("User {UserId} registered with email {Email}", user.Id, user.Email);
        return AuthResult.Success(IssueTokens(user));
    }

    public async Task<AuthResult> LoginAsync(LoginRequestDto request, CancellationToken ct = default)
    {
        var normalizedEmail = request.Email.Trim();

        var user = await _db.Users
            .AsNoTracking()
            .FirstOrDefaultAsync(u => u.Email != null && u.Email.ToLower() == normalizedEmail.ToLower(), ct);

        // Uniform failure message to avoid user enumeration.
        const string invalidCredentials = "Invalid email or password.";

        if (user is null || string.IsNullOrEmpty(user.PasswordHash))
        {
            _logger.LogWarning("Login failed: user not found for {Email}", normalizedEmail);
            return AuthResult.Fail(invalidCredentials);
        }

        if (!user.IsActive)
        {
            _logger.LogWarning("Login failed: inactive account {UserId}", user.Id);
            return AuthResult.Fail("This account is disabled.");
        }

        var verification = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, request.Password);
        if (verification == PasswordVerificationResult.Failed)
        {
            _logger.LogWarning("Login failed: bad password for {UserId}", user.Id);
            return AuthResult.Fail(invalidCredentials);
        }

        _logger.LogInformation("User {UserId} logged in", user.Id);
        return AuthResult.Success(IssueTokens(user));
    }

    public async Task<AuthResult> RefreshAsync(string refreshToken, CancellationToken ct = default)
    {
        var existing = _refreshTokenStore.Get(refreshToken);
        if (existing is null || !existing.IsActive)
        {
            _logger.LogWarning("Refresh failed: token missing or inactive");
            return AuthResult.Fail("Invalid or expired refresh token.");
        }

        // Rotation: immediately invalidate the presented token.
        _refreshTokenStore.Revoke(refreshToken);

        if (!Guid.TryParse(existing.UserId, out var refreshUserId))
        {
            return AuthResult.Fail("Invalid or expired refresh token.");
        }

        var user = await _db.Users
            .AsNoTracking()
            .FirstOrDefaultAsync(u => u.Id == refreshUserId, ct);

        if (user is null || !user.IsActive)
        {
            _logger.LogWarning("Refresh failed: user {UserId} missing or inactive", existing.UserId);
            return AuthResult.Fail("Invalid or expired refresh token.");
        }

        return AuthResult.Success(IssueTokens(user));
    }

    public void Logout(string refreshToken) => _refreshTokenStore.Revoke(refreshToken);

    public async Task<UserInfoDto?> GetUserInfoAsync(string userId, CancellationToken ct = default)
    {
        if (!Guid.TryParse(userId, out var parsedUserId))
        {
            return null;
        }

        var user = await _db.Users
            .AsNoTracking()
            .FirstOrDefaultAsync(u => u.Id == parsedUserId, ct);

        if (user is null)
        {
            return null;
        }

        return new UserInfoDto
        {
            Id = user.Id.ToString(),
            UserName = user.UserName,
            Email = user.Email,
        };
    }

    private TokenResponseDto IssueTokens(ApplicationUser user)
    {
        var accessToken = _tokenService.CreateAccessToken(user);
        var refreshToken = _tokenService.CreateRefreshToken(user.Id.ToString());

        return new TokenResponseDto
        {
            AccessToken = accessToken,
            RefreshToken = refreshToken.Token,
            ExpiresInSeconds = _tokenService.AccessTokenLifetimeSeconds,
            TokenType = "Bearer",
        };
    }
}
