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.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 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,
        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();
        _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" },
            };

            var result = await _fetchService.FetchAsync(request, cancellationToken).ConfigureAwait(false);
            if (result.IsNotModified)
            {
                await UpdateCursorAsync(cursor, cancellationToken).ConfigureAwait(false);
                return;
            }

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

            var pendingDocuments = cursor.PendingDocuments.ToHashSet();
            pendingDocuments.Add(result.Document.Id);

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

            await UpdateCursorAsync(updatedCursor, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            _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)
            {
                _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;
            }

            KevCatalogDto? catalog;
            try
            {
                var rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
                catalog = JsonSerializer.Deserialize<KevCatalogDto>(rawBytes, SerializerOptions);
            }
            catch (Exception ex)
            {
                _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;
            }

            if (catalog is null)
            {
                _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;
            }

            try
            {
                var payloadJson = JsonSerializer.Serialize(catalog, SerializerOptions);
                var payload = BsonDocument.Parse(payloadJson);
                var entryCount = catalog.Vulnerabilities?.Count ?? 0;
                var released = catalog.DateReleased?.ToUniversalTime();

                _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);
            _logger.LogInformation(
                "Mapped {AdvisoryCount} KEV advisories from catalog version {CatalogVersion}",
                advisories.Count,
                catalog.CatalogVersion ?? "(unknown)");
            _diagnostics.AdvisoriesMapped(catalog.CatalogVersion, advisories.Count);

            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 static Uri? TryParseUri(string? value)
        => Uri.TryCreate(value, UriKind.Absolute, out var uri) ? uri : null;
}
