using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Learning;
using Tiku.Domain.Content;

namespace Tiku.Infrastructure.Learning;

public sealed partial class LearningActivityService
{
    public async Task<LearningList<WordProgressItem>> GetWordProgressAsync(
        LearningActor actor,
        LearningLimitFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.UserWordProgress
            .AsNoTracking()
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.UserId == actor.UserId);

        if (filter.UnitId.HasValue)
            query = query.Where(item => dbContext.VocabularyWords.Any(word =>
                word.TenantId == actor.TenantId &&
                word.Id == item.WordId &&
                word.UnitId == filter.UnitId.Value));

        if (TryParseWordProgressStatus(filter.Status, out var status))
            query = query.Where(item => item.Status == status);

        var items = await query
            .OrderBy(item => item.NextReviewAt == null)
            .ThenBy(item => item.NextReviewAt)
            .ThenByDescending(item => item.UpdatedAt)
            .Take(ResolveLimit(filter.Limit))
            .Select(item => new WordProgressItem(
                item.WordId,
                item.Status,
                item.CorrectCount,
                item.WrongCount,
                item.LastReviewAt,
                item.NextReviewAt,
                item.ReviewCount,
                item.CorrectStreak,
                item.LastResult,
                item.DueLevel,
                item.Metadata))
            .ToArrayAsync(cancellationToken);

