using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.Learning;
using Tiku.Application.QuestionBanks;
using Tiku.Application.Security;
using Tiku.Domain.Common;
using Tiku.Domain.Content;
using Tiku.Domain.Learning;
using Tiku.Domain.QuestionBanks;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Learning;

public sealed partial class LearningActivityService
{
    private async Task<PracticeAssembly> BuildPracticeAssemblyAsync(
        Guid tenantId,
        PracticeSessionCommand command,
        CancellationToken cancellationToken)
    {
        var mode = NormalizeMode(command.Mode);
        var assembly = new PracticeAssembly(
            mode,
            command.TargetType,
            command.TargetId,
            command.BlueprintId,
            command.CollectionId,
            command.EntryId,
            command.ContentNodeId,
            Math.Clamp(command.QuestionLimit ?? 100, 1, MaxLimit),
            command.DurationMinutes,
            command.TotalScore);

        if (!command.BlueprintId.HasValue) return assembly;

        var blueprint = await dbContext.PracticeBlueprints
            .AsNoTracking()
            .SingleOrDefaultAsync(
                item =>
                    item.TenantId == tenantId &&
                    item.Id == command.BlueprintId.Value &&
                    item.Status == ContentStatus.Active,
                cancellationToken);

        if (blueprint is null)
            throw new LearningResourceNotFoundException("practice_blueprint_not_found",
                "Practice blueprint was not found.");

        return assembly with
        {
            Mode = NormalizeMode(blueprint.Mode.ToString()),
            TargetType = command.TargetType ?? "blueprint",
            TargetId = command.TargetId ?? blueprint.Id,
            CollectionId = command.CollectionId ?? blueprint.CollectionId,
            EntryId = command.EntryId ?? blueprint.EntryId,
            ContentNodeId = command.ContentNodeId ?? blueprint.NodeId,
            QuestionLimit = Math.Clamp(command.QuestionLimit ?? blueprint.QuestionLimit ?? 100, 1, MaxLimit),
            DurationMinutes = command.DurationMinutes ?? blueprint.DurationMinutes,
            TotalScore = command.TotalScore ?? blueprint.TotalScore
        };
    }

    private async Task<List<Guid>> CollectQuestionReferenceIdsAsync(
        LearningActor actor,
        PracticeAssembly assembly,
        CancellationToken cancellationToken)
    {
        if (assembly.Mode == "wrong_review")
            return await dbContext.WrongQuestions
                .AsNoTracking()
                .Where(item =>
                    item.TenantId == actor.TenantId &&
                    item.UserId == actor.UserId &&
                    item.ResolvedAt == null)
                .OrderByDescending(item => item.WrongCount)
                .ThenBy(item => item.LastWrongAt)
                .Take(assembly.QuestionLimit)
                .Select(item => item.QuestionReferenceId)
                .ToListAsync(cancellationToken);

        if (assembly.Mode == "favorite_review")
            return await dbContext.FavoriteQuestions
                .AsNoTracking()
                .Where(item =>
                    item.TenantId == actor.TenantId &&
                    item.UserId == actor.UserId)
                .OrderByDescending(item => item.CreatedAt)
                .Take(assembly.QuestionLimit)
                .Select(item => item.QuestionReferenceId)
                .ToListAsync(cancellationToken);

        if (assembly.CollectionId.HasValue)
            return await dbContext.QuestionCollectionItems
                .AsNoTracking()
                .Where(item =>
                    item.TenantId == actor.TenantId &&
                    item.CollectionId == assembly.CollectionId.Value)
                .OrderBy(item => item.SortOrder)
                .Take(assembly.QuestionLimit)
                .Select(item => item.QuestionReferenceId)
                .ToListAsync(cancellationToken);

        var query = dbContext.Questions
            .AsNoTracking()
            .Where(question =>
                question.TenantId == actor.TenantId &&
                question.Status == QuestionStatus.Published);

        if (assembly.ContentNodeId.HasValue)
            query = query.Where(question => question.ContentNodeId == assembly.ContentNodeId.Value);
        else if (assembly.EntryId.HasValue)
            query = query.Where(question => question.EntryId == assembly.EntryId.Value);
        else if (assembly.TargetId.HasValue && !string.IsNullOrWhiteSpace(assembly.TargetType))
            query = ApplyLegacyTargetFilter(query, assembly.TargetType, assembly.TargetId.Value);
        else
            throw new LearningValidationException("practice_target_required", "Practice target is required.");

        var questionIds = await query
            .OrderBy(question => question.CreatedAt)
            .Take(assembly.QuestionLimit)
            .Select(question => question.Id)
            .ToListAsync(cancellationToken);
        var referenceIds = new List<Guid>(questionIds.Count);
        foreach (var questionId in questionIds)
        {
            var reference = await questionReferenceService.ResolveAsync(
                actor.TenantId,
                actor.UserId,
                new QuestionLocator(QuestionSource.Tenant, questionId),
                cancellationToken);
            referenceIds.Add(reference.Id);
        }

        return referenceIds;
    }

