using Microsoft.EntityFrameworkCore;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Domain.Catalog;
using Tiku.Infrastructure.Security;

namespace Tiku.Infrastructure.Content;

public sealed partial class DirectContentService
{
    public async Task<CatalogList<ScorelineField>> GetScorelineFieldsAsync(
        DirectContentActor actor,
        AdminLimitFilter filter,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var regionIds = scope.RegionIds.ToArray();
        var query = dbContext.ScorelineFields.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId)
            .ApplyDataScope(scope, null, item => item.RegionId.HasValue && regionIds.Contains(item.RegionId.Value));
        if (filter.RegionId.HasValue)
            query = query.Where(item => item.RegionId == filter.RegionId.Value || item.RegionId == null);

        if (!string.IsNullOrWhiteSpace(filter.Keyword))
        {
            var keyword = filter.Keyword.Trim();
            query = query.Where(item => item.FieldKey.Contains(keyword) || item.FieldName.Contains(keyword));
        }

        return new CatalogList<ScorelineField>(await query
            .OrderBy(item => item.SortOrder)
            .ThenBy(item => item.FieldName)
            .Take(ResolveLimit(filter.Limit))
            .ToArrayAsync(cancellationToken));
    }

    public async Task<ContentManagementResult<ScorelineField>> UpsertScorelineFieldAsync(
        DirectContentActor actor,
        ScorelineFieldCommand command,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        ArgumentException.ThrowIfNullOrWhiteSpace(command.FieldKey);
        ArgumentException.ThrowIfNullOrWhiteSpace(command.FieldName);
        if (!ScorelineFieldKeyRegex.IsMatch(command.FieldKey.Trim()))
            throw new ContentManagementException("Scoreline field key is invalid.", "scoreline_field_key_invalid");

        await AssertReferenceAsync<Region>(actor.TenantId, command.RegionId, "region_not_found", cancellationToken);
        var item = await ResolveByIdOrLegacyAsync(dbContext.ScorelineFields, actor.TenantId, command.Id,
            command.LegacyId, cancellationToken);
        var isNew = item is null;
        EnsureRegionWriteAllowed(scope, actor, item?.RegionId, command.RegionId, isNew, "scoreline_field_not_found");
        item ??= new ScorelineField { Id = command.Id ?? Guid.NewGuid(), TenantId = actor.TenantId };
        item.RegionId = command.RegionId;
        item.LegacyId = Normalize(command.LegacyId);
        item.FieldKey = command.FieldKey.Trim();
        item.FieldName = command.FieldName.Trim();
        item.FieldType = Normalize(command.FieldType) ?? "text";
        item.Unit = Normalize(command.Unit);
        item.IsFilter = command.IsFilter ?? item.IsFilter;
        item.IsRequired = command.IsRequired ?? item.IsRequired;
        item.IsVisible = command.IsVisible ?? item.IsVisible;
        item.IsTrend = command.IsTrend ?? item.IsTrend;
        item.Options = JsonArrayOrDefault(command.Options);
        item.Placeholder = Normalize(command.Placeholder);
        item.Description = Normalize(command.Description);
        item.SortOrder = command.Order ?? item.SortOrder;
        if (isNew) dbContext.ScorelineFields.Add(item);

        await dbContext.SaveChangesAsync(cancellationToken);
        return new ContentManagementResult<ScorelineField>(item);
    }

    public async Task<CatalogList<ScorelineRecord>> GetScorelineRecordsAsync(
        DirectContentActor actor,
        AdminLimitFilter filter,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var regionIds = scope.RegionIds.ToArray();
        var query = dbContext.ScorelineRecords.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId)
            .ApplyDataScope(scope, null, item => item.RegionId.HasValue && regionIds.Contains(item.RegionId.Value));
        if (filter.RegionId.HasValue) query = query.Where(item => item.RegionId == filter.RegionId.Value);

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

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

        if (filter.Year.HasValue) query = query.Where(item => item.Year == filter.Year.Value);

        if (!string.IsNullOrWhiteSpace(filter.Keyword))
        {
            var keyword = filter.Keyword.Trim();
            query = query.Where(item =>
                (item.SchoolName != null && item.SchoolName.Contains(keyword)) ||
                (item.MajorName != null && item.MajorName.Contains(keyword)));
        }

        return new CatalogList<ScorelineRecord>(await query
            .OrderByDescending(item => item.Year)
            .ThenBy(item => item.SchoolName)
            .ThenBy(item => item.MajorName)
            .Take(ResolveLimit(filter.Limit))
            .ToArrayAsync(cancellationToken));
    }

    public async Task<ContentManagementResult<ScorelineRecord>> UpsertScorelineRecordAsync(
        DirectContentActor actor,
        ScorelineRecordCommand command,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        if (command.Year is < 1900 or > 3000)
            throw new ContentManagementException("Scoreline record year is invalid.", "scoreline_year_invalid");

        await AssertReferenceAsync<Region>(actor.TenantId, command.RegionId, "region_not_found", cancellationToken);
        await AssertReferenceAsync<School>(actor.TenantId, command.SchoolId, "school_not_found", cancellationToken);
        await AssertReferenceAsync<Major>(actor.TenantId, command.MajorId, "major_not_found", cancellationToken);
        var item = await ResolveByIdOrLegacyAsync(dbContext.ScorelineRecords, actor.TenantId, command.Id,
            command.LegacyId, cancellationToken);
        var isNew = item is null;
        EnsureRegionWriteAllowed(scope, actor, item?.RegionId, command.RegionId, isNew, "scoreline_record_not_found");
        item ??= new ScorelineRecord { Id = command.Id ?? Guid.NewGuid(), TenantId = actor.TenantId };
        item.RegionId = command.RegionId;
        item.SchoolId = command.SchoolId;
        item.MajorId = command.MajorId;
        item.LegacyId = Normalize(command.LegacyId);
        item.Year = command.Year;
        item.SchoolName = Normalize(command.SchoolName);
        item.MajorName = Normalize(command.MajorName);
        item.FieldValues = JsonObjectOrDefault(command.FieldValues);
        if (isNew) dbContext.ScorelineRecords.Add(item);

        await dbContext.SaveChangesAsync(cancellationToken);
        return new ContentManagementResult<ScorelineRecord>(item);
    }

    public async Task<CatalogList<int>> GetScorelineYearsAsync(
        DirectContentActor actor,
        AdminLimitFilter filter,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var regionIds = scope.RegionIds.ToArray();
        var query = dbContext.ScorelineRecords.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId)
            .ApplyDataScope(scope, null, item => item.RegionId.HasValue && regionIds.Contains(item.RegionId.Value));
        if (filter.RegionId.HasValue) query = query.Where(item => item.RegionId == filter.RegionId.Value);

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

        var years = await query
            .Select(item => item.Year)
            .Distinct()
            .OrderByDescending(year => year)
            .Take(ResolveLimit(filter.Limit))
            .ToArrayAsync(cancellationToken);
        return new CatalogList<int>(years);
    }

    public async Task<CatalogList<ScorelineTrendItem>> GetScorelineTrendAsync(
        DirectContentActor actor,
        AdminLimitFilter filter,
        CancellationToken cancellationToken = default)
    {
        var years = await GetScorelineYearsAsync(actor, filter, cancellationToken);
        var items = new List<ScorelineTrendItem>();
        foreach (var year in years.Items)
        {
            var schoolCount = await dbContext.ScorelineRecords.AsNoTracking()
                .Where(item => item.TenantId == actor.TenantId && item.Year == year)
                .Select(item => item.SchoolId)
                .Where(id => id.HasValue)
                .Distinct()
                .CountAsync(cancellationToken);
            var majorCount = await dbContext.ScorelineRecords.AsNoTracking()
                .Where(item => item.TenantId == actor.TenantId && item.Year == year)
                .Select(item => item.MajorId)
                .Where(id => id.HasValue)
                .Distinct()
                .CountAsync(cancellationToken);
            items.Add(new ScorelineTrendItem(year, schoolCount, majorCount));
        }

        return new CatalogList<ScorelineTrendItem>(items);
    }
}