using Microsoft.EntityFrameworkCore;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Domain.Common;
using Tiku.Domain.Content;
using Tiku.Domain.QuestionBanks;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Content;

public sealed class ContentNavigationQueryService(TikuDbContext dbContext) : IContentNavigationQueryService
{
    private const int DefaultLimit = 100;
    private const int MaxLimit = 1000;

    public async Task<CatalogList<ContentEntryCatalogItem>> GetContentEntriesAsync(
        ContentNavigationFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.ContentEntries
            .AsNoTracking()
            .Where(entry =>
                entry.TenantId == filter.TenantId &&
                entry.IsActive);

        if (!filter.IncludeHidden)
        {
            query = query.Where(entry => entry.Visibility != ContentVisibility.Hidden);
        }

        if (filter.RegionId.HasValue)
        {
            query = query.Where(entry => entry.RegionId == filter.RegionId.Value || entry.RegionId == null);
        }

        if (TryParseEntryType(filter.EntryType, out var entryType))
        {
            query = query.Where(entry => entry.EntryType == entryType);
        }

        query = ApplyKeyword(query, filter.Keyword);

        var items = await query
            .OrderBy(entry => entry.SortOrder)
            .ThenBy(entry => entry.CreatedAt)
            .Take(ResolveLimit(filter.Limit, 500))
            .Select(entry => new ContentEntryCatalogItem(
                entry.Id,
                entry.RegionId,
                entry.LegacyId,
                entry.EntryKey,
                entry.Name,
                entry.EntryType,
                entry.Icon,
                entry.Route,
                entry.Description,
                entry.Visibility,
                entry.AccessRules,
                entry.LayoutConfig,
                entry.SortOrder))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<ContentEntryCatalogItem>(items);
    }

    public async Task<CatalogList<ContentNodeCatalogItem>> GetContentNodesAsync(
        ContentNavigationFilter filter,
        CancellationToken cancellationToken = default)
    {
        if (!filter.EntryId.HasValue)
        {
            throw new RequiredFieldException("entryId is required.");
        }

        var query = dbContext.ContentNodes
            .AsNoTracking()
            .Where(node =>
                node.TenantId == filter.TenantId &&
                node.EntryId == filter.EntryId.Value);

        if (!filter.IncludeInactive)
        {
            query = query.Where(node => node.IsActive);
        }

        if (filter.RegionId.HasValue)
        {
            query = query.Where(node => node.RegionId == filter.RegionId.Value || node.RegionId == null);
        }

        if (filter.ParentWasSpecified)
        {
            query = filter.ParentIsRoot
                ? query.Where(node => node.ParentId == null)
                : query.Where(node => node.ParentId == filter.ParentId);
        }

        if (TryParseMarkerType(filter.MarkerType, out var markerType))
        {
            query = query.Where(node => node.MarkerType == markerType);
        }

        query = ApplyKeyword(query, filter.Keyword);

        query = string.Equals(filter.Mode, "flat", StringComparison.OrdinalIgnoreCase)
            ? query.OrderBy(node => node.Path).ThenBy(node => node.SortOrder)
            : query.OrderBy(node => node.SortOrder).ThenBy(node => node.CreatedAt);

        var items = await query
            .Take(ResolveLimit(filter.Limit, 1000))
            .Select(node => new ContentNodeCatalogItem(
                node.Id,
                node.EntryId,
                node.RegionId,
                node.ParentId,
                node.LegacyId,
                node.NodeKey,
                node.Name,
                node.NodeType,
                node.MarkerType,
                node.MarkerConfig,
                node.Path,
                node.Depth,
                node.SortOrder,
                node.IsSelectable,
                node.IsLeaf,
                node.AccessRules,
                node.Metadata))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<ContentNodeCatalogItem>(items);
    }

    public async Task<CatalogList<QuestionCollectionCatalogItem>> GetQuestionCollectionsAsync(
        ContentNavigationFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.QuestionCollections
            .AsNoTracking()
            .Where(collection =>
                collection.TenantId == filter.TenantId &&
                collection.Status == ContentStatus.Active);

        if (filter.RegionId.HasValue)
        {
            query = query.Where(collection => collection.RegionId == filter.RegionId.Value || collection.RegionId == null);
        }

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

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

        if (TryParseCollectionType(filter.CollectionType, out var collectionType))
        {
            query = query.Where(collection => collection.CollectionType == collectionType);
        }

        query = ApplyKeyword(query, filter.Keyword);

        var items = await query
            .OrderBy(collection => collection.SortOrder)
            .ThenBy(collection => collection.CreatedAt)
            .Take(ResolveLimit(filter.Limit, 500))
            .Select(collection => new QuestionCollectionCatalogItem(
                collection.Id,
                collection.RegionId,
                collection.EntryId,
                collection.NodeId,
                collection.SubjectId,
                collection.CategoryId,
                collection.QuestionBankId,
                collection.LegacyId,
                collection.Name,
                collection.CollectionType,
                collection.SourceType,
                collection.Filters,
                collection.QuestionCount,
                collection.TotalScore,
                collection.DurationMinutes,
                collection.Status,
                collection.SortOrder,
                collection.AccessRules,
                collection.Metadata))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<QuestionCollectionCatalogItem>(items);
    }