    private static IQueryable<Question> ApplyLegacyTargetFilter(
        IQueryable<Question> query,
        string? targetType,
        Guid targetId)
    {
        return NormalizeEnumValue(targetType) switch
        {
            "subject" => query.Where(question => question.SubjectId == targetId),
            "category" => query.Where(question => question.CategoryId == targetId),
            "node" => query.Where(question => question.NodeId == targetId),
            "questionbank" => query.Where(question => question.QuestionBankId == targetId),
            "contentnode" => query.Where(question => question.ContentNodeId == targetId),
            "entry" => query.Where(question => question.EntryId == targetId),
            _ => query.Where(_ => false)
        };
    }

    private async Task<IReadOnlyList<QuestionSelection>> LoadQuestionSelectionsAsync(
        Guid tenantId,
        IReadOnlyCollection<Guid> questionReferenceIds,
        CancellationToken cancellationToken)
    {
        var rows = await tenantExecutionScope.ExecuteAsync(
            new SystemScopeRequest(
                tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(LearningActivityService),
                "Lock published question versions for a new practice session", Guid.NewGuid().ToString("N")),
            async (provider, token) =>
            {
                var systemDbContext = provider.GetRequiredService<TikuDbContext>();
                return await (
                        from reference in systemDbContext.TenantQuestionReferences.AsNoTracking()
                        join question in systemDbContext.Questions.AsNoTracking()
                            on new { TenantId = reference.QuestionOwnerTenantId, Id = reference.QuestionId }
                            equals new { question.TenantId, question.Id }
                        join version in systemDbContext.QuestionVersions.AsNoTracking()
                            on new
                            {
                                TenantId = reference.QuestionOwnerTenantId,
                                reference.QuestionId,
                                Id = question.CurrentVersionId
                            }
                            equals new
                            {
                                version.TenantId,
                                version.QuestionId,
                                Id = (Guid?)version.Id
                            }
                        where reference.TenantId == tenantId &&
                              questionReferenceIds.Contains(reference.Id) &&
                              question.Status == QuestionStatus.Published
                        select new QuestionSelection(
                            reference.Id,
                            reference.QuestionOwnerTenantId,
                            reference.QuestionId,
                            version.Id,
                            question.Type,
                            question.TypeLabel,
                            question.Difficulty,
                            question.Tags,
                            version.Content,
                            version.Options,
                            version.CorrectOptionIndex,
                            version.CorrectOptionIndices,
                            version.AnswerText,
                            version.Explanation))
                    .ToArrayAsync(token);
            },
            cancellationToken);

        var byReference = rows.ToDictionary(row => row.QuestionReferenceId);
        if (byReference.Count != questionReferenceIds.Distinct().Count())
            throw new LearningValidationException(
                "practice_question_unavailable",
                "One or more practice questions have no published version.");

        return questionReferenceIds.Select(referenceId => byReference[referenceId]).ToArray();
    }

