using System.Buffers.Binary;
using System.Security.Cryptography;
using System.Text;
using Ical.Net;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;
using Ical.Net.Serialization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using UniVerse.Application.DTOs.Common;
using UniVerse.Application.DTOs.Lectures;
using UniVerse.Application.DTOs.Users;
using UniVerse.Application.Interfaces;
using UniVerse.Application.Mappings;
using UniVerse.Domain.Enums;
using UniVerse.Domain.Exceptions;
using UniVerse.Domain.Services;
using UniVerse.Infrastructure.Data;

namespace UniVerse.Infrastructure.Services;

public class UserService : IUserService
{
    private const byte CalendarTokenVersion = 1;
    private const int CalendarTokenPayloadLength = 5;
    private const int CalendarTokenSignatureLength = 32;
    private const string CalendarTokenKeyContext = "universe-calendar-subscription-v1";

    private readonly AppDbContext _db;
    private readonly IGamificationService _gamification;
    private readonly IConfiguration _config;

    public UserService(AppDbContext db, IGamificationService gamification, IConfiguration config)
    {
        _db = db;
        _gamification = gamification;
        _config = config;
    }

    public async Task<UserDto> GetByIdAsync(int id)
    {
        var user = await _db.Users
            .Include(u => u.Roles)
            .FirstOrDefaultAsync(u => u.Id == id)
            ?? throw new NotFoundException("User", id);
        return user.ToDto(await _gamification.CalculateLevelAsync(user.Xp));
    }

    public async Task<UserDto> UpdateProfileAsync(int id, UpdateUserRequest request)
    {
        var user = await _db.Users
            .Include(u => u.Roles)
            .FirstOrDefaultAsync(u => u.Id == id)
            ?? throw new NotFoundException("User", id);

        if (request.DisplayName != null) user.DisplayName = request.DisplayName;
        if (request.AvatarUrl != null) user.AvatarUrl = request.AvatarUrl;
        user.UpdatedAt = DateTime.UtcNow;

        await _db.SaveChangesAsync();
        await _gamification.CheckAndAwardAchievementsAsync(id);
        return user.ToDto(await _gamification.CalculateLevelAsync(user.Xp));
    }

    public async Task<UserStatsDto> GetStatsAsync(int id)
    {
        var user = await _db.Users.FindAsync(id)
            ?? throw new NotFoundException("User", id);

        var totalLectures = await _db.LectureEnrollments.CountAsync(e => e.UserId == id);
        var attended = await _db.LectureEnrollments.CountAsync(e => e.UserId == id && e.Attended);
        var reviews = await _db.Reviews.CountAsync(r => r.UserId == id);
        var achievements = await _db.UserAchievements.CountAsync(ua => ua.UserId == id);
        var activeEnrollments = await _db.LectureEnrollments
            .CountAsync(e => e.UserId == id && !e.Attended);

        var level = await _gamification.CalculateLevelAsync(user.Xp);
        var levelProgress = await _gamification.GetLevelProgressAsync(user.Xp);
        var slotLimit = EnrollmentSlotPolicy.GetLimitForLevel(level);
        var slotRules = EnrollmentSlotPolicy.Rules
            .Select(rule => new EnrollmentSlotRuleDto(rule.Level, rule.Slots))
            .ToList();

        return new UserStatsDto(
            totalLectures, attended, reviews,
            user.Xp, user.Coins, level, achievements,
            levelProgress.CurrentLevelXp, levelProgress.NextLevelXp,
            activeEnrollments, slotLimit, slotRules
        );
    }

    public async Task<AdminDashboardStatsDto> GetAdminDashboardStatsAsync()
    {
        var usersCount = await _db.Users
            .CountAsync(user => !user.Roles.Any(role => role.Role == UserRole.Teacher));
        var lecturesCount = await _db.Lectures.CountAsync();
        var enrollmentsCount = await _db.LectureEnrollments.CountAsync();
        var pendingReviewsCount = await _db.Reviews.CountAsync(review => review.LlmStatus == ReviewLlmStatus.Pending);

        return new AdminDashboardStatsDto(usersCount, lecturesCount, enrollmentsCount, pendingReviewsCount);
    }

