using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Normalization.Text;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Common.Fetch;
using StellaOps.Feedser.Source.Cve.Configuration;
using StellaOps.Feedser.Source.Cve.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.Cve;

public sealed class CveConnector : IFeedConnector
{
    private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
    {
        PropertyNameCaseInsensitive = true,
        WriteIndented = false,
    };

    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 CveOptions _options;
    private readonly CveDiagnostics _diagnostics;
    private readonly TimeProvider _timeProvider;
    private readonly ILogger<CveConnector> _logger;

    public CveConnector(
        SourceFetchService fetchService,
        RawDocumentStorage rawDocumentStorage,
        IDocumentStore documentStore,
        IDtoStore dtoStore,
        IAdvisoryStore advisoryStore,
        ISourceStateRepository stateRepository,
        IOptions<CveOptions> options,
        CveDiagnostics diagnostics,
        TimeProvider? timeProvider,
        ILogger<CveConnector> 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 ?? throw new ArgumentNullException(nameof(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 => CveConnectorPlugin.SourceName;

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

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

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

        var since = cursor.CurrentWindowStart ?? cursor.LastModifiedExclusive ?? now - _options.InitialBackfill;
        if (since > now)
        {
            since = now;
        }

        var windowEnd = cursor.CurrentWindowEnd ?? now;
        if (windowEnd <= since)
        {
            windowEnd = since + TimeSpan.FromMinutes(1);
        }

        var page = cursor.NextPage <= 0 ? 1 : cursor.NextPage;
        var pagesFetched = 0;
        var hasMorePages = true;
        DateTimeOffset? maxModified = cursor.LastModifiedExclusive;

        while (hasMorePages && pagesFetched < _options.MaxPagesPerFetch)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var requestUri = BuildListRequestUri(since, windowEnd, page, _options.PageSize);
            var metadata = new Dictionary<string, string>(StringComparer.Ordinal)
            {
                ["since"] = since.ToString("O"),
                ["until"] = windowEnd.ToString("O"),
                ["page"] = page.ToString(CultureInfo.InvariantCulture),
                ["pageSize"] = _options.PageSize.ToString(CultureInfo.InvariantCulture),
            };

            SourceFetchContentResult listResult;
            try
            {
                _diagnostics.FetchAttempt();
                listResult = await _fetchService.FetchContentAsync(
                    new SourceFetchRequest(
                        CveOptions.HttpClientName,
                        SourceName,
                        requestUri)
                    {
                        Metadata = metadata,
                        AcceptHeaders = new[] { "application/json" },
                    },
                    cancellationToken).ConfigureAwait(false);
            }
            catch (HttpRequestException ex)
            {
                _diagnostics.FetchFailure();
                await _stateRepository.MarkFailureAsync(SourceName, now, _options.FailureBackoff, ex.Message, cancellationToken).ConfigureAwait(false);
                throw;
            }

            if (listResult.IsNotModified)
            {
                _diagnostics.FetchUnchanged();
                break;
            }

            if (!listResult.IsSuccess || listResult.Content is null)
            {
                _diagnostics.FetchFailure();
                break;
            }

            var pageModel = CveListParser.Parse(listResult.Content, page, _options.PageSize);

            if (pageModel.Items.Count == 0)
            {
                hasMorePages = false;
            }

            foreach (var item in pageModel.Items)
            {
                cancellationToken.ThrowIfCancellationRequested();

                var detailUri = BuildDetailRequestUri(item.CveId);
                var detailMetadata = new Dictionary<string, string>(StringComparer.Ordinal)
                {
                    ["cveId"] = item.CveId,
                    ["page"] = page.ToString(CultureInfo.InvariantCulture),
                    ["since"] = since.ToString("O"),
                    ["until"] = windowEnd.ToString("O"),
                };

                SourceFetchResult detailResult;
                try
                {
                    detailResult = await _fetchService.FetchAsync(
                        new SourceFetchRequest(
                            CveOptions.HttpClientName,
                            SourceName,
                            detailUri)
                        {
                            Metadata = detailMetadata,
                            AcceptHeaders = new[] { "application/json" },
                        },
                        cancellationToken).ConfigureAwait(false);
                }
                catch (HttpRequestException ex)
                {
                    _diagnostics.FetchFailure();
                    _logger.LogWarning(ex, "Failed fetching CVE record {CveId}", item.CveId);
                    continue;
                }

                if (detailResult.IsNotModified)
                {
                    _diagnostics.FetchUnchanged();
                    continue;
                }

                if (!detailResult.IsSuccess || detailResult.Document is null)
                {
                    _diagnostics.FetchFailure();
                    continue;
                }

                _diagnostics.FetchDocument();
                pendingDocuments.Add(detailResult.Document.Id);
                pendingMappings.Add(detailResult.Document.Id);
            }

            if (pageModel.MaxModified.HasValue)
            {
                if (!maxModified.HasValue || pageModel.MaxModified > maxModified)
                {
                    maxModified = pageModel.MaxModified;
                }
            }

            hasMorePages = pageModel.HasMorePages;
            page = pageModel.NextPageCandidate;
            pagesFetched++;

            if (hasMorePages && _options.RequestDelay > TimeSpan.Zero)
            {
                await Task.Delay(_options.RequestDelay, cancellationToken).ConfigureAwait(false);
            }
        }

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

        if (hasMorePages)
        {
            updatedCursor = updatedCursor
                .WithCurrentWindowStart(since)
                .WithCurrentWindowEnd(windowEnd)
                .WithNextPage(page);
        }
        else
        {
            var nextSince = maxModified ?? windowEnd;
            updatedCursor = updatedCursor
                .WithLastModifiedExclusive(nextSince)
                .WithCurrentWindowStart(null)
                .WithCurrentWindowEnd(null)
                .WithNextPage(1);
        }

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

    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();

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

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

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

            byte[] rawBytes;
            try
            {
                rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                _diagnostics.ParseFailure();
                _logger.LogError(ex, "Unable to download CVE raw document {DocumentId}", documentId);
                throw;
            }

            CveRecordDto dto;
            try
            {
                dto = CveRecordParser.Parse(rawBytes);
            }
            catch (JsonException ex)
            {
                _diagnostics.ParseQuarantine();
                _logger.LogError(ex, "Malformed CVE JSON for {DocumentId}", documentId);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                remainingDocuments.Remove(documentId);
                continue;
            }

            var payload = BsonDocument.Parse(JsonSerializer.Serialize(dto, SerializerOptions));
            var dtoRecord = new DtoRecord(
                Guid.NewGuid(),
                document.Id,
                SourceName,
                "cve/5.0",
                payload,
                _timeProvider.GetUtcNow());

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

            remainingDocuments.Remove(documentId);
            _diagnostics.ParseSuccess();
        }

        var updatedCursor = cursor
            .WithPendingDocuments(remainingDocuments);

        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.ToList();

        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)
            {
                _logger.LogWarning("Skipping CVE mapping for {DocumentId}: DTO or document missing", documentId);
                pendingMappings.Remove(documentId);
                continue;
            }

