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

namespace StellaOps.Feedser.Source.Vndr.Apple.Internal;

internal sealed record AppleCursor(
    DateTimeOffset? LastPosted,
    IReadOnlyCollection<string> ProcessedIds,
    IReadOnlyCollection<Guid> PendingDocuments,
    IReadOnlyCollection<Guid> PendingMappings)
{
    private static readonly IReadOnlyCollection<Guid> EmptyGuidCollection = Array.Empty<Guid>();
    private static readonly IReadOnlyCollection<string> EmptyStringCollection = Array.Empty<string>();

    public static AppleCursor Empty { get; } = new(null, EmptyStringCollection, EmptyGuidCollection, EmptyGuidCollection);

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

        if (LastPosted.HasValue)
        {
            document["lastPosted"] = LastPosted.Value.UtcDateTime;
        }

        if (ProcessedIds.Count > 0)
        {
            document["processedIds"] = new BsonArray(ProcessedIds);
        }

        return document;
    }

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

        var lastPosted = document.TryGetValue("lastPosted", out var lastPostedValue)
            ? ParseDate(lastPostedValue)
            : null;

        var processedIds = document.TryGetValue("processedIds", out var processedValue) && processedValue is BsonArray processedArray
            ? processedArray.OfType<BsonValue>()
                .Where(static value => value.BsonType == BsonType.String)
                .Select(static value => value.AsString.Trim())
                .Where(static value => value.Length > 0)
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToArray()
            : EmptyStringCollection;

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

        return new AppleCursor(lastPosted, processedIds, pendingDocuments, pendingMappings);
    }

    public AppleCursor WithLastPosted(DateTimeOffset timestamp, IEnumerable<string>? processedIds = null)
    {
        var ids = processedIds is null
            ? ProcessedIds
            : processedIds.Where(static id => !string.IsNullOrWhiteSpace(id))
                .Select(static id => id.Trim())
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToArray();

        return this with
        {
            LastPosted = timestamp.ToUniversalTime(),
            ProcessedIds = ids,
        };
    }

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

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

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

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

        return results;
    }

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