using System;
using System.Collections.Generic;
using System.Linq;
using MongoDB.Bson;
using StellaOps.Feedser.Storage.Mongo.Documents;

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

internal sealed record OracleCursor(
    DateTimeOffset? LastProcessed,
    IReadOnlyCollection<Guid> PendingDocuments,
    IReadOnlyCollection<Guid> PendingMappings,
    IReadOnlyDictionary<string, OracleFetchCacheEntry> FetchCache)
{
    private static readonly IReadOnlyCollection<Guid> EmptyGuidCollection = Array.Empty<Guid>();
    private static readonly IReadOnlyDictionary<string, OracleFetchCacheEntry> EmptyFetchCache =
        new Dictionary<string, OracleFetchCacheEntry>(StringComparer.OrdinalIgnoreCase);

    public static OracleCursor Empty { get; } = new(null, EmptyGuidCollection, EmptyGuidCollection, EmptyFetchCache);

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

        if (LastProcessed.HasValue)
        {
            document["lastProcessed"] = LastProcessed.Value.UtcDateTime;
        }

        if (FetchCache.Count > 0)
        {
            var cacheDocument = new BsonDocument();
            foreach (var (key, entry) in FetchCache)
            {
                cacheDocument[key] = entry.ToBsonDocument();
            }

            document["fetchCache"] = cacheDocument;
        }

        return document;
    }

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

        var lastProcessed = document.TryGetValue("lastProcessed", out var value)
            ? ParseDate(value)
            : null;

        return new OracleCursor(
            lastProcessed,
            ReadGuidArray(document, "pendingDocuments"),
            ReadGuidArray(document, "pendingMappings"),
            ReadFetchCache(document));
    }

    public OracleCursor WithLastProcessed(DateTimeOffset? timestamp)
        => this with { LastProcessed = timestamp };

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

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

    public OracleCursor WithFetchCache(IDictionary<string, OracleFetchCacheEntry> cache)
    {
        if (cache is null || cache.Count == 0)
        {
            return this with { FetchCache = EmptyFetchCache };
        }

        return this with { FetchCache = new Dictionary<string, OracleFetchCacheEntry>(cache, StringComparer.OrdinalIgnoreCase) };
    }

    public bool TryGetFetchCache(string key, out OracleFetchCacheEntry entry)
    {
        if (FetchCache.Count == 0)
        {
            entry = OracleFetchCacheEntry.Empty;
            return false;
        }

        return FetchCache.TryGetValue(key, out entry!);
    }

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

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

        var result = new List<Guid>(array.Count);
        foreach (var element in array)
        {
            if (element is null)
            {
                continue;
            }

            if (Guid.TryParse(element.ToString(), out var guid))
            {
                result.Add(guid);
            }
        }

        return result;
    }

    private static IReadOnlyDictionary<string, OracleFetchCacheEntry> ReadFetchCache(BsonDocument document)
    {
        if (!document.TryGetValue("fetchCache", out var raw) || raw is not BsonDocument cacheDocument || cacheDocument.ElementCount == 0)
        {
            return EmptyFetchCache;
        }

        var cache = new Dictionary<string, OracleFetchCacheEntry>(StringComparer.OrdinalIgnoreCase);
        foreach (var element in cacheDocument.Elements)
        {
            if (element.Value is not BsonDocument entryDocument)
            {
                continue;
            }

            cache[element.Name] = OracleFetchCacheEntry.FromBson(entryDocument);
        }

        return cache;
    }
}

internal sealed record OracleFetchCacheEntry(string? Sha256, string? ETag, DateTimeOffset? LastModified)
{
    public static OracleFetchCacheEntry Empty { get; } = new(string.Empty, null, null);

    public BsonDocument ToBsonDocument()
    {
        var document = new BsonDocument
        {
            ["sha256"] = Sha256 ?? string.Empty,
        };

        if (!string.IsNullOrWhiteSpace(ETag))
        {
            document["etag"] = ETag;
        }

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

        return document;
    }

    public static OracleFetchCacheEntry FromBson(BsonDocument document)
    {
        var sha = document.TryGetValue("sha256", out var shaValue) ? shaValue.ToString() : string.Empty;
        string? etag = null;
        if (document.TryGetValue("etag", out var etagValue) && !etagValue.IsBsonNull)
        {
            etag = etagValue.ToString();
        }

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

        return new OracleFetchCacheEntry(sha, etag, lastModified);
    }

    public static OracleFetchCacheEntry FromDocument(DocumentRecord document)
    {
        ArgumentNullException.ThrowIfNull(document);
        return new OracleFetchCacheEntry(
            document.Sha256 ?? string.Empty,
            document.Etag,
            document.LastModified?.ToUniversalTime());
    }

    public bool Matches(DocumentRecord document)
    {
        ArgumentNullException.ThrowIfNull(document);

        if (!string.IsNullOrEmpty(Sha256) && !string.IsNullOrEmpty(document.Sha256))
        {
            return string.Equals(Sha256, document.Sha256, StringComparison.OrdinalIgnoreCase);
        }

        if (!string.IsNullOrEmpty(ETag) && !string.IsNullOrEmpty(document.Etag))
        {
            return string.Equals(ETag, document.Etag, StringComparison.Ordinal);
        }

        if (LastModified.HasValue && document.LastModified.HasValue)
        {
            return LastModified.Value.ToUniversalTime() == document.LastModified.Value.ToUniversalTime();
        }

        return false;
    }
}