    public async Task<PagedResult<LectureDto>> GetEnrollmentsAsync(int id, PaginationRequest pagination)
    {
        if (!await _db.Users.AnyAsync(u => u.Id == id))
            throw new NotFoundException("User", id);

        var query = _db.LectureEnrollments
            .Where(e => e.UserId == id)
            .Include(e => e.Lecture)
            .ThenInclude(l => l.Course)
            .Include(e => e.Lecture)
            .ThenInclude(l => l.Teacher)
            .Include(e => e.Lecture)
            .ThenInclude(l => l.Location)
            .Include(e => e.Lecture)
            .ThenInclude(l => l.Enrollments);

        var total = await query.CountAsync();
        var enrollments = await query
            .OrderBy(e => e.Lecture.StartsAt)
            .Skip((pagination.Page - 1) * pagination.PageSize)
            .Take(pagination.PageSize)
            .ToListAsync();

        return PagedResult<LectureDto>.Create(
            enrollments.Select(e => e.Lecture.ToDto(isEnrolled: true)).ToList(),
            total,
            pagination.Page,
            pagination.PageSize);
    }


    public async Task<string> GetMyEnrollmentsIcsAsync(int userId)
    {
        if (!await _db.Users.AnyAsync(u => u.Id == userId))
            throw new NotFoundException("User", userId);

        var lectures = await _db.LectureEnrollments
            .Where(e => e.UserId == userId)
            .Include(e => e.Lecture)
            .ThenInclude(l => l.Teacher)
            .Include(e => e.Lecture)
            .ThenInclude(l => l.Location)
            .OrderBy(e => e.Lecture.StartsAt)
            .Select(e => e.Lecture)
            .ToListAsync();

        return BuildIcs(lectures, userId);
    }

    public async Task<string> GetEnrollmentIcsAsync(int userId, int lectureId)
    {
        if (!await _db.Users.AnyAsync(u => u.Id == userId))
            throw new NotFoundException("User", userId);

        var lecture = await _db.Lectures
            .Include(l => l.Teacher)
            .Include(l => l.Location)
            .FirstOrDefaultAsync(l => l.Id == lectureId)
            ?? throw new NotFoundException("Lecture", lectureId);

        return BuildIcs([lecture], userId);
    }

    public async Task<string> GetCalendarSubscriptionTokenAsync(int userId)
    {
        if (!await _db.Users.AnyAsync(u => u.Id == userId))
            throw new NotFoundException("User", userId);

        Span<byte> payload = stackalloc byte[CalendarTokenPayloadLength];
        payload[0] = CalendarTokenVersion;
        BinaryPrimitives.WriteInt32BigEndian(payload[1..], userId);

        var signature = SignCalendarTokenPayload(payload);
        var tokenBytes = new byte[CalendarTokenPayloadLength + CalendarTokenSignatureLength];
        payload.CopyTo(tokenBytes);
        signature.CopyTo(tokenBytes.AsSpan(CalendarTokenPayloadLength));

        return ToBase64Url(tokenBytes);
    }

    public async Task<string> GetEnrollmentsIcsBySubscriptionTokenAsync(string token)
    {
        var userId = ValidateCalendarSubscriptionToken(token);
        return await GetMyEnrollmentsIcsAsync(userId);
    }

    private int ValidateCalendarSubscriptionToken(string token)
    {
        var tokenBytes = FromBase64Url(token);
        if (tokenBytes.Length != CalendarTokenPayloadLength + CalendarTokenSignatureLength)
            throw new ForbiddenException("Invalid calendar subscription token.");

        var payload = tokenBytes.AsSpan(0, CalendarTokenPayloadLength);
        var signature = tokenBytes.AsSpan(CalendarTokenPayloadLength, CalendarTokenSignatureLength);
        if (payload[0] != CalendarTokenVersion)
            throw new ForbiddenException("Invalid calendar subscription token.");

        var expectedSignature = SignCalendarTokenPayload(payload);
        if (!CryptographicOperations.FixedTimeEquals(signature, expectedSignature))
            throw new ForbiddenException("Invalid calendar subscription token.");

        var userId = BinaryPrimitives.ReadInt32BigEndian(payload[1..]);
        if (userId <= 0)
            throw new ForbiddenException("Invalid calendar subscription token.");

        return userId;
    }

    private byte[] SignCalendarTokenPayload(ReadOnlySpan<byte> payload)
    {
        var calendarKey = DeriveCalendarTokenKey();
        return HMACSHA256.HashData(calendarKey, payload);
    }