        return new LearningList<WordProgressItem>(items);
    }

    public async Task<WordProgressItem> UpdateWordProgressAsync(
        LearningActor actor,
        WordProgressCommand command,
        CancellationToken cancellationToken = default)
    {
        await EnsureWordExistsAsync(actor.TenantId, command.WordId, cancellationToken);

        var now = DateTimeOffset.UtcNow;
        var item = await dbContext.UserWordProgress
            .SingleOrDefaultAsync(
                progress =>
                    progress.TenantId == actor.TenantId &&
                    progress.UserId == actor.UserId &&
                    progress.WordId == command.WordId,
                cancellationToken);

        if (item is null)
        {
            item = new UserWordProgress
            {
                TenantId = actor.TenantId,
                UserId = actor.UserId,
                WordId = command.WordId
            };
            dbContext.UserWordProgress.Add(item);
        }

        if (TryParseWordProgressStatus(command.Status, out var status))
            item.Status = status;
        else if (string.IsNullOrWhiteSpace(command.Status))
            item.Status = WordProgressStatus.Learning;
        else
            throw new LearningValidationException("invalid_word_status", "Word progress status is invalid.");

        var correctDelta = command.CorrectDelta ?? 0;
        var wrongDelta = command.WrongDelta ?? 0;
        item.CorrectCount += correctDelta;
        item.WrongCount += wrongDelta;
        item.ReviewCount += correctDelta + wrongDelta;
        item.CorrectStreak = wrongDelta > 0
            ? 0
            : item.CorrectStreak + correctDelta;
        item.LastResult = wrongDelta > 0
            ? WordReviewResult.Wrong
            : correctDelta > 0
                ? WordReviewResult.Correct
                : item.LastResult;
        item.LastReviewAt = correctDelta + wrongDelta > 0 ? now : item.LastReviewAt;
        item.NextReviewAt = command.NextReviewAt ?? item.NextReviewAt;
        item.DueLevel = item.Status == WordProgressStatus.Mastered
            ? WordDueLevel.Mastered
            : item.WrongCount > 0 && item.CorrectStreak == 0
                ? WordDueLevel.Again
                : item.DueLevel;

        await dbContext.SaveChangesAsync(cancellationToken);
        return ToItem(item);
    }

    public async Task<WordReviewPlan> GetWordReviewPlanAsync(
        LearningActor actor,
        LearningLimitFilter filter,
        CancellationToken cancellationToken = default)
    {
        var now = DateTimeOffset.UtcNow;
        var query = dbContext.UserWordProgress.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId);
        if (filter.UnitId.HasValue)
            query = query.Where(item => dbContext.VocabularyWords.Any(word =>
                word.TenantId == actor.TenantId &&
                word.Id == item.WordId &&
                word.UnitId == filter.UnitId.Value));

        var items = await query
            .Where(item => item.NextReviewAt == null || item.NextReviewAt <= now)
            .OrderBy(item => item.NextReviewAt == null)
            .ThenBy(item => item.NextReviewAt)
            .ThenByDescending(item => item.WrongCount)
            .Take(ResolveLimit(filter.Limit))
            .Select(item => new WordReviewPlanItem(
                item.WordId,
                item.Status,
                item.NextReviewAt,
                item.CorrectCount,
                item.WrongCount,
                item.DueLevel))
            .ToArrayAsync(cancellationToken);
        return new WordReviewPlan(
            items,
            JsonSerializer.SerializeToElement(new
            {
                mode = "word_review",
                wordCount = items.Length
            }));
    }

    public Task<WordProgressItem> ReviewWordAsync(
        LearningActor actor,
        WordReviewCommand command,
        CancellationToken cancellationToken = default)
    {
        var correct = string.Equals(command.Result, "correct", StringComparison.OrdinalIgnoreCase) ||
                      string.Equals(command.Result, "known", StringComparison.OrdinalIgnoreCase);
        return UpdateWordProgressAsync(
            actor,
            new WordProgressCommand(
                command.WordId,
                correct ? "Reviewing" : "Learning",
                correct ? 1 : 0,
                correct ? 0 : 1,
                command.NextReviewAt ?? DateTimeOffset.UtcNow.AddDays(correct ? 2 : 1)),
            cancellationToken);
    }

    public async Task<WordStatsItem> GetWordStatsAsync(
        LearningActor actor,
        LearningLimitFilter filter,
        CancellationToken cancellationToken = default)
    {
        var now = DateTimeOffset.UtcNow;
        var query = dbContext.UserWordProgress.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId);
        if (filter.UnitId.HasValue)
            query = query.Where(item => dbContext.VocabularyWords.Any(word =>
                word.TenantId == actor.TenantId &&
                word.Id == item.WordId &&
                word.UnitId == filter.UnitId.Value));

        return new WordStatsItem(
            await query.CountAsync(cancellationToken),
            await query.CountAsync(item => item.Status == WordProgressStatus.New, cancellationToken),
            await query.CountAsync(item => item.Status == WordProgressStatus.Learning, cancellationToken),
            await query.CountAsync(item => item.Status == WordProgressStatus.Reviewing, cancellationToken),
            await query.CountAsync(item => item.Status == WordProgressStatus.Mastered, cancellationToken),
            await query.CountAsync(item => item.NextReviewAt == null || item.NextReviewAt <= now, cancellationToken),
            await dbContext.UserWordFavorites.AsNoTracking().CountAsync(
                item => item.TenantId == actor.TenantId && item.UserId == actor.UserId,
                cancellationToken));
    }

    public async Task<LearningList<FavoriteWordItem>> GetFavoriteWordsAsync(
        LearningActor actor,
        LearningLimitFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.UserWordFavorites
            .AsNoTracking()
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.UserId == actor.UserId);

        if (filter.UnitId.HasValue)
            query = query.Where(item => dbContext.VocabularyWords.Any(word =>
                word.TenantId == actor.TenantId &&
                word.Id == item.WordId &&
                word.UnitId == filter.UnitId.Value));

        var items = await query
            .OrderByDescending(item => item.FavoritedAt ?? item.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .Select(item => new FavoriteWordItem(
                item.WordId,
                item.Note,
                item.FavoritedAt))
            .ToArrayAsync(cancellationToken);

        return new LearningList<FavoriteWordItem>(items);
    }

    public async Task<LearningActionResult> ToggleFavoriteWordAsync(
        LearningActor actor,
        FavoriteWordCommand command,
        CancellationToken cancellationToken = default)
    {
        await EnsureWordExistsAsync(actor.TenantId, command.WordId, cancellationToken);

        var favorite = command.Favorite ?? true;
        var item = await dbContext.UserWordFavorites
            .SingleOrDefaultAsync(
                favoriteWord =>
                    favoriteWord.TenantId == actor.TenantId &&
                    favoriteWord.UserId == actor.UserId &&
                    favoriteWord.WordId == command.WordId,
                cancellationToken);

        if (favorite)
        {
            if (item is null)
            {
                item = new UserWordFavorite
                {
                    TenantId = actor.TenantId,
                    UserId = actor.UserId,
                    WordId = command.WordId
                };
                dbContext.UserWordFavorites.Add(item);
            }

            item.Note = command.Note ?? item.Note;
            item.FavoritedAt ??= DateTimeOffset.UtcNow;
        }
        else if (item is not null)
        {
            dbContext.UserWordFavorites.Remove(item);
        }

        await dbContext.SaveChangesAsync(cancellationToken);
        return new LearningActionResult(true, favorite);
    }
}