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

namespace StellaOps.Feedser.Source.Cccs.Internal;

internal sealed record CccsCursor(
    IReadOnlyCollection<Guid> PendingDocuments,
    IReadOnlyCollection<Guid> PendingMappings,
    IReadOnlyDictionary<string, string> KnownEntryHashes,
    DateTimeOffset? LastFetchAt)
{
    private static readonly IReadOnlyCollection<Guid> EmptyGuidCollection = Array.Empty<Guid>();
    private static readonly IReadOnlyDictionary<string, string> EmptyHashes = new Dictionary<string, string>(StringComparer.Ordinal);

    public static CccsCursor Empty { get; } = new(EmptyGuidCollection, EmptyGuidCollection, EmptyHashes, null);

    public CccsCursor WithPendingDocuments(IEnumerable<Guid> documents)
    {
        var distinct = (documents ?? Enumerable.Empty<Guid>()).Distinct().ToArray();
        return this with { PendingDocuments = distinct };
    }

    public CccsCursor WithPendingMappings(IEnumerable<Guid> mappings)
    {
        var distinct = (mappings ?? Enumerable.Empty<Guid>()).Distinct().ToArray();
        return this with { PendingMappings = distinct };
    }

    public CccsCursor WithKnownEntryHashes(IReadOnlyDictionary<string, string> hashes)
    {
        var map = hashes is null || hashes.Count == 0
            ? EmptyHashes
            : new Dictionary<string, string>(hashes, StringComparer.Ordinal);
        return this with { KnownEntryHashes = map };
    }

    public CccsCursor WithLastFetch(DateTimeOffset? timestamp)
        => this with { LastFetchAt = timestamp };

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

        if (KnownEntryHashes.Count > 0)
        {
            var hashes = new BsonArray();
            foreach (var kvp in KnownEntryHashes)
            {
                hashes.Add(new BsonDocument
                {
                    ["uri"] = kvp.Key,
                    ["hash"] = kvp.Value,
                });
            }

            doc["knownEntryHashes"] = hashes;
        }

        if (LastFetchAt.HasValue)
        {
            doc["lastFetchAt"] = LastFetchAt.Value.UtcDateTime;
        }

        return doc;
    }

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

        var pendingDocuments = ReadGuidArray(document, "pendingDocuments");
        var pendingMappings = ReadGuidArray(document, "pendingMappings");
        var hashes = ReadHashMap(document);
        var lastFetch = document.TryGetValue("lastFetchAt", out var value)
            ? ParseDateTime(value)
            : null;

        return new CccsCursor(pendingDocuments, pendingMappings, hashes, lastFetch);
    }

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

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

        return items;
    }

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

        var map = new Dictionary<string, string>(array.Count, StringComparer.Ordinal);
        foreach (var element in array)
        {
            if (element is not BsonDocument entry)
            {
                continue;
            }

            if (!entry.TryGetValue("uri", out var uriValue) || uriValue.IsBsonNull || string.IsNullOrWhiteSpace(uriValue.AsString))
            {
                continue;
            }

            var hash = entry.TryGetValue("hash", out var hashValue) && !hashValue.IsBsonNull
                ? hashValue.AsString
                : string.Empty;
            map[uriValue.AsString] = hash;
        }

        return map;
    }

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