using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Globalization;
using UniVerse.Application.DTOs.Achievements;
using UniVerse.Application.DTOs.Common;
using UniVerse.Application.DTOs.Gamification;
using UniVerse.Application.DTOs.Notifications;
using UniVerse.Application.Interfaces;
using UniVerse.Application.Mappings;
using UniVerse.Domain.Entities;
using UniVerse.Domain.Enums;
using UniVerse.Infrastructure.Data;

namespace UniVerse.Infrastructure.Services;

public class GamificationService : IGamificationService
{
    private readonly AppDbContext _db;
    private readonly INotificationService _notifications;
    private readonly ILogger<GamificationService> _logger;
    private List<LevelThreshold>? _levelThresholds;

    public GamificationService(
        AppDbContext db,
        INotificationService notifications,
        ILogger<GamificationService> logger)
    {
        _db = db; _notifications = notifications; _logger = logger;
    }

    public async Task AwardCoinsAsync(int userId, int amount, CoinTransactionType type,
        int? reviewId = null, int? achievementId = null, string? description = null)
    {
        var user = await _db.Users.FindAsync(userId);
        if (user == null) return;
        user.Coins += amount;
        user.Xp += amount;
        _db.CoinTransactions.Add(new CoinTransaction
        {
            UserId = userId, Amount = amount, Type = type,
            ReviewId = reviewId, AchievementId = achievementId, Description = description
        });
        await _db.SaveChangesAsync();
        _logger.LogInformation("Awarded {Amount} coins to user {UserId} ({Type})", amount, userId, type);
    }

    public async Task CheckAndAwardAchievementsAsync(int userId)
    {
        var user = await _db.Users
            .Include(u => u.StudentProfile)
            .FirstOrDefaultAsync(u => u.Id == userId);
        if (user == null) return;

        var achievements = await _db.Achievements.OrderBy(a => a.Id).ToListAsync();
        var existing = await _db.UserAchievements.Where(ua => ua.UserId == userId)
            .Select(ua => ua.AchievementId).ToListAsync();
        var reviews = await _db.Reviews.CountAsync(r => r.UserId == userId);
        var registered = await _db.LectureEnrollments.CountAsync(e => e.UserId == userId);
        var attended = await _db.LectureEnrollments.CountAsync(e => e.UserId == userId && e.Attended);
        var activeRegistrations = await _db.LectureEnrollments
            .Include(e => e.Lecture)
            .CountAsync(e => e.UserId == userId && e.Lecture.StartsAt > DateTime.UtcNow);
        var earnedCoins = await _db.CoinTransactions
            .Where(ct => ct.UserId == userId && ct.Amount > 0)
            .SumAsync(ct => (int?)ct.Amount) ?? 0;
        var attendanceStreakWeeks = await CalculateAttendanceStreakWeeksAsync(userId);
        var profileCompleted = !string.IsNullOrWhiteSpace(user.DisplayName)
            && !string.IsNullOrWhiteSpace(user.AvatarUrl);
        var firstActivity = registered > 0 || reviews > 0 || attended > 0;

        foreach (var achievement in achievements.Where(a => !existing.Contains(a.Id)))
        {
            if (!TryParseCondition(achievement.Condition, out var type, out var value)) continue;

            var earned = type switch
            {
                "lectures_attended" => attended >= value,
                "reviews_written" => reviews >= value,
                "lectures_registered" => registered >= value,
                "active_registrations" => activeRegistrations >= value,
                "attendance_streak_weeks" => attendanceStreakWeeks >= value,
                "attended_registered" => attended >= value,
                "coins_earned" => earnedCoins >= value,
                "level_reached" => await CalculateLevelAsync(user.Xp) >= value,
                "profile_completed" => profileCompleted && value <= 1,
                "first_activity" => firstActivity && value <= 1,
                _ => false
            };
            if (!earned) continue;

            _db.UserAchievements.Add(new UserAchievement { UserId = userId, AchievementId = achievement.Id });
            if (achievement.CoinReward > 0)
            {
                await AwardCoinsAsync(userId, achievement.CoinReward, CoinTransactionType.AchievementReward,
                    achievementId: achievement.Id, description: $"Achievement: {achievement.Name}");
                earnedCoins += achievement.CoinReward;
            }

            await TryNotifyAchievementAsync(user, achievement);
        }
        await _db.SaveChangesAsync();
    }

