using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Content;
using Tiku.Application.Learning;
using Tiku.Application.QuestionBanks;
using Tiku.Domain.Catalog;
using Tiku.Domain.Common;
using Tiku.Domain.Content;
using Tiku.Domain.Learning;
using Tiku.Domain.Operations;
using Tiku.Domain.QuestionBanks;

namespace Tiku.Infrastructure.Content;

internal abstract partial class DirectContentServiceBase
{
    protected async Task<Banner> UpsertBannerAsync(DirectContentActor actor, OperationContentCommand command,
        CancellationToken cancellationToken)
    {
        await AssertReferenceAsync<Region>(actor.TenantId, command.RegionId, "region_not_found", cancellationToken);
        var item = await ResolveByIdOrLegacyAsync(jobsOperationsPersistence.Banners, actor.TenantId, command.Id, command.LegacyId,
            cancellationToken);
        var isNew = item is null;
        item ??= new Banner { Id = command.Id ?? Guid.NewGuid(), TenantId = actor.TenantId };
        item.RegionId = command.RegionId;
        item.LegacyId = Normalize(command.LegacyId);
        item.Title = Normalize(command.Title);
        item.Subtitle = Normalize(command.Subtitle);
        item.Content = Normalize(command.Content);
        item.ButtonText = Normalize(command.ButtonText);
        item.ButtonLink = Normalize(command.ButtonLink);
        item.BackgroundColor = Normalize(command.BackgroundColor);
        item.BorderColor = Normalize(command.BorderColor);
        item.SortOrder = command.Order ?? item.SortOrder;
        item.IsActive = command.IsActive ?? item.IsActive;
        if (isNew) jobsOperationsPersistence.Banners.Add(item);

        await unitOfWork.SaveChangesAsync(cancellationToken);
        return item;
    }

    protected async Task<Faq> UpsertFaqAsync(DirectContentActor actor, OperationContentCommand command,
        CancellationToken cancellationToken)
    {
        await AssertReferenceAsync<Region>(actor.TenantId, command.RegionId, "region_not_found", cancellationToken);
        var item = await ResolveByIdOrLegacyAsync(jobsOperationsPersistence.Faqs, actor.TenantId, command.Id, command.LegacyId,
            cancellationToken);
        var isNew = item is null;
        item ??= new Faq { Id = command.Id ?? Guid.NewGuid(), TenantId = actor.TenantId };
        item.RegionId = command.RegionId;
        item.LegacyId = Normalize(command.LegacyId);
        item.Question = Normalize(command.Question) ?? Normalize(command.Title);
        item.Answer = Normalize(command.Answer) ?? Normalize(command.Content);
        item.SortOrder = command.Order ?? item.SortOrder;
        item.IsActive = command.IsActive ?? item.IsActive;
        if (isNew) jobsOperationsPersistence.Faqs.Add(item);

        await unitOfWork.SaveChangesAsync(cancellationToken);
        return item;
    }

    protected async Task<Announcement> UpsertAnnouncementAsync(DirectContentActor actor, OperationContentCommand command,
        CancellationToken cancellationToken)
    {
        var item = await ResolveByIdOrLegacyAsync(jobsOperationsPersistence.Announcements, actor.TenantId, command.Id, command.LegacyId,
            cancellationToken);
        var isNew = item is null;
        item ??= new Announcement { Id = command.Id ?? Guid.NewGuid(), TenantId = actor.TenantId };
        item.LegacyId = Normalize(command.LegacyId);
        item.Content = Normalize(command.Content) ?? Normalize(command.Title);
        item.Link = Normalize(command.Link);
        item.BackgroundColor = Normalize(command.BackgroundColor);
        item.SortOrder = command.Order ?? item.SortOrder;
        item.IsActive = command.IsActive ?? item.IsActive;
        if (isNew) jobsOperationsPersistence.Announcements.Add(item);

        await unitOfWork.SaveChangesAsync(cancellationToken);
        return item;
    }

