using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using UniVerse.Application.DTOs.Sync;
using UniVerse.Application.Interfaces;
using UniVerse.Domain.Entities;
using UniVerse.Domain.Enums;
using UniVerse.Infrastructure.Data;

namespace UniVerse.Infrastructure.Services;

public class ScheduleSyncService : IScheduleSyncService
{
    private readonly AppDbContext _db;
    private readonly IModeusApiClient _modeus;
    private readonly ILogger<ScheduleSyncService> _logger;
    private static SyncStatusDto _lastStatus = new(null, "idle", null);

    public ScheduleSyncService(AppDbContext db, IModeusApiClient modeus, ILogger<ScheduleSyncService> logger)
    {
        _db = db; _modeus = modeus; _logger = logger;
    }

    public async Task<SyncResultDto> SyncScheduleAsync(SyncScheduleRequest request)
    {
        const string stage = "schedule";
        int created = 0, updated = 0, skipped = 0;
        try
        {
            var events = await _modeus.SearchEventsAsync(request);
            var embeddedRoomCapacityById = BuildRoomCapacityLookup(events.Embedded?.Rooms);
            IReadOnlyDictionary<string, int>? syncedRoomCapacityById = null;

            foreach (var ev in events.EventItems)
            {
                if (string.IsNullOrWhiteSpace(ev.Id) || string.IsNullOrWhiteSpace(ev.Name))
                {
                    skipped++;
                    continue;
                }

                var courseUnitId = GetHrefId(ev.Links?.CourseUnitRealization?.Href);
                var courseUnit = events.Embedded?.CourseUnitRealizations?
                    .FirstOrDefault(c => c.Id == courseUnitId);
                var courseExternalId = courseUnit?.Id ?? ev.TypeId ?? ev.Id;
                var courseName = courseUnit?.Name ?? ev.Name;
                var location = await UpsertEventLocationAsync(events, ev.Id);
                var teacher = await UpsertEventTeacherAsync(events, ev.Id);
                var roomId = GetEventRoomId(events, ev.Id);
                var maxEnrollments = GetRoomCapacity(embeddedRoomCapacityById, roomId);
                if (maxEnrollments is null && !string.IsNullOrWhiteSpace(roomId))
                {
                    syncedRoomCapacityById ??= await LoadRoomCapacityLookupAsync();
                    maxEnrollments = GetRoomCapacity(syncedRoomCapacityById, roomId);
                }

                var lectureCapacity = maxEnrollments ?? GetEventTeamSize(events, ev.Id) ?? 0;
                var mandatoryAttendeesCount = GetMandatoryAttendeesCount(ev.IctisStats);
                var startsAt = EnsureUtc(ev.StartsAt);
                var endsAt = EnsureUtc(ev.EndsAt);

                var existing = await _db.Lectures.FirstOrDefaultAsync(l => l.ExternalId == ev.Id);
                if (existing != null)
                {
                    existing.Title = ev.Name;
                    existing.Description = ev.Description;
                    existing.StartsAt = startsAt;
                    existing.EndsAt = endsAt;
                    existing.LocationId = location?.Id;
                    existing.TeacherId = teacher?.Id;
                    existing.MaxEnrollments = lectureCapacity;
                    existing.MandatoryAttendeesCount = mandatoryAttendeesCount;
                    existing.UpdatedAt = DateTime.UtcNow;
                    updated++;
                }
                else
                {
                    var course = await _db.Courses.FirstOrDefaultAsync(c => c.ExternalId == courseExternalId);
                    if (course == null)
                    {
                        course = new Course { Name = courseName, ExternalId = courseExternalId, IsSynced = true };
                        _db.Courses.Add(course);
                        await _db.SaveChangesAsync();
                    }

                    _db.Lectures.Add(new Lecture
                    {
                        CourseId = course.Id,
                        TeacherId = teacher?.Id,
                        LocationId = location?.Id,
                        Title = ev.Name,
                        Description = ev.Description,
                        ExternalId = ev.Id,
                        StartsAt = startsAt,
                        EndsAt = endsAt,
                        MaxEnrollments = lectureCapacity,
                        MandatoryAttendeesCount = mandatoryAttendeesCount
                    });
                    created++;
                }
            }
            await _db.SaveChangesAsync();
            var result = new SyncResultDto(created, updated, skipped, null);
            _lastStatus = new SyncStatusDto(DateTime.UtcNow, "completed", result);
            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Schedule sync failed");
            var result = new SyncResultDto(created, updated, skipped, ex.Message, BuildErrorDetails(
                ex,
                stage,
                created,
                updated,
                skipped,
                [
                    "endpoint=POST /api/ictis?includeCounts=true",
                    $"timeMin={request.TimeMin:O}",
                    $"timeMax={request.TimeMax:O}"
                ]));
            _lastStatus = new SyncStatusDto(DateTime.UtcNow, "failed", result);
            return result;
        }
    }

