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

namespace StellaOps.Feedser.Source.Ghsa.Internal;

internal sealed record GhsaCursor(
    DateTimeOffset? LastUpdatedExclusive,
    DateTimeOffset? CurrentWindowStart,
    DateTimeOffset? CurrentWindowEnd,
    int NextPage,
    IReadOnlyCollection<Guid> PendingDocuments,
    IReadOnlyCollection<Guid> PendingMappings)
{
    private static readonly IReadOnlyCollection<Guid> EmptyGuidList = Array.Empty<Guid>();

    public static GhsaCursor Empty { get; } = new(
        null,
        null,
        null,
        1,
        EmptyGuidList,
        EmptyGuidList);

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

        if (LastUpdatedExclusive.HasValue)
        {
            document["lastUpdatedExclusive"] = LastUpdatedExclusive.Value.UtcDateTime;
        }

        if (CurrentWindowStart.HasValue)
        {
            document["currentWindowStart"] = CurrentWindowStart.Value.UtcDateTime;
        }

        if (CurrentWindowEnd.HasValue)
        {
            document["currentWindowEnd"] = CurrentWindowEnd.Value.UtcDateTime;
        }

        return document;
    }

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

        var lastUpdatedExclusive = document.TryGetValue("lastUpdatedExclusive", out var lastUpdated)
            ? ParseDate(lastUpdated)
            : null;
        var windowStart = document.TryGetValue("currentWindowStart", out var windowStartValue)
            ? ParseDate(windowStartValue)
            : null;
        var windowEnd = document.TryGetValue("currentWindowEnd", out var windowEndValue)
            ? ParseDate(windowEndValue)
            : null;
        var nextPage = document.TryGetValue("nextPage", out var nextPageValue) && nextPageValue.IsInt32
            ? Math.Max(1, nextPageValue.AsInt32)
            : 1;

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

        return new GhsaCursor(
            lastUpdatedExclusive,
            windowStart,
            windowEnd,
            nextPage,
            pendingDocuments,
            pendingMappings);
    }

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

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

    public GhsaCursor WithLastUpdatedExclusive(DateTimeOffset? timestamp)
        => this with { LastUpdatedExclusive = timestamp };

    public GhsaCursor WithCurrentWindowStart(DateTimeOffset? timestamp)
        => this with { CurrentWindowStart = timestamp };

    public GhsaCursor WithCurrentWindowEnd(DateTimeOffset? timestamp)
        => this with { CurrentWindowEnd = timestamp };

    public GhsaCursor WithNextPage(int page)
        => this with { NextPage = page < 1 ? 1 : page };

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

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