    protected async Task<ExamDate> UpsertExamDateAsync(DirectContentActor actor, OperationContentCommand command,
        CancellationToken cancellationToken)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(command.ExamName);
        await AssertReferenceAsync<Region>(actor.TenantId, command.RegionId, "region_not_found", cancellationToken);
        await AssertReferenceAsync<School>(actor.TenantId, command.SchoolId, "school_not_found", cancellationToken);
        var item = await ResolveByIdOrLegacyAsync(learningPersistence.ExamDates, actor.TenantId, command.Id, command.LegacyId,
            cancellationToken);
        var isNew = item is null;
        item ??= new ExamDate { Id = command.Id ?? Guid.NewGuid(), TenantId = actor.TenantId };
        item.RegionId = command.RegionId;
        item.SchoolId = command.SchoolId;
        item.LegacyId = Normalize(command.LegacyId);
        item.ExamName = command.ExamName.Trim();
        item.ExamAt = command.ExamAt;
        item.ExamType = Normalize(command.ExamType);
        item.Description = Normalize(command.Description) ?? Normalize(command.Content);
        item.SortOrder = command.Order ?? item.SortOrder;
        item.IsActive = command.IsActive ?? item.IsActive;
        item.Metadata = JsonObjectOrDefault(command.Metadata);
        if (isNew) learningPersistence.ExamDates.Add(item);

