using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Application.App.Common.Geo;
using TakeoutSaaS.Domain.Common.Enums;
using TakeoutSaaS.Domain.Merchants.Entities;
using TakeoutSaaS.Domain.Stores.Entities;
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Infrastructure.App.Persistence;
using TakeoutSaaS.Shared.Abstractions.Tenancy;

namespace TakeoutSaaS.TenantApi.Services;

/// <summary>
/// 商户/门店/租户地理定位编排服务。
/// </summary>
public sealed class GeoLocationOrchestrator(
    TakeoutAppDbContext dbContext,
    IAddressGeocodingService geocodingService,
    ITenantContextAccessor tenantContextAccessor,
    ILogger<GeoLocationOrchestrator> logger)
{
    private const int BatchSizePerEntity = 30;

    /// <summary>
    /// 手动重试门店地理编码。
    /// </summary>
    public async Task<bool> RetryStoreAsync(
        long tenantId,
        long merchantId,
        long storeId,
        CancellationToken cancellationToken)
    {
        var store = await dbContext.Stores
            .FirstOrDefaultAsync(
                x => x.Id == storeId && x.TenantId == tenantId && x.MerchantId == merchantId,
                cancellationToken);
        if (store is null)
        {
            logger.LogWarning("手动重试门店定位失败：Store={StoreId}, Tenant={TenantId}, Merchant={MerchantId}", storeId, tenantId, merchantId);
            return false;
        }

        var merchant = await dbContext.Merchants
            .AsNoTracking()
            .FirstOrDefaultAsync(x => x.Id == merchantId && x.TenantId == tenantId, cancellationToken);
        store.GeoRetryCount = 0;
        await GeocodeStoreAsync(store, merchant, DateTime.UtcNow, isRetry: true, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        logger.LogInformation("手动重试门店定位已执行：Store={StoreId}", storeId);
        return true;
    }

    /// <summary>
    /// 手动重试商户地理编码。
    /// </summary>
    public async Task<bool> RetryMerchantAsync(
        long tenantId,
        long merchantId,
        CancellationToken cancellationToken)
    {
        var merchant = await dbContext.Merchants
            .FirstOrDefaultAsync(x => x.Id == merchantId && x.TenantId == tenantId, cancellationToken);
        if (merchant is null)
        {
            logger.LogWarning("手动重试商户定位失败：Merchant={MerchantId}, Tenant={TenantId}", merchantId, tenantId);
            return false;
        }

        merchant.GeoRetryCount = 0;
        await GeocodeMerchantAsync(merchant, DateTime.UtcNow, isRetry: true, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        logger.LogInformation("手动重试商户定位已执行：Merchant={MerchantId}", merchantId);
        return true;
    }

    /// <summary>
    /// 后台批量处理待定位记录。
    /// </summary>
    public async Task<int> ProcessPendingAsync(CancellationToken cancellationToken)
    {
        var now = DateTime.UtcNow;
        var processedCount = 0;
        var tenantIds = await CollectPendingTenantIdsAsync(now, cancellationToken);
        foreach (var tenantId in tenantIds)
        {
            processedCount += await ProcessTenantEntitiesAsync(tenantId, now, cancellationToken);
        }

        var pendingTenants = await dbContext.Tenants
            .Where(x =>
                x.GeoStatus != GeoLocationStatus.Success &&
                x.GeoRetryCount < GeoLocationStateHelper.MaxRetryCount &&
                (x.GeoNextRetryAt == null || x.GeoNextRetryAt <= now))
            .OrderBy(x => x.GeoNextRetryAt)
            .ThenBy(x => x.Id)
            .Take(BatchSizePerEntity)
            .ToListAsync(cancellationToken);

        foreach (var tenant in pendingTenants)
        {
            await GeocodeTenantAsync(tenant, now, isRetry: true, cancellationToken);
        }

        processedCount += pendingTenants.Count;

        if (processedCount > 0)
        {
            await dbContext.SaveChangesAsync(cancellationToken);
            logger.LogDebug("地理定位批处理完成，处理记录数：{Count}", processedCount);
        }

        return processedCount;
    }

    private async Task<IReadOnlyList<long>> CollectPendingTenantIdsAsync(DateTime now, CancellationToken cancellationToken)
    {
        var storeTenantIds = await dbContext.Stores
            .IgnoreQueryFilters()
            .AsNoTracking()
            .Where(x =>
                x.DeletedAt == null &&
                x.GeoStatus != GeoLocationStatus.Success &&
                x.GeoRetryCount < GeoLocationStateHelper.MaxRetryCount &&
                (x.GeoNextRetryAt == null || x.GeoNextRetryAt <= now))
            .Select(x => x.TenantId)
            .Distinct()
            .ToListAsync(cancellationToken);

        var merchantTenantIds = await dbContext.Merchants
            .IgnoreQueryFilters()
            .AsNoTracking()
            .Where(x =>
                x.DeletedAt == null &&
                x.GeoStatus != GeoLocationStatus.Success &&
                x.GeoRetryCount < GeoLocationStateHelper.MaxRetryCount &&
                (x.GeoNextRetryAt == null || x.GeoNextRetryAt <= now))
            .Select(x => x.TenantId)
            .Distinct()
            .ToListAsync(cancellationToken);

        return storeTenantIds
            .Concat(merchantTenantIds)
            .Distinct()
            .ToList();
    }

    private async Task<int> ProcessTenantEntitiesAsync(
        long tenantId,
        DateTime now,
        CancellationToken cancellationToken)
    {
        var previousContext = tenantContextAccessor.Current;
        tenantContextAccessor.Current = new TenantContext(tenantId, $"t{tenantId}", "geo-location-retry");
        try
        {
            var processedCount = 0;
            var pendingStores = await dbContext.Stores
                .Where(x =>
                    x.GeoStatus != GeoLocationStatus.Success &&
                    x.GeoRetryCount < GeoLocationStateHelper.MaxRetryCount &&
                    (x.GeoNextRetryAt == null || x.GeoNextRetryAt <= now))
                .OrderBy(x => x.GeoNextRetryAt)
                .ThenBy(x => x.Id)
                .Take(BatchSizePerEntity)
                .ToListAsync(cancellationToken);

            foreach (var store in pendingStores)
            {
                var merchant = await dbContext.Merchants
                    .AsNoTracking()
                    .FirstOrDefaultAsync(x => x.Id == store.MerchantId, cancellationToken);
                await GeocodeStoreAsync(store, merchant, now, isRetry: true, cancellationToken);
            }

            processedCount += pendingStores.Count;

            var pendingMerchants = await dbContext.Merchants
                .Where(x =>
                    x.GeoStatus != GeoLocationStatus.Success &&
                    x.GeoRetryCount < GeoLocationStateHelper.MaxRetryCount &&
                    (x.GeoNextRetryAt == null || x.GeoNextRetryAt <= now))
                .OrderBy(x => x.GeoNextRetryAt)
                .ThenBy(x => x.Id)
                .Take(BatchSizePerEntity)
                .ToListAsync(cancellationToken);

            foreach (var merchant in pendingMerchants)
            {
                await GeocodeMerchantAsync(merchant, now, isRetry: true, cancellationToken);
            }

            processedCount += pendingMerchants.Count;

            if (processedCount > 0)
            {
                await dbContext.SaveChangesAsync(cancellationToken);
            }

            return processedCount;
        }
        finally
        {
            dbContext.ChangeTracker.Clear();
            tenantContextAccessor.Current = previousContext;
        }
    }

    private async Task GeocodeStoreAsync(
        Store store,
        Merchant? merchant,
        DateTime now,
        bool isRetry,
        CancellationToken cancellationToken)
    {
        var candidates = GeoAddressBuilder.BuildStoreCandidates(store, merchant);
        if (candidates.Count == 0)
        {
            WriteFailedState(store, "缺少地址信息，无法定位", now, isRetry);
            return;
        }

        var geocodeResult = await ResolveCandidatesAsync(candidates, cancellationToken);
        if (geocodeResult.Succeeded && geocodeResult.Latitude is not null && geocodeResult.Longitude is not null)
        {
            GeoLocationStateHelper.MarkSuccess(
                store,
                (double)geocodeResult.Latitude.Value,
                (double)geocodeResult.Longitude.Value,
                now);
            return;
        }

        WriteFailedState(store, geocodeResult.Message, now, isRetry);
    }

    private async Task GeocodeMerchantAsync(
        Merchant merchant,
        DateTime now,
        bool isRetry,
        CancellationToken cancellationToken)
    {
        var candidates = GeoAddressBuilder.BuildMerchantCandidates(merchant);
        if (candidates.Count == 0)
        {
            WriteFailedState(merchant, "缺少地址信息，无法定位", now, isRetry);
            return;
        }

        var geocodeResult = await ResolveCandidatesAsync(candidates, cancellationToken);
        if (geocodeResult.Succeeded && geocodeResult.Latitude is not null && geocodeResult.Longitude is not null)
        {
            GeoLocationStateHelper.MarkSuccess(
                merchant,
                (double)geocodeResult.Latitude.Value,
                (double)geocodeResult.Longitude.Value,
                now);
            return;
        }

        WriteFailedState(merchant, geocodeResult.Message, now, isRetry);
    }

    private async Task GeocodeTenantAsync(
        Tenant tenant,
        DateTime now,
        bool isRetry,
        CancellationToken cancellationToken)
    {
        var candidates = GeoAddressBuilder.BuildTenantCandidates(tenant);
        if (candidates.Count == 0)
        {
            WriteFailedState(tenant, "缺少地址信息，无法定位", now, isRetry);
            return;
        }

        var geocodeResult = await ResolveCandidatesAsync(candidates, cancellationToken);
        if (geocodeResult.Succeeded && geocodeResult.Latitude is not null && geocodeResult.Longitude is not null)
        {
            GeoLocationStateHelper.MarkSuccess(
                tenant,
                (double)geocodeResult.Latitude.Value,
                (double)geocodeResult.Longitude.Value,
                now);
            return;
        }

        WriteFailedState(tenant, geocodeResult.Message, now, isRetry);
    }

    private async Task<AddressGeocodingResult> ResolveCandidatesAsync(
        IReadOnlyList<string> candidates,
        CancellationToken cancellationToken)
    {
        string? lastError = null;
        foreach (var candidate in candidates)
        {
            var result = await geocodingService.GeocodeAsync(candidate, cancellationToken);
            if (result.Succeeded)
            {
                return result;
            }

            lastError = result.Message;
        }

        return AddressGeocodingResult.Failed(lastError ?? "地址地理编码失败");
    }

    private static void WriteFailedState(Store store, string? reason, DateTime now, bool isRetry)
    {
        if (isRetry)
        {
            GeoLocationStateHelper.MarkRetryFailure(store, reason, now);
            return;
        }

        GeoLocationStateHelper.MarkPending(store, reason, now);
    }

    private static void WriteFailedState(Merchant merchant, string? reason, DateTime now, bool isRetry)
    {
        if (isRetry)
        {
            GeoLocationStateHelper.MarkRetryFailure(merchant, reason, now);
            return;
        }

        GeoLocationStateHelper.MarkPending(merchant, reason, now);
    }

    private static void WriteFailedState(Tenant tenant, string? reason, DateTime now, bool isRetry)
    {
        if (isRetry)
        {
            GeoLocationStateHelper.MarkRetryFailure(tenant, reason, now);
            return;
        }

        GeoLocationStateHelper.MarkPending(tenant, reason, now);
    }
}
