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

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

internal sealed record RedHatCursor(
    DateTimeOffset? LastReleasedOn,
    IReadOnlyCollection<string> ProcessedAdvisoryIds,
    IReadOnlyCollection<Guid> PendingDocuments,
    IReadOnlyCollection<Guid> PendingMappings,
    IReadOnlyDictionary<string, RedHatCachedFetchMetadata> FetchCache)
{
    private static readonly IReadOnlyCollection<string> EmptyStringList = Array.Empty<string>();
    private static readonly IReadOnlyCollection<Guid> EmptyGuidList = Array.Empty<Guid>();
    private static readonly IReadOnlyDictionary<string, RedHatCachedFetchMetadata> EmptyCache =
        new Dictionary<string, RedHatCachedFetchMetadata>(StringComparer.OrdinalIgnoreCase);

    public static RedHatCursor Empty { get; } = new(null, EmptyStringList, EmptyGuidList, EmptyGuidList, EmptyCache);

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

        DateTimeOffset? lastReleased = null;
        if (document.TryGetValue("lastReleasedOn", out var lastReleasedValue))
        {
            lastReleased = ReadDateTimeOffset(lastReleasedValue);
        }

        var processed = ReadStringSet(document, "processedAdvisories");
        var pendingDocuments = ReadGuidSet(document, "pendingDocuments");
        var pendingMappings = ReadGuidSet(document, "pendingMappings");
        var fetchCache = ReadFetchCache(document);

        return new RedHatCursor(lastReleased, processed, pendingDocuments, pendingMappings, fetchCache);
    }

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

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

        var cacheArray = new BsonArray();
        foreach (var (key, metadata) in FetchCache)
        {
            var cacheDoc = new BsonDocument
            {
                ["uri"] = key
            };

            if (!string.IsNullOrWhiteSpace(metadata.ETag))
            {
                cacheDoc["etag"] = metadata.ETag;
            }

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

            cacheArray.Add(cacheDoc);
        }

        document["fetchCache"] = cacheArray;
        return document;
    }

    public RedHatCursor WithLastReleased(DateTimeOffset? releasedOn, IEnumerable<string> advisoryIds)
    {
        var normalizedIds = advisoryIds?.Where(static id => !string.IsNullOrWhiteSpace(id))
            .Select(static id => id.Trim())
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray() ?? Array.Empty<string>();

        return this with
        {
            LastReleasedOn = releasedOn,
            ProcessedAdvisoryIds = normalizedIds
        };
    }

    public RedHatCursor AddProcessedAdvisories(IEnumerable<string> advisoryIds)
    {
        if (advisoryIds is null)
        {
            return this;
        }

        var set = new HashSet<string>(ProcessedAdvisoryIds, StringComparer.OrdinalIgnoreCase);
        foreach (var id in advisoryIds)
        {
            if (!string.IsNullOrWhiteSpace(id))
            {
                set.Add(id.Trim());
            }
        }

        return this with { ProcessedAdvisoryIds = set.ToArray() };
    }

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

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

    public RedHatCursor WithFetchCache(string requestUri, string? etag, DateTimeOffset? lastModified)
    {
        var cache = new Dictionary<string, RedHatCachedFetchMetadata>(FetchCache, StringComparer.OrdinalIgnoreCase)
        {
            [requestUri] = new RedHatCachedFetchMetadata(etag, lastModified)
        };

        return this with { FetchCache = cache };
    }

    public RedHatCursor PruneFetchCache(IEnumerable<string> keepUris)
    {
        if (FetchCache.Count == 0)
        {
            return this;
        }

        var keepSet = new HashSet<string>(keepUris ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
        if (keepSet.Count == 0)
        {
            return this with { FetchCache = EmptyCache };
        }

        var cache = new Dictionary<string, RedHatCachedFetchMetadata>(StringComparer.OrdinalIgnoreCase);
        foreach (var uri in keepSet)
        {
            if (FetchCache.TryGetValue(uri, out var metadata))
            {
                cache[uri] = metadata;
            }
        }

        return this with { FetchCache = cache };
    }

    public RedHatCachedFetchMetadata? TryGetFetchCache(string requestUri)
    {
        if (FetchCache.TryGetValue(requestUri, out var metadata))
        {
            return metadata;
        }

        return null;
    }

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

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

        return results;
    }

    private static IReadOnlyCollection<Guid> ReadGuidSet(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 (element.BsonType == BsonType.String && Guid.TryParse(element.AsString, out var guid))
            {
                results.Add(guid);
            }
        }

        return results;
    }

    private static IReadOnlyDictionary<string, RedHatCachedFetchMetadata> ReadFetchCache(BsonDocument document)
    {
        if (!document.TryGetValue("fetchCache", out var value) || value is not BsonArray array || array.Count == 0)
        {
            return EmptyCache;
        }

        var results = new Dictionary<string, RedHatCachedFetchMetadata>(StringComparer.OrdinalIgnoreCase);
        foreach (var element in array.OfType<BsonDocument>())
        {
            if (!element.TryGetValue("uri", out var uriValue) || uriValue.BsonType != BsonType.String)
            {
                continue;
            }

            var uri = uriValue.AsString;
            var etag = element.TryGetValue("etag", out var etagValue) && etagValue.BsonType == BsonType.String
                ? etagValue.AsString
                : null;
            DateTimeOffset? lastModified = null;
            if (element.TryGetValue("lastModified", out var lastModifiedValue))
            {
                lastModified = ReadDateTimeOffset(lastModifiedValue);
            }

            results[uri] = new RedHatCachedFetchMetadata(etag, lastModified);
        }

        return results;
    }

    private static DateTimeOffset? ReadDateTimeOffset(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,
        };
    }
}

internal sealed record RedHatCachedFetchMetadata(string? ETag, DateTimeOffset? LastModified);
