using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Net.Http;
using System.Text.Json;
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.Vndr.Msrc.Configuration;
using StellaOps.Feedser.Source.Vndr.Msrc.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.Vndr.Msrc;

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

    private readonly MsrcApiClient _apiClient;
    private readonly MsrcDetailParser _detailParser;
    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 MsrcOptions _options;
    private readonly TimeProvider _timeProvider;
    private readonly ILogger<MsrcConnector> _logger;
    private readonly MsrcDiagnostics _diagnostics;

    public MsrcConnector(
        MsrcApiClient apiClient,
        MsrcDetailParser detailParser,
        SourceFetchService fetchService,
        RawDocumentStorage rawDocumentStorage,
        IDocumentStore documentStore,
        IDtoStore dtoStore,
        IAdvisoryStore advisoryStore,
        ISourceStateRepository stateRepository,
        IOptions<MsrcOptions> options,
        TimeProvider? timeProvider,
        MsrcDiagnostics diagnostics,
        ILogger<MsrcConnector> logger)
    {
        _apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
        _detailParser = detailParser ?? throw new ArgumentNullException(nameof(detailParser));
        _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();
        _timeProvider = timeProvider ?? TimeProvider.System;
        _diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public string SourceName => MsrcConnectorPlugin.SourceName;

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

        var cursor = await GetCursorAsync(cancellationToken).ConfigureAwait(false);
        var now = _timeProvider.GetUtcNow();
        var from = cursor.LastModifiedCursor ?? _options.InitialLastModified ?? now.AddDays(-30);
        from = from.Add(-_options.CursorOverlap);
        var to = now;

        _diagnostics.SummaryFetchAttempt();
        IReadOnlyList<MsrcVulnerabilitySummary> summaries;

        try
        {
            summaries = await _apiClient.FetchSummariesAsync(from, to, cancellationToken).ConfigureAwait(false);
            var windowHours = (to - from).TotalHours;
            _diagnostics.SummaryFetchSuccess(summaries.Count, windowHours);
        }
        catch (Exception ex)
        {
            _diagnostics.SummaryFetchFailure("exception");
            _logger.LogError(ex, "MSRC summary fetch failed");
            await _stateRepository.MarkFailureAsync(SourceName, now, _options.FailureBackoff, ex.Message, cancellationToken).ConfigureAwait(false);
            throw;
        }

        if (summaries.Count == 0)
        {
            await UpdateCursorAsync(cursor.WithLastModifiedCursor(to), cancellationToken).ConfigureAwait(false);
            return;
        }

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

        foreach (var summary in summaries.OrderBy(static s => s.LastModifiedDate ?? DateTimeOffset.MinValue))
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (processed >= _options.MaxAdvisoriesPerFetch)
            {
                break;
            }

            var vulnerabilityId = string.IsNullOrWhiteSpace(summary.VulnerabilityId) ? summary.Id : summary.VulnerabilityId!;
            var detailUri = _apiClient.BuildDetailUri(vulnerabilityId).ToString();

            try
            {
                var existing = await _documentStore.FindBySourceAndUriAsync(SourceName, detailUri, cancellationToken).ConfigureAwait(false);
                if (existing is not null && !ShouldRefresh(summary, existing))
                {
                    _diagnostics.DetailFetchNotModified();
                    continue;
                }

                _diagnostics.DetailFetchAttempt();
                if (existing?.GridFsId is { } oldGridId)
                {
                    await _rawDocumentStorage.DeleteAsync(oldGridId, cancellationToken).ConfigureAwait(false);
                }

                var bytes = await _apiClient.FetchDetailAsync(vulnerabilityId, cancellationToken).ConfigureAwait(false);
                var sha = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();

                var gridId = await _rawDocumentStorage.UploadAsync(SourceName, detailUri, bytes, "application/json", cancellationToken).ConfigureAwait(false);

                var metadata = MsrcDocumentMetadata.CreateMetadata(summary);
                var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                {
                    ["content-type"] = "application/json",
                };

                var documentId = existing?.Id ?? Guid.NewGuid();
                var record = new DocumentRecord(
                    documentId,
                    SourceName,
                    detailUri,
                    now,
                    sha,
                    DocumentStatuses.PendingParse,
                    ContentType: "application/json",
                    Headers: headers,
                    metadata,
                    existing?.Etag,
                    summary.LastModifiedDate,
                    gridId);

                var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);

                pendingDocuments.Add(upserted.Id);
                pendingMappings.Remove(upserted.Id);
                _diagnostics.DetailFetchSuccess();
                processed++;

                if (_options.DownloadCvrf)
                {
                    await FetchCvrfAsync(summary, now, cancellationToken).ConfigureAwait(false);
                }

                if (_options.RequestDelay > TimeSpan.Zero)
                {
                    await Task.Delay(_options.RequestDelay, cancellationToken).ConfigureAwait(false);
                }
            }
            catch (Exception ex)
            {
                _diagnostics.DetailFetchFailure("exception");
                failures++;
                _logger.LogError(ex, "MSRC detail fetch failed for {VulnerabilityId}", vulnerabilityId);
                await _stateRepository.MarkFailureAsync(SourceName, now, _options.FailureBackoff, ex.Message, cancellationToken).ConfigureAwait(false);
                throw;
            }
        }

        _diagnostics.DetailEnqueued(processed);

        if (processed > 0 || failures > 0)
        {
            _logger.LogInformation("MSRC fetch cycle enqueued {Processed} advisories (failures={Failures}, pendingDocuments={PendingDocuments}, pendingMappings={PendingMappings})", processed, failures, pendingDocuments.Count, pendingMappings.Count);
        }

        var latestCursor = summaries
            .Where(static s => s.LastModifiedDate.HasValue)
            .Select(static s => s.LastModifiedDate!.Value)
            .DefaultIfEmpty(to)
            .Max();

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

        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.ToHashSet();
        var pendingMappings = cursor.PendingMappings.ToHashSet();
        var now = _timeProvider.GetUtcNow();

        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)
            {
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                remainingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
                _diagnostics.ParseFailure("missing_payload");
                continue;
            }

            byte[] payload;
            try
            {
                payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                _diagnostics.ParseFailure("download_failed");
                _logger.LogError(ex, "MSRC unable to download document {DocumentId}", document.Id);
                throw;
            }

            MsrcVulnerabilityDetailDto? detail;
            try
            {
                detail = JsonSerializer.Deserialize<MsrcVulnerabilityDetailDto>(payload, SerializerOptions);
            }
            catch (Exception ex)
            {
                _diagnostics.ParseFailure("deserialize_failed");
                _logger.LogError(ex, "MSRC failed to deserialize detail payload for document {DocumentId}", document.Id);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                remainingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
                continue;
            }

            if (detail is null)
            {
                _diagnostics.ParseFailure("empty_payload");
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                remainingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
                continue;
            }

            var dto = _detailParser.Parse(detail);
            var bson = BsonDocument.Parse(JsonSerializer.Serialize(dto, SerializerOptions));
            var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, SourceName, "msrc.detail.v1", bson, now);
            await _dtoStore.UpsertAsync(dtoRecord, cancellationToken).ConfigureAwait(false);
            await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.PendingMap, cancellationToken).ConfigureAwait(false);
            remainingDocuments.Remove(documentId);
            pendingMappings.Add(document.Id);
            _diagnostics.ParseSuccess(dto.Products.Count, dto.KbIds.Count);
        }

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

        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 document = await _documentStore.FindAsync(documentId, cancellationToken).ConfigureAwait(false);
            if (document is null)
            {
                pendingMappings.Remove(documentId);
                continue;
            }

            var dtoRecord = await _dtoStore.FindByDocumentIdAsync(documentId, cancellationToken).ConfigureAwait(false);
            if (dtoRecord is null)
            {
                _diagnostics.MapFailure("missing_dto");
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pendingMappings.Remove(documentId);
                continue;
            }

            MsrcAdvisoryDto? dto;
            try
            {
                dto = JsonSerializer.Deserialize<MsrcAdvisoryDto>(dtoRecord.Payload.ToJson(), SerializerOptions);
            }
            catch (Exception ex)
            {
                _diagnostics.MapFailure("deserialize_dto_failed");
                _logger.LogError(ex, "MSRC failed to deserialize DTO for document {DocumentId}", document.Id);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pendingMappings.Remove(documentId);
                continue;
            }

            if (dto is null)
            {
                _diagnostics.MapFailure("null_dto");
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pendingMappings.Remove(documentId);
                continue;
            }

            try
            {
                var advisory = MsrcMapper.Map(dto, document, dtoRecord.ValidatedAt);
                await _advisoryStore.UpsertAsync(advisory, cancellationToken).ConfigureAwait(false);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);
                pendingMappings.Remove(documentId);
                _diagnostics.MapSuccess(advisory.Aliases.Length, advisory.AffectedPackages.Length);
            }
            catch (Exception ex)
            {
                _diagnostics.MapFailure("exception");
                _logger.LogError(ex, "MSRC mapping failed for document {DocumentId}", document.Id);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pendingMappings.Remove(documentId);
            }
        }

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

    private bool ShouldRefresh(MsrcVulnerabilitySummary summary, DocumentRecord existing)
    {
        if (existing.Status == DocumentStatuses.Failed)
        {
            return true;
        }

        if (summary.LastModifiedDate is null)
        {
            return true;
        }

        if (existing.Metadata is null || !existing.Metadata.TryGetValue("msrc.lastModified", out var stored))
        {
            return true;
        }

        return !string.Equals(stored, summary.LastModifiedDate.Value.ToString("O"), StringComparison.OrdinalIgnoreCase);
    }

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

    private async Task FetchCvrfAsync(MsrcVulnerabilitySummary summary, DateTimeOffset fetchedAt, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(summary.CvrfUrl))
        {
            return;
        }

        try
        {
            var uri = new Uri(summary.CvrfUrl);
            var metadata = MsrcDocumentMetadata.CreateCvrfMetadata(summary);
            var existing = await _documentStore.FindBySourceAndUriAsync(SourceName, uri.ToString(), cancellationToken).ConfigureAwait(false);
            var request = new SourceFetchRequest(
                MsrcOptions.HttpClientName,
                SourceName,
                HttpMethod.Get,
                uri,
                metadata,
                existing?.Etag,
                existing?.LastModified,
                AcceptHeaders: new[] { "application/zip", "application/xml", "application/json" });

            var result = await _fetchService.FetchAsync(request, cancellationToken).ConfigureAwait(false);
            if (result.IsNotModified || !result.IsSuccess || result.Document is null)
            {
                return;
            }

            await _documentStore.UpdateStatusAsync(result.Document.Id, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);
            _logger.LogInformation("MSRC CVRF artefact captured for {AdvisoryId} ({Uri})", summary.VulnerabilityId ?? summary.Id, uri);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "MSRC CVRF download failed for {CvrfUrl}", summary.CvrfUrl);
        }
    }

    private Task UpdateCursorAsync(MsrcCursor cursor, CancellationToken cancellationToken)
    {
        var document = cursor.ToBsonDocument();
        var completedAt = _timeProvider.GetUtcNow();
        return _stateRepository.UpdateCursorAsync(SourceName, document, completedAt, cancellationToken);
    }
}
