using Microsoft.IdentityModel.Tokens;
using Nexus.Api.DTOs;
using Nexus.Api.Data;
using Nexus.Api.Repositories;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;

namespace Nexus.Api.Services;

public interface IAuthService
{
    Task<AuthSession?> LoginAsync(LoginRequest request, CancellationToken ct = default);
    Task<AuthSession?> RefreshAsync(string refreshToken, CancellationToken ct = default);
    Task RevokeAsync(string refreshToken, CancellationToken ct = default);
    Task<NexusUser?> GetUserAsync(Guid userId, CancellationToken ct = default);
    Task<NexusUser?> UpdateProfileAsync(Guid userId, UpdateProfileRequest request, CancellationToken ct = default);
    Task<bool> ChangePasswordAsync(Guid userId, ChangePasswordRequest request, CancellationToken ct = default);
    Task<bool> AdminResetPasswordAsync(string email, string newPassword, string adminToken, CancellationToken ct = default);
}

public sealed record AuthSession(
    string AccessToken,
    string RefreshToken,
    DateTimeOffset ExpiresAt,
    UserInfo User);

public sealed class AuthService : IAuthService
{
    private readonly IUserRepository _users;
    private readonly IConfiguration _config;
    private readonly ILogger<AuthService> _logger;

    private static string AdminResetToken => Environment.GetEnvironmentVariable("Admin__ResetToken") ?? string.Empty;

    public AuthService(IUserRepository users, IConfiguration config, ILogger<AuthService> logger)
    {
        _users = users;
        _config = config;
        _logger = logger;
    }

    public async Task<AuthSession?> LoginAsync(LoginRequest request, CancellationToken ct = default)
    {
        var normalizedEmail = NormalizeEmail(request.Email);
        var user = await _users.GetByEmailAsync(normalizedEmail, ct);

        if (user is null || !PasswordSecurity.Verify(request.Password, user.PasswordHash, out var needsUpgrade))
        {
            _logger.LogWarning("Rejected login attempt");
            return null;
        }

        if (needsUpgrade) user.PasswordHash = PasswordSecurity.Hash(request.Password);
        user.LastLoginAt = DateTimeOffset.UtcNow;
        user.UpdatedAt = DateTimeOffset.UtcNow;

        // Persist user changes (password upgrade, login timestamp) immediately.
        // Relying solely on RemoveExpiredTokensAsync / AddRefreshTokenAsync to
        // trigger SaveChangesAsync is fragile — if zero tokens are expired the
        // tracked changes might not be flushed before the response is produced.
        await _users.UpdateAsync(user, ct);
        await _users.RemoveExpiredTokensAsync(user.Id, ct);
        return await CreateSessionAsync(user, Guid.NewGuid(), null, ct);
    }

    public async Task<AuthSession?> RefreshAsync(string refreshToken, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(refreshToken)) return null;

        var tokenHash = HashToken(refreshToken);
        var token = await _users.GetRefreshTokenByHashAsync(tokenHash, ct);

        if (token is null) return null;

        if (token.RevokedAt is not null)
        {
            await _users.RevokeFamilyAsync(token.FamilyId, ct);
            _logger.LogWarning("Refresh token reuse detected for family {FamilyId}", token.FamilyId);
            return null;
        }

        if (token.ExpiresAt <= DateTimeOffset.UtcNow) return null;

