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

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

internal sealed record VmwareCursor(
    DateTimeOffset? LastModified,
    IReadOnlyCollection<string> ProcessedIds,
    IReadOnlyCollection<Guid> PendingDocuments,
    IReadOnlyCollection<Guid> PendingMappings,
    IReadOnlyDictionary<string, VmwareFetchCacheEntry> FetchCache)
{
    private static readonly IReadOnlyCollection<Guid> EmptyGuidList = Array.Empty<Guid>();
    private static readonly IReadOnlyCollection<string> EmptyStringList = Array.Empty<string>();
    private static readonly IReadOnlyDictionary<string, VmwareFetchCacheEntry> EmptyFetchCache =
        new Dictionary<string, VmwareFetchCacheEntry>(StringComparer.OrdinalIgnoreCase);

    public static VmwareCursor Empty { get; } = new(null, EmptyStringList, EmptyGuidList, EmptyGuidList, EmptyFetchCache);

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

        if (LastModified.HasValue)
        {
            document["lastModified"] = LastModified.Value.UtcDateTime;
        }

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

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

            document["fetchCache"] = cacheDocument;
        }

        return document;
    }

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

        var lastModified = document.TryGetValue("lastModified", out var value)
            ? ParseDate(value)
            : null;

        var processedIds = document.TryGetValue("processedIds", out var processedValue) && processedValue is BsonArray idsArray
            ? idsArray.OfType<BsonValue>()
                .Where(static x => x.BsonType == BsonType.String)
                .Select(static x => x.AsString)
                .ToArray()
            : EmptyStringList;

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

        return new VmwareCursor(lastModified, processedIds, pendingDocuments, pendingMappings, fetchCache);
    }

    public VmwareCursor WithLastModified(DateTimeOffset timestamp, IEnumerable<string> processedIds)
        => this with
        {
            LastModified = timestamp.ToUniversalTime(),
            ProcessedIds = processedIds?.Where(static id => !string.IsNullOrWhiteSpace(id))
                .Select(static id => id.Trim())
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToArray() ?? EmptyStringList,
        };

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

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

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

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

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

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

    public VmwareCursor AddProcessedId(string id)
    {
        if (string.IsNullOrWhiteSpace(id))
        {
            return this;
        }

        var set = new HashSet<string>(ProcessedIds, StringComparer.OrdinalIgnoreCase) { id.Trim() };
        return this with { ProcessedIds = set.ToArray() };
    }

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

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

        return results;
    }

    private static IReadOnlyDictionary<string, VmwareFetchCacheEntry> ReadFetchCache(BsonDocument document)
    {
        if (!document.TryGetValue("fetchCache", out var value) || value is not BsonDocument cacheDocument || cacheDocument.ElementCount == 0)
        {
            return EmptyFetchCache;
        }

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

        return cache;
    }

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