    private Task<PracticeSessionQuestionItem[]> LoadSessionQuestionItemsAsync(
        Guid tenantId,
        Guid practiceSessionId,
        CancellationToken cancellationToken)
    {
        return tenantExecutionScope.ExecuteAsync(
            new SystemScopeRequest(
                tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(LearningActivityService),
                "Read locked question versions for a tenant practice session", Guid.NewGuid().ToString("N")),
            async (provider, token) =>
            {
                var systemDbContext = provider.GetRequiredService<TikuDbContext>();
                return await (
                        from sessionQuestion in systemDbContext.PracticeSessionQuestions.AsNoTracking()
                        where sessionQuestion.TenantId == tenantId &&
                              sessionQuestion.PracticeSessionId == practiceSessionId
                        orderby sessionQuestion.Position
                        select new PracticeSessionQuestionItem(
                            sessionQuestion.Id,
                            sessionQuestion.QuestionReferenceId,
                            new QuestionLocator(
                                sessionQuestion.QuestionOwnerTenantId == tenantId
                                    ? QuestionSource.Tenant
                                    : QuestionSource.Platform,
                                sessionQuestion.QuestionId),
                            sessionQuestion.QuestionId,
                            sessionQuestion.QuestionType,
                            sessionQuestion.TypeLabelSnapshot,
                            sessionQuestion.DifficultySnapshot,
                            sessionQuestion.TagsSnapshot,
                            sessionQuestion.QuestionVersionId,
                            sessionQuestion.ContentSnapshot,
                            sessionQuestion.OptionsSnapshot))
                    .ToArrayAsync(token);
            },
            cancellationToken);
    }

    private async Task<PracticeSession> GetPracticeSessionAsync(
        LearningActor actor,
        Guid? practiceSessionId,
        CancellationToken cancellationToken)
    {
        if (!practiceSessionId.HasValue)
            throw new LearningValidationException("practice_session_id_required", "Practice session id is required.");

        var session = await dbContext.PracticeSessions
            .SingleOrDefaultAsync(
                item =>
                    item.TenantId == actor.TenantId &&
                    item.UserId == actor.UserId &&
                    item.Id == practiceSessionId.Value,
                cancellationToken);

        if (session is null)
            throw new LearningResourceNotFoundException("practice_session_not_found",
                "Practice session was not found.");

        return session;
    }

    private async Task<PracticeSessionReport> BuildPracticeSessionReportAsync(
        LearningActor actor,
        PracticeSession session,
        CancellationToken cancellationToken)
    {
        var sessionQuestions = await dbContext.PracticeSessionQuestions.AsNoTracking()
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.PracticeSessionId == session.Id)
            .OrderBy(item => item.Position)
            .ToArrayAsync(cancellationToken);
        if (sessionQuestions.Length == 0)
            throw new LearningValidationException("practice_session_empty",
                "Practice session has no question snapshot.");

