using System;
using System.Collections.Generic;
using System.Linq;
using MongoDB.Bson;

namespace StellaOps.Feedser.Source.Vndr.Adobe.Internal;

internal sealed record AdobeCursor(
    DateTimeOffset? LastPublished,
    IReadOnlyCollection<Guid> PendingDocuments,
    IReadOnlyCollection<Guid> PendingMappings,
    IReadOnlyDictionary<string, AdobeFetchCacheEntry>? FetchCache)
{
    public static AdobeCursor Empty { get; } = new(null, Array.Empty<Guid>(), Array.Empty<Guid>(), null);

    public BsonDocument ToBsonDocument()
    {
        var document = new BsonDocument();
        if (LastPublished.HasValue)
        {
            document["lastPublished"] = LastPublished.Value.UtcDateTime;
        }

        document["pendingDocuments"] = new BsonArray(PendingDocuments.Select(id => id.ToString()));
        document["pendingMappings"] = new BsonArray(PendingMappings.Select(id => id.ToString()));

        if (FetchCache is { Count: > 0 })
        {
            var cacheDocument = new BsonDocument();
            foreach (var (key, entry) in FetchCache)
            {
                cacheDocument[key] = entry.ToBson();
            }

            document["fetchCache"] = cacheDocument;
        }

        return document;
    }

    public static AdobeCursor FromBsonDocument(BsonDocument? document)
    {
        if (document is null || document.ElementCount == 0)
        {
            return Empty;
        }

        DateTimeOffset? lastPublished = null;
        if (document.TryGetValue("lastPublished", out var lastPublishedValue))
        {
            lastPublished = ReadDateTime(lastPublishedValue);
        }

        var pendingDocuments = ReadGuidArray(document, "pendingDocuments");
        var pendingMappings = ReadGuidArray(document, "pendingMappings");
        var fetchCache = ReadFetchCache(document);

        return new AdobeCursor(lastPublished, pendingDocuments, pendingMappings, fetchCache);
    }

    public AdobeCursor WithLastPublished(DateTimeOffset? value)
        => this with { LastPublished = value?.ToUniversalTime() };

    public AdobeCursor WithPendingDocuments(IEnumerable<Guid> ids)
        => this with { PendingDocuments = ids?.Distinct().ToArray() ?? Array.Empty<Guid>() };

    public AdobeCursor WithPendingMappings(IEnumerable<Guid> ids)
        => this with { PendingMappings = ids?.Distinct().ToArray() ?? Array.Empty<Guid>() };

    public AdobeCursor WithFetchCache(IDictionary<string, AdobeFetchCacheEntry>? cache)
    {
        if (cache is null)
        {
            return this with { FetchCache = null };
        }

        var target = new Dictionary<string, AdobeFetchCacheEntry>(cache, StringComparer.Ordinal);
        return this with { FetchCache = target };
    }

    public bool TryGetFetchCache(string key, out AdobeFetchCacheEntry entry)
    {
        var cache = FetchCache;
        if (cache is null)
        {
            entry = AdobeFetchCacheEntry.Empty;
            return false;
        }

        if (cache.TryGetValue(key, out var value) && value is not null)
        {
            entry = value;
            return true;
        }

        entry = AdobeFetchCacheEntry.Empty;
        return false;
    }

    private static DateTimeOffset? ReadDateTime(BsonValue value)
    {
        return value.BsonType switch
        {
            BsonType.DateTime => DateTime.SpecifyKind(value.ToUniversalTime(), DateTimeKind.Utc),
            BsonType.String when DateTimeOffset.TryParse(value.AsString, out var parsed) => parsed.ToUniversalTime(),
            _ => null,
        };
    }

    private static IReadOnlyCollection<Guid> ReadGuidArray(BsonDocument document, string field)
    {
        if (!document.TryGetValue(field, out var value) || value is not BsonArray array)
        {
            return Array.Empty<Guid>();
        }

        var list = new List<Guid>(array.Count);
        foreach (var element in array)
        {
            if (Guid.TryParse(element.ToString(), out var guid))
            {
                list.Add(guid);
            }
        }

        return list;
    }

    private static IReadOnlyDictionary<string, AdobeFetchCacheEntry>? ReadFetchCache(BsonDocument document)
    {
        if (!document.TryGetValue("fetchCache", out var value) || value is not BsonDocument cacheDocument)
        {
            return null;
        }

        var dictionary = new Dictionary<string, AdobeFetchCacheEntry>(StringComparer.Ordinal);
        foreach (var element in cacheDocument.Elements)
        {
            if (element.Value is BsonDocument entryDocument)
            {
                dictionary[element.Name] = AdobeFetchCacheEntry.FromBson(entryDocument);
            }
        }

        return dictionary;
    }
}

internal sealed record AdobeFetchCacheEntry(string Sha256)
{
    public static AdobeFetchCacheEntry Empty { get; } = new(string.Empty);

    public BsonDocument ToBson()
    {
        var document = new BsonDocument
        {
            ["sha256"] = Sha256,
        };

        return document;
    }

    public static AdobeFetchCacheEntry FromBson(BsonDocument document)
    {
        var sha = document.TryGetValue("sha256", out var shaValue) ? shaValue.AsString : string.Empty;
        return new AdobeFetchCacheEntry(sha);
    }
}
