using System.Text.Json;
using Kbx.Shared.ExternalData.Generated;
using Kbx.Shared.Providers;

namespace Kbx.Shared.ExternalData;

public sealed record KbxNormalizedExternalData<T>(T? Data, DateTimeOffset? ProviderObservedAt = null);

public interface IKbxExternalDataRefreshScheduler
{
    Task ScheduleEntryAsync(Guid tenantId, string datasetId, string cacheKey, CancellationToken cancellationToken);
    Task ScheduleDatasetAsync(Guid tenantId, string datasetId, CancellationToken cancellationToken);
}

public sealed class KbxExternalDataCacheCoordinator(KbxExternalDataRepository repository, IKbxExternalDataRefreshScheduler refreshScheduler)
{
    public async Task<KbxExternalDataEnvelope<T>> GetOrFetchAsync<T>(
        Guid tenantId,
        string datasetId,
        IReadOnlyDictionary<string,string?> cacheKeyValues,
        Func<CancellationToken,Task<KbxProviderResult<byte[]>>> fetch,
        Func<ReadOnlyMemory<byte>,KbxNormalizedExternalData<T>> normalize,
        string correlationId,
        CancellationToken ct)
    {
        if (!KbxExternalDataCatalog.All.TryGetValue(datasetId, out var dataset)) throw new ArgumentOutOfRangeException(nameof(datasetId));
        if (dataset.FreshnessMode == "provider-defined" && dataset.FreshForSeconds is null)
            throw new InvalidOperationException($"Dataset '{datasetId}' requires an approved service-specific freshness policy before runtime use.");

        var now=DateTimeOffset.UtcNow;
        var cacheKey=KbxExternalDataProvenanceFactory.CacheKey(cacheKeyValues);
        var requestDescriptor=JsonSerializer.SerializeToDocument(cacheKeyValues.OrderBy(x=>x.Key,StringComparer.Ordinal).ToDictionary(x=>x.Key,x=>x.Value));
        var cached=await repository.GetAsync(tenantId,datasetId,cacheKey,ct);
        if(cached is not null)
        {
            var state=KbxExternalDataFreshnessPolicy.Evaluate(dataset,now,cached.FreshUntil,cached.UsableUntil);
            if(state==KbxExternalDataState.Fresh)
                return FromCache<T>(dataset,cached,state,false);
            if(state==KbxExternalDataState.Stale && dataset.FreshnessMode=="stale-while-revalidate")
            {
                await refreshScheduler.ScheduleEntryAsync(tenantId,datasetId,cacheKey,ct);
                return FromCache<T>(dataset,cached,state,true,"공급자 데이터를 다시 확인하는 동안 마지막 정상값을 표시합니다.");
            }
        }

        var requestedAt=DateTimeOffset.UtcNow;
        var result=await fetch(ct);
        var receivedAt=DateTimeOffset.UtcNow;
        if(result.Kind==KbxProviderResultKind.NoData)
        {
            var empty=JsonDocument.Parse("null");
            var (freshUntil,usableUntil)=KbxExternalDataFreshnessPolicy.Window(dataset,receivedAt);
            var entry=new KbxExternalDataCacheEntry(tenantId,dataset.Id,dataset.ProviderId,cacheKey,requestDescriptor,empty,null,requestedAt,receivedAt,DateTimeOffset.UtcNow,freshUntil,usableUntil,KbxExternalDataProvenanceFactory.PayloadSha256(Array.Empty<byte>()),dataset.NormalizerVersion,"fresh",correlationId);
            await repository.UpsertAsync(entry,ct);
            return new(default,KbxExternalDataProvenanceFactory.Create(dataset,KbxExternalDataState.Fresh,requestedAt,receivedAt,entry.IngestedAt,null,freshUntil,usableUntil,entry.PayloadSha256,false));
        }
        if(result.Kind!=KbxProviderResultKind.Success || result.Value is null)
            throw new KbxExternalDataUnavailableException(dataset.Id,result.Code,result.Message);

        var raw=result.Value;
        var normalized=normalize(raw);
        var ingestedAt=DateTimeOffset.UtcNow;
        var window=KbxExternalDataFreshnessPolicy.Window(dataset,receivedAt);
        var document=JsonSerializer.SerializeToDocument(normalized.Data);
        var payloadSha=KbxExternalDataProvenanceFactory.PayloadSha256(raw);
        var stored=new KbxExternalDataCacheEntry(tenantId,dataset.Id,dataset.ProviderId,cacheKey,requestDescriptor,document,normalized.ProviderObservedAt,requestedAt,receivedAt,ingestedAt,window.FreshUntil,window.UsableUntil,payloadSha,dataset.NormalizerVersion,"fresh",correlationId);
        await repository.UpsertAsync(stored,ct);
        return new(normalized.Data,KbxExternalDataProvenanceFactory.Create(dataset,KbxExternalDataState.Fresh,requestedAt,receivedAt,ingestedAt,normalized.ProviderObservedAt,window.FreshUntil,window.UsableUntil,payloadSha,false));
    }

    private static KbxExternalDataEnvelope<T> FromCache<T>(KbxExternalDataDatasetDefinition dataset,KbxExternalDataCacheEntry entry,KbxExternalDataState state,bool refreshing,string? warning=null)
    {
        var data=entry.NormalizedData.RootElement.ValueKind==JsonValueKind.Null?default:entry.NormalizedData.RootElement.Deserialize<T>();
        return new(data,KbxExternalDataProvenanceFactory.Create(dataset,state,entry.RequestedAt,entry.ReceivedAt,entry.IngestedAt,entry.ProviderObservedAt,entry.FreshUntil,entry.UsableUntil,entry.PayloadSha256,true,refreshing,warning));
    }
}

public sealed class KbxExternalDataUnavailableException(string datasetId,string? code,string? detail):Exception($"External dataset '{datasetId}' is unavailable. {code}: {detail}")
{
    public string DatasetId { get; }=datasetId;
    public string? ProviderCode { get; }=code;
}
