using MongoDB.Bson;

namespace StellaOps.Feedser.Source.Acsc.Internal;

internal enum AcscEndpointPreference
{
    Auto = 0,
    Direct = 1,
    Relay = 2,
}

internal sealed record AcscCursor(
    AcscEndpointPreference PreferredEndpoint,
    IReadOnlyDictionary<string, DateTimeOffset?> LastPublishedByFeed,
    IReadOnlyCollection<Guid> PendingDocuments,
    IReadOnlyCollection<Guid> PendingMappings)
{
    private static readonly IReadOnlyCollection<Guid> EmptyGuidList = Array.Empty<Guid>();
    private static readonly IReadOnlyDictionary<string, DateTimeOffset?> EmptyFeedDictionary =
        new Dictionary<string, DateTimeOffset?>(StringComparer.OrdinalIgnoreCase);

    public static AcscCursor Empty { get; } = new(
        AcscEndpointPreference.Auto,
        EmptyFeedDictionary,
        EmptyGuidList,
        EmptyGuidList);

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

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

    public AcscCursor WithPreferredEndpoint(AcscEndpointPreference preference)
        => this with { PreferredEndpoint = preference };

    public AcscCursor WithLastPublished(IDictionary<string, DateTimeOffset?> values)
    {
        var snapshot = new Dictionary<string, DateTimeOffset?>(StringComparer.OrdinalIgnoreCase);
        if (values is not null)
        {
            foreach (var kvp in values)
            {
                snapshot[kvp.Key] = kvp.Value;
            }
        }

        return this with { LastPublishedByFeed = snapshot };
    }

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

        var feedsDocument = new BsonDocument();
        foreach (var kvp in LastPublishedByFeed)
        {
            if (kvp.Value.HasValue)
            {
                feedsDocument[kvp.Key] = kvp.Value.Value.UtcDateTime;
            }
        }

        document["feeds"] = feedsDocument;
        return document;
    }

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

        var preferredEndpoint = document.TryGetValue("preferredEndpoint", out var endpointValue)
            ? ParseEndpointPreference(endpointValue.AsString)
            : AcscEndpointPreference.Auto;

        var feeds = new Dictionary<string, DateTimeOffset?>(StringComparer.OrdinalIgnoreCase);
        if (document.TryGetValue("feeds", out var feedsValue) && feedsValue is BsonDocument feedsDocument)
        {
            foreach (var element in feedsDocument.Elements)
            {
                feeds[element.Name] = ParseDate(element.Value);
            }
        }

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

        return new AcscCursor(
            preferredEndpoint,
            feeds,
            pendingDocuments,
            pendingMappings);
    }

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

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

        return list;
    }

    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 AcscEndpointPreference ParseEndpointPreference(string? value)
    {
        if (Enum.TryParse<AcscEndpointPreference>(value, ignoreCase: true, out var parsed))
        {
            return parsed;
        }

        return AcscEndpointPreference.Auto;
    }
}