    private async Task TryNotifyAchievementAsync(User user, Achievement achievement)
    {
        try
        {
            var title = $"Новое достижение: {achievement.Name}";
            var rewardText = achievement.CoinReward > 0
                ? $" Награда: {achievement.CoinReward} монет."
                : string.Empty;
            var body = $"{achievement.Description ?? "Вы выполнили условие достижения."}{rewardText}";

            await _notifications.CreateUserNotificationAsync(user.Id, "achievement", title, body);

            await _notifications.SendAsync(new NotificationMessage(
                NotificationChannels.Email,
                user.Email,
                title,
                $"Здравствуйте, {user.DisplayName ?? user.Email}!\n\nПоздравляем: вы получили достижение «{achievement.Name}» в UniVerse.\n\n{body}",
                user.DisplayName,
                new Dictionary<string, string>
                {
                    ["event"] = "achievement_earned",
                    ["achievement_id"] = achievement.Id.ToString(),
                    ["achievement_name"] = achievement.Name
                }));
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to send achievement notification {AchievementId} to user {UserId}", achievement.Id, user.Id);
        }
    }

    private static bool TryParseCondition(string? condition, out string type, out int value)
    {
        type = string.Empty;
        value = 0;

        var parts = condition?.Split(':', 2, StringSplitOptions.TrimEntries);
        if (parts is not { Length: 2 } || string.IsNullOrWhiteSpace(parts[0])) return false;
        if (!int.TryParse(parts[1], out value)) return false;

        type = parts[0];
        return value >= 0;
    }

    private async Task<int> CalculateAttendanceStreakWeeksAsync(int userId)
    {
        var lectureDates = await _db.LectureEnrollments
            .Include(e => e.Lecture)
            .Where(e => e.UserId == userId && e.Attended)
            .Select(e => e.Lecture.StartsAt)
            .ToListAsync();

        var weekStarts = lectureDates
            .Select(GetIsoWeekStart)
            .Distinct()
            .OrderBy(d => d)
            .ToList();

        var best = 0;
        var current = 0;
        DateOnly? previous = null;

        foreach (var weekStart in weekStarts)
        {
            current = previous.HasValue && weekStart.DayNumber - previous.Value.DayNumber == 7
                ? current + 1
                : 1;
            best = Math.Max(best, current);
            previous = weekStart;
        }

        return best;
    }

    private static DateOnly GetIsoWeekStart(DateTime date)
    {
        var isoYear = ISOWeek.GetYear(date);
        var isoWeek = ISOWeek.GetWeekOfYear(date);
        return DateOnly.FromDateTime(ISOWeek.ToDateTime(isoYear, isoWeek, DayOfWeek.Monday));
    }

    public async Task<int> CalculateLevelAsync(int xp)
    {
        var thresholds = await GetLevelThresholdsAsync();
        return thresholds
            .Where(t => xp >= t.RequiredXp)
            .OrderBy(t => t.RequiredXp)
            .ThenBy(t => t.Level)
            .LastOrDefault()?.Level ?? thresholds[0].Level;
    }

    public async Task<LevelProgressDto> GetLevelProgressAsync(int xp)
    {
        var thresholds = await GetLevelThresholdsAsync();
        var current = thresholds
            .Where(t => xp >= t.RequiredXp)
            .OrderBy(t => t.RequiredXp)
            .ThenBy(t => t.Level)
            .LastOrDefault() ?? thresholds[0];
        var next = thresholds
            .Where(t => t.RequiredXp > current.RequiredXp)
            .OrderBy(t => t.RequiredXp)
            .ThenBy(t => t.Level)
            .FirstOrDefault();

        return new LevelProgressDto(current.RequiredXp, next?.RequiredXp);
    }

    private async Task<List<LevelThreshold>> GetLevelThresholdsAsync()
    {
        if (_levelThresholds is { Count: > 0 }) return _levelThresholds;

        _levelThresholds = await _db.LevelThresholds
            .AsNoTracking()
            .OrderBy(t => t.RequiredXp)
            .ThenBy(t => t.Level)
            .ToListAsync();

        if (_levelThresholds.Count == 0)
            _levelThresholds.Add(new LevelThreshold { Level = 1, RequiredXp = 0 });

        return _levelThresholds;
    }

    public async Task<List<UserAchievementDto>> GetUserAchievementsAsync(int userId) =>
        await _db.UserAchievements.Include(ua => ua.Achievement)
            .Where(ua => ua.UserId == userId).OrderByDescending(ua => ua.AwardedAt)
            .Select(ua => ua.ToDto()).ToListAsync();

    public async Task<PagedResult<CoinTransactionDto>> GetTransactionsAsync(int userId, PaginationRequest pagination)
    {
        var query = _db.CoinTransactions.Where(ct => ct.UserId == userId);
        var total = await query.CountAsync();
        var items = await query.OrderByDescending(ct => ct.CreatedAt)
            .Skip((pagination.Page - 1) * pagination.PageSize).Take(pagination.PageSize)
            .Select(ct => ct.ToDto()).ToListAsync();
        return PagedResult<CoinTransactionDto>.Create(items, total, pagination.Page, pagination.PageSize);
    }
}
