using Microsoft.EntityFrameworkCore;
using UniVerse.Application.DTOs.Common;
using UniVerse.Application.DTOs.Lectures;
using UniVerse.Application.DTOs.Notifications;
using UniVerse.Application.Interfaces;
using UniVerse.Application.Mappings;
using UniVerse.Domain.Entities;
using UniVerse.Domain.Exceptions;
using UniVerse.Domain.Services;
using UniVerse.Infrastructure.Data;

namespace UniVerse.Infrastructure.Services;

public class LectureService : ILectureService
{
    private readonly AppDbContext _db;
    private readonly IGamificationService _gamification;
    private readonly INotificationScheduler _notificationScheduler;

    public LectureService(
        AppDbContext db,
        IGamificationService gamification,
        INotificationScheduler notificationScheduler)
    {
        _db = db;
        _gamification = gamification;
        _notificationScheduler = notificationScheduler;
    }

    private IQueryable<Lecture> BaseQuery() => _db.Lectures
        .Include(l => l.Course).Include(l => l.Teacher)
        .Include(l => l.Location).Include(l => l.Enrollments);

    public async Task<PagedResult<LectureDto>> GetAllAsync(LectureFilterRequest filter, int? currentUserId = null)
    {
        var query = BaseQuery();
        if (filter.CourseId.HasValue) query = query.Where(l => l.CourseId == filter.CourseId);
        if (filter.TeacherId.HasValue) query = query.Where(l => l.TeacherId == filter.TeacherId);
        if (filter.Format.HasValue) query = query.Where(l => l.Format == filter.Format);
        if (filter.IsOpen.HasValue) query = query.Where(l => l.IsOpen == filter.IsOpen);
        if (filter.DateFrom.HasValue)
            query = query.Where(l => DateOnly.FromDateTime(l.StartsAt) >= filter.DateFrom);
        if (filter.DateTo.HasValue)
            query = query.Where(l => DateOnly.FromDateTime(l.StartsAt) <= filter.DateTo);
        if (!string.IsNullOrEmpty(filter.Search))
            query = query.Where(l => l.Title.ToLower().Contains(filter.Search.ToLower()));
        if (filter.TagId.HasValue)
            query = query.Where(l => l.Course.CourseTags.Any(ct => ct.TagId == filter.TagId));

        var total = await query.CountAsync();
        var items = await query.OrderBy(l => l.StartsAt)
            .Skip((filter.Page - 1) * filter.PageSize).Take(filter.PageSize).ToListAsync();
        return PagedResult<LectureDto>.Create(
            items.Select(l => l.ToDto(currentUserId.HasValue && l.Enrollments.Any(e => e.UserId == currentUserId.Value))).ToList(),
            total,
            filter.Page,
            filter.PageSize);
    }

    public async Task<LectureDetailDto> GetByIdAsync(int id, int? currentUserId = null)
    {
        var lecture = await BaseQuery().FirstOrDefaultAsync(l => l.Id == id)
            ?? throw new NotFoundException("Lecture", id);
        var isEnrolled = currentUserId.HasValue &&
            lecture.Enrollments.Any(e => e.UserId == currentUserId.Value);
        return lecture.ToDetailDto(isEnrolled);
    }

    public async Task<LectureDto> CreateAsync(CreateLectureRequest req)
    {
        _ = await _db.Courses.FindAsync(req.CourseId) ?? throw new NotFoundException("Course", req.CourseId);
        var lecture = new Lecture
        {
            CourseId = req.CourseId, TeacherId = req.TeacherId, LocationId = req.LocationId,
            Title = req.Title, Description = req.Description, Format = req.Format,
            StartsAt = req.StartsAt, EndsAt = req.EndsAt, IsOpen = req.IsOpen,
            MaxEnrollments = req.MaxEnrollments, OnlineUrl = req.OnlineUrl
        };
        _db.Lectures.Add(lecture);
        await _db.SaveChangesAsync();
        var full = await BaseQuery().FirstAsync(l => l.Id == lecture.Id);
        return full.ToDto();
    }

    public async Task<LectureDto> UpdateAsync(int id, UpdateLectureRequest req, int currentUserId, bool isAdmin = false)
    {
        var lecture = await _db.Lectures
            .Include(l => l.Location)
            .Include(l => l.Enrollments)
            .ThenInclude(e => e.User)
            .FirstOrDefaultAsync(l => l.Id == id) ?? throw new NotFoundException("Lecture", id);
        EnsureTeacherOwnsLecture(lecture, currentUserId, isAdmin);
        lecture.TeacherId = req.TeacherId; lecture.LocationId = req.LocationId;
        lecture.Title = req.Title; lecture.Description = req.Description;
        lecture.Format = req.Format; lecture.StartsAt = req.StartsAt; lecture.EndsAt = req.EndsAt;
        lecture.IsOpen = req.IsOpen; lecture.MaxEnrollments = req.MaxEnrollments;
        lecture.OnlineUrl = req.OnlineUrl; lecture.UpdatedAt = DateTime.UtcNow;
        lecture.Location = req.LocationId.HasValue
            ? await _db.Locations.FindAsync(req.LocationId.Value)
            : null;
        await _db.SaveChangesAsync();
        await RescheduleLectureRemindersAsync(lecture);
        var full = await BaseQuery().FirstAsync(l => l.Id == id);
        return full.ToDto();
    }

