using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using UniVerse.Application.DTOs.Auth;
using UniVerse.Application.DTOs.Notifications;
using UniVerse.Application.DTOs.Users;
using UniVerse.Application.Interfaces;
using UniVerse.Application.Mappings;
using UniVerse.Domain.Entities;
using UniVerse.Domain.Enums;
using UniVerse.Domain.Exceptions;
using UniVerse.Infrastructure.Data;

namespace UniVerse.Infrastructure.Services;

public class AuthService : IAuthService
{
    private readonly AppDbContext _db;
    private readonly IConfiguration _config;
    private readonly IMicrosoftAuthClient _microsoftAuth;
    private readonly IGamificationService _gamification;
    private readonly INotificationService _notifications;
    private readonly ILogger<AuthService> _logger;

    public AuthService(
        AppDbContext db,
        IConfiguration config,
        IMicrosoftAuthClient microsoftAuth,
        IGamificationService gamification,
        INotificationService notifications,
        ILogger<AuthService> logger)
    {
        _db = db;
        _config = config;
        _microsoftAuth = microsoftAuth;
        _gamification = gamification;
        _notifications = notifications;
        _logger = logger;
    }

    public async Task<AuthResult> LoginWithMicrosoftAsync(string authorizationCode, string? redirectUri = null, string? ipAddress = null)
    {
        var effectiveRedirectUri = redirectUri
                                   ?? _config["AzureAd:RedirectUri"]
                                   ?? "http://localhost:5173/auth/callback";
        var result = await _microsoftAuth.ExchangeAuthorizationCodeAsync(authorizationCode, effectiveRedirectUri);

        // Parse claims directly from the ID token provided by Microsoft
        var handler = new JwtSecurityTokenHandler();
        var idToken = handler.ReadJwtToken(result.IdToken);
        
        var email = idToken.Claims.FirstOrDefault(c => c.Type == "preferred_username" || c.Type == "email" || c.Type == ClaimTypes.Upn)?.Value;
        var name = idToken.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
        var microsoftSub = idToken.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Sub || c.Type == "sub")?.Value;

        if (string.IsNullOrEmpty(email))
            throw new UnauthorizedException("Email не найден в токене Microsoft.");
        if (string.IsNullOrWhiteSpace(microsoftSub))
            throw new UnauthorizedException("Sub ID не найден в токене Microsoft.");

        // Automatically provision user
        var user = await _db.Users
            .Include(u => u.Roles)
            .Include(u => u.TeacherProfile)
            .FirstOrDefaultAsync(u => u.MicrosoftId == microsoftSub);
        user ??= await _db.Users
            .Include(u => u.Roles)
            .Include(u => u.TeacherProfile)
            .FirstOrDefaultAsync(u => u.Email == email);
        if (user == null)
        {
            user = new User
            {
                Email = email,
                DisplayName = name ?? email.Split('@')[0],
                MicrosoftId = microsoftSub,
                IsActive = true
            };
            _db.Users.Add(user);
            await _db.SaveChangesAsync();

            user.Roles.Add(new UserRoleAssignment { UserId = user.Id, Role = UserRole.Student });
            _db.StudentProfiles.Add(new StudentProfile { UserId = user.Id });
            await _db.SaveChangesAsync();
        }
        else if (!user.IsActive)
        {
            throw new ForbiddenException("Аккаунт деактивирован.");
        }
        else
        {
            user.Email = email;
            user.DisplayName = name ?? user.DisplayName ?? email.Split('@')[0];
            user.MicrosoftId = microsoftSub;
            user.UpdatedAt = DateTime.UtcNow;
            await _db.SaveChangesAsync();
        }

        if (user.Roles.Count == 0)
        {
            user.Roles.Add(new UserRoleAssignment { UserId = user.Id, Role = UserRole.Student });
            await EnsureProfilesForRolesAsync(user.Id, [UserRole.Student]);
            await _db.SaveChangesAsync();
        }

        var accessToken = GenerateAccessToken(user);
        var refreshToken = await GenerateRefreshTokenAsync(user.Id);
        await TrySendLoginNotificationAsync(user, ipAddress);

