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

namespace StellaOps.Feedser.Source.Distro.Ubuntu.Internal;

internal sealed record UbuntuCursor(
    DateTimeOffset? LastPublished,
    IReadOnlyCollection<string> ProcessedNoticeIds,
    IReadOnlyCollection<Guid> PendingDocuments,
    IReadOnlyCollection<Guid> PendingMappings,
    IReadOnlyDictionary<string, UbuntuFetchCacheEntry> FetchCache)
{
    private static readonly IReadOnlyCollection<string> EmptyIds = Array.Empty<string>();
    private static readonly IReadOnlyCollection<Guid> EmptyGuidList = Array.Empty<Guid>();
    private static readonly IReadOnlyDictionary<string, UbuntuFetchCacheEntry> EmptyCache =
        new Dictionary<string, UbuntuFetchCacheEntry>(StringComparer.OrdinalIgnoreCase);

    public static UbuntuCursor Empty { get; } = new(null, EmptyIds, EmptyGuidList, EmptyGuidList, EmptyCache);

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

        DateTimeOffset? lastPublished = null;
        if (document.TryGetValue("lastPublished", out var value))
        {
            lastPublished = value.BsonType switch
            {
                BsonType.DateTime => DateTime.SpecifyKind(value.ToUniversalTime(), DateTimeKind.Utc),
                BsonType.String when DateTimeOffset.TryParse(value.AsString, out var parsed) => parsed.ToUniversalTime(),
                _ => null
            };
        }

        var processed = ReadStringSet(document, "processedIds");
        var pendingDocuments = ReadGuidSet(document, "pendingDocuments");
        var pendingMappings = ReadGuidSet(document, "pendingMappings");
        var cache = ReadCache(document);

        return new UbuntuCursor(lastPublished, processed, pendingDocuments, pendingMappings, cache);
    }

    public BsonDocument ToBsonDocument()
    {
        var doc = new BsonDocument
        {
            ["pendingDocuments"] = new BsonArray(PendingDocuments.Select(id => id.ToString())),
            ["pendingMappings"] = new BsonArray(PendingMappings.Select(id => id.ToString()))
        };

        if (LastPublished.HasValue)
        {
            doc["lastPublished"] = LastPublished.Value.UtcDateTime;
        }

        if (ProcessedNoticeIds.Count > 0)
        {
            doc["processedIds"] = new BsonArray(ProcessedNoticeIds);
        }

        if (FetchCache.Count > 0)
        {
            var cacheDoc = new BsonDocument();
            foreach (var (key, entry) in FetchCache)
            {
                cacheDoc[key] = entry.ToBsonDocument();
            }

            doc["fetchCache"] = cacheDoc;
        }

        return doc;
    }

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

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

    public UbuntuCursor WithFetchCache(IDictionary<string, UbuntuFetchCacheEntry>? cache)
    {
        if (cache is null || cache.Count == 0)
        {
            return this with { FetchCache = EmptyCache };
        }

        return this with { FetchCache = new Dictionary<string, UbuntuFetchCacheEntry>(cache, StringComparer.OrdinalIgnoreCase) };
    }

    public UbuntuCursor WithProcessed(DateTimeOffset published, IEnumerable<string> ids)
        => this with
        {
            LastPublished = published.ToUniversalTime(),
            ProcessedNoticeIds = ids?.Where(static id => !string.IsNullOrWhiteSpace(id))
                .Select(static id => id.Trim())
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToArray() ?? EmptyIds
        };

    public bool TryGetCache(string key, out UbuntuFetchCacheEntry entry)
    {
        if (FetchCache.Count == 0)
        {
            entry = UbuntuFetchCacheEntry.Empty;
            return false;
        }

        return FetchCache.TryGetValue(key, out entry!);
    }

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

        var list = new List<string>(array.Count);
        foreach (var element in array)
        {
            if (element.BsonType == BsonType.String)
            {
                var str = element.AsString.Trim();
                if (!string.IsNullOrWhiteSpace(str))
                {
                    list.Add(str);
                }
            }
        }

        return list;
    }

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

        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, UbuntuFetchCacheEntry> ReadCache(BsonDocument document)
    {
        if (!document.TryGetValue("fetchCache", out var value) || value is not BsonDocument cacheDoc || cacheDoc.ElementCount == 0)
        {
            return EmptyCache;
        }

        var cache = new Dictionary<string, UbuntuFetchCacheEntry>(StringComparer.OrdinalIgnoreCase);
        foreach (var element in cacheDoc.Elements)
        {
            if (element.Value is BsonDocument entryDoc)
            {
                cache[element.Name] = UbuntuFetchCacheEntry.FromBson(entryDoc);
            }
        }

        return cache;
    }
}