    public async Task<CatalogList<PracticeBlueprintCatalogItem>> GetPracticeBlueprintsAsync(
        ContentNavigationFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.PracticeBlueprints
            .AsNoTracking()
            .Where(blueprint =>
                blueprint.TenantId == filter.TenantId &&
                blueprint.Status == ContentStatus.Active);

        if (filter.RegionId.HasValue)
        {
            query = query.Where(blueprint => blueprint.RegionId == filter.RegionId.Value || blueprint.RegionId == null);
        }

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

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

        if (filter.CollectionId.HasValue)
        {
            query = query.Where(blueprint => blueprint.CollectionId == filter.CollectionId.Value);
        }

        if (TryParsePracticeMode(filter.Mode, out var mode))
        {
            query = query.Where(blueprint => blueprint.Mode == mode);
        }

        query = ApplyKeyword(query, filter.Keyword);

        var items = await query
            .OrderBy(blueprint => blueprint.SortOrder)
            .ThenBy(blueprint => blueprint.CreatedAt)
            .Take(ResolveLimit(filter.Limit, 500))
            .Select(blueprint => new PracticeBlueprintCatalogItem(
                blueprint.Id,
                blueprint.RegionId,
                blueprint.EntryId,
                blueprint.NodeId,
                blueprint.CollectionId,
                blueprint.LegacyId,
                blueprint.Name,
                blueprint.Mode,
                blueprint.AssemblyType,
                blueprint.QuestionLimit,
                blueprint.DurationMinutes,
                blueprint.TotalScore,
                blueprint.PassScore,
                blueprint.Sections,
                blueprint.Rules,
                blueprint.AccessRules,
                blueprint.Status,
                blueprint.SortOrder))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<PracticeBlueprintCatalogItem>(items);
    }

    public async Task<CatalogList<CollectionQuestionCatalogItem>> GetCollectionQuestionsAsync(
        ContentNavigationFilter filter,
        CancellationToken cancellationToken = default)
    {
        if (!filter.CollectionId.HasValue)
        {
            throw new RequiredFieldException("collectionId is required.");
        }

        var collectionExists = await dbContext.QuestionCollections
            .AsNoTracking()
            .AnyAsync(
                collection =>
                    collection.TenantId == filter.TenantId &&
                    collection.Id == filter.CollectionId.Value &&
                    collection.Status == ContentStatus.Active,
                cancellationToken);

        if (!collectionExists)
        {
            throw new ContentNavigationNotFoundException("Question collection was not found.");
        }

        var emptyOptions = JsonDefaults.Array();
        var emptyCorrectOptionIndices = JsonDefaults.Array();
        var emptySubQuestions = JsonDefaults.Array();
        var query =
            from item in dbContext.QuestionCollectionItems.AsNoTracking()
            join question in dbContext.Questions.AsNoTracking()
                on new { item.TenantId, QuestionId = item.QuestionId } equals new { question.TenantId, QuestionId = question.Id }
            join version in dbContext.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()
            where item.TenantId == filter.TenantId &&
                  item.CollectionId == filter.CollectionId.Value &&
                  question.Status == QuestionStatus.Published
            orderby item.SectionKey, item.SortOrder, question.CreatedAt
            select new CollectionQuestionCatalogItem(
                question.Id,
                question.LegacyId,
                question.EntryId,
                question.ContentNodeId,
                question.PrimaryCollectionId,
                question.SubjectId,
                question.CategoryId,
                question.NodeId,
                question.Type,
                question.TypeLabel,
                question.Difficulty,
                question.Tags,
                question.ExamMarkers,
                question.MediaUrl,
                question.HasVideoExplanation,
                item.SectionKey,
                item.Score,
                item.SortOrder,
                version == null ? null : version.Id,
                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);

        var items = await query
            .Take(ResolveLimit(filter.Limit, MaxLimit))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<CollectionQuestionCatalogItem>(items);
    }

    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(ContentEntry.Name)).Contains(trimmed));
    }

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

    private static bool TryParseEntryType(string? value, out ContentEntryType type)
    {
        return Enum.TryParse(NormalizeEnumValue(value), ignoreCase: true, out type);
    }

    private static bool TryParseCollectionType(string? value, out QuestionCollectionType type)
    {
        return Enum.TryParse(NormalizeEnumValue(value), ignoreCase: true, out type);
    }

    private static bool TryParsePracticeMode(string? value, out PracticeMode mode)
    {
        return Enum.TryParse(NormalizeEnumValue(value), ignoreCase: true, out mode);
    }

    private static bool TryParseMarkerType(string? value, out ContentMarkerType type)
    {
        return Enum.TryParse(NormalizeEnumValue(value), ignoreCase: true, out type);
    }

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

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

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