        return new AuthResult(
            new AuthResponse(
                accessToken,
                DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()),
                user.ToAuthDto()
            ),
            refreshToken
        );
    }

    public async Task<AuthResult> DevLoginAsync(string email, string? displayName, IReadOnlyCollection<UserRole> roles, string? ipAddress = null)
    {
        var normalizedRoles = (roles.Count > 0 ? roles : [UserRole.Student]).Distinct().ToList();
        var user = await _db.Users
            .Include(u => u.Roles)
            .FirstOrDefaultAsync(u => u.Email == email);

        if (user == null)
        {
            user = new User
            {
                Email = email,
                DisplayName = displayName ?? email.Split('@')[0],
                IsActive = true
            };
            _db.Users.Add(user);
            await _db.SaveChangesAsync();
        }

        var existing = user.Roles.Select(r => r.Role).ToHashSet();
        var toRemove = user.Roles.Where(r => !normalizedRoles.Contains(r.Role)).ToList();
        foreach (var item in toRemove)
            user.Roles.Remove(item);

        foreach (var role in normalizedRoles.Where(r => !existing.Contains(r)))
            user.Roles.Add(new UserRoleAssignment { UserId = user.Id, Role = role });

        await EnsureProfilesForRolesAsync(user.Id, normalizedRoles);
        await _db.SaveChangesAsync();

        if (!user.IsActive)
            throw new ForbiddenException("Аккаунт деактивирован.");

        var accessToken = GenerateAccessToken(user);
        var refreshToken = await GenerateRefreshTokenAsync(user.Id);
        await TrySendLoginNotificationAsync(user, ipAddress);

        return new AuthResult(
            new AuthResponse(
                accessToken,
                DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()),
                user.ToAuthDto()
            ),
            refreshToken
        );
    }

    public async Task<AuthResult> RefreshTokenAsync(string refreshToken)
    {
        var token = await _db.RefreshTokens
            .Include(rt => rt.User)
            .ThenInclude(u => u.Roles)
            .FirstOrDefaultAsync(rt => rt.Token == refreshToken);

        if (token == null || !token.IsActive)
            throw new ForbiddenException("Неверный или просроченный токен обновления.");

        if (!token.User.IsActive)
        {
            token.RevokedAt = DateTime.UtcNow;
            await _db.SaveChangesAsync();
            throw new ForbiddenException("Аккаунт деактивирован.");
        }

        // Revoke old token
        token.RevokedAt = DateTime.UtcNow;

        // Generate new tokens
        var accessToken = GenerateAccessToken(token.User);
        var newRefreshToken = await GenerateRefreshTokenAsync(token.UserId);

        await _db.SaveChangesAsync();

        return new AuthResult(
            new AuthResponse(
                accessToken,
                DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()),
                token.User.ToAuthDto()
            ),
            newRefreshToken
        );
    }

    public async Task RevokeRefreshTokenAsync(string refreshToken)
    {
        var token = await _db.RefreshTokens.FirstOrDefaultAsync(rt => rt.Token == refreshToken);
        if (token != null && token.IsActive)
        {
            token.RevokedAt = DateTime.UtcNow;
            await _db.SaveChangesAsync();
        }
    }

    public async Task<CurrentUserDto> GetCurrentUserAsync(int userId)
    {
        var user = await _db.Users
            .Include(u => u.Roles)
            .FirstOrDefaultAsync(u => u.Id == userId)
            ?? throw new NotFoundException("User", userId);
        if (!user.IsActive)
            throw new ForbiddenException("Аккаунт деактивирован.");

        return user.ToCurrentUserDto(await _gamification.CalculateLevelAsync(user.Xp));
    }

    private async Task TrySendLoginNotificationAsync(User user, string? ipAddress)
    {
        try
        {
            var loginIpAddress = string.IsNullOrWhiteSpace(ipAddress) ? "unknown" : ipAddress;
            var loginTime = DateTimeOffset.UtcNow;
            var message = new NotificationMessage(
                NotificationChannels.Email,
                user.Email,
                "Вход в аккаунт UniVerse",
                $"Здравствуйте, {user.DisplayName ?? user.Email}!\n\nБыл выполнен вход в ваш аккаунт UniVerse в {loginTime:O} с IP-адреса: {loginIpAddress}.\n\nЕсли это были не вы, пожалуйста, немедленно свяжитесь с поддержкой.",
                user.DisplayName,
                new Dictionary<string, string>
                {
                    ["event"] = "account_login",
                    ["ip_address"] = loginIpAddress,
                    ["login_time_utc"] = loginTime.ToString("O")
                });

            await _notifications.SendAsync(message);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to send login notification to user {UserId}", user.Id);
        }
    }

    private string GenerateAccessToken(User user)
    {
        var key = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_config["Jwt:Secret"]!));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var claims = new List<Claim>
        {
            new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
            new(JwtRegisteredClaimNames.Email, user.Email),
            new("display_name", user.DisplayName ?? "")
        };
        var roles = user.Roles.Select(r => r.Role).Distinct().ToList();
        if (roles.Count == 0) roles.Add(UserRole.Student);
        claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role.ToString())));

        var token = new JwtSecurityToken(
            issuer: _config["Jwt:Issuer"],
            audience: _config["Jwt:Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()),
            signingCredentials: creds
        );

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

    private async Task EnsureProfilesForRolesAsync(int userId, IReadOnlyCollection<UserRole> roles)
    {
        if (roles.Contains(UserRole.Student))
        {
            var hasStudentProfile = await _db.StudentProfiles.AnyAsync(p => p.UserId == userId);
            if (!hasStudentProfile)
                _db.StudentProfiles.Add(new StudentProfile { UserId = userId });
        }

        if (roles.Contains(UserRole.Teacher))
        {
            var hasTeacherProfile = await _db.TeacherProfiles.AnyAsync(p => p.UserId == userId);
            if (!hasTeacherProfile)
                _db.TeacherProfiles.Add(new TeacherProfile { UserId = userId });
        }
    }

    private async Task<string> GenerateRefreshTokenAsync(int userId)
    {
        var randomBytes = RandomNumberGenerator.GetBytes(64);
        var tokenString = Convert.ToBase64String(randomBytes);

        var refreshToken = new RefreshToken
        {
            UserId = userId,
            Token = tokenString,
            ExpiresAt = DateTime.UtcNow.AddDays(GetRefreshTokenExpiration()),
            CreatedAt = DateTime.UtcNow
        };

        _db.RefreshTokens.Add(refreshToken);
        await _db.SaveChangesAsync();

        return tokenString;
    }

    private int GetAccessTokenExpiration() =>
        int.Parse(_config["Jwt:AccessTokenExpirationMinutes"] ?? "30");

    private int GetRefreshTokenExpiration() =>
        int.Parse(_config["Jwt:RefreshTokenExpirationDays"] ?? "30");
}
