using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Catalog;
using Tiku.Application.Scoreline;
using Tiku.Domain.Catalog;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Scoreline;

public sealed partial class ScorelineQueryService(TikuDbContext dbContext) : IScorelineQueryService
{
    public async Task<CatalogList<ScorelineFieldItem>> GetFieldsAsync(
        ScorelineFilter filter,
        CancellationToken cancellationToken = default)
    {
        var query = dbContext.ScorelineFields.AsNoTracking()
            .Where(field => field.TenantId == filter.TenantId);
        if (filter.RegionId.HasValue)
        {
            query = query.Where(field => field.RegionId == filter.RegionId.Value || field.RegionId == null);
        }

        var fields = await query
            .OrderBy(field => field.SortOrder)
            .ThenBy(field => field.FieldName)
            .Take(ResolveLimit(filter.Limit))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<ScorelineFieldItem>(fields.Select(ToFieldItem).ToArray());
    }

    public async Task<ScorelineRecordPage> GetRecordsAsync(
        ScorelineFilter filter,
        CancellationToken cancellationToken = default)
    {
        var page = Math.Max(filter.Page ?? 1, 1);
        var pageSize = Math.Clamp(filter.PageSize ?? 20, 1, 200);
        var records = await BuildRecordQuery(filter)
            .OrderByDescending(record => record.Year)
            .ThenBy(record => record.SchoolName)
            .ThenBy(record => record.MajorName)
            .ToArrayAsync(cancellationToken);

        var filtered = await ApplyDynamicFiltersAsync(filter, records, cancellationToken);
        var items = filtered
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(ToRecordItem)
            .ToArray();

        return new ScorelineRecordPage(page, pageSize, filtered.Count, items);
    }

    public async Task<CatalogList<ScorelineRecordItem>> GetTrendAsync(
        ScorelineFilter filter,
        CancellationToken cancellationToken = default)
    {
        var records = await BuildRecordQuery(filter)
            .OrderBy(record => record.Year)
            .ThenBy(record => record.MajorName)
            .Take(ResolveLimit(filter.Limit, 100))
            .ToArrayAsync(cancellationToken);
        var filtered = await ApplyDynamicFiltersAsync(filter, records, cancellationToken);
        return new CatalogList<ScorelineRecordItem>(filtered.Select(ToRecordItem).ToArray());
    }

    public async Task<CatalogList<int>> GetYearsAsync(
        ScorelineFilter filter,
        CancellationToken cancellationToken = default)
    {
        var years = await BuildRecordQuery(filter)
            .Select(record => record.Year)
            .Distinct()
            .OrderByDescending(year => year)
            .Take(ResolveLimit(filter.Limit))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<int>(years);
    }

    private IQueryable<ScorelineRecord> BuildRecordQuery(ScorelineFilter filter)
    {
        var query = dbContext.ScorelineRecords.AsNoTracking()
            .Where(record => record.TenantId == filter.TenantId);
        if (filter.RegionId.HasValue)
        {
            query = query.Where(record => record.RegionId == filter.RegionId.Value);
        }

        if (filter.SchoolId.HasValue)
        {
            query = query.Where(record => record.SchoolId == filter.SchoolId.Value);
        }

        if (filter.MajorId.HasValue)
        {
            query = query.Where(record => record.MajorId == filter.MajorId.Value);
        }

        if (filter.Year.HasValue)
        {
            query = query.Where(record => record.Year == filter.Year.Value);
        }

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

        return query;
    }

    private async Task<IReadOnlyList<ScorelineRecord>> ApplyDynamicFiltersAsync(
        ScorelineFilter filter,
        IReadOnlyCollection<ScorelineRecord> records,
        CancellationToken cancellationToken)
    {
        if (filter.DynamicFilters is null || filter.DynamicFilters.Count == 0)
        {
            return records.ToArray();
        }

        var allowedFields = await dbContext.ScorelineFields.AsNoTracking()
            .Where(field =>
                field.TenantId == filter.TenantId &&
                field.IsFilter &&
                (filter.RegionId == null || field.RegionId == filter.RegionId || field.RegionId == null))
            .ToArrayAsync(cancellationToken);
        var allowed = allowedFields
            .GroupBy(field => field.FieldKey, StringComparer.Ordinal)
            .ToDictionary(group => group.Key, group => group.OrderBy(field => field.SortOrder).First(), StringComparer.Ordinal);

        foreach (var dynamicFilter in filter.DynamicFilters)
        {
            if (!FieldKeyRegex().IsMatch(dynamicFilter.FieldKey))
            {
                throw new ScorelineQueryException("Scoreline field filter key is invalid.", "scoreline_field_filter_key_invalid");
            }

            if (!allowed.ContainsKey(dynamicFilter.FieldKey))
            {
                throw new ScorelineQueryException("Scoreline field filter is not enabled.", "scoreline_field_filter_not_allowed");
            }
        }

        return records
            .Where(record => filter.DynamicFilters.All(dynamicFilter => MatchDynamicFilter(record, allowed[dynamicFilter.FieldKey], dynamicFilter)))
            .ToArray();
    }