    public async Task<SyncResultDto> SyncRoomsAsync()
    {
        const string stage = "rooms";
        int created = 0, updated = 0, skipped = 0;
        try
        {
            var rooms = await _modeus.SearchRoomsAsync();
            foreach (var room in rooms?.RoomItems ?? [])
            {
                if (room is null || string.IsNullOrWhiteSpace(room.Id) || string.IsNullOrWhiteSpace(room.Name))
                {
                    skipped++;
                    continue;
                }

                var existing = await _db.Locations.FirstOrDefaultAsync(l => l.ExternalId == room.Id);
                if (existing != null)
                {
                    existing.Name = room.Name;
                    existing.Room = room.NameShort;
                    existing.Building = room.Building?.Name ?? room.Building?.NameShort;
                    existing.Address = room.Building?.Address;
                    updated++;
                }
                else
                {
                    _db.Locations.Add(new Location
                    {
                        Name = room.Name,
                        Room = room.NameShort,
                        Building = room.Building?.Name ?? room.Building?.NameShort,
                        Address = room.Building?.Address,
                        ExternalId = room.Id
                    });
                    created++;
                }
            }

            await _db.SaveChangesAsync();
            var result = new SyncResultDto(created, updated, skipped, null);
            _lastStatus = new SyncStatusDto(DateTime.UtcNow, "completed", result);
            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Rooms sync failed");
            var result = new SyncResultDto(created, updated, skipped, ex.Message, BuildErrorDetails(
                ex,
                stage,
                created,
                updated,
                skipped,
                ["request=name:<empty>, sort:+building.name,+name, deleted:false, page size:100"]));
            _lastStatus = new SyncStatusDto(DateTime.UtcNow, "failed", result);
            return result;
        }
    }

    public async Task<List<EmployeeDto>> SearchEmployeesAsync(string fullname)
    {
        var employees = await _modeus.SearchEmployeeAsync(fullname);
        return employees.Select(e => new EmployeeDto(e.Id, e.FullName, e.Department)).ToList();
    }

    public Task<SyncStatusDto> GetLastSyncStatusAsync() => Task.FromResult(_lastStatus);

    private async Task<User?> UpsertEventTeacherAsync(ModeusEventsResponse events, string eventId)
    {
        var personId = events.Embedded?.EventAttendees?
            .Where(attendee => string.Equals(attendee.RoleId, "TEACH", StringComparison.OrdinalIgnoreCase))
            .Select(attendee => new
            {
                EventId = GetHrefId(attendee.Links?.Event?.Href),
                PersonId = GetHrefId(attendee.Links?.Person?.Href)
            })
            .FirstOrDefault(link => link.EventId == eventId)
            ?.PersonId;

        if (string.IsNullOrWhiteSpace(personId))
            return null;

        var person = events.Embedded?.Persons?.FirstOrDefault(item => item.Id == personId);
        var fullName = BuildPersonFullName(person);
        if (string.IsNullOrWhiteSpace(fullName))
            return null;

        var existingProfile = await _db.TeacherProfiles
            .Include(profile => profile.User)
            .ThenInclude(user => user.Roles)
            .FirstOrDefaultAsync(profile => profile.ModeusId == personId);
        var subId = existingProfile?.User.MicrosoftId;
        if (string.IsNullOrWhiteSpace(subId))
            subId = await TryGetTeacherSubIdAsync(fullName);

        User? ssoUser = null;
        if (!string.IsNullOrWhiteSpace(subId))
        {
            ssoUser = await _db.Users
                .Include(item => item.Roles)
                .Include(item => item.TeacherProfile)
                .FirstOrDefaultAsync(item => item.MicrosoftId == subId);
        }

        if (existingProfile != null && ssoUser != null && existingProfile.UserId != ssoUser.Id)
            return await MergeTeacherPlaceholderAsync(existingProfile, ssoUser, fullName, subId);

        if (existingProfile != null)
        {
            existingProfile.User.DisplayName = fullName;
            if (!string.IsNullOrWhiteSpace(subId))
                existingProfile.User.MicrosoftId = subId;
            existingProfile.User.UpdatedAt = DateTime.UtcNow;
            EnsureTeacherRole(existingProfile.User);
            return existingProfile.User;
        }

        if (ssoUser != null)
        {
            ssoUser.DisplayName = fullName;
            ssoUser.UpdatedAt = DateTime.UtcNow;
            EnsureTeacherRole(ssoUser);
            EnsureTeacherProfile(ssoUser, personId);
            await _db.SaveChangesAsync();
            return ssoUser;
        }

        var email = BuildModeusTeacherEmail(personId);
        var user = await _db.Users
            .Include(item => item.Roles)
            .Include(item => item.TeacherProfile)
            .FirstOrDefaultAsync(item => item.Email == email);

        if (user == null)
        {
            user = new User
            {
                Email = email,
                DisplayName = fullName,
                MicrosoftId = subId,
                IsActive = true,
                TeacherProfile = new TeacherProfile { ModeusId = personId }
            };
            user.Roles.Add(new UserRoleAssignment { User = user, Role = UserRole.Teacher });
            _db.Users.Add(user);
            await _db.SaveChangesAsync();
            return user;
        }

        user.DisplayName = fullName;
        if (!string.IsNullOrWhiteSpace(subId))
            user.MicrosoftId = subId;
        user.UpdatedAt = DateTime.UtcNow;
        if (user.TeacherProfile == null)
            user.TeacherProfile = new TeacherProfile { UserId = user.Id, ModeusId = personId };
        else
            user.TeacherProfile.ModeusId = personId;

        EnsureTeacherRole(user);

        await _db.SaveChangesAsync();
        return user;
    }

