using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Storage.Mongo.Aliases;

namespace StellaOps.Feedser.Storage.Mongo.Advisories;

public sealed class AdvisoryStore : IAdvisoryStore
{
    private readonly IMongoCollection<AdvisoryDocument> _collection;
    private readonly ILogger<AdvisoryStore> _logger;
    private readonly IAliasStore _aliasStore;
    private readonly TimeProvider _timeProvider;
    private readonly MongoStorageOptions _options;

    public AdvisoryStore(
        IMongoDatabase database,
        IAliasStore aliasStore,
        ILogger<AdvisoryStore> logger,
        IOptions<MongoStorageOptions> options,
        TimeProvider? timeProvider = null)
    {
        _collection = (database ?? throw new ArgumentNullException(nameof(database)))
            .GetCollection<AdvisoryDocument>(MongoStorageDefaults.Collections.Advisory);
        _aliasStore = aliasStore ?? throw new ArgumentNullException(nameof(aliasStore));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _options = options?.Value ?? throw new ArgumentNullException(nameof(options));
        _timeProvider = timeProvider ?? TimeProvider.System;
    }


    public async Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(advisory);

        var missing = ProvenanceInspector.FindMissingProvenance(advisory);
        var primarySource = advisory.Provenance.FirstOrDefault()?.Source ?? "unknown";
        foreach (var item in missing)
        {
            var source = string.IsNullOrWhiteSpace(item.Source) ? primarySource : item.Source;
            _logger.LogWarning(
                "Missing provenance detected for {Component} in advisory {AdvisoryKey} (source {Source}).",
                item.Component,
                advisory.AdvisoryKey,
                source);
            ProvenanceDiagnostics.RecordMissing(source, item.Component, item.RecordedAt, item.FieldMask);
        }

        var payload = CanonicalJsonSerializer.Serialize(advisory);
        var normalizedVersions = _options.EnableSemVerStyle
            ? NormalizedVersionDocumentFactory.Create(advisory)
            : null;
        var document = new AdvisoryDocument
        {
            AdvisoryKey = advisory.AdvisoryKey,
            Payload = BsonDocument.Parse(payload),
            Modified = advisory.Modified?.UtcDateTime ?? DateTime.UtcNow,
            Published = advisory.Published?.UtcDateTime,
            NormalizedVersions = normalizedVersions,
        };

        var options = new ReplaceOptions { IsUpsert = true };
        await _collection.ReplaceOneAsync(x => x.AdvisoryKey == advisory.AdvisoryKey, document, options, cancellationToken).ConfigureAwait(false);
        _logger.LogDebug("Upserted advisory {AdvisoryKey}", advisory.AdvisoryKey);

        var aliasEntries = BuildAliasEntries(advisory);
        var updatedAt = _timeProvider.GetUtcNow();
        await _aliasStore.ReplaceAsync(advisory.AdvisoryKey, aliasEntries, updatedAt, cancellationToken).ConfigureAwait(false);
    }

    public async Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken)
    {
        ArgumentException.ThrowIfNullOrEmpty(advisoryKey);
        var document = await _collection.Find(x => x.AdvisoryKey == advisoryKey)
            .FirstOrDefaultAsync(cancellationToken)
            .ConfigureAwait(false);

        return document is null ? null : Deserialize(document.Payload);
    }

    private static IEnumerable<AliasEntry> BuildAliasEntries(Advisory advisory)
    {
        foreach (var alias in advisory.Aliases)
        {
            if (AliasSchemeRegistry.TryGetScheme(alias, out var scheme))
            {
                yield return new AliasEntry(scheme, alias);
            }
            else
            {
                yield return new AliasEntry(AliasStoreConstants.UnscopedScheme, alias);
            }
        }

        yield return new AliasEntry(AliasStoreConstants.PrimaryScheme, advisory.AdvisoryKey);
    }

    public async Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken)
    {
        var cursor = await _collection.Find(FilterDefinition<AdvisoryDocument>.Empty)
            .SortByDescending(x => x.Modified)
            .Limit(limit)
            .ToListAsync(cancellationToken)
            .ConfigureAwait(false);

        return cursor.Select(static doc => Deserialize(doc.Payload)).ToArray();
    }

    public async IAsyncEnumerable<Advisory> StreamAsync([EnumeratorCancellation] CancellationToken cancellationToken)
    {
        var options = new FindOptions<AdvisoryDocument>
        {
            Sort = Builders<AdvisoryDocument>.Sort.Ascending(static doc => doc.AdvisoryKey),
        };

        using var cursor = await _collection.FindAsync(
                FilterDefinition<AdvisoryDocument>.Empty,
                options,
                cancellationToken)
            .ConfigureAwait(false);

        while (await cursor.MoveNextAsync(cancellationToken).ConfigureAwait(false))
        {
            foreach (var document in cursor.Current)
            {
                cancellationToken.ThrowIfCancellationRequested();
                yield return Deserialize(document.Payload);
            }
        }
    }

    private static Advisory Deserialize(BsonDocument payload)
    {
        ArgumentNullException.ThrowIfNull(payload);

        var advisoryKey = payload.GetValue("advisoryKey", defaultValue: null)?.AsString
            ?? throw new InvalidOperationException("advisoryKey missing from payload.");
        var title = payload.GetValue("title", defaultValue: null)?.AsString ?? advisoryKey;

        string? summary = payload.TryGetValue("summary", out var summaryValue) && summaryValue.IsString ? summaryValue.AsString : null;
        string? description = payload.TryGetValue("description", out var descriptionValue) && descriptionValue.IsString ? descriptionValue.AsString : null;
        string? language = payload.TryGetValue("language", out var languageValue) && languageValue.IsString ? languageValue.AsString : null;
        DateTimeOffset? published = TryReadDateTime(payload, "published");
        DateTimeOffset? modified = TryReadDateTime(payload, "modified");
        string? severity = payload.TryGetValue("severity", out var severityValue) && severityValue.IsString ? severityValue.AsString : null;
        var exploitKnown = payload.TryGetValue("exploitKnown", out var exploitValue) && exploitValue.IsBoolean && exploitValue.AsBoolean;

        var aliases = payload.TryGetValue("aliases", out var aliasValue) && aliasValue is BsonArray aliasArray
            ? aliasArray.OfType<BsonValue>().Where(static x => x.IsString).Select(static x => x.AsString)
            : Array.Empty<string>();

        var credits = payload.TryGetValue("credits", out var creditsValue) && creditsValue is BsonArray creditsArray
            ? creditsArray.OfType<BsonDocument>().Select(DeserializeCredit).ToArray()
            : Array.Empty<AdvisoryCredit>();

        var references = payload.TryGetValue("references", out var referencesValue) && referencesValue is BsonArray referencesArray
            ? referencesArray.OfType<BsonDocument>().Select(DeserializeReference).ToArray()
            : Array.Empty<AdvisoryReference>();

        var affectedPackages = payload.TryGetValue("affectedPackages", out var affectedValue) && affectedValue is BsonArray affectedArray
            ? affectedArray.OfType<BsonDocument>().Select(DeserializeAffectedPackage).ToArray()
            : Array.Empty<AffectedPackage>();

        var cvssMetrics = payload.TryGetValue("cvssMetrics", out var cvssValue) && cvssValue is BsonArray cvssArray
            ? cvssArray.OfType<BsonDocument>().Select(DeserializeCvssMetric).ToArray()
            : Array.Empty<CvssMetric>();

        var cwes = payload.TryGetValue("cwes", out var cweValue) && cweValue is BsonArray cweArray
            ? cweArray.OfType<BsonDocument>().Select(DeserializeWeakness).ToArray()
            : Array.Empty<AdvisoryWeakness>();

        string? canonicalMetricId = payload.TryGetValue("canonicalMetricId", out var canonicalMetricValue) && canonicalMetricValue.IsString
            ? canonicalMetricValue.AsString
            : null;

        var provenance = payload.TryGetValue("provenance", out var provenanceValue) && provenanceValue is BsonArray provenanceArray
            ? provenanceArray.OfType<BsonDocument>().Select(DeserializeProvenance).ToArray()
            : Array.Empty<AdvisoryProvenance>();

        return new Advisory(
            advisoryKey,
            title,
            summary,
            language,
            published,
            modified,
            severity,
            exploitKnown,
            aliases,
            credits,
            references,
            affectedPackages,
            cvssMetrics,
            provenance,
            description,
            cwes,
            canonicalMetricId);
    }

    private static AdvisoryReference DeserializeReference(BsonDocument document)
    {
        var url = document.GetValue("url", defaultValue: null)?.AsString
            ?? throw new InvalidOperationException("reference.url missing from payload.");
        string? kind = document.TryGetValue("kind", out var kindValue) && kindValue.IsString ? kindValue.AsString : null;
        string? sourceTag = document.TryGetValue("sourceTag", out var sourceTagValue) && sourceTagValue.IsString ? sourceTagValue.AsString : null;
        string? summary = document.TryGetValue("summary", out var summaryValue) && summaryValue.IsString ? summaryValue.AsString : null;
        var provenance = document.TryGetValue("provenance", out var provenanceValue) && provenanceValue.IsBsonDocument
            ? DeserializeProvenance(provenanceValue.AsBsonDocument)
            : AdvisoryProvenance.Empty;

        return new AdvisoryReference(url, kind, sourceTag, summary, provenance);
    }

    private static AdvisoryCredit DeserializeCredit(BsonDocument document)
    {
        var displayName = document.GetValue("displayName", defaultValue: null)?.AsString
            ?? throw new InvalidOperationException("credits.displayName missing from payload.");
        string? role = document.TryGetValue("role", out var roleValue) && roleValue.IsString ? roleValue.AsString : null;
        var contacts = document.TryGetValue("contacts", out var contactsValue) && contactsValue is BsonArray contactsArray
            ? contactsArray.OfType<BsonValue>().Where(static value => value.IsString).Select(static value => value.AsString).ToArray()
            : Array.Empty<string>();
        var provenance = document.TryGetValue("provenance", out var provenanceValue) && provenanceValue.IsBsonDocument
            ? DeserializeProvenance(provenanceValue.AsBsonDocument)
            : AdvisoryProvenance.Empty;

        return new AdvisoryCredit(displayName, role, contacts, provenance);
    }

    private static AffectedPackage DeserializeAffectedPackage(BsonDocument document)
    {
        var type = document.GetValue("type", defaultValue: null)?.AsString
            ?? throw new InvalidOperationException("affectedPackages.type missing from payload.");
        var identifier = document.GetValue("identifier", defaultValue: null)?.AsString
            ?? throw new InvalidOperationException("affectedPackages.identifier missing from payload.");
        string? platform = document.TryGetValue("platform", out var platformValue) && platformValue.IsString ? platformValue.AsString : null;

        var versionRanges = document.TryGetValue("versionRanges", out var rangesValue) && rangesValue is BsonArray rangesArray
            ? rangesArray.OfType<BsonDocument>().Select(DeserializeVersionRange).ToArray()
            : Array.Empty<AffectedVersionRange>();

        var statuses = document.TryGetValue("statuses", out var statusesValue) && statusesValue is BsonArray statusesArray
            ? statusesArray.OfType<BsonDocument>().Select(DeserializeStatus).ToArray()
            : Array.Empty<AffectedPackageStatus>();

        var normalizedVersions = document.TryGetValue("normalizedVersions", out var normalizedValue) && normalizedValue is BsonArray normalizedArray
            ? normalizedArray.OfType<BsonDocument>().Select(DeserializeNormalizedVersionRule).ToArray()
            : Array.Empty<NormalizedVersionRule>();

        var provenance = document.TryGetValue("provenance", out var provenanceValue) && provenanceValue is BsonArray provenanceArray
            ? provenanceArray.OfType<BsonDocument>().Select(DeserializeProvenance).ToArray()
            : Array.Empty<AdvisoryProvenance>();

        return new AffectedPackage(type, identifier, platform, versionRanges, statuses, provenance, normalizedVersions);
    }

    private static AffectedVersionRange DeserializeVersionRange(BsonDocument document)
    {
        var rangeKind = document.GetValue("rangeKind", defaultValue: null)?.AsString
            ?? throw new InvalidOperationException("versionRanges.rangeKind missing from payload.");
        string? introducedVersion = document.TryGetValue("introducedVersion", out var introducedValue) && introducedValue.IsString ? introducedValue.AsString : null;
        string? fixedVersion = document.TryGetValue("fixedVersion", out var fixedValue) && fixedValue.IsString ? fixedValue.AsString : null;
        string? lastAffectedVersion = document.TryGetValue("lastAffectedVersion", out var lastValue) && lastValue.IsString ? lastValue.AsString : null;
        string? rangeExpression = document.TryGetValue("rangeExpression", out var expressionValue) && expressionValue.IsString ? expressionValue.AsString : null;
        var provenance = document.TryGetValue("provenance", out var provenanceValue) && provenanceValue.IsBsonDocument
            ? DeserializeProvenance(provenanceValue.AsBsonDocument)
            : AdvisoryProvenance.Empty;
        RangePrimitives? primitives = null;
        if (document.TryGetValue("primitives", out var primitivesValue) && primitivesValue.IsBsonDocument)
        {
            primitives = DeserializePrimitives(primitivesValue.AsBsonDocument);
        }

        return new AffectedVersionRange(rangeKind, introducedVersion, fixedVersion, lastAffectedVersion, rangeExpression, provenance, primitives);
    }

    private static AffectedPackageStatus DeserializeStatus(BsonDocument document)
    {
        var status = document.GetValue("status", defaultValue: null)?.AsString
            ?? throw new InvalidOperationException("statuses.status missing from payload.");
        var provenance = document.TryGetValue("provenance", out var provenanceValue) && provenanceValue.IsBsonDocument
            ? DeserializeProvenance(provenanceValue.AsBsonDocument)
            : AdvisoryProvenance.Empty;

        return new AffectedPackageStatus(status, provenance);
    }

    private static CvssMetric DeserializeCvssMetric(BsonDocument document)
    {
        var version = document.GetValue("version", defaultValue: null)?.AsString
            ?? throw new InvalidOperationException("cvssMetrics.version missing from payload.");
        var vector = document.GetValue("vector", defaultValue: null)?.AsString
            ?? throw new InvalidOperationException("cvssMetrics.vector missing from payload.");
        var baseScore = document.TryGetValue("baseScore", out var scoreValue) && scoreValue.IsNumeric ? scoreValue.ToDouble() : 0d;
        var baseSeverity = document.GetValue("baseSeverity", defaultValue: null)?.AsString
            ?? throw new InvalidOperationException("cvssMetrics.baseSeverity missing from payload.");
        var provenance = document.TryGetValue("provenance", out var provenanceValue) && provenanceValue.IsBsonDocument
            ? DeserializeProvenance(provenanceValue.AsBsonDocument)
            : AdvisoryProvenance.Empty;

        return new CvssMetric(version, vector, baseScore, baseSeverity, provenance);
    }

    private static AdvisoryWeakness DeserializeWeakness(BsonDocument document)
    {
        var taxonomy = document.GetValue("taxonomy", defaultValue: null)?.AsString
            ?? throw new InvalidOperationException("cwes.taxonomy missing from payload.");
        var identifier = document.GetValue("identifier", defaultValue: null)?.AsString
            ?? throw new InvalidOperationException("cwes.identifier missing from payload.");
        string? name = document.TryGetValue("name", out var nameValue) && nameValue.IsString ? nameValue.AsString : null;
        string? uri = document.TryGetValue("uri", out var uriValue) && uriValue.IsString ? uriValue.AsString : null;
        var provenance = document.TryGetValue("provenance", out var provenanceValue) && provenanceValue.IsBsonDocument
            ? DeserializeProvenance(provenanceValue.AsBsonDocument)
            : AdvisoryProvenance.Empty;

        return new AdvisoryWeakness(taxonomy, identifier, name, uri, new[] { provenance });
    }

    private static AdvisoryProvenance DeserializeProvenance(BsonDocument document)
    {
        var source = document.GetValue("source", defaultValue: null)?.AsString
            ?? throw new InvalidOperationException("provenance.source missing from payload.");
        var kind = document.GetValue("kind", defaultValue: null)?.AsString
            ?? throw new InvalidOperationException("provenance.kind missing from payload.");
        string? value = document.TryGetValue("value", out var valueElement) && valueElement.IsString ? valueElement.AsString : null;
        string? decisionReason = document.TryGetValue("decisionReason", out var reasonElement) && reasonElement.IsString ? reasonElement.AsString : null;
        var recordedAt = TryConvertDateTime(document.GetValue("recordedAt", defaultValue: null));
        IEnumerable<string>? fieldMask = null;
        if (document.TryGetValue("fieldMask", out var fieldMaskValue) && fieldMaskValue is BsonArray fieldMaskArray)
        {
            fieldMask = fieldMaskArray
                .OfType<BsonValue>()
                .Where(static element => element.IsString)
                .Select(static element => element.AsString);
        }

        return new AdvisoryProvenance(
            source,
            kind,
            value ?? string.Empty,
            recordedAt ?? DateTimeOffset.UtcNow,
            fieldMask,
            decisionReason);
    }

    private static NormalizedVersionRule DeserializeNormalizedVersionRule(BsonDocument document)
    {
        var scheme = document.GetValue("scheme", defaultValue: null)?.AsString
            ?? throw new InvalidOperationException("normalizedVersions.scheme missing from payload.");
        var type = document.GetValue("type", defaultValue: null)?.AsString
            ?? throw new InvalidOperationException("normalizedVersions.type missing from payload.");

        string? min = document.TryGetValue("min", out var minValue) && minValue.IsString ? minValue.AsString : null;
        bool? minInclusive = document.TryGetValue("minInclusive", out var minInclusiveValue) && minInclusiveValue.IsBoolean ? minInclusiveValue.AsBoolean : null;
        string? max = document.TryGetValue("max", out var maxValue) && maxValue.IsString ? maxValue.AsString : null;
        bool? maxInclusive = document.TryGetValue("maxInclusive", out var maxInclusiveValue) && maxInclusiveValue.IsBoolean ? maxInclusiveValue.AsBoolean : null;
        string? value = document.TryGetValue("value", out var valueElement) && valueElement.IsString ? valueElement.AsString : null;
        string? notes = document.TryGetValue("notes", out var notesValue) && notesValue.IsString ? notesValue.AsString : null;

        return new NormalizedVersionRule(
            scheme,
            type,
            min,
            minInclusive,
            max,
            maxInclusive,
            value,
            notes);
    }

    private static RangePrimitives? DeserializePrimitives(BsonDocument document)
    {
        SemVerPrimitive? semVer = null;
        NevraPrimitive? nevra = null;
        EvrPrimitive? evr = null;
        IReadOnlyDictionary<string, string>? vendor = null;

        if (document.TryGetValue("semVer", out var semverValue) && semverValue.IsBsonDocument)
        {
            var semverDoc = semverValue.AsBsonDocument;
            semVer = new SemVerPrimitive(
                semverDoc.TryGetValue("introduced", out var semIntroduced) && semIntroduced.IsString ? semIntroduced.AsString : null,
                semverDoc.TryGetValue("introducedInclusive", out var semIntroducedInclusive) && semIntroducedInclusive.IsBoolean && semIntroducedInclusive.AsBoolean,
                semverDoc.TryGetValue("fixed", out var semFixed) && semFixed.IsString ? semFixed.AsString : null,
                semverDoc.TryGetValue("fixedInclusive", out var semFixedInclusive) && semFixedInclusive.IsBoolean && semFixedInclusive.AsBoolean,
                semverDoc.TryGetValue("lastAffected", out var semLast) && semLast.IsString ? semLast.AsString : null,
                semverDoc.TryGetValue("lastAffectedInclusive", out var semLastInclusive) && semLastInclusive.IsBoolean && semLastInclusive.AsBoolean,
                semverDoc.TryGetValue("constraintExpression", out var constraint) && constraint.IsString ? constraint.AsString : null,
                semverDoc.TryGetValue("exactValue", out var exact) && exact.IsString ? exact.AsString : null);
        }

        if (document.TryGetValue("nevra", out var nevraValue) && nevraValue.IsBsonDocument)
        {
            var nevraDoc = nevraValue.AsBsonDocument;
            nevra = new NevraPrimitive(
                DeserializeNevraComponent(nevraDoc, "introduced"),
                DeserializeNevraComponent(nevraDoc, "fixed"),
                DeserializeNevraComponent(nevraDoc, "lastAffected"));
        }

        if (document.TryGetValue("evr", out var evrValue) && evrValue.IsBsonDocument)
        {
            var evrDoc = evrValue.AsBsonDocument;
            evr = new EvrPrimitive(
                DeserializeEvrComponent(evrDoc, "introduced"),
                DeserializeEvrComponent(evrDoc, "fixed"),
                DeserializeEvrComponent(evrDoc, "lastAffected"));
        }

        if (document.TryGetValue("vendorExtensions", out var vendorValue) && vendorValue.IsBsonDocument)
        {
            vendor = vendorValue.AsBsonDocument.Elements
                .Where(static e => e.Value.IsString)
                .ToDictionary(static e => e.Name, static e => e.Value.AsString, StringComparer.Ordinal);
            if (vendor.Count == 0)
            {
                vendor = null;
            }
        }

        if (semVer is null && nevra is null && evr is null && vendor is null)
        {
            return null;
        }

        return new RangePrimitives(semVer, nevra, evr, vendor);
    }

    private static NevraComponent? DeserializeNevraComponent(BsonDocument parent, string field)
    {
        if (!parent.TryGetValue(field, out var value) || !value.IsBsonDocument)
        {
            return null;
        }

        var component = value.AsBsonDocument;
        var name = component.TryGetValue("name", out var nameValue) && nameValue.IsString ? nameValue.AsString : null;
        var version = component.TryGetValue("version", out var versionValue) && versionValue.IsString ? versionValue.AsString : null;
        if (name is null || version is null)
        {
            return null;
        }

        var epoch = component.TryGetValue("epoch", out var epochValue) && epochValue.IsNumeric ? epochValue.ToInt32() : 0;
        var release = component.TryGetValue("release", out var releaseValue) && releaseValue.IsString ? releaseValue.AsString : string.Empty;
        var architecture = component.TryGetValue("architecture", out var archValue) && archValue.IsString ? archValue.AsString : null;

        return new NevraComponent(name, epoch, version, release, architecture);
    }

    private static EvrComponent? DeserializeEvrComponent(BsonDocument parent, string field)
    {
        if (!parent.TryGetValue(field, out var value) || !value.IsBsonDocument)
        {
            return null;
        }

        var component = value.AsBsonDocument;
        var epoch = component.TryGetValue("epoch", out var epochValue) && epochValue.IsNumeric ? epochValue.ToInt32() : 0;
        var upstream = component.TryGetValue("upstreamVersion", out var upstreamValue) && upstreamValue.IsString ? upstreamValue.AsString : null;
        if (upstream is null)
        {
            return null;
        }

        var revision = component.TryGetValue("revision", out var revisionValue) && revisionValue.IsString ? revisionValue.AsString : null;
        return new EvrComponent(epoch, upstream, revision);
    }

    private static DateTimeOffset? TryReadDateTime(BsonDocument document, string field)
        => document.TryGetValue(field, out var value) ? TryConvertDateTime(value) : null;

    private static DateTimeOffset? TryConvertDateTime(BsonValue? value)
    {
        if (value is null)
        {
            return null;
        }

        return value switch
        {
            BsonDateTime dateTime => DateTime.SpecifyKind(dateTime.ToUniversalTime(), DateTimeKind.Utc),
            BsonString stringValue when DateTimeOffset.TryParse(stringValue.AsString, out var parsed) => parsed.ToUniversalTime(),
            _ => null,
        };
    }

}