    private static bool MatchDynamicFilter(
        ScorelineRecord record,
        ScorelineField field,
        ScorelineDynamicFilter filter)
    {
        if (record.FieldValues.ValueKind != JsonValueKind.Object ||
            !record.FieldValues.TryGetProperty(filter.FieldKey, out var value))
        {
            return false;
        }

        var fieldType = field.FieldType.Trim().ToLowerInvariant();
        if (filter.Operator is "min" or "max")
        {
            if (!IsNumericType(fieldType))
            {
                throw new ScorelineQueryException("Scoreline range filter requires a numeric field.", "scoreline_field_range_type_invalid");
            }

            if (!TryGetDecimal(value, out var actual) || !decimal.TryParse(filter.Value, out var expected))
            {
                throw new ScorelineQueryException("Scoreline range filter value must be numeric.", "scoreline_field_range_value_invalid");
            }

            return filter.Operator == "min" ? actual >= expected : actual <= expected;
        }

        if (IsNumericType(fieldType))
        {
            if (!TryGetDecimal(value, out var actual) || !decimal.TryParse(filter.Value, out var expected))
            {
                throw new ScorelineQueryException("Scoreline numeric filter value must be numeric.", "scoreline_field_value_invalid");
            }

            return actual == expected;
        }

        var actualText = value.ValueKind == JsonValueKind.String ? value.GetString() : value.ToString();
        return fieldType switch
        {
            "text" or "textarea" or "string" => actualText?.Contains(filter.Value, StringComparison.OrdinalIgnoreCase) == true,
            "boolean" or "bool" => string.Equals(actualText, filter.Value, StringComparison.OrdinalIgnoreCase),
            _ => string.Equals(actualText, filter.Value, StringComparison.OrdinalIgnoreCase)
        };
    }

    private static bool IsNumericType(string fieldType)
    {
        return fieldType is "number" or "integer" or "decimal" or "float";
    }

    private static bool TryGetDecimal(JsonElement value, out decimal result)
    {
        if (value.ValueKind == JsonValueKind.Number)
        {
            return value.TryGetDecimal(out result);
        }

        if (value.ValueKind == JsonValueKind.String)
        {
            return decimal.TryParse(value.GetString(), out result);
        }

        result = 0;
        return false;
    }

    private static ScorelineFieldItem ToFieldItem(ScorelineField field)
    {
        return new ScorelineFieldItem(
            field.Id,
            field.LegacyId,
            field.RegionId,
            field.FieldKey,
            field.FieldName,
            field.FieldType,
            field.Unit,
            field.IsFilter,
            field.IsRequired,
            field.IsVisible,
            field.IsTrend,
            field.Options,
            field.Placeholder,
            field.Description,
            field.SortOrder);
    }

    private static ScorelineRecordItem ToRecordItem(ScorelineRecord record)
    {
        return new ScorelineRecordItem(
            record.Id,
            record.LegacyId,
            record.RegionId,
            record.SchoolId,
            record.MajorId,
            record.Year,
            record.SchoolName,
            record.MajorName,
            record.FieldValues);
    }

    private static int ResolveLimit(int? limit, int fallback = 500)
    {
        return Math.Clamp(limit ?? fallback, 1, 2000);
    }

    [GeneratedRegex("^[A-Za-z][A-Za-z0-9_]{0,63}$")]
    private static partial Regex FieldKeyRegex();
}

public sealed class ScorelineQueryException(string message, string code) : Exception(message)
{
    public string Code { get; } = code;
}