    private async Task<string?> TryGetTeacherSubIdAsync(string fullName)
    {
        try
        {
            return await _modeus.GetSubIdByFullNameAsync(fullName);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Could not resolve SSO sub id for teacher {TeacherFullName}. A placeholder teacher will be used until a future sync succeeds.", fullName);
            return null;
        }
    }

    private async Task<User> MergeTeacherPlaceholderAsync(
        TeacherProfile placeholderProfile,
        User targetUser,
        string fullName,
        string? subId)
    {
        var placeholderUser = placeholderProfile.User;

        var lectures = await _db.Lectures
            .Where(lecture => lecture.TeacherId == placeholderUser.Id)
            .ToListAsync();
        foreach (var lecture in lectures)
            lecture.TeacherId = targetUser.Id;

        targetUser.DisplayName = fullName;
        if (!string.IsNullOrWhiteSpace(subId))
            targetUser.MicrosoftId = subId;
        targetUser.UpdatedAt = DateTime.UtcNow;
        EnsureTeacherRole(targetUser);

        if (targetUser.TeacherProfile == null)
        {
            placeholderProfile.UserId = targetUser.Id;
            placeholderProfile.User = targetUser;
            targetUser.TeacherProfile = placeholderProfile;
            placeholderUser.TeacherProfile = null;
        }
        else
        {
            targetUser.TeacherProfile.ModeusId = placeholderProfile.ModeusId;
            _db.TeacherProfiles.Remove(placeholderProfile);
        }

        if (await CanDeletePlaceholderUserAsync(placeholderUser.Id))
            _db.Users.Remove(placeholderUser);

        await _db.SaveChangesAsync();
        return targetUser;
    }

    private async Task<bool> CanDeletePlaceholderUserAsync(int userId) =>
        !await _db.StudentProfiles.AnyAsync(profile => profile.UserId == userId)
        && !await _db.RefreshTokens.AnyAsync(token => token.UserId == userId)
        && !await _db.LectureEnrollments.AnyAsync(enrollment => enrollment.UserId == userId)
        && !await _db.Reviews.AnyAsync(review => review.UserId == userId)
        && !await _db.UserAchievements.AnyAsync(achievement => achievement.UserId == userId)
        && !await _db.CoinTransactions.AnyAsync(transaction => transaction.UserId == userId)
        && !await _db.UserNotifications.AnyAsync(notification => notification.UserId == userId);

    private static void EnsureTeacherProfile(User user, string modeusId)
    {
        if (user.TeacherProfile == null)
            user.TeacherProfile = new TeacherProfile { UserId = user.Id, ModeusId = modeusId };
        else
            user.TeacherProfile.ModeusId = modeusId;
    }

    private static void EnsureTeacherRole(User user)
    {
        if (!user.Roles.Any(role => role.Role == UserRole.Teacher))
            user.Roles.Add(new UserRoleAssignment { UserId = user.Id, Role = UserRole.Teacher });
    }

    private async Task<Location?> UpsertEventLocationAsync(ModeusEventsResponse events, string eventId)
    {
        var roomId = GetEventRoomId(events, eventId);

        if (string.IsNullOrWhiteSpace(roomId))
            return null;

        var room = events.Embedded?.Rooms?.FirstOrDefault(item => item.Id == roomId);
        if (room == null || string.IsNullOrWhiteSpace(room.Name))
            return null;

        var existing = await _db.Locations.FirstOrDefaultAsync(location => location.ExternalId == room.Id);
        if (existing != null)
        {
            existing.Name = room.Name;
            existing.Room = room.NameShort;
            existing.Building = room.Building?.Name ?? room.Building?.NameShort;
            existing.Address = room.Building?.Address;
            return existing;
        }

        var location = new Location
        {
            Name = room.Name,
            Room = room.NameShort,
            Building = room.Building?.Name ?? room.Building?.NameShort,
            Address = room.Building?.Address,
            ExternalId = room.Id
        };

        _db.Locations.Add(location);
        await _db.SaveChangesAsync();
        return location;
    }