        var answers = await dbContext.AnswerRecords
            .AsNoTracking()
            .Where(answer =>
                answer.TenantId == actor.TenantId &&
                answer.UserId == actor.UserId &&
                answer.PracticeSessionId == session.Id &&
                answer.IsCurrent)
            .ToArrayAsync(cancellationToken);
        var latestAnswers = answers.ToDictionary(answer => answer.SessionQuestionId);
        var totalQuestions = sessionQuestions.Length;
        var answeredCount = sessionQuestions.Count(question => latestAnswers.ContainsKey(question.Id));
        var correctCount = sessionQuestions.Count(question =>
            latestAnswers.TryGetValue(question.Id, out var answer) &&
            answer.GradingStatus == AnswerGradingStatus.Correct);
        var wrongCount = sessionQuestions.Count(question =>
            latestAnswers.TryGetValue(question.Id, out var answer) &&
            answer.GradingStatus == AnswerGradingStatus.Incorrect);
        var pendingReviewCount = sessionQuestions.Count(question =>
            latestAnswers.TryGetValue(question.Id, out var answer) &&
            answer.GradingStatus == AnswerGradingStatus.PendingReview);
        var unansweredCount = Math.Max(0, totalQuestions - answeredCount);
        var totalScore = sessionQuestions.Sum(question => question.Score ?? 1);
        var score = Math.Round(answers.Sum(answer => answer.AwardedScore ?? 0), 2);
        var objectivelyGradedCount = correctCount + wrongCount;
        var accuracy = objectivelyGradedCount == 0
            ? 0
            : Math.Round((decimal)correctCount / objectivelyGradedCount, 4);
        var isFinal = pendingReviewCount == 0;
        var submittedAt = DateTimeOffset.UtcNow;
        var durationSeconds = Math.Max(0, (int)(submittedAt - session.StartedAt).TotalSeconds);
        var wrongQuestionIds = sessionQuestions
            .Where(question =>
                latestAnswers.TryGetValue(question.Id, out var answer) &&
                answer.GradingStatus == AnswerGradingStatus.Incorrect)
            .Select(question => question.QuestionReferenceId)
            .ToArray();
        var questionResults = sessionQuestions
            .Select(question =>
            {
                latestAnswers.TryGetValue(question.Id, out var answer);
                return new
                {
                    sessionQuestionId = question.Id,
                    questionReferenceId = question.QuestionReferenceId,
                    questionId = question.QuestionId,
                    source = question.QuestionOwnerTenantId == actor.TenantId ? "tenant" : "platform",
                    answered = answer is not null,
                    gradingStatus = answer?.GradingStatus.ToString(),
                    isCorrect = isFinal ? answer?.IsCorrect : null,
                    score = answer?.AwardedScore,
                    totalScore = question.Score ?? 1,
                    answeredAt = answer?.AnsweredAt,
                    correctOptionIndex = isFinal ? question.CorrectOptionIndexSnapshot : null,
                    correctOptionIndices = isFinal ? question.CorrectOptionIndicesSnapshot : JsonDefaults.Array(),
                    answerText = isFinal ? question.AnswerTextSnapshot : null,
                    explanation = isFinal ? question.ExplanationSnapshot : null
                };
            })
            .ToArray();
        var sectionStats = new[]
        {
            new
            {
                key = "default",
                title = "默认",
                questionCount = totalQuestions,
                answeredCount,
                correctCount,
                wrongCount,
                unansweredCount,
                score,
                totalScore,
                accuracy,
                pendingReviewCount,
                sortOrder = 0
            }
        };

        var report = new PracticeSessionReport
        {
            TenantId = actor.TenantId,
            UserId = actor.UserId,
            PracticeSessionId = session.Id,
            BlueprintId = session.BlueprintId,
            CollectionId = session.CollectionId,
            Mode = session.Mode,
            TotalQuestions = totalQuestions,
            AnsweredCount = answeredCount,
            CorrectCount = correctCount,
            WrongCount = wrongCount,
            UnansweredCount = unansweredCount,
            Score = score,
            TotalScore = totalScore,
            Accuracy = accuracy,
            DurationSeconds = durationSeconds,
            StartedAt = session.StartedAt,
            SubmittedAt = submittedAt,
            Status = isFinal ? PracticeReportStatus.Final : PracticeReportStatus.PendingReview,
            Version = 1,
            IsFinal = isFinal,
            PendingReviewCount = pendingReviewCount,
            ScoringVersion = 1,
            SectionStats = JsonSerializer.SerializeToElement(sectionStats),
            QuestionResults = JsonSerializer.SerializeToElement(questionResults),
            WrongQuestionIds = JsonSerializer.SerializeToElement(wrongQuestionIds),
            Metadata = JsonSerializer.SerializeToElement(new
            {
                scoringVersion = 1
            })
        };
        dbContext.PracticeSessionReports.Add(report);
        dbContext.PracticeSessionReportSections.Add(new PracticeSessionReportSection
        {
            TenantId = actor.TenantId,
            ReportId = report.Id,
            PracticeSessionId = session.Id,
            SectionKey = "default",
            SectionName = "默认",
            QuestionCount = totalQuestions,
            AnsweredCount = answeredCount,
            CorrectCount = correctCount,
            WrongCount = wrongCount,
            UnansweredCount = unansweredCount,
            Score = score,
            TotalScore = totalScore,
            Accuracy = accuracy,
            SortOrder = 0
        });

