using System;
using System.Linq;
using MongoDB.Bson;

namespace StellaOps.Feedser.Source.CertBund.Internal;

internal sealed record CertBundCursor(
    IReadOnlyCollection<Guid> PendingDocuments,
    IReadOnlyCollection<Guid> PendingMappings,
    IReadOnlyCollection<string> KnownAdvisories,
    DateTimeOffset? LastPublished,
    DateTimeOffset? LastFetchAt)
{
    private static readonly IReadOnlyCollection<Guid> EmptyGuids = Array.Empty<Guid>();
    private static readonly IReadOnlyCollection<string> EmptyStrings = Array.Empty<string>();

    public static CertBundCursor Empty { get; } = new(EmptyGuids, EmptyGuids, EmptyStrings, null, null);

    public CertBundCursor WithPendingDocuments(IEnumerable<Guid> documents)
        => this with { PendingDocuments = Distinct(documents) };

    public CertBundCursor WithPendingMappings(IEnumerable<Guid> mappings)
        => this with { PendingMappings = Distinct(mappings) };

    public CertBundCursor WithKnownAdvisories(IEnumerable<string> advisories)
        => this with { KnownAdvisories = advisories?.Distinct(StringComparer.OrdinalIgnoreCase).ToArray() ?? EmptyStrings };

    public CertBundCursor WithLastPublished(DateTimeOffset? published)
        => this with { LastPublished = published };

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

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

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

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

        return document;
    }

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

        var pendingDocuments = ReadGuidArray(document, "pendingDocuments");
        var pendingMappings = ReadGuidArray(document, "pendingMappings");
        var knownAdvisories = ReadStringArray(document, "knownAdvisories");
        var lastPublished = document.TryGetValue("lastPublished", out var publishedValue)
            ? ParseDate(publishedValue)
            : null;
        var lastFetch = document.TryGetValue("lastFetchAt", out var fetchValue)
            ? ParseDate(fetchValue)
            : null;

        return new CertBundCursor(pendingDocuments, pendingMappings, knownAdvisories, lastPublished, lastFetch);
    }

    private static IReadOnlyCollection<Guid> Distinct(IEnumerable<Guid>? values)
        => values?.Distinct().ToArray() ?? EmptyGuids;

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

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

        return items;
    }

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

        return array.Select(element => element?.ToString() ?? string.Empty)
            .Where(static s => !string.IsNullOrWhiteSpace(s))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    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,
        };
}
