using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Kavita.API.Database;
using Kavita.API.Services.Reading;
using Kavita.Models.DTOs.Progress;
using Kavita.Models.Entities.Enums;
using Kavita.Models.Entities.Progress;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Kavita.Services.Reading;

public class ReadingHistoryService(IDataContext context, ILogger<ReadingHistoryService> logger)
    : IReadingHistoryService
{

    private sealed record ChapterMetadata(int Id, string? Range, float VolumeNumber, string SeriesName, string? LocalizedSeriesName, string LibraryName, LibraryType LibraryType);
    private sealed record SeriesMetadata(int Id, string Name, string? LocalizedName, string LibraryName, LibraryType LibraryType);

    public async Task AggregateYesterdaysActivity(CancellationToken ct = default)
    {
        var yesterdayUtc = DateTime.UtcNow.Date.AddDays(-1);
        var startUtc = yesterdayUtc;
        var endUtc = yesterdayUtc.AddDays(1).AddTicks(-1);

        var usersToProcess = await GetUsersPendingAggregation(startUtc, endUtc, yesterdayUtc, ct);

        foreach (var userId in usersToProcess)
        {
            await AggregateUserActivity(userId, startUtc, endUtc, yesterdayUtc, ct);
        }

        await context.SaveChangesAsync(ct);
    }

    private async Task<List<int>> GetUsersPendingAggregation(DateTime start, DateTime end, DateTime reportDate, CancellationToken ct = default)
    {
        var needAggregationUserIds = await context.AppUserReadingSession
            .Where(s => s.StartTime >= start && s.StartTime <= end)
            .Where(s => !s.IsActive && s.EndTime != null)
            .Select(s => s.AppUserId)
            .Distinct()
            .ToListAsync(ct);

        var alreadyHasHistoryUserIds = await context.AppUserReadingHistory
            .Where(h => h.DateUtc == reportDate)
            .Select(h => h.AppUserId)
            .ToListAsync(ct);

        return needAggregationUserIds.Except(alreadyHasHistoryUserIds).ToList();
    }

    private async Task AggregateUserActivity(int userId, DateTime start, DateTime end, DateTime reportDate, CancellationToken ct = default)
    {
        var sessions = await context.AppUserReadingSession
            .Include(s => s.ActivityData)
            .Where(s => s.AppUserId == userId &&
                        s.StartTime >= start && s.StartTime <= end &&
                        !s.IsActive && s.EndTime != null)
            .ToListAsync(ct);

        if (sessions.Count == 0) return;

        var chapterMeta = await GetChapterMetadata(sessions);
        var seriesMeta = await GetSeriesMetadata(sessions);

        var dailyData = CalculateDailyData(sessions, chapterMeta, seriesMeta);

        context.AppUserReadingHistory.Add(new AppUserReadingHistory
        {
            AppUserId = userId,
            DateUtc = reportDate,
            ClientInfoUsed = ExtractClientInfo(sessions),
            Data = dailyData
        });
    }

    private async Task<Dictionary<int, ChapterMetadata>> GetChapterMetadata(List<AppUserReadingSession> sessions)
    {
        var ids = sessions.SelectMany(s => s.ActivityData.Select(ad => ad.ChapterId)).Distinct().ToList();
        return await context.Chapter
            .Where(c => ids.Contains(c.Id))
            .Select(c => new ChapterMetadata(
                c.Id, c.Range, c.Volume.MinNumber, c.Volume.Series.Name,
                c.Volume.Series.LocalizedName, c.Volume.Series.Library.Name,
                c.Volume.Series.Library.Type))
            .ToDictionaryAsync(c => c.Id);
    }

    private async Task<Dictionary<int, SeriesMetadata>> GetSeriesMetadata(List<AppUserReadingSession> sessions)
    {
        var ids = sessions.SelectMany(s => s.ActivityData.Select(ad => ad.SeriesId)).Distinct().ToList();
        return await context.Series
            .Where(s => ids.Contains(s.Id))
            .Select(s => new SeriesMetadata(s.Id, s.Name, s.LocalizedName, s.Library.Name, s.Library.Type))
            .ToDictionaryAsync(s => s.Id);
    }

    private static DailyReadingDataDto CalculateDailyData(List<AppUserReadingSession> sessions,
        Dictionary<int, ChapterMetadata> chapterMeta, Dictionary<int, SeriesMetadata> seriesMeta)
    {
        var totalMinutes = 0;
        var totalPages = 0;
        var totalWords = 0;
        var longestSession = 0;
        var seriesIds = new HashSet<int>();
        var chapterIds = new HashSet<int>();
        var activities = new List<ReadingActivitySnapshotDto>();

        foreach (var session in sessions)
        {
            var duration = (int)(session.EndTime!.Value - session.StartTime).TotalMinutes;
            totalMinutes += duration;
            longestSession = Math.Max(longestSession, duration);

            foreach (var activity in session.ActivityData)
            {
                totalPages += activity.PagesRead;
                totalWords += activity.WordsRead;
                chapterIds.Add(activity.ChapterId);
                seriesIds.Add(activity.SeriesId);

                activities.Add(MapToSnapshot(activity, chapterMeta, seriesMeta));
            }
        }

        return new DailyReadingDataDto
        {
            TotalMinutesRead = totalMinutes,
            TotalPagesRead = totalPages,
            TotalWordsRead = totalWords,
            LongestSessionMinutes = longestSession,
            SeriesIds = seriesIds.Cast<int?>().ToList(),
            ChapterIds = chapterIds.Cast<int?>().ToList(),
            Activities = activities
        };
    }

    private static ReadingActivitySnapshotDto MapToSnapshot( AppUserReadingSessionActivityData activity,
        Dictionary<int, ChapterMetadata> chapterLookup, Dictionary<int, SeriesMetadata> seriesLookup)
    {
        var minutesRead = activity.EndTimeUtc.HasValue
            ? (int)(activity.EndTimeUtc.Value - activity.StartTimeUtc).TotalMinutes
            : 0;

        var snapshot = new ReadingActivitySnapshotDto
        {
            ChapterId = activity.ChapterId,
            VolumeId = activity.VolumeId,
            SeriesId = activity.SeriesId,
            LibraryId = activity.LibraryId,
            Format = activity.Format,
            PagesRead = activity.PagesRead,
            WordsRead = activity.WordsRead,
            MinutesRead = minutesRead,
            StartTimeUtc = activity.StartTimeUtc,
            EndTimeUtc = activity.EndTimeUtc ?? activity.StartTimeUtc,

            // Set defaults for required strings
            SeriesName = string.Empty,
            LibraryName = string.Empty,
            ChapterRange = string.Empty
        };

        if (chapterLookup.TryGetValue(activity.ChapterId, out var c))
        {
            snapshot.SeriesName = c.SeriesName;
            snapshot.LocalizedSeriesName = c.LocalizedSeriesName;
            snapshot.ChapterRange = c.Range ?? string.Empty;
            snapshot.VolumeNumber = c.VolumeNumber;
            snapshot.LibraryName = c.LibraryName;
            snapshot.LibraryType = c.LibraryType;
        }
        else if (seriesLookup.TryGetValue(activity.SeriesId, out var s))
        {
            snapshot.SeriesName = s.Name;
            snapshot.LocalizedSeriesName = s.LocalizedName;
            snapshot.LibraryName = s.LibraryName;
            snapshot.LibraryType = s.LibraryType;
            snapshot.ChapterRange = "[Deleted]";
        }
        else
        {
            snapshot.SeriesName = "[Deleted Data]";
            snapshot.ChapterRange = "[Deleted]";
        }

        return snapshot;
    }

    private static List<ClientInfoData> ExtractClientInfo(List<AppUserReadingSession> sessions)
    {
        return sessions
            .SelectMany(s => s.ActivityData)
            .Select(a => a.ClientInfo)
            .Where(c => c != null)
            .Select(c => c!)
            .DistinctBy(c => new { c.UserAgent, c.IpAddress, c.Platform })
            .ToList();
    }
}