        return await CreateSessionAsync(token.User, token.FamilyId, token, ct);
    }

    public async Task RevokeAsync(string refreshToken, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(refreshToken)) return;
        var tokenHash = HashToken(refreshToken);
        await _users.RevokeTokenAsync(tokenHash, ct);
    }

    public Task<NexusUser?> GetUserAsync(Guid userId, CancellationToken ct = default)
        => _users.GetByIdAsync(userId, ct).AsTask();

    public async Task<NexusUser?> UpdateProfileAsync(Guid userId, UpdateProfileRequest request, CancellationToken ct = default)
    {
        var user = await _users.GetByIdAsync(userId, ct);
        if (user is null) return null;

        if (!string.IsNullOrWhiteSpace(request.DisplayName))
        {
            user.DisplayName = request.DisplayName.Trim();
        }

        user.UpdatedAt = DateTimeOffset.UtcNow;
        await _users.UpdateAsync(user, ct);
        return user;
    }

    public async Task<bool> ChangePasswordAsync(Guid userId, ChangePasswordRequest request, CancellationToken ct = default)
    {
        var user = await _users.GetByIdAsync(userId, ct);
        if (user is null) return false;

        if (!PasswordSecurity.Verify(request.CurrentPassword, user.PasswordHash, out _))
            return false;

        user.PasswordHash = PasswordSecurity.Hash(request.NewPassword);
        user.UpdatedAt = DateTimeOffset.UtcNow;
        await _users.UpdateAsync(user, ct);
        return true;
    }

    public async Task<bool> AdminResetPasswordAsync(string email, string newPassword, string adminToken, CancellationToken ct = default)
    {
        // Validate admin token
        if (string.IsNullOrWhiteSpace(adminToken) || string.IsNullOrWhiteSpace(AdminResetToken))
        {
            _logger.LogWarning("Admin password reset attempted without admin token or token not configured");
            return false;
        }

        if (!CryptographicOperations.FixedTimeEquals(
                Encoding.UTF8.GetBytes(adminToken),
                Encoding.UTF8.GetBytes(AdminResetToken)))
        {
            _logger.LogWarning("Invalid admin reset token provided");
            return false;
        }

        if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(newPassword))
            return false;

        if (newPassword.Length < 10)
            return false;

        var normalizedEmail = NormalizeEmail(email);
        var user = await _users.GetByEmailAsync(normalizedEmail, ct);

        if (user is null)
        {
            _logger.LogWarning("Admin password reset: user {Email} not found", email);
            return false;
        }

        user.PasswordHash = PasswordSecurity.Hash(newPassword);
        user.UpdatedAt = DateTimeOffset.UtcNow;
        await _users.UpdateAsync(user, ct);

        _logger.LogInformation("Admin password reset completed for {Email}", email);
        return true;
    }

    private async Task<AuthSession?> CreateSessionAsync(
        NexusUser user,
        Guid familyId,
        RefreshToken? replacedToken,
        CancellationToken ct)
    {
        var accessExpiresAt = DateTimeOffset.UtcNow.AddMinutes(GetAccessTokenExpirationMinutes());
        var rawRefreshToken = GenerateRefreshToken();
        var refreshTokenHash = HashToken(rawRefreshToken);

        if (replacedToken is not null)
        {
            replacedToken.RevokedAt = DateTimeOffset.UtcNow;
            replacedToken.ReplacedByTokenHash = refreshTokenHash;
            replacedToken.ConcurrencyStamp = Guid.NewGuid();
            await _users.UpdateRefreshTokenAsync(replacedToken, ct);
        }

        await _users.AddRefreshTokenAsync(new RefreshToken
        {
            UserId = user.Id,
            TokenHash = refreshTokenHash,
            FamilyId = familyId,
            ExpiresAt = DateTimeOffset.UtcNow.AddDays(GetRefreshTokenExpirationDays())
        }, ct);

        return new AuthSession(
            GenerateAccessToken(user, accessExpiresAt),
            rawRefreshToken,
            accessExpiresAt,
            ToUserInfo(user));
    }

    private string GenerateAccessToken(NexusUser user, DateTimeOffset expiresAt)
    {
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(GetRequiredConfig("Jwt:Key")));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
            new Claim(JwtRegisteredClaimNames.Email, user.Email),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim(ClaimTypes.Role, user.Role),
            new Claim("display_name", user.DisplayName)
        };

        var token = new JwtSecurityToken(
            issuer: GetRequiredConfig("Jwt:Issuer"),
            audience: GetRequiredConfig("Jwt:Audience"),
            claims: claims,
            notBefore: DateTime.UtcNow,
            expires: expiresAt.UtcDateTime,
            signingCredentials: credentials);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    private static string GenerateRefreshToken()
    {
        var value = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
        return value.TrimEnd('=').Replace('+', '-').Replace('/', '_');
    }

    private static string HashToken(string token)
        => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token)));

    public static string NormalizeEmail(string email) => email.Trim().ToUpperInvariant();

    private static UserInfo ToUserInfo(NexusUser user) => new()
    {
        Id = user.Id,
        Email = user.Email,
        DisplayName = user.DisplayName,
        Role = user.Role
    };

    private string GetRequiredConfig(string key)
        => _config[key] ?? throw new InvalidOperationException($"Missing required configuration: {key}");

    private int GetAccessTokenExpirationMinutes()
        => _config.GetValue<int?>("Jwt:AccessTokenExpirationMinutes") ?? 15;

    private int GetRefreshTokenExpirationDays()
        => _config.GetValue<int?>("Jwt:RefreshTokenExpirationDays") ?? 7;
}