            CveRecordDto dto;
            try
            {
                dto = JsonSerializer.Deserialize<CveRecordDto>(dtoRecord.Payload.ToJson(), SerializerOptions)
                    ?? throw new InvalidOperationException("Deserialized DTO was null.");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to deserialize CVE DTO for {DocumentId}", documentId);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pendingMappings.Remove(documentId);
                continue;
            }

            var recordedAt = dtoRecord.ValidatedAt;
            var advisory = CveMapper.Map(dto, document, recordedAt);

            await _advisoryStore.UpsertAsync(advisory, cancellationToken).ConfigureAwait(false);
            await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);
            pendingMappings.Remove(documentId);
            _diagnostics.MapSuccess(1);
        }

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

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

    private async Task UpdateCursorAsync(CveCursor cursor, CancellationToken cancellationToken)
    {
        await _stateRepository.UpdateCursorAsync(SourceName, cursor.ToBsonDocument(), _timeProvider.GetUtcNow(), cancellationToken).ConfigureAwait(false);
    }

    private static Uri BuildListRequestUri(DateTimeOffset since, DateTimeOffset until, int page, int pageSize)
    {
        var query = $"time_modified.gte={Uri.EscapeDataString(since.ToString("O"))}&time_modified.lte={Uri.EscapeDataString(until.ToString("O"))}&page={page}&size={pageSize}";
        return new Uri($"cve?{query}", UriKind.Relative);
    }

    private static Uri BuildDetailRequestUri(string cveId)
    {
        var encoded = Uri.EscapeDataString(cveId);
        return new Uri($"cve/{encoded}", UriKind.Relative);
    }
}
