using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.Catalog;
using Tiku.Application.QuestionBanks;
using Tiku.Application.Security;
using Tiku.Domain.Common;
using Tiku.Domain.Content;
using Tiku.Domain.QuestionBanks;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.QuestionBanks;

public sealed class QuestionBankQueryService(
    TikuDbContext dbContext,
    IPublicQuestionAccessPolicy accessPolicy,
    ITenantExecutionScope tenantExecutionScope) : IQuestionBankQueryService
{
    private const int DefaultQuestionLimit = 200;
    private const int MaxQuestionLimit = 500;
    private const int DefaultBankLimit = 100;
    private const int MaxBankLimit = 500;

    public async Task<CatalogList<QuestionBankCatalogItem>> GetQuestionBanksAsync(
        QuestionBankFilter filter,
        CancellationToken cancellationToken = default)
    {
        var limit = ResolveLimit(filter.Limit, DefaultBankLimit, MaxBankLimit);
        var tenantItems = filter.Source == QuestionSource.Platform
            ? []
            : await dbContext.QuestionBanks
            .AsNoTracking()
            .Where(bank =>
                bank.TenantId == filter.TenantId &&
                bank.Status == QuestionBankStatus.Active &&
                (!filter.RegionId.HasValue || bank.RegionId == filter.RegionId.Value || bank.RegionId == null) &&
                (string.IsNullOrWhiteSpace(filter.Keyword) || bank.Name.Contains(filter.Keyword.Trim())))
            .OrderBy(bank => bank.Name)
            .ThenBy(bank => bank.CreatedAt)
            .Take(limit)
            .Select(bank => new QuestionBankCatalogItem(
                bank.Id,
                bank.RegionId,
                bank.Name,
                QuestionSource.Tenant,
                bank.Status,
                bank.Metadata))
            .ToArrayAsync(cancellationToken);

        var platformItems = filter.Source == QuestionSource.Tenant || !await CanAccessPlatformAsync(filter.TenantId, cancellationToken)
            ? []
            : await tenantExecutionScope.ExecuteAsync(
                new SystemScopeRequest(
                    filter.TenantId, SystemScopeCallerType.PublicQuestionBank, nameof(QuestionBankQueryService),
                    "List platform question banks for an entitled tenant", Guid.NewGuid().ToString("N")),
                async (provider, token) =>
                {
                    var systemDbContext = provider.GetRequiredService<TikuDbContext>();
                    return await systemDbContext.QuestionBanks.AsNoTracking()
                        .Join(
                            systemDbContext.Tenants.AsNoTracking().Where(tenant => tenant.Mode == TenantMode.PlatformOwned),
                            bank => bank.TenantId,
                            tenant => tenant.Id,
                            (bank, tenant) => bank)
                        .Where(bank =>
                            bank.Status == QuestionBankStatus.Active &&
                            (!filter.RegionId.HasValue || bank.RegionId == filter.RegionId.Value || bank.RegionId == null) &&
                            (string.IsNullOrWhiteSpace(filter.Keyword) || bank.Name.Contains(filter.Keyword.Trim())))
                        .OrderBy(bank => bank.Name)
                        .Take(limit)
                        .Select(bank => new QuestionBankCatalogItem(
                            bank.Id,
                            bank.RegionId,
                            bank.Name,
                            QuestionSource.Platform,
                            bank.Status,
                            bank.Metadata))
                        .ToArrayAsync(token);
                },
                cancellationToken);

        return new CatalogList<QuestionBankCatalogItem>(tenantItems
            .Concat(platformItems)
            .OrderBy(item => item.Name)
            .Take(limit)
            .ToArray());
    }

    public async Task<CatalogList<QuestionCatalogItem>> GetQuestionsAsync(
        QuestionBankFilter filter,
        CancellationToken cancellationToken = default)
    {
        var limit = ResolveLimit(filter.Limit, DefaultQuestionLimit, MaxQuestionLimit);
        var tenantItems = filter.Source == QuestionSource.Platform
            ? []
            : await ProjectQuestions(
                    dbContext,
                    ApplyQuestionFilters(BaseQuestionQuery(), filter)
                        .OrderByDescending(question => question.CreatedAt)
                        .Take(limit),
                    QuestionSource.Tenant)
                .ToArrayAsync(cancellationToken);
        var platformItems = filter.Source == QuestionSource.Tenant || !await CanAccessPlatformAsync(filter.TenantId, cancellationToken)
            ? []
            : await GetPlatformQuestionsAsync(filter, limit, cancellationToken);
        return new CatalogList<QuestionCatalogItem>(tenantItems.Concat(platformItems).Take(limit).ToArray());
    }

    public async Task<QuestionCatalogItem> GetQuestionAsync(
        QuestionBankFilter filter,
        CancellationToken cancellationToken = default)
    {
        if (!filter.QuestionId.HasValue)
        {
            throw new QuestionBankRequiredFieldException("questionId is required.");
        }

        var result = await GetQuestionsAsync(filter with { Limit = 2 }, cancellationToken);
        return result.Items.SingleOrDefault()
               ?? throw new QuestionBankNotFoundException("Question was not found.");
    }

    public async Task<CatalogList<QuestionVersionCatalogItem>> GetQuestionVersionsAsync(
        QuestionBankFilter filter,
        CancellationToken cancellationToken = default)
    {
        if (!filter.QuestionId.HasValue)
        {
            throw new QuestionBankRequiredFieldException("questionId is required.");
        }

        if (filter.Source == QuestionSource.Platform)
        {
            await accessPolicy.EnsureCanStartAsync(filter.TenantId, cancellationToken);
            var platformItems = await GetPlatformVersionsAsync(filter.QuestionId.Value, filter.TenantId, cancellationToken);
            return new CatalogList<QuestionVersionCatalogItem>(platformItems);
        }

        var questionExists = await dbContext.Questions
            .AsNoTracking()
            .AnyAsync(
                question =>
                    question.TenantId == filter.TenantId &&
                    question.Id == filter.QuestionId.Value &&
                    question.Status == QuestionStatus.Published,
                cancellationToken);

        if (!questionExists)
        {
            throw new QuestionBankNotFoundException("Question was not found.");
        }

        var items = await dbContext.QuestionVersions
            .AsNoTracking()
            .Where(version =>
                version.TenantId == filter.TenantId &&
                version.QuestionId == filter.QuestionId.Value)
            .OrderByDescending(version => version.VersionNo)
            .Select(version => new QuestionVersionCatalogItem(
                version.Id,
                version.QuestionId,
                version.VersionNo,
                version.Content,
                version.Options,
                version.CorrectOptionIndex,
                version.CorrectOptionIndices,
                version.AnswerText,
                version.Explanation,
                version.SubQuestions,
                version.CodeLang,
                version.CodeTemplate,
                version.CreatedAt))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<QuestionVersionCatalogItem>(items);
    }

    private IQueryable<Question> BaseQuestionQuery()
    {
        return dbContext.Questions
            .AsNoTracking()
            .Where(question => question.Status == QuestionStatus.Published);
    }

    private IQueryable<Question> ApplyQuestionFilters(
        IQueryable<Question> query,
        QuestionBankFilter filter)
    {
        query = query.Where(question => question.TenantId == filter.TenantId);

        if (filter.QuestionBankId.HasValue)
        {
            query = query.Where(question => question.QuestionBankId == filter.QuestionBankId.Value);
        }

        if (filter.SubjectId.HasValue)
        {
            query = query.Where(question => question.SubjectId == filter.SubjectId.Value);
        }

        if (filter.CategoryId.HasValue)
        {
            query = query.Where(question => question.CategoryId == filter.CategoryId.Value);
        }

        if (filter.NodeId.HasValue)
        {
            query = query.Where(question => question.NodeId == filter.NodeId.Value);
        }

        if (filter.EntryId.HasValue)
        {
            query = query.Where(question => question.EntryId == filter.EntryId.Value);
        }

        if (filter.ContentNodeId.HasValue)
        {
            query = query.Where(question => question.ContentNodeId == filter.ContentNodeId.Value);
        }

        if (filter.CollectionId.HasValue)
        {
            query = query.Where(question =>
                question.PrimaryCollectionId == filter.CollectionId.Value ||
                dbContext.QuestionCollectionItems.Any(item =>
                    item.TenantId == question.TenantId &&
                    item.QuestionId == question.Id &&
                    item.CollectionId == filter.CollectionId.Value));
        }

        if (filter.QuestionIds?.Count > 0)
        {
            query = query.Where(question => filter.QuestionIds.Contains(question.Id));
        }

        if (!string.IsNullOrWhiteSpace(filter.Type))
        {
            var type = filter.Type.Trim();
            query = query.Where(question => question.Type == type);
        }

        if (!string.IsNullOrWhiteSpace(filter.Keyword))
        {
            var keyword = filter.Keyword.Trim();
            query = query.Where(question =>
                question.Type.Contains(keyword) ||
                (question.TypeLabel != null && question.TypeLabel.Contains(keyword)) ||
                dbContext.QuestionVersions.Any(version =>
                    version.TenantId == question.TenantId &&
                    version.QuestionId == question.Id &&
                    version.Id == question.CurrentVersionId &&
                    version.Content != null &&
                    version.Content.Contains(keyword)));
        }

        return query;
    }

    private static IQueryable<T> ApplyKeyword<T>(IQueryable<T> query, string? keyword)
        where T : class
    {
        if (string.IsNullOrWhiteSpace(keyword))
        {
            return query;
        }

        var trimmed = keyword.Trim();
        return query.Where(entity => EF.Property<string>(entity, nameof(QuestionBank.Name)).Contains(trimmed));
    }

    private static IQueryable<QuestionCatalogItem> ProjectQuestions(
        TikuDbContext context,
        IQueryable<Question> questions,
        QuestionSource source)
    {
        var emptyOptions = JsonDefaults.Array();
        var emptyCorrectOptionIndices = JsonDefaults.Array();
        var emptySubQuestions = JsonDefaults.Array();
        return
            from question in questions
            join version in context.QuestionVersions.AsNoTracking()
                on new { question.TenantId, QuestionId = question.Id, VersionId = question.CurrentVersionId }
                equals new { version.TenantId, version.QuestionId, VersionId = (Guid?)version.Id }
                into versions
            from version in versions.DefaultIfEmpty()
            select new QuestionCatalogItem(
                question.Id,
                question.QuestionBankId,
                question.SubjectId,
                question.CategoryId,
                question.NodeId,
                question.EntryId,
                question.ContentNodeId,
                question.PrimaryCollectionId,
                question.LegacyId,
                question.LegacySubjectId,
                question.LegacyCategoryId,
                question.LegacyNodeId,
                question.Type,
                question.TypeLabel,
                question.Difficulty,
                question.Tags,
                question.ExamMarkers,
                question.MediaUrl,
                question.HasVideoExplanation,
                question.Status,
                version == null ? null : version.Id,
                version == null ? null : version.VersionNo,
                version == null ? null : version.Content,
                version == null ? emptyOptions : version.Options,
                version == null ? null : version.CorrectOptionIndex,
                version == null ? emptyCorrectOptionIndices : version.CorrectOptionIndices,
                version == null ? null : version.AnswerText,
                version == null ? null : version.Explanation,
                version == null ? emptySubQuestions : version.SubQuestions,
                version == null ? null : version.CodeLang,
                version == null ? null : version.CodeTemplate,
                new QuestionLocator(source, question.Id));
    }

    private async Task<bool> CanAccessPlatformAsync(Guid tenantId, CancellationToken cancellationToken)
    {
        try
        {
            await accessPolicy.EnsureCanStartAsync(tenantId, cancellationToken);
            return true;
        }
        catch (PublicQuestionAccessDeniedException)
        {
            return false;
        }
    }

    private Task<QuestionCatalogItem[]> GetPlatformQuestionsAsync(
        QuestionBankFilter filter,
        int limit,
        CancellationToken cancellationToken)
    {
        return tenantExecutionScope.ExecuteAsync(
            new SystemScopeRequest(
                filter.TenantId, SystemScopeCallerType.PublicQuestionBank, nameof(QuestionBankQueryService),
                "List platform questions for an entitled tenant", Guid.NewGuid().ToString("N")),
            async (provider, token) =>
            {
                var systemDbContext = provider.GetRequiredService<TikuDbContext>();
                var platformTenantId = await systemDbContext.Tenants.AsNoTracking()
                    .Where(tenant => tenant.Mode == TenantMode.PlatformOwned)
                    .Select(tenant => tenant.Id)
                    .SingleAsync(token);
                var query = systemDbContext.Questions.AsNoTracking().Where(question =>
                    question.TenantId == platformTenantId &&
                    question.Status == QuestionStatus.Published &&
                    (!filter.QuestionId.HasValue || question.Id == filter.QuestionId.Value) &&
                    (!filter.QuestionBankId.HasValue || question.QuestionBankId == filter.QuestionBankId.Value) &&
                    (!filter.SubjectId.HasValue || question.SubjectId == filter.SubjectId.Value) &&
                    (!filter.CategoryId.HasValue || question.CategoryId == filter.CategoryId.Value) &&
                    (!filter.NodeId.HasValue || question.NodeId == filter.NodeId.Value) &&
                    (!filter.EntryId.HasValue || question.EntryId == filter.EntryId.Value) &&
                    (!filter.ContentNodeId.HasValue || question.ContentNodeId == filter.ContentNodeId.Value) &&
                    (filter.QuestionIds == null || filter.QuestionIds.Count == 0 || filter.QuestionIds.Contains(question.Id)) &&
                    (string.IsNullOrWhiteSpace(filter.Type) || question.Type == filter.Type.Trim()));
                if (!string.IsNullOrWhiteSpace(filter.Keyword))
                {
                    var keyword = filter.Keyword.Trim();
                    query = query.Where(question =>
                        question.Type.Contains(keyword) ||
                        (question.TypeLabel != null && question.TypeLabel.Contains(keyword)));
                }

                return await ProjectQuestions(
                        systemDbContext,
                        query.OrderByDescending(question => question.CreatedAt).Take(limit),
                        QuestionSource.Platform)
                    .ToArrayAsync(token);
            },
            cancellationToken);
    }

    private Task<QuestionVersionCatalogItem[]> GetPlatformVersionsAsync(
        Guid questionId,
        Guid tenantId,
        CancellationToken cancellationToken)
    {
        return tenantExecutionScope.ExecuteAsync(
            new SystemScopeRequest(
                tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(QuestionBankQueryService),
                "Read platform question versions for an entitled tenant", Guid.NewGuid().ToString("N")),
            async (provider, token) =>
            {
                var systemDbContext = provider.GetRequiredService<TikuDbContext>();
                var platformQuestion = await systemDbContext.Questions.AsNoTracking()
                    .Where(question => question.Id == questionId && question.Status == QuestionStatus.Published)
                    .Join(
                        systemDbContext.Tenants.AsNoTracking().Where(tenant => tenant.Mode == TenantMode.PlatformOwned),
                        question => question.TenantId,
                        tenant => tenant.Id,
                        (question, tenant) => new { question.TenantId, question.Id })
                    .SingleOrDefaultAsync(token);
                if (platformQuestion is null)
                {
                    throw new QuestionBankNotFoundException("Question was not found.");
                }

                return await systemDbContext.QuestionVersions.AsNoTracking()
                    .Where(version =>
                        version.TenantId == platformQuestion.TenantId &&
                        version.QuestionId == platformQuestion.Id)
                    .OrderByDescending(version => version.VersionNo)
                    .Select(version => new QuestionVersionCatalogItem(
                        version.Id,
                        version.QuestionId,
                        version.VersionNo,
                        version.Content,
                        version.Options,
                        version.CorrectOptionIndex,
                        version.CorrectOptionIndices,
                        version.AnswerText,
                        version.Explanation,
                        version.SubQuestions,
                        version.CodeLang,
                        version.CodeTemplate,
                        version.CreatedAt))
                    .ToArrayAsync(token);
            },
            cancellationToken);
    }

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

public sealed class QuestionBankRequiredFieldException(string message) : Exception(message);

public sealed class QuestionBankNotFoundException(string message) : Exception(message);