    public async Task DeleteAsync(int id)
    {
        var lecture = await _db.Lectures
            .Include(l => l.Enrollments)
            .FirstOrDefaultAsync(l => l.Id == id) ?? throw new NotFoundException("Lecture", id);
        await CancelLectureRemindersAsync(lecture);
        _db.Lectures.Remove(lecture);
        await _db.SaveChangesAsync();
    }

    public async Task EnrollAsync(int lectureId, int userId)
    {
        var lecture = await _db.Lectures
            .Include(l => l.Location)
            .Include(l => l.Enrollments)
            .FirstOrDefaultAsync(l => l.Id == lectureId) ?? throw new NotFoundException("Lecture", lectureId);
        var user = await _db.Users.FindAsync(userId) ?? throw new NotFoundException("User", userId);
        if (!lecture.IsOpen) throw new ConflictException("Lecture is not open for enrollment.");
        var occupiedSeatsCount = Math.Max(0, lecture.MandatoryAttendeesCount) + lecture.Enrollments.Count;
        if (lecture.MaxEnrollments > 0 && occupiedSeatsCount >= lecture.MaxEnrollments)
            throw new ConflictException("Lecture is full.");
        if (lecture.Enrollments.Any(e => e.UserId == userId))
            throw new ConflictException("Already enrolled.");

        var level = await _gamification.CalculateLevelAsync(user.Xp);
        var enrollmentLimit = EnrollmentSlotPolicy.GetLimitForLevel(level);
        var activeEnrollments = await _db.LectureEnrollments
            .CountAsync(e => e.UserId == userId && !e.Attended);

        if (activeEnrollments >= enrollmentLimit)
            throw new ConflictException("Enrollment limit reached for your level.");

        _db.LectureEnrollments.Add(new LectureEnrollment { LectureId = lectureId, UserId = userId });
        await _db.SaveChangesAsync();
        await ScheduleLectureRemindersAsync(lecture, user);
        await _gamification.CheckAndAwardAchievementsAsync(userId);
    }

    public async Task UnenrollAsync(int lectureId, int userId)
    {
        var enrollment = await _db.LectureEnrollments
            .FirstOrDefaultAsync(e => e.LectureId == lectureId && e.UserId == userId)
            ?? throw new NotFoundException("Enrollment not found.");
        await CancelLectureRemindersAsync(lectureId, userId);
        _db.LectureEnrollments.Remove(enrollment);
        await _db.SaveChangesAsync();
    }

    public async Task MarkAttendanceAsync(int lectureId, int userId, bool attended, int currentUserId, bool isAdmin = false)
    {
        await EnsureTeacherOwnsLectureAsync(lectureId, currentUserId, isAdmin);
        var enrollment = await _db.LectureEnrollments
            .FirstOrDefaultAsync(e => e.LectureId == lectureId && e.UserId == userId)
            ?? throw new NotFoundException("Enrollment not found.");
        enrollment.Attended = attended;
        await _db.SaveChangesAsync();
        if (attended)
            await _gamification.CheckAndAwardAchievementsAsync(userId);
    }

    public async Task<PagedResult<EnrollmentDto>> GetEnrollmentsAsync(int lectureId, PaginationRequest pagination, int currentUserId, bool isAdmin = false)
    {
        await EnsureTeacherOwnsLectureAsync(lectureId, currentUserId, isAdmin);
        var query = _db.LectureEnrollments.Include(e => e.User)
            .Where(e => e.LectureId == lectureId);
        var total = await query.CountAsync();
        var items = await query.OrderBy(e => e.CreatedAt)
            .Skip((pagination.Page - 1) * pagination.PageSize).Take(pagination.PageSize).ToListAsync();
        return PagedResult<EnrollmentDto>.Create(items.Select(e => e.ToDto()).ToList(), total, pagination.Page, pagination.PageSize);
    }

    private async Task EnsureTeacherOwnsLectureAsync(int lectureId, int currentUserId, bool isAdmin)
    {
        if (isAdmin)
            return;

        var lecture = await _db.Lectures.FirstOrDefaultAsync(l => l.Id == lectureId)
            ?? throw new NotFoundException("Lecture", lectureId);
        EnsureTeacherOwnsLecture(lecture, currentUserId, isAdmin: false);
    }

