using Microsoft.EntityFrameworkCore;
using Tiku.Application.Catalog;
using Tiku.Domain.Catalog;
using Tiku.Domain.Commerce;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Catalog;

public sealed class CatalogQueryService(TikuDbContext dbContext) : ICatalogQueryService
{
    private const int DefaultLimit = 500;
    private const int MaxLimit = 2000;

    public async Task<CatalogList<RegionCatalogItem>> GetRegionsAsync(
        CatalogFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.Regions
            .AsNoTracking()
            .Where(region =>
                region.TenantId == filter.TenantId &&
                region.IsActive);

        query = ApplyKeyword(query, filter.Keyword);

        var items = await query
            .OrderBy(region => region.SortOrder)
            .ThenBy(region => region.Name)
            .ThenBy(region => region.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .Select(region => new RegionCatalogItem(
                region.Id,
                region.LegacyId,
                region.Name,
                region.Code,
                region.ShortName,
                region.FullName,
                region.Icon,
                region.Pinyin,
                region.SortOrder,
                region.IsHot))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<RegionCatalogItem>(items);
    }

    public async Task<CatalogList<RegionModuleCatalogItem>> GetRegionModulesAsync(
        CatalogFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.RegionModules
            .AsNoTracking()
            .Where(module =>
                module.TenantId == filter.TenantId &&
                module.IsActive);

        if (filter.RegionId.HasValue) query = query.Where(module => module.RegionId == filter.RegionId.Value);

        query = ApplyKeyword(query, filter.Keyword);

        var items = await query
            .OrderBy(module => module.SortOrder)
            .ThenBy(module => module.Name)
            .ThenBy(module => module.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .Select(module => new RegionModuleCatalogItem(
                module.Id,
                module.LegacyId,
                module.RegionId,
                module.Name,
                module.Type,
                module.Icon,
                module.Color,
                module.TextColor,
                module.Description,
                module.Route,
                module.SortOrder,
                module.IsPrimarySchoolModule))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<RegionModuleCatalogItem>(items);
    }

    public async Task<CatalogList<ModuleNodeCatalogItem>> GetModuleNodesAsync(
        CatalogFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.ModuleNodes
            .AsNoTracking()
            .Where(node =>
                node.TenantId == filter.TenantId &&
                node.IsActive);

        if (filter.RegionId.HasValue) query = query.Where(node => node.RegionId == filter.RegionId.Value);

        if (filter.ModuleId.HasValue) query = query.Where(node => node.ModuleId == filter.ModuleId.Value);

        if (filter.ParentIsRoot)
            query = query.Where(node => node.ParentId == null);
        else if (filter.ParentId.HasValue) query = query.Where(node => node.ParentId == filter.ParentId.Value);

        if (TryParseModuleNodeType(filter.Type, out var nodeType)) query = query.Where(node => node.Type == nodeType);

        query = ApplyKeyword(query, filter.Keyword);

        var items = await query
            .OrderBy(node => node.SortOrder)
            .ThenBy(node => node.Name)
            .ThenBy(node => node.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .Select(node => new ModuleNodeCatalogItem(
                node.Id,
                node.LegacyId,
                node.RegionId,
                node.ModuleId,
                node.ParentId,
                node.LegacyParentId,
                node.Type,
                node.Name,
                node.Path,
                node.SortOrder,
                node.Metadata))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<ModuleNodeCatalogItem>(items);
    }

    public async Task<CatalogList<SchoolCatalogItem>> GetSchoolsAsync(
        CatalogFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.Schools
            .AsNoTracking()
            .Where(school => school.TenantId == filter.TenantId);

        if (filter.RegionId.HasValue) query = query.Where(school => school.RegionId == filter.RegionId.Value);

        if (filter.ModuleId.HasValue) query = query.Where(school => school.ModuleId == filter.ModuleId.Value);

        query = ApplyKeyword(query, filter.Keyword);

        var items = await query
            .OrderBy(school => school.Name)
            .ThenBy(school => school.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .Select(school => new SchoolCatalogItem(
                school.Id,
                school.LegacyId,
                school.RegionId,
                school.ModuleId,
                school.Name,
                school.ProfessionalExamDate,
                school.Metadata))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<SchoolCatalogItem>(items);
    }

    public async Task<CatalogList<MajorCatalogItem>> GetMajorsAsync(
        CatalogFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.Majors
            .AsNoTracking()
            .Where(major =>
                major.TenantId == filter.TenantId &&
                major.IsActive);

        if (filter.RegionId.HasValue) query = query.Where(major => major.RegionId == filter.RegionId.Value);

        if (filter.SchoolId.HasValue) query = query.Where(major => major.SchoolId == filter.SchoolId.Value);

        query = ApplyKeyword(query, filter.Keyword);

        var items = await query
            .OrderBy(major => major.SortOrder)
            .ThenBy(major => major.Name)
            .ThenBy(major => major.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .Select(major => new MajorCatalogItem(
                major.Id,
                major.LegacyId,
                major.RegionId,
                major.SchoolId,
                major.Name,
                major.Description,
                major.StudyTips,
                major.SortOrder))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<MajorCatalogItem>(items);
    }

    public async Task<CatalogList<SubjectCatalogItem>> GetSubjectsAsync(
        CatalogFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.Subjects
            .AsNoTracking()
            .Where(subject =>
                subject.TenantId == filter.TenantId &&
                subject.IsActive);

        if (filter.RegionId.HasValue) query = query.Where(subject => subject.RegionId == filter.RegionId.Value);

        if (filter.SchoolId.HasValue) query = query.Where(subject => subject.SchoolId == filter.SchoolId.Value);

        if (filter.MajorId.HasValue) query = query.Where(subject => subject.MajorId == filter.MajorId.Value);

        if (filter.ModuleId.HasValue) query = query.Where(subject => subject.ModuleId == filter.ModuleId.Value);

        if (TryParseSubjectType(filter.Type, out var subjectType))
            query = query.Where(subject => subject.Type == subjectType);

        query = ApplyKeyword(query, filter.Keyword);

        var items = await query
            .OrderBy(subject => subject.SortOrder)
            .ThenBy(subject => subject.Name)
            .ThenBy(subject => subject.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .Select(subject => new SubjectCatalogItem(
                subject.Id,
                subject.LegacyId,
                subject.RegionId,
                subject.ModuleId,
                subject.SchoolId,
                subject.MajorId,
                subject.NodeId,
                subject.Name,
                subject.Type,
                subject.Icon,
                subject.Description,
                subject.Stats,
                subject.SortOrder))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<SubjectCatalogItem>(items);
    }

    public async Task<CatalogList<CategoryCatalogItem>> GetCategoriesAsync(
        CatalogFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.Categories
            .AsNoTracking()
            .Where(category =>
                category.TenantId == filter.TenantId &&
                category.IsActive);

        if (filter.SubjectId.HasValue) query = query.Where(category => category.SubjectId == filter.SubjectId.Value);

        if (filter.NodeId.HasValue) query = query.Where(category => category.NodeId == filter.NodeId.Value);

        if (TryParseCategoryType(filter.Type, out var categoryType))
            query = query.Where(category => category.CategoryType == categoryType);

        query = ApplyKeyword(query, filter.Keyword);

        var items = await query
            .OrderBy(category => category.SortOrder)
            .ThenBy(category => category.Name)
            .ThenBy(category => category.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .Select(category => new CategoryCatalogItem(
                category.Id,
                category.LegacyId,
                category.SubjectId,
                category.NodeId,
                category.Name,
                category.CategoryType,
                category.SortOrder,
                category.SvipQuestionLimit))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<CategoryCatalogItem>(items);
    }

    public async Task<CatalogList<BannerCatalogItem>> GetBannersAsync(
        CatalogFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.Banners
            .AsNoTracking()
            .Where(banner =>
                banner.TenantId == filter.TenantId &&
                banner.IsActive);

        if (filter.RegionId.HasValue) query = query.Where(banner => banner.RegionId == filter.RegionId.Value);

        if (!string.IsNullOrWhiteSpace(filter.Keyword))
        {
            var keyword = filter.Keyword.Trim();
            query = query.Where(banner =>
                (banner.Title != null && banner.Title.Contains(keyword)) ||
                (banner.Subtitle != null && banner.Subtitle.Contains(keyword)) ||
                (banner.Content != null && banner.Content.Contains(keyword)));
        }

        var items = await query
            .OrderBy(banner => banner.SortOrder)
            .ThenBy(banner => banner.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .Select(banner => new BannerCatalogItem(
                banner.Id,
                banner.LegacyId,
                banner.RegionId,
                banner.Title,
                banner.Subtitle,
                banner.Content,
                banner.ButtonText,
                banner.ButtonLink,
                banner.BackgroundColor,
                banner.BorderColor,
                banner.SortOrder,
                banner.IsActive))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<BannerCatalogItem>(items);
    }

    public async Task<CatalogList<FaqCatalogItem>> GetFaqsAsync(
        CatalogFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.Faqs
            .AsNoTracking()
            .Where(faq =>
                faq.TenantId == filter.TenantId &&
                faq.IsActive);

        if (filter.RegionId.HasValue) query = query.Where(faq => faq.RegionId == filter.RegionId.Value);

        if (!string.IsNullOrWhiteSpace(filter.Keyword))
        {
            var keyword = filter.Keyword.Trim();
            query = query.Where(faq =>
                (faq.Question != null && faq.Question.Contains(keyword)) ||
                (faq.Answer != null && faq.Answer.Contains(keyword)));
        }

        var items = await query
            .OrderBy(faq => faq.SortOrder)
            .ThenBy(faq => faq.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .Select(faq => new FaqCatalogItem(
                faq.Id,
                faq.LegacyId,
                faq.RegionId,
                faq.Question,
                faq.Answer,
                faq.SortOrder,
                faq.IsActive))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<FaqCatalogItem>(items);
    }

    public async Task<CatalogList<AnnouncementCatalogItem>> GetAnnouncementsAsync(
        CatalogFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.Announcements
            .AsNoTracking()
            .Where(announcement =>
                announcement.TenantId == filter.TenantId &&
                announcement.IsActive);

        if (!string.IsNullOrWhiteSpace(filter.Keyword))
        {
            var keyword = filter.Keyword.Trim();
            query = query.Where(announcement =>
                announcement.Content != null && announcement.Content.Contains(keyword));
        }

        var items = await query
            .OrderBy(announcement => announcement.SortOrder)
            .ThenBy(announcement => announcement.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .Select(announcement => new AnnouncementCatalogItem(
                announcement.Id,
                announcement.LegacyId,
                announcement.Content,
                announcement.Link,
                announcement.BackgroundColor,
                announcement.SortOrder,
                announcement.IsActive))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<AnnouncementCatalogItem>(items);
    }

    public async Task<CatalogList<ExamDateCatalogItem>> GetExamDatesAsync(
        CatalogFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.ExamDates
            .AsNoTracking()
            .Where(examDate =>
                examDate.TenantId == filter.TenantId &&
                examDate.IsActive);

        if (filter.RegionId.HasValue)
            query = query.Where(examDate =>
                examDate.RegionId == filter.RegionId.Value ||
                examDate.RegionId == null);

        if (filter.SchoolId.HasValue)
            query = query.Where(examDate =>
                examDate.SchoolId == filter.SchoolId.Value ||
                examDate.SchoolId == null);

        if (!string.IsNullOrWhiteSpace(filter.Type))
            query = query.Where(examDate => examDate.ExamType == filter.Type.Trim());

        if (!string.IsNullOrWhiteSpace(filter.Keyword))
        {
            var keyword = filter.Keyword.Trim();
            query = query.Where(examDate =>
                examDate.ExamName.Contains(keyword) ||
                (examDate.Description != null && examDate.Description.Contains(keyword)));
        }

        var today = DateTimeOffset.UtcNow.Date;
        var rows = await query
            .OrderBy(examDate => examDate.ExamAt == null)
            .ThenBy(examDate => examDate.ExamAt)
            .ThenBy(examDate => examDate.SortOrder)
            .Take(ResolveLimit(filter.Limit))
            .Select(examDate => new
            {
                examDate.Id,
                examDate.LegacyId,
                examDate.RegionId,
                examDate.SchoolId,
                examDate.ExamName,
                examDate.ExamAt,
                examDate.ExamType,
                examDate.Description,
                examDate.Metadata,
                examDate.SortOrder,
                examDate.IsActive
            })
            .ToArrayAsync(cancellationToken);

        var items = rows
            .Select(examDate => new ExamDateCatalogItem(
                examDate.Id,
                examDate.LegacyId,
                examDate.RegionId,
                examDate.SchoolId,
                examDate.ExamName,
                examDate.ExamAt,
                examDate.ExamType,
                examDate.Description,
                examDate.Metadata,
                examDate.SortOrder,
                examDate.IsActive,
                examDate.ExamAt.HasValue
                    ? (examDate.ExamAt.Value.Date - today).Days
                    : null))
            .ToArray();

        return new CatalogList<ExamDateCatalogItem>(items);
    }

    public async Task<CatalogList<ProductCatalogItem>> GetProductsAsync(
        CatalogFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.Products
            .AsNoTracking()
            .Where(product =>
                product.TenantId == filter.TenantId &&
                product.IsActive);

        if (filter.RegionId.HasValue) query = query.Where(product => product.RegionId == filter.RegionId.Value);

        if (TryParseProductType(filter.Type, out var productType))
            query = query.Where(product => product.Type == productType);

        if (!string.IsNullOrWhiteSpace(filter.Keyword))
        {
            var keyword = filter.Keyword.Trim();
            query = query.Where(product => product.Title.Contains(keyword));
        }

        var items = await query
            .OrderBy(product => product.SortOrder)
            .ThenByDescending(product => product.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .Select(product => new ProductCatalogItem(
                product.Id,
                product.LegacyId,
                product.RegionId,
                product.Title,
                null,
                product.LegacyPriceText,
                product.Link,
                product.Type,
                product.Tags,
                product.Cover,
                product.PreviewIframe,
                product.DetailImages,
                product.SortOrder,
                product.IsActive))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<ProductCatalogItem>(items);
    }

    public async Task<CatalogList<SvipPlanCatalogItem>> GetSvipPlansAsync(
        CatalogFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.SvipPlans
            .AsNoTracking()
            .Where(plan =>
                plan.TenantId == filter.TenantId &&
                plan.IsActive);

        if (filter.RegionId.HasValue)
            query = query.Where(plan =>
                plan.RegionId == filter.RegionId.Value ||
                plan.RegionId == null);

        if (!string.IsNullOrWhiteSpace(filter.Keyword))
        {
            var keyword = filter.Keyword.Trim();
            query = query.Where(plan =>
                plan.Name.Contains(keyword) ||
                (plan.Description != null && plan.Description.Contains(keyword)));
        }

        var items = await query
            .OrderBy(plan => plan.SortOrder)
            .ThenBy(plan => plan.PriceCents)
            .Take(ResolveLimit(filter.Limit))
            .Select(plan => new SvipPlanCatalogItem(
                plan.Id,
                plan.LegacyId,
                plan.RegionId,
                plan.Name,
                plan.PriceCents,
                plan.PriceCents / 100m,
                plan.OriginalPriceCents,
                plan.OriginalPriceCents.HasValue ? plan.OriginalPriceCents.Value / 100m : null,
                plan.Days,
                plan.Description,
                plan.PerDayLabel,
                plan.Badge,
                plan.Recommended,
                plan.CouponOnly,
                plan.VpProductId,
                plan.VpEnabled,
                plan.SortOrder,
                plan.IsActive))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<SvipPlanCatalogItem>(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(Region.Name)).Contains(trimmed));
    }

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

    private static bool TryParseSubjectType(string? value, out SubjectType type)
    {
        return Enum.TryParse(NormalizeEnumValue(value), true, out type);
    }

    private static bool TryParseModuleNodeType(string? value, out ModuleNodeType type)
    {
        return Enum.TryParse(NormalizeEnumValue(value), true, out type);
    }

    private static bool TryParseCategoryType(string? value, out CategoryType type)
    {
        return Enum.TryParse(NormalizeEnumValue(value), true, out type);
    }

    private static bool TryParseProductType(string? value, out ProductType type)
    {
        return Enum.TryParse(NormalizeEnumValue(value), 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);
    }
}