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

namespace StellaOps.Feedser.Source.Ics.Kaspersky.Internal;

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

    public static KasperskyCursor Empty { get; } = new(null, 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 (LastPublished.HasValue)
        {
            document["lastPublished"] = LastPublished.Value.UtcDateTime;
        }

        if (FetchCache.Count > 0)
        {
            var cacheArray = new BsonArray();
            foreach (var (uri, metadata) in FetchCache)
            {
                var cacheDocument = new BsonDocument
                {
                    ["uri"] = uri,
                };

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

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

                cacheArray.Add(cacheDocument);
            }

            document["fetchCache"] = cacheArray;
        }

        return document;
    }

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

        var lastPublished = document.TryGetValue("lastPublished", out var lastPublishedValue)
            ? ParseDate(lastPublishedValue)
            : null;

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

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

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

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

    public KasperskyCursor WithLastPublished(DateTimeOffset? timestamp)
        => this with { LastPublished = timestamp };

    public KasperskyCursor WithFetchMetadata(string requestUri, string? etag, DateTimeOffset? lastModified)
    {
        if (string.IsNullOrWhiteSpace(requestUri))
        {
            return this;
        }

        var cache = new Dictionary<string, KasperskyFetchMetadata>(FetchCache, StringComparer.OrdinalIgnoreCase)
        {
            [requestUri] = new KasperskyFetchMetadata(etag, lastModified),
        };

        return this with { FetchCache = cache };
    }

    public KasperskyCursor 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;
        }

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

        return this with { FetchCache = cache };
    }

    public bool TryGetFetchMetadata(string requestUri, out KasperskyFetchMetadata metadata)
    {
        if (FetchCache.TryGetValue(requestUri, out metadata!))
        {
            return true;
        }

        metadata = default!;
        return false;
    }

    private static DateTimeOffset? ParseDate(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 results = new List<Guid>(array.Count);
        foreach (var element in array)
        {
            if (element is null)
            {
                continue;
            }

            if (Guid.TryParse(element.ToString(), out var guid))
            {
                results.Add(guid);
            }
        }

        return results;
    }

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

        var cache = new Dictionary<string, KasperskyFetchMetadata>(StringComparer.OrdinalIgnoreCase);
        foreach (var element in array)
        {
            if (element is not BsonDocument cacheDocument)
            {
                continue;
            }

            if (!cacheDocument.TryGetValue("uri", out var uriValue) || uriValue.BsonType != BsonType.String)
            {
                continue;
            }

            var uri = uriValue.AsString;
            string? etag = cacheDocument.TryGetValue("etag", out var etagValue) && etagValue.IsString ? etagValue.AsString : null;
            DateTimeOffset? lastModified = cacheDocument.TryGetValue("lastModified", out var lastModifiedValue)
                ? ParseDate(lastModifiedValue)
                : null;

            cache[uri] = new KasperskyFetchMetadata(etag, lastModified);
        }

        return cache.Count == 0 ? EmptyFetchCache : cache;
    }
}

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