using MongoDB.Bson;
using StellaOps.Feedser.Source.Common.Cursors;

namespace StellaOps.Feedser.Source.CertCc.Internal;

internal sealed record CertCcCursor(
    TimeWindowCursorState SummaryState,
    IReadOnlyCollection<Guid> PendingSummaries,
    IReadOnlyCollection<string> PendingNotes,
    IReadOnlyCollection<Guid> PendingDocuments,
    IReadOnlyCollection<Guid> PendingMappings,
    DateTimeOffset? LastRun)
{
    private static readonly Guid[] EmptyGuidArray = Array.Empty<Guid>();
    private static readonly string[] EmptyStringArray = Array.Empty<string>();

    public static CertCcCursor Empty { get; } = new(
        TimeWindowCursorState.Empty,
        EmptyGuidArray,
        EmptyStringArray,
        EmptyGuidArray,
        EmptyGuidArray,
        null);

    public BsonDocument ToBsonDocument()
    {
        var document = new BsonDocument();

        var summary = new BsonDocument();
        SummaryState.WriteTo(summary, "start", "end");
        document["summary"] = summary;

        document["pendingSummaries"] = new BsonArray(PendingSummaries.Select(static id => id.ToString()));
        document["pendingNotes"] = new BsonArray(PendingNotes.Select(static note => note));
        document["pendingDocuments"] = new BsonArray(PendingDocuments.Select(static id => id.ToString()));
        document["pendingMappings"] = new BsonArray(PendingMappings.Select(static id => id.ToString()));

        if (LastRun.HasValue)
        {
            document["lastRun"] = LastRun.Value.UtcDateTime;
        }

        return document;
    }

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

        TimeWindowCursorState summaryState = TimeWindowCursorState.Empty;
        if (document.TryGetValue("summary", out var summaryValue) && summaryValue is BsonDocument summaryDocument)
        {
            summaryState = TimeWindowCursorState.FromBsonDocument(summaryDocument, "start", "end");
        }

        var pendingSummaries = ReadGuidArray(document, "pendingSummaries");
        var pendingNotes = ReadStringArray(document, "pendingNotes");
        var pendingDocuments = ReadGuidArray(document, "pendingDocuments");
        var pendingMappings = ReadGuidArray(document, "pendingMappings");

        DateTimeOffset? lastRun = null;
        if (document.TryGetValue("lastRun", out var lastRunValue))
        {
            lastRun = lastRunValue.BsonType switch
            {
                BsonType.DateTime => DateTime.SpecifyKind(lastRunValue.ToUniversalTime(), DateTimeKind.Utc),
                BsonType.String when DateTimeOffset.TryParse(lastRunValue.AsString, out var parsed) => parsed.ToUniversalTime(),
                _ => null,
            };
        }

        return new CertCcCursor(summaryState, pendingSummaries, pendingNotes, pendingDocuments, pendingMappings, lastRun);
    }

    public CertCcCursor WithSummaryState(TimeWindowCursorState state)
        => this with { SummaryState = state ?? TimeWindowCursorState.Empty };

    public CertCcCursor WithPendingSummaries(IEnumerable<Guid>? ids)
        => this with { PendingSummaries = NormalizeGuidSet(ids) };

    public CertCcCursor WithPendingNotes(IEnumerable<string>? notes)
        => this with { PendingNotes = NormalizeStringSet(notes) };

    public CertCcCursor WithPendingDocuments(IEnumerable<Guid>? ids)
        => this with { PendingDocuments = NormalizeGuidSet(ids) };

    public CertCcCursor WithPendingMappings(IEnumerable<Guid>? ids)
        => this with { PendingMappings = NormalizeGuidSet(ids) };

    public CertCcCursor WithLastRun(DateTimeOffset? timestamp)
        => this with { LastRun = timestamp };

    private static Guid[] ReadGuidArray(BsonDocument document, string field)
    {
        if (!document.TryGetValue(field, out var value) || value is not BsonArray array || array.Count == 0)
        {
            return EmptyGuidArray;
        }

        var results = new List<Guid>(array.Count);
        foreach (var element in array)
        {
            if (element is BsonString bsonString && Guid.TryParse(bsonString.AsString, out var parsed))
            {
                results.Add(parsed);
                continue;
            }

            if (element is BsonBinaryData binary && binary.GuidRepresentation != GuidRepresentation.Unspecified)
            {
                results.Add(binary.ToGuid());
            }
        }

        return results.Count == 0 ? EmptyGuidArray : results.Distinct().ToArray();
    }

    private static string[] ReadStringArray(BsonDocument document, string field)
    {
        if (!document.TryGetValue(field, out var value) || value is not BsonArray array || array.Count == 0)
        {
            return EmptyStringArray;
        }

        var results = new List<string>(array.Count);
        foreach (var element in array)
        {
            switch (element)
            {
                case BsonString bsonString when !string.IsNullOrWhiteSpace(bsonString.AsString):
                    results.Add(bsonString.AsString.Trim());
                    break;
                case BsonDocument bsonDocument when bsonDocument.TryGetValue("value", out var inner) && inner.IsString:
                    results.Add(inner.AsString.Trim());
                    break;
            }
        }

        return results.Count == 0
            ? EmptyStringArray
            : results
                .Where(static value => !string.IsNullOrWhiteSpace(value))
                .Select(static value => value.Trim())
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToArray();
    }

    private static Guid[] NormalizeGuidSet(IEnumerable<Guid>? ids)
        => ids?.Where(static id => id != Guid.Empty).Distinct().ToArray() ?? EmptyGuidArray;

    private static string[] NormalizeStringSet(IEnumerable<string>? values)
        => values is null
            ? EmptyStringArray
            : values
                .Where(static value => !string.IsNullOrWhiteSpace(value))
                .Select(static value => value.Trim())
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToArray();
}