        foreach (var question in sessionQuestions.Where(question =>
                     latestAnswers.TryGetValue(question.Id, out var answer) &&
                     answer.GradingStatus == AnswerGradingStatus.Incorrect))
        {
            var wrongQuestion = await dbContext.WrongQuestions.FindAsync(
                [actor.TenantId, actor.UserId, question.QuestionReferenceId], cancellationToken);
            if (wrongQuestion is null)
            {
                dbContext.WrongQuestions.Add(new WrongQuestion
                {
                    TenantId = actor.TenantId,
                    UserId = actor.UserId,
                    QuestionReferenceId = question.QuestionReferenceId,
                    QuestionOwnerTenantId = question.QuestionOwnerTenantId,
                    QuestionId = question.QuestionId,
                    WrongCount = 1,
                    LastWrongAt = submittedAt
                });
            }
            else
            {
                wrongQuestion.WrongCount++;
                wrongQuestion.LastWrongAt = submittedAt;
                wrongQuestion.ResolvedAt = null;
            }
        }

        return report;
    }

    private async Task EnsureQuestionExistsAsync(
        Guid tenantId,
        Guid questionId,
        CancellationToken cancellationToken)
    {
        var exists = await dbContext.Questions.AnyAsync(
            question =>
                question.TenantId == tenantId &&
                question.Id == questionId &&
                question.Status == QuestionStatus.Published,
            cancellationToken);

        if (!exists) throw new LearningResourceNotFoundException("question_not_found", "Question was not found.");
    }

    private async Task EnsureWordExistsAsync(
        Guid tenantId,
        Guid wordId,
        CancellationToken cancellationToken)
    {
        var exists = await dbContext.VocabularyWords.AnyAsync(
            word =>
                word.TenantId == tenantId &&
                word.Id == wordId &&
                word.IsActive,
            cancellationToken);

        if (!exists) throw new LearningResourceNotFoundException("word_not_found", "Word was not found.");
    }

    private static AnswerRecordItem ToItem(AnswerRecord record, long sessionVersion)
    {
        return new AnswerRecordItem(
            record.Id,
            record.SessionQuestionId,
            record.PracticeSessionId,
            record.SelectedOptions,
            record.AnswerText,
            record.GradingStatus == AnswerGradingStatus.PendingReview ? "pending_review" : "accepted",
            record.Revision,
            record.ClientSequence,
            sessionVersion,
            record.AnsweredAt);
    }

    private static WordProgressItem ToItem(UserWordProgress item)
    {
        return new WordProgressItem(
            item.WordId,
            item.Status,
            item.CorrectCount,
            item.WrongCount,
            item.LastReviewAt,
            item.NextReviewAt,
            item.ReviewCount,
            item.CorrectStreak,
            item.LastResult,
            item.DueLevel,
            item.Metadata);
    }

    private static PracticeSessionItem ToItem(PracticeSession item)
    {
        return new PracticeSessionItem(
            item.Id,
            item.Mode,
            item.TargetType,
            item.TargetId,
            item.BlueprintId,
            item.CollectionId,
            item.EntryId,
            item.ContentNodeId,
            item.QuestionCount,
            item.DurationMinutes,
            item.TotalScore,
            item.AccessMode,
            item.AccessEntitlementId,
            item.ConsumedFreeQuota,
            item.AccessSnapshot,
            item.StartedAt,
            item.FinishedAt,
            item.ExpiresAt,
            item.Metadata,
            item.Status,
            item.Version,
            item.LastClientSequence);
    }

    private static PracticeSessionReportItem ToItem(PracticeSessionReport item)
    {
        return new PracticeSessionReportItem(
            item.Id,
            item.PracticeSessionId,
            item.BlueprintId,
            item.CollectionId,
            item.Mode,
            item.TotalQuestions,
            item.AnsweredCount,
            item.CorrectCount,
            item.WrongCount,
            item.UnansweredCount,
            item.Score,
            item.TotalScore,
            item.Accuracy,
            item.DurationSeconds,
            item.StartedAt,
            item.SubmittedAt,
            item.Status,
            item.Version,
            item.IsFinal,
            item.PendingReviewCount,
            item.ScoringVersion,
            item.SectionStats,
            item.QuestionResults,
            item.WrongQuestionIds,
            item.Metadata);
    }

    private static List<Guid> ReadGuidArray(JsonElement value)
    {
        if (value.ValueKind is not JsonValueKind.Array) return [];

        return value.EnumerateArray()
            .Select(item => item.ValueKind == JsonValueKind.String && Guid.TryParse(item.GetString(), out var id)
                ? (Guid?)id
                : null)
            .Where(id => id.HasValue)
            .Select(id => id!.Value)
            .ToList();
    }

    private static void EnsureAnswerSessionState(
        PracticeSession session,
        SubmitAnswerCommand command)
    {
        if (session.Status != PracticeSessionStatus.Active)
            throw new LearningValidationException("practice_session_not_active",
                "Only an active practice session accepts answers.");
        if (session.Version != command.ExpectedSessionVersion)
            throw new LearningValidationException("practice_session_version_conflict",
                "The practice session changed. Reload it before answering.");
        if (command.ClientSequence <= session.LastClientSequence)
            throw new LearningValidationException("practice_client_sequence_conflict",
                "Client sequence must increase within a practice session.");
    }

    private static JsonElement BuildGradingRules(QuestionSelection selection)
    {
        return JsonSerializer.SerializeToElement(new
        {
            version = 1,
            normalization = selection.QuestionType.Equals("fill_blank", StringComparison.OrdinalIgnoreCase)
                ? "nfkc_trim_casefold_whitespace"
                : "exact"
        });
    }

    private static string HashAnswer(SubmitAnswerCommand command)
    {
        return Hash(JsonSerializer.Serialize(new
        {
            command.SessionQuestionId,
            command.ExpectedSessionVersion,
            command.ClientSequence,
            selectedOptionIndices = command.SelectedOptionIndices?.Distinct().Order().ToArray() ?? [],
            answerText = command.AnswerText?.Trim()
        }));
    }

    private static string HashSubmission(SubmitPracticeSessionCommand command)
    {
        return Hash(JsonSerializer.Serialize(new
        {
            command.PracticeSessionId,
            command.ExpectedSessionVersion
        }));
    }

    private static string Hash(string value)
    {
        return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant();
    }

    private static string ResolvePracticeSessionHistoryStatus(PracticeSession session, DateTimeOffset now)
    {
        if (session.Status is PracticeSessionStatus.Submitted or PracticeSessionStatus.PendingReview) return "finished";

        if (session.Status == PracticeSessionStatus.Expired ||
            (session.ExpiresAt.HasValue && session.ExpiresAt.Value <= now))
            return "expired";

        return "active";
    }

    private static string NormalizeMode(string? mode)
    {
        return NormalizeEnumValue(mode) switch
        {
            "sequential" => "sequential",
            "random" => "random",
            "mockexam" => "mock_exam",
            "paper" => "paper",
            "wrongreview" => "wrong_review",
            "favoritereview" => "favorite_review",
            _ => "chapter"
        };
    }

    private static bool TryParseWordProgressStatus(string? value, out WordProgressStatus status)
    {
        return Enum.TryParse(NormalizeEnumValue(value), true, out status);
    }

    private static int ResolveLimit(int? limit)
    {
        return Math.Clamp(limit ?? DefaultLimit, 1, MaxLimit);
    }

    private static string? NormalizeEnumValue(string? value)
    {
        return string.IsNullOrWhiteSpace(value)
            ? null
            : value.Replace("_", string.Empty, StringComparison.Ordinal)
                .Replace("-", string.Empty, StringComparison.Ordinal);
    }

    private sealed record PracticeAssembly(
        string Mode,
        string? TargetType,
        Guid? TargetId,
        Guid? BlueprintId,
        Guid? CollectionId,
        Guid? EntryId,
        Guid? ContentNodeId,
        int QuestionLimit,
        int? DurationMinutes,
        decimal? TotalScore);

    private sealed record QuestionSelection(
        Guid QuestionReferenceId,
        Guid QuestionOwnerTenantId,
        Guid QuestionId,
        Guid QuestionVersionId,
        string QuestionType,
        string? TypeLabel,
        int? Difficulty,
        JsonElement Tags,
        string? Content,
        JsonElement Options,
        int? CorrectOptionIndex,
        JsonElement CorrectOptionIndices,
        string? AnswerText,
        string? Explanation);
}