    private static void EnsureTeacherOwnsLecture(Lecture lecture, int currentUserId, bool isAdmin)
    {
        if (!isAdmin && lecture.TeacherId != currentUserId)
            throw new ForbiddenException("Teacher can access only their own lectures.");
    }

    private async Task RescheduleLectureRemindersAsync(Lecture lecture)
    {
        foreach (var enrollment in lecture.Enrollments)
        {
            await ScheduleLectureRemindersAsync(lecture, enrollment.User);
        }
    }

    private async Task ScheduleLectureRemindersAsync(Lecture lecture, User user)
    {
        if (string.IsNullOrWhiteSpace(user.Email))
        {
            await CancelLectureRemindersAsync(lecture.Id, user.Id);
            return;
        }

        foreach (var reminder in GetLectureReminders(lecture))
        {
            var jobId = GetReminderJobId(lecture.Id, user.Id, reminder.Kind);
            await _notificationScheduler.CancelAsync(jobId);

            if (reminder.SendAt <= DateTimeOffset.UtcNow)
                continue;

            var message = new NotificationMessage(
                NotificationChannels.Email,
                user.Email,
                reminder.Subject,
                reminder.Body,
                user.DisplayName,
                new Dictionary<string, string>
                {
                    ["lectureId"] = lecture.Id.ToString(),
                    ["reminderType"] = reminder.Kind
                });

            await _notificationScheduler.ScheduleAsync(message, reminder.SendAt, jobId);
        }
    }

    private async Task CancelLectureRemindersAsync(Lecture lecture)
    {
        foreach (var enrollment in lecture.Enrollments)
        {
            await CancelLectureRemindersAsync(lecture.Id, enrollment.UserId);
        }
    }

    private async Task CancelLectureRemindersAsync(int lectureId, int userId)
    {
        foreach (var kind in new[] { "starts-in-3-hours", "starts-in-1-hour", "ended" })
        {
            await _notificationScheduler.CancelAsync(GetReminderJobId(lectureId, userId, kind));
        }
    }

    private static IEnumerable<LectureReminder> GetLectureReminders(Lecture lecture)
    {
        var startsAt = ToUtcOffset(lecture.StartsAt);
        var endsAt = ToUtcOffset(lecture.EndsAt);
        var place = !string.IsNullOrWhiteSpace(lecture.OnlineUrl)
            ? $"Ссылка: {lecture.OnlineUrl}"
            : lecture.Location is null
                ? null
                : $"Место проведения: {lecture.Location.Name}";

        yield return new LectureReminder(
            "starts-in-3-hours",
            startsAt.AddHours(-3),
            $"Лекция \"{lecture.Title}\" начнется через 3 часа",
            BuildStartsSoonBody(lecture, startsAt, place, "3 часа"));

        yield return new LectureReminder(
            "starts-in-1-hour",
            startsAt.AddHours(-1),
            $"Лекция \"{lecture.Title}\" начнется через 1 час",
            BuildStartsSoonBody(lecture, startsAt, place, "1 час"));

        yield return new LectureReminder(
            "ended",
            endsAt,
            $"Оцените лекцию \"{lecture.Title}\"",
            BuildLectureEndedBody(lecture));
    }

    private static string BuildStartsSoonBody(Lecture lecture, DateTimeOffset startsAt, string? place, string interval)
    {
        var lines = new List<string>
        {
            $"Напоминаем: лекция \"{lecture.Title}\" начнется через {interval}.",
            $"Начало: {FormatDateTime(startsAt)}."
        };

        if (!string.IsNullOrWhiteSpace(place))
            lines.Add(place);

        return string.Join(Environment.NewLine, lines);
    }

    private static string BuildLectureEndedBody(Lecture lecture) =>
        $"Лекция \"{lecture.Title}\" завершилась. Пожалуйста, оставьте оценку и отзыв в UniVerse: это поможет преподавателю и другим студентам.";

    private static string GetReminderJobId(int lectureId, int userId, string kind) =>
        $"lecture-{lectureId}-user-{userId}-{kind}";

    private static DateTimeOffset ToUtcOffset(DateTime value)
    {
        var utc = value.Kind == DateTimeKind.Utc
            ? value
            : DateTime.SpecifyKind(value, DateTimeKind.Utc);
        return new DateTimeOffset(utc);
    }

    private static string FormatDateTime(DateTimeOffset value) => value.ToString("dd.MM.yyyy HH:mm 'UTC'");

    private sealed record LectureReminder(string Kind, DateTimeOffset SendAt, string Subject, string Body);
}
