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

namespace StellaOps.Feedser.Source.Osv.Internal;

internal sealed record OsvCursor(
    IReadOnlyDictionary<string, DateTimeOffset?> LastModifiedByEcosystem,
    IReadOnlyDictionary<string, IReadOnlyCollection<string>> ProcessedIdsByEcosystem,
    IReadOnlyDictionary<string, OsvArchiveMetadata> ArchiveMetadataByEcosystem,
    IReadOnlyCollection<Guid> PendingDocuments,
    IReadOnlyCollection<Guid> PendingMappings)
{
    private static readonly IReadOnlyDictionary<string, DateTimeOffset?> EmptyLastModified =
        new Dictionary<string, DateTimeOffset?>(StringComparer.OrdinalIgnoreCase);
    private static readonly IReadOnlyDictionary<string, IReadOnlyCollection<string>> EmptyProcessedIds =
        new Dictionary<string, IReadOnlyCollection<string>>(StringComparer.OrdinalIgnoreCase);
    private static readonly IReadOnlyDictionary<string, OsvArchiveMetadata> EmptyArchiveMetadata =
        new Dictionary<string, OsvArchiveMetadata>(StringComparer.OrdinalIgnoreCase);
    private static readonly IReadOnlyCollection<Guid> EmptyGuidList = Array.Empty<Guid>();
    private static readonly IReadOnlyCollection<string> EmptyStringList = Array.Empty<string>();

    public static OsvCursor Empty { get; } = new(EmptyLastModified, EmptyProcessedIds, EmptyArchiveMetadata, EmptyGuidList, EmptyGuidList);

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

        if (LastModifiedByEcosystem.Count > 0)
        {
            var lastModifiedDoc = new BsonDocument();
            foreach (var (ecosystem, timestamp) in LastModifiedByEcosystem)
            {
                lastModifiedDoc[ecosystem] = timestamp.HasValue ? BsonValue.Create(timestamp.Value.UtcDateTime) : BsonNull.Value;
            }

            document["lastModified"] = lastModifiedDoc;
        }

        if (ProcessedIdsByEcosystem.Count > 0)
        {
            var processedDoc = new BsonDocument();
            foreach (var (ecosystem, ids) in ProcessedIdsByEcosystem)
            {
                processedDoc[ecosystem] = new BsonArray(ids.Select(id => id));
            }

            document["processed"] = processedDoc;
        }

        if (ArchiveMetadataByEcosystem.Count > 0)
        {
            var metadataDoc = new BsonDocument();
            foreach (var (ecosystem, metadata) in ArchiveMetadataByEcosystem)
            {
                var element = new BsonDocument();
                if (!string.IsNullOrWhiteSpace(metadata.ETag))
                {
                    element["etag"] = metadata.ETag;
                }

                if (metadata.LastModified.HasValue)
                {
                    element["lastModified"] = metadata.LastModified.Value.UtcDateTime;
                }

                metadataDoc[ecosystem] = element;
            }

            document["archive"] = metadataDoc;
        }

        return document;
    }

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

        var lastModified = ReadLastModified(document.TryGetValue("lastModified", out var lastModifiedValue) ? lastModifiedValue : null);
        var processed = ReadProcessedIds(document.TryGetValue("processed", out var processedValue) ? processedValue : null);
        var archiveMetadata = ReadArchiveMetadata(document.TryGetValue("archive", out var archiveValue) ? archiveValue : null);
        var pendingDocuments = ReadGuidList(document, "pendingDocuments");
        var pendingMappings = ReadGuidList(document, "pendingMappings");

        return new OsvCursor(lastModified, processed, archiveMetadata, pendingDocuments, pendingMappings);
    }

    public DateTimeOffset? GetLastModified(string ecosystem)
    {
        ArgumentException.ThrowIfNullOrEmpty(ecosystem);
        return LastModifiedByEcosystem.TryGetValue(ecosystem, out var value) ? value : null;
    }

    public bool HasProcessedId(string ecosystem, string id)
    {
        ArgumentException.ThrowIfNullOrEmpty(ecosystem);
        ArgumentException.ThrowIfNullOrEmpty(id);

        return ProcessedIdsByEcosystem.TryGetValue(ecosystem, out var ids)
            && ids.Contains(id, StringComparer.OrdinalIgnoreCase);
    }

    public OsvCursor WithLastModified(string ecosystem, DateTimeOffset timestamp, IEnumerable<string> processedIds)
    {
        ArgumentException.ThrowIfNullOrEmpty(ecosystem);

        var lastModified = new Dictionary<string, DateTimeOffset?>(LastModifiedByEcosystem, StringComparer.OrdinalIgnoreCase)
        {
            [ecosystem] = timestamp.ToUniversalTime(),
        };

        var processed = new Dictionary<string, IReadOnlyCollection<string>>(ProcessedIdsByEcosystem, StringComparer.OrdinalIgnoreCase)
        {
            [ecosystem] = processedIds?.Where(static id => !string.IsNullOrWhiteSpace(id))
                .Select(static id => id.Trim())
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToArray() ?? EmptyStringList,
        };

        return this with { LastModifiedByEcosystem = lastModified, ProcessedIdsByEcosystem = processed };
    }

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

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

    public OsvCursor AddProcessedId(string ecosystem, string id)
    {
        ArgumentException.ThrowIfNullOrEmpty(ecosystem);
        ArgumentException.ThrowIfNullOrEmpty(id);

        var processed = new Dictionary<string, IReadOnlyCollection<string>>(ProcessedIdsByEcosystem, StringComparer.OrdinalIgnoreCase);
        if (!processed.TryGetValue(ecosystem, out var ids))
        {
            ids = EmptyStringList;
        }

        var set = new HashSet<string>(ids, StringComparer.OrdinalIgnoreCase)
        {
            id.Trim(),
        };

        processed[ecosystem] = set.ToArray();
        return this with { ProcessedIdsByEcosystem = processed };
    }

    public bool TryGetArchiveMetadata(string ecosystem, out OsvArchiveMetadata metadata)
    {
        ArgumentException.ThrowIfNullOrEmpty(ecosystem);
        return ArchiveMetadataByEcosystem.TryGetValue(ecosystem, out metadata!);
    }

    public OsvCursor WithArchiveMetadata(string ecosystem, string? etag, DateTimeOffset? lastModified)
    {
        ArgumentException.ThrowIfNullOrEmpty(ecosystem);

        var metadata = new Dictionary<string, OsvArchiveMetadata>(ArchiveMetadataByEcosystem, StringComparer.OrdinalIgnoreCase)
        {
            [ecosystem] = new OsvArchiveMetadata(etag?.Trim(), lastModified?.ToUniversalTime()),
        };

        return this with { ArchiveMetadataByEcosystem = metadata };
    }

    private static IReadOnlyDictionary<string, DateTimeOffset?> ReadLastModified(BsonValue? value)
    {
        if (value is not BsonDocument document)
        {
            return EmptyLastModified;
        }

        var dictionary = new Dictionary<string, DateTimeOffset?>(StringComparer.OrdinalIgnoreCase);
        foreach (var element in document.Elements)
        {
            if (element.Value is null || element.Value.IsBsonNull)
            {
                dictionary[element.Name] = null;
                continue;
            }

            dictionary[element.Name] = ParseDate(element.Value);
        }

        return dictionary;
    }

    private static IReadOnlyDictionary<string, IReadOnlyCollection<string>> ReadProcessedIds(BsonValue? value)
    {
        if (value is not BsonDocument document)
        {
            return EmptyProcessedIds;
        }

        var dictionary = new Dictionary<string, IReadOnlyCollection<string>>(StringComparer.OrdinalIgnoreCase);
        foreach (var element in document.Elements)
        {
            if (element.Value is not BsonArray array)
            {
                continue;
            }

            var ids = new List<string>(array.Count);
            foreach (var idValue in array)
            {
                if (idValue?.BsonType == BsonType.String)
                {
                    var str = idValue.AsString.Trim();
                    if (!string.IsNullOrWhiteSpace(str))
                    {
                        ids.Add(str);
                    }
                }
            }

            dictionary[element.Name] = ids.Count == 0
                ? EmptyStringList
                : ids.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
        }

        return dictionary;
    }

    private static IReadOnlyDictionary<string, OsvArchiveMetadata> ReadArchiveMetadata(BsonValue? value)
    {
        if (value is not BsonDocument document)
        {
            return EmptyArchiveMetadata;
        }

        var dictionary = new Dictionary<string, OsvArchiveMetadata>(StringComparer.OrdinalIgnoreCase);
        foreach (var element in document.Elements)
        {
            if (element.Value is not BsonDocument metadataDocument)
            {
                continue;
            }

            string? etag = metadataDocument.TryGetValue("etag", out var etagValue) && etagValue.IsString ? etagValue.AsString : null;
            DateTimeOffset? lastModified = metadataDocument.TryGetValue("lastModified", out var lastModifiedValue)
                ? ParseDate(lastModifiedValue)
                : null;

            dictionary[element.Name] = new OsvArchiveMetadata(etag, lastModified);
        }

        return dictionary.Count == 0 ? EmptyArchiveMetadata : dictionary;
    }

    private static IReadOnlyCollection<Guid> ReadGuidList(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,
        };
    }
}

internal sealed record OsvArchiveMetadata(string? ETag, DateTimeOffset? LastModified);