    private byte[] DeriveCalendarTokenKey()
    {
        var jwtSecret = _config["Jwt:Secret"];
        if (string.IsNullOrWhiteSpace(jwtSecret))
            throw new InvalidOperationException("Jwt:Secret is not configured.");

        return HMACSHA256.HashData(
            Encoding.UTF8.GetBytes(jwtSecret),
            Encoding.UTF8.GetBytes(CalendarTokenKeyContext));
    }

    private static string ToBase64Url(ReadOnlySpan<byte> bytes) =>
        Convert.ToBase64String(bytes)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');

    private static byte[] FromBase64Url(string value)
    {
        try
        {
            var padded = value.Replace('-', '+').Replace('_', '/');
            padded = padded.PadRight(padded.Length + (4 - padded.Length % 4) % 4, '=');
            return Convert.FromBase64String(padded);
        }
        catch (FormatException)
        {
            throw new ForbiddenException("Invalid calendar subscription token.");
        }
    }

    private static string BuildIcs(List<Domain.Entities.Lecture> lectures, int userId)
    {
        var calendar = new Calendar
        {
            Method = "PUBLISH",
            ProductId = "-//UniVerse//Lectures Calendar//EN"
        };

        foreach (var lecture in lectures)
        {
            var location = lecture.Location is null
                ? string.Empty
                : $"{lecture.Location.Building}{(string.IsNullOrWhiteSpace(lecture.Location.Room) ? string.Empty : $", ауд. {lecture.Location.Room}")}";

            var teacherName = lecture.Teacher?.DisplayName
                ?? lecture.Teacher?.Email
                ?? "не указан";

            calendar.Events.Add(new CalendarEvent
            {
                Uid = $"lecture-{lecture.Id}-user-{userId}@universe.local",
                Summary = lecture.Title,
                Description = $"{lecture.Description}\nПреподаватель: {teacherName}",
                Location = location,
                DtStart = new CalDateTime(DateTime.SpecifyKind(lecture.StartsAt, DateTimeKind.Utc)),
                DtEnd = new CalDateTime(DateTime.SpecifyKind(lecture.EndsAt, DateTimeKind.Utc)),
                DtStamp = new CalDateTime(DateTime.UtcNow)
            });
        }

        return new CalendarSerializer().SerializeToString(calendar) ?? string.Empty;
    }

    public async Task<PagedResult<UserDto>> GetAllAsync(UserFilterRequest filter)
    {
        var query = _db.Users.AsQueryable();

        if (!string.IsNullOrEmpty(filter.Search))
        {
            var search = filter.Search.ToLower();
            query = query.Where(u =>
                u.Email.ToLower().Contains(search) ||
                (u.DisplayName != null && u.DisplayName.ToLower().Contains(search)));
        }

        query = query.Include(u => u.Roles);

        if (filter.Role.HasValue)
        {
            var role = filter.Role.Value;
            query = query.Where(u =>
                u.Roles.Count == 1 &&
                u.Roles.Any(ur => ur.Role == role));
        }

        if (filter.IsActive.HasValue)
            query = query.Where(u => u.IsActive == filter.IsActive.Value);

        var total = await query.CountAsync();

        var users = await query
            .OrderByDescending(u => u.CreatedAt)
            .Skip((filter.Page - 1) * filter.PageSize)
            .Take(filter.PageSize)
            .ToListAsync();

        var items = new List<UserDto>(users.Count);
        foreach (var user in users)
            items.Add(user.ToDto(await _gamification.CalculateLevelAsync(user.Xp)));

        return PagedResult<UserDto>.Create(items, total, filter.Page, filter.PageSize);
    }

    public async Task SetRolesAsync(int id, IReadOnlyCollection<UserRole> roles)
    {
        var normalizedRoles = roles.Distinct().ToList();
        if (normalizedRoles.Count == 0)
            throw new ForbiddenException("At least one role is required.");

        var user = await _db.Users
            .Include(u => u.Roles)
            .FirstOrDefaultAsync(u => u.Id == id)
            ?? throw new NotFoundException("User", id);

        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);

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

        await EnsureProfilesForRolesAsync(user.Id, normalizedRoles);
        user.UpdatedAt = DateTime.UtcNow;
        await _db.SaveChangesAsync();
    }

    public async Task SetActiveAsync(int id, bool isActive)
    {
        var user = await _db.Users.FindAsync(id)
            ?? throw new NotFoundException("User", id);
        user.IsActive = isActive;
        user.UpdatedAt = DateTime.UtcNow;
        await _db.SaveChangesAsync();
    }

    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 Domain.Entities.StudentProfile { UserId = userId });
        }

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