using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Common.Fetch;
using StellaOps.Feedser.Source.Common.Json;
using StellaOps.Feedser.Source.Kev.Configuration;
using StellaOps.Feedser.Source.Kev.Internal;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Storage.Mongo.Advisories;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Storage.Mongo.Dtos;
using StellaOps.Plugin;

namespace StellaOps.Feedser.Source.Kev;

public sealed class KevConnector : IFeedConnector
{
    private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
    {
        PropertyNameCaseInsensitive = true,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    };

    private const string SchemaVersion = "kev.catalog.v1";

    private readonly SourceFetchService _fetchService;
    private readonly RawDocumentStorage _rawDocumentStorage;
    private readonly IDocumentStore _documentStore;
    private readonly IDtoStore _dtoStore;
    private readonly IAdvisoryStore _advisoryStore;
    private readonly ISourceStateRepository _stateRepository;
    private readonly KevOptions _options;
    private readonly IJsonSchemaValidator _schemaValidator;
    private readonly TimeProvider _timeProvider;
    private readonly ILogger<KevConnector> _logger;
    private readonly KevDiagnostics _diagnostics;

    public KevConnector(
        SourceFetchService fetchService,
        RawDocumentStorage rawDocumentStorage,
        IDocumentStore documentStore,
        IDtoStore dtoStore,
        IAdvisoryStore advisoryStore,
        ISourceStateRepository stateRepository,
        IOptions<KevOptions> options,
        IJsonSchemaValidator schemaValidator,
        KevDiagnostics diagnostics,
        TimeProvider? timeProvider,
        ILogger<KevConnector> logger)
    {
        _fetchService = fetchService ?? throw new ArgumentNullException(nameof(fetchService));
        _rawDocumentStorage = rawDocumentStorage ?? throw new ArgumentNullException(nameof(rawDocumentStorage));
        _documentStore = documentStore ?? throw new ArgumentNullException(nameof(documentStore));
        _dtoStore = dtoStore ?? throw new ArgumentNullException(nameof(dtoStore));
        _advisoryStore = advisoryStore ?? throw new ArgumentNullException(nameof(advisoryStore));
        _stateRepository = stateRepository ?? throw new ArgumentNullException(nameof(stateRepository));
        _options = options?.Value ?? throw new ArgumentNullException(nameof(options));
        _options.Validate();
        _schemaValidator = schemaValidator ?? throw new ArgumentNullException(nameof(schemaValidator));
        _diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
        _timeProvider = timeProvider ?? TimeProvider.System;
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public string SourceName => KevConnectorPlugin.SourceName;

    public async Task FetchAsync(IServiceProvider services, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(services);

        var cursor = await GetCursorAsync(cancellationToken).ConfigureAwait(false);
        var now = _timeProvider.GetUtcNow();

        try
        {
            var existing = await _documentStore.FindBySourceAndUriAsync(SourceName, _options.FeedUri.ToString(), cancellationToken).ConfigureAwait(false);

            var request = new SourceFetchRequest(
                KevOptions.HttpClientName,
                SourceName,
                _options.FeedUri)
            {
                Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
                {
                    ["kev.cursor.catalogVersion"] = cursor.CatalogVersion ?? string.Empty,
                    ["kev.cursor.catalogReleased"] = cursor.CatalogReleased?.ToString("O") ?? string.Empty,
                },
                ETag = existing?.Etag,
                LastModified = existing?.LastModified,
                TimeoutOverride = _options.RequestTimeout,
                AcceptHeaders = new[] { "application/json", "text/json" },
            };

            _diagnostics.FetchAttempt();
            var result = await _fetchService.FetchAsync(request, cancellationToken).ConfigureAwait(false);
            if (result.IsNotModified)
            {
                _diagnostics.FetchUnchanged();
                _logger.LogInformation(
                    "KEV catalog not modified (catalogVersion={CatalogVersion}, etag={Etag})",
                    cursor.CatalogVersion ?? "(unknown)",
                    existing?.Etag ?? "(none)");
                await UpdateCursorAsync(cursor, cancellationToken).ConfigureAwait(false);
                return;
            }

            if (!result.IsSuccess || result.Document is null)
            {
                _diagnostics.FetchFailure();
                await _stateRepository.MarkFailureAsync(SourceName, now, TimeSpan.FromMinutes(5), "KEV feed returned no content.", cancellationToken).ConfigureAwait(false);
                return;
            }

            _diagnostics.FetchSuccess();

            var pendingDocuments = cursor.PendingDocuments.ToHashSet();
            var pendingMappings = cursor.PendingMappings.ToHashSet();
            var pendingDocumentsBefore = pendingDocuments.Count;
            var pendingMappingsBefore = pendingMappings.Count;

            pendingDocuments.Add(result.Document.Id);

            var updatedCursor = cursor
                .WithPendingDocuments(pendingDocuments)
                .WithPendingMappings(pendingMappings);

            var document = result.Document;
            var lastModified = document.LastModified?.ToUniversalTime().ToString("O") ?? "(unknown)";
            _logger.LogInformation(
                "Fetched KEV catalog document {DocumentId} (etag={Etag}, lastModified={LastModified}) pendingDocuments={PendingDocumentsBefore}->{PendingDocumentsAfter} pendingMappings={PendingMappingsBefore}->{PendingMappingsAfter}",
                document.Id,
                document.Etag ?? "(none)",
                lastModified,
                pendingDocumentsBefore,
                pendingDocuments.Count,
                pendingMappingsBefore,
                pendingMappings.Count);

            await UpdateCursorAsync(updatedCursor, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            _diagnostics.FetchFailure();
            _logger.LogError(ex, "KEV fetch failed for {Uri}", _options.FeedUri);
            await _stateRepository.MarkFailureAsync(SourceName, now, TimeSpan.FromMinutes(5), ex.Message, cancellationToken).ConfigureAwait(false);
            throw;
        }
    }

    public async Task ParseAsync(IServiceProvider services, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(services);

        var cursor = await GetCursorAsync(cancellationToken).ConfigureAwait(false);
        if (cursor.PendingDocuments.Count == 0)
        {
            return;
        }

        var remainingDocuments = cursor.PendingDocuments.ToList();
        var pendingMappings = cursor.PendingMappings.ToHashSet();
        var latestCatalogVersion = cursor.CatalogVersion;
        var latestCatalogReleased = cursor.CatalogReleased;

        foreach (var documentId in cursor.PendingDocuments)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var document = await _documentStore.FindAsync(documentId, cancellationToken).ConfigureAwait(false);
            if (document is null)
            {
                remainingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
                continue;
            }

            if (!document.GridFsId.HasValue)
            {
                _diagnostics.ParseFailure("missingPayload", cursor.CatalogVersion);
                _logger.LogWarning("KEV document {DocumentId} missing GridFS payload", document.Id);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                remainingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
                continue;
            }

            byte[] rawBytes;
            try
            {
                rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                _diagnostics.ParseFailure("download", cursor.CatalogVersion);
                _logger.LogError(ex, "KEV parse failed for document {DocumentId}", document.Id);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                remainingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
                continue;
            }

            KevCatalogDto? catalog = null;
            string? catalogVersion = null;
            try
            {
                using var jsonDocument = JsonDocument.Parse(rawBytes);
                catalogVersion = TryGetCatalogVersion(jsonDocument.RootElement);
                _schemaValidator.Validate(jsonDocument, KevSchemaProvider.Schema, document.Uri);
                catalog = jsonDocument.RootElement.Deserialize<KevCatalogDto>(SerializerOptions);
            }
            catch (JsonSchemaValidationException ex)
            {
                _diagnostics.ParseFailure("schema", catalogVersion);
                _logger.LogWarning(ex, "KEV schema validation failed for document {DocumentId}", document.Id);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                remainingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
                continue;
            }
            catch (JsonException ex)
            {
                _diagnostics.ParseFailure("invalidJson", catalogVersion);
                _logger.LogError(ex, "KEV JSON parsing failed for document {DocumentId}", document.Id);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                remainingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
                continue;
            }
            catch (Exception ex)
            {
                _diagnostics.ParseFailure("deserialize", catalogVersion);
                _logger.LogError(ex, "KEV catalog deserialization failed for document {DocumentId}", document.Id);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                remainingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
                continue;
            }

            if (catalog is null)
            {
                _diagnostics.ParseFailure("emptyCatalog", catalogVersion);
                _logger.LogWarning("KEV catalog payload was empty for document {DocumentId}", document.Id);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                remainingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
                continue;
            }

            var entryCount = catalog.Vulnerabilities?.Count ?? 0;
            var released = catalog.DateReleased?.ToUniversalTime();
            RecordCatalogAnomalies(catalog);

            try
            {
                var payloadJson = JsonSerializer.Serialize(catalog, SerializerOptions);
                var payload = BsonDocument.Parse(payloadJson);

                _logger.LogInformation(
                    "Parsed KEV catalog document {DocumentId} (version={CatalogVersion}, released={Released}, entries={EntryCount})",
                    document.Id,
                    catalog.CatalogVersion ?? "(unknown)",
                    released,
                    entryCount);
                _diagnostics.CatalogParsed(catalog.CatalogVersion, entryCount);

                var dtoRecord = new DtoRecord(
                    Guid.NewGuid(),
                    document.Id,
                    SourceName,
                    SchemaVersion,
                    payload,
                    _timeProvider.GetUtcNow());

                await _dtoStore.UpsertAsync(dtoRecord, cancellationToken).ConfigureAwait(false);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.PendingMap, cancellationToken).ConfigureAwait(false);

                remainingDocuments.Remove(documentId);
                pendingMappings.Add(document.Id);

                latestCatalogVersion = catalog.CatalogVersion ?? latestCatalogVersion;
                latestCatalogReleased = catalog.DateReleased ?? latestCatalogReleased;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "KEV DTO persistence failed for document {DocumentId}", document.Id);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                remainingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
            }
        }

        var updatedCursor = cursor
            .WithPendingDocuments(remainingDocuments)
            .WithPendingMappings(pendingMappings)
            .WithCatalogMetadata(latestCatalogVersion, latestCatalogReleased);

        await UpdateCursorAsync(updatedCursor, cancellationToken).ConfigureAwait(false);
    }

    public async Task MapAsync(IServiceProvider services, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(services);

        var cursor = await GetCursorAsync(cancellationToken).ConfigureAwait(false);
        if (cursor.PendingMappings.Count == 0)
        {
            return;
        }

        var pendingMappings = cursor.PendingMappings.ToHashSet();

        foreach (var documentId in cursor.PendingMappings)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var dtoRecord = await _dtoStore.FindByDocumentIdAsync(documentId, cancellationToken).ConfigureAwait(false);
            var document = await _documentStore.FindAsync(documentId, cancellationToken).ConfigureAwait(false);

            if (dtoRecord is null || document is null)
            {
                pendingMappings.Remove(documentId);
                continue;
            }

            KevCatalogDto? catalog;
            try
            {
                var dtoJson = dtoRecord.Payload.ToJson(new MongoDB.Bson.IO.JsonWriterSettings
                {
                    OutputMode = MongoDB.Bson.IO.JsonOutputMode.RelaxedExtendedJson,
                });

                catalog = JsonSerializer.Deserialize<KevCatalogDto>(dtoJson, SerializerOptions);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "KEV mapping: failed to deserialize DTO for document {DocumentId}", document.Id);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pendingMappings.Remove(documentId);
                continue;
            }

            if (catalog is null)
            {
                _logger.LogWarning("KEV mapping: DTO payload was empty for document {DocumentId}", document.Id);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pendingMappings.Remove(documentId);
                continue;
            }

            var feedUri = TryParseUri(document.Uri) ?? _options.FeedUri;
            var advisories = KevMapper.Map(catalog, SourceName, feedUri, document.FetchedAt, dtoRecord.ValidatedAt);
            var entryCount = catalog.Vulnerabilities?.Count ?? 0;
            var mappedCount = advisories.Count;
            var skippedCount = Math.Max(0, entryCount - mappedCount);
            _logger.LogInformation(
                "Mapped {MappedCount}/{EntryCount} KEV advisories from catalog version {CatalogVersion} (skipped={SkippedCount})",
                mappedCount,
                entryCount,
                catalog.CatalogVersion ?? "(unknown)",
                skippedCount);
            _diagnostics.AdvisoriesMapped(catalog.CatalogVersion, mappedCount);

            foreach (var advisory in advisories)
            {
                await _advisoryStore.UpsertAsync(advisory, cancellationToken).ConfigureAwait(false);
            }

            await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);
            pendingMappings.Remove(documentId);
        }

        var updatedCursor = cursor.WithPendingMappings(pendingMappings);
        await UpdateCursorAsync(updatedCursor, cancellationToken).ConfigureAwait(false);
    }

    private async Task<KevCursor> GetCursorAsync(CancellationToken cancellationToken)
    {
        var state = await _stateRepository.TryGetAsync(SourceName, cancellationToken).ConfigureAwait(false);
        return state is null ? KevCursor.Empty : KevCursor.FromBson(state.Cursor);
    }

    private Task UpdateCursorAsync(KevCursor cursor, CancellationToken cancellationToken)
    {
        return _stateRepository.UpdateCursorAsync(SourceName, cursor.ToBsonDocument(), _timeProvider.GetUtcNow(), cancellationToken);
    }

    private void RecordCatalogAnomalies(KevCatalogDto catalog)
    {
        ArgumentNullException.ThrowIfNull(catalog);

        var version = catalog.CatalogVersion;
        var vulnerabilities = catalog.Vulnerabilities ?? Array.Empty<KevVulnerabilityDto>();

        if (catalog.Count != vulnerabilities.Count)
        {
            _diagnostics.RecordAnomaly("countMismatch", version);
        }

        foreach (var entry in vulnerabilities)
        {
            if (entry is null)
            {
                _diagnostics.RecordAnomaly("nullEntry", version);
                continue;
            }

            if (string.IsNullOrWhiteSpace(entry.CveId))
            {
                _diagnostics.RecordAnomaly("missingCveId", version);
            }
        }
    }

    private static string? TryGetCatalogVersion(JsonElement root)
    {
        if (root.ValueKind != JsonValueKind.Object)
        {
            return null;
        }

        if (root.TryGetProperty("catalogVersion", out var versionElement) && versionElement.ValueKind == JsonValueKind.String)
        {
            return versionElement.GetString();
        }

        return null;
    }

    private static Uri? TryParseUri(string? value)
        => Uri.TryCreate(value, UriKind.Absolute, out var uri) ? uri : null;
}