        await unitOfWork.SaveChangesAsync(cancellationToken);
        return item;
    }

    protected static void ApplyQuestion(Question question, QuestionWriteCommand command)
    {
        question.QuestionBankId = command.QuestionBankId;
        question.SubjectId = command.SubjectId;
        question.CategoryId = command.CategoryId;
        question.NodeId = command.NodeId;
        question.EntryId = command.EntryId;
        question.ContentNodeId = command.ContentNodeId;
        question.PrimaryCollectionId = command.PrimaryCollectionId;
        question.LegacyId = Normalize(command.LegacyId);
        question.Type = Normalize(command.Type) ?? question.Type;
        question.TypeLabel = Normalize(command.TypeLabel);
        question.Difficulty = command.Difficulty;
        question.Tags = JsonArrayOrDefault(command.Tags);
        question.ExamMarkers = JsonObjectOrDefault(command.ExamMarkers);
        question.MediaUrl = Normalize(command.MediaUrl);
        question.Status = Parse(command.Status, QuestionStatus.Published, "question_status_invalid");
    }

    protected static void ValidateQuestionForPublication(QuestionWriteCommand command)
    {
        var status = Parse(command.Status, QuestionStatus.Published, "question_status_invalid");
        if (status != QuestionStatus.Published) return;

        var type = Normalize(command.Type) ?? "choice";
        if (!QuestionGrader.HasValidAuthoritativeAnswer(
                type,
                command.CorrectOptionIndex,
                command.CorrectOptionIndices,
                command.AnswerText))
            throw new ContentManagementException(
                "Published questions require a valid authoritative answer.",
                "question_grading_rule_invalid");
    }

    protected static QuestionVersion BuildQuestionVersion(
        DirectContentActor actor,
        Guid questionId,
        int versionNo,
        QuestionWriteCommand command)
    {
        var version = new QuestionVersion
        {
            TenantId = actor.TenantId,
            QuestionId = questionId,
            VersionNo = versionNo,
            CreatedBy = actor.UserId
        };
        ApplyQuestionVersion(version, command);
        return version;
    }

    protected static void ApplyQuestionVersion(QuestionVersion version, QuestionWriteCommand command)
    {
        version.Content = Normalize(command.Content);
        version.Options = JsonArrayOrDefault(command.Options);
        version.CorrectOptionIndex = command.CorrectOptionIndex;
        version.CorrectOptionIndices = JsonArrayOrDefault(command.CorrectOptionIndices);
        version.AnswerText = Normalize(command.AnswerText);
        version.Explanation = Normalize(command.Explanation);
        version.SubQuestions = JsonArrayOrDefault(command.SubQuestions);
        version.CodeLang = Normalize(command.CodeLang);
        version.CodeTemplate = Normalize(command.CodeTemplate);
        version.SourceHash = Normalize(command.SourceHash);
    }

    protected async Task AssertQuestionReferencesAsync(Guid tenantId, QuestionWriteCommand command,
        CancellationToken cancellationToken)
    {
        await AssertReferenceAsync<QuestionBank>(tenantId, command.QuestionBankId, "question_bank_not_found",
            cancellationToken);
        await AssertReferenceAsync<Subject>(tenantId, command.SubjectId, "subject_not_found", cancellationToken);
        await AssertReferenceAsync<Category>(tenantId, command.CategoryId, "category_not_found", cancellationToken);
        await AssertReferenceAsync<ModuleNode>(tenantId, command.NodeId, "module_node_not_found", cancellationToken);
        await AssertReferenceAsync<ContentEntry>(tenantId, command.EntryId, "entry_not_found", cancellationToken);
        await AssertReferenceAsync<ContentNode>(tenantId, command.ContentNodeId, "node_not_found", cancellationToken);
        await AssertReferenceAsync<QuestionCollection>(tenantId, command.PrimaryCollectionId, "collection_not_found",
            cancellationToken);
    }

    protected async Task SyncPrimaryCollectionItemAsync(
        DirectContentActor actor,
        Question question,
        CancellationToken cancellationToken)
    {
        if (!question.PrimaryCollectionId.HasValue) return;

        var existing = await questionBankPersistence.QuestionCollectionItems.SingleOrDefaultAsync(
            item =>
                item.TenantId == actor.TenantId &&
                item.CollectionId == question.PrimaryCollectionId.Value &&
                item.QuestionId == question.Id,
            cancellationToken);
        if (existing is null)
        {
            var reference = await questionReferenceService.ResolveAsync(
                actor.TenantId,
                actor.UserId,
                new QuestionLocator(QuestionSource.Tenant, question.Id),
                cancellationToken);
            var nextOrder = await questionBankPersistence.QuestionCollectionItems
                .Where(item =>
                    item.TenantId == actor.TenantId && item.CollectionId == question.PrimaryCollectionId.Value)
                .Select(item => (int?)item.SortOrder)
                .MaxAsync(cancellationToken) ?? -1;
            questionBankPersistence.QuestionCollectionItems.Add(new QuestionCollectionItem
            {
                TenantId = actor.TenantId,
                CollectionId = question.PrimaryCollectionId.Value,
                QuestionReferenceId = reference.Id,
                QuestionOwnerTenantId = reference.QuestionOwnerTenantId,
                QuestionId = question.Id,
                SortOrder = nextOrder + 1
            });
        }

        var collection = await questionBankPersistence.QuestionCollections.SingleAsync(
            item => item.TenantId == actor.TenantId && item.Id == question.PrimaryCollectionId.Value,
            cancellationToken);
        collection.QuestionCount = await questionBankPersistence.QuestionCollectionItems.CountAsync(
            item => item.TenantId == actor.TenantId && item.CollectionId == question.PrimaryCollectionId.Value,
            cancellationToken) + (existing is null ? 1 : 0);
        collection.UpdatedBy = actor.UserId;
    }

    protected async Task<(Guid? EntryId, Guid? ContentNodeId)> ResolveVocabularyNavigationAsync(
        Guid tenantId,
        Guid? unitId,
        Guid? entryId,
        Guid? contentNodeId,
        CancellationToken cancellationToken)
    {
        if (!unitId.HasValue) return (entryId, contentNodeId);

        var unit = await contentAssetPersistence.VocabularyUnits.AsNoTracking().SingleOrDefaultAsync(
            item => item.TenantId == tenantId && item.Id == unitId.Value,
            cancellationToken);
        if (unit is null)
            throw new ContentManagementException("Vocabulary unit was not found.", "vocabulary_unit_not_found");

        return (entryId ?? unit.EntryId, contentNodeId ?? unit.ContentNodeId);
    }

    protected async Task<(Guid? EntryId, Guid? ContentNodeId)> ResolveHandbookSubjectNavigationAsync(
        Guid tenantId,
        Guid? subjectId,
        Guid? entryId,
        Guid? contentNodeId,
        CancellationToken cancellationToken)
    {
        if (!subjectId.HasValue) return (entryId, contentNodeId);

        var subject = await contentAssetPersistence.HandbookSubjects.AsNoTracking().SingleOrDefaultAsync(
            item => item.TenantId == tenantId && item.Id == subjectId.Value,
            cancellationToken);
        if (subject is null)
            throw new ContentManagementException("Handbook subject was not found.", "handbook_subject_not_found");

        return (entryId ?? subject.EntryId, contentNodeId ?? subject.ContentNodeId);
    }

    protected async Task<(Guid? EntryId, Guid? ContentNodeId)> ResolveHandbookChapterNavigationAsync(
        Guid tenantId,
        Guid? chapterId,
        Guid? entryId,
        Guid? contentNodeId,
        CancellationToken cancellationToken)
    {
        if (!chapterId.HasValue) return (entryId, contentNodeId);

        var chapter = await contentAssetPersistence.HandbookChapters.AsNoTracking().SingleOrDefaultAsync(
            item => item.TenantId == tenantId && item.Id == chapterId.Value,
            cancellationToken);
        if (chapter is null)
            throw new ContentManagementException("Handbook chapter was not found.", "handbook_chapter_not_found");

        return (entryId ?? chapter.EntryId, contentNodeId ?? chapter.ContentNodeId);
    }
}