    private static string? GetEventRoomId(ModeusEventsResponse events, string eventId) =>
        events.Embedded?.EventRooms?
            .Select(eventRoom => new
            {
                EventId = GetHrefId(eventRoom.Links?.Event?.Href),
                RoomId = GetHrefId(eventRoom.Links?.Room?.Href)
            })
            .FirstOrDefault(link => link.EventId == eventId)
            ?.RoomId;

    private async Task<IReadOnlyDictionary<string, int>> LoadRoomCapacityLookupAsync()
    {
        try
        {
            var rooms = await _modeus.SearchRoomsAsync();
            return BuildRoomCapacityLookup(rooms.RoomItems);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Could not load room capacities from Modeus rooms search.");
            return new Dictionary<string, int>();
        }
    }

    private static IReadOnlyDictionary<string, int> BuildRoomCapacityLookup(IEnumerable<ModeusRoom>? rooms)
    {
        var result = new Dictionary<string, int>();
        foreach (var room in rooms ?? [])
        {
            var capacity = NormalizeCapacity(room.WorkingCapacity) ?? NormalizeCapacity(room.TotalCapacity);
            if (!string.IsNullOrWhiteSpace(room.Id) && capacity.HasValue)
                result.TryAdd(room.Id, capacity.Value);
        }

        return result;
    }

    private static int? GetRoomCapacity(IReadOnlyDictionary<string, int> roomCapacityById, string? roomId) =>
        !string.IsNullOrWhiteSpace(roomId) && roomCapacityById.TryGetValue(roomId, out var capacity)
            ? capacity
            : null;

    private static int? GetEventTeamSize(ModeusEventsResponse events, string eventId) =>
        NormalizeCapacity(events.Embedded?.EventTeams?
            .FirstOrDefault(team => team.EventId == eventId)?.Size);

    private static int? NormalizeCapacity(int? capacity) =>
        capacity is > 0 ? capacity : null;

    private static int GetMandatoryAttendeesCount(ModeusIctisStats? stats) =>
        Math.Max(0, stats?.StudentCount ?? 0) + Math.Max(0, stats?.TeacherCount ?? 0);

    private static string BuildModeusTeacherEmail(string personId) =>
        $"modeus-{personId}@modeus.local".ToLowerInvariant();

    private static string? BuildPersonFullName(ModeusPerson? person)
    {
        if (person == null)
            return null;

        if (!string.IsNullOrWhiteSpace(person.FullName))
            return person.FullName.Trim();

        var parts = new[] { person.LastName, person.FirstName, person.MiddleName }
            .Where(part => !string.IsNullOrWhiteSpace(part))
            .Select(part => part!.Trim());

        var fullName = string.Join(" ", parts);
        return string.IsNullOrWhiteSpace(fullName) ? null : fullName;
    }

    private static IReadOnlyList<string> BuildErrorDetails(
        Exception exception,
        string stage,
        int created,
        int updated,
        int skipped,
        IReadOnlyList<string> context)
    {
        var details = new List<string>
        {
            $"stage={stage}",
            $"exceptionType={exception.GetType().FullName}",
            $"message={exception.Message}",
            $"partialResult=created:{created}, updated:{updated}, skipped:{skipped}"
        };

        if (exception is HttpRequestException httpException && httpException.StatusCode.HasValue)
            details.Add($"httpStatus={(int)httpException.StatusCode.Value} {httpException.StatusCode.Value}");

        if (exception.InnerException != null)
            details.Add($"innerException={exception.InnerException.GetType().FullName}: {exception.InnerException.Message}");

        details.AddRange(context);
        return details;
    }

    private static string? GetHrefId(string? href)
    {
        if (string.IsNullOrWhiteSpace(href))
            return null;

        var index = href.LastIndexOf('/');
        return index >= 0 && index < href.Length - 1
            ? href[(index + 1)..]
            : href;
    }

    private static DateTime EnsureUtc(DateTime value) =>
        value.Kind switch
        {
            DateTimeKind.Utc => value,
            DateTimeKind.Local => value.ToUniversalTime(),
            _ => DateTime.SpecifyKind(value, DateTimeKind.Local).ToUniversalTime()
        };
}
