using System;
using System.Collections.Generic;
using System.Linq;
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 MongoDB.Bson.IO;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Common.Fetch;
using StellaOps.Feedser.Source.Vndr.Vmware.Configuration;
using StellaOps.Feedser.Source.Vndr.Vmware.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.Feedser.Storage.Mongo.PsirtFlags;
using StellaOps.Plugin;

namespace StellaOps.Feedser.Source.Vndr.Vmware;

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

    private readonly IHttpClientFactory _httpClientFactory;
    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 IPsirtFlagStore _psirtFlagStore;
    private readonly VmwareOptions _options;
    private readonly TimeProvider _timeProvider;
    private readonly VmwareDiagnostics _diagnostics;
    private readonly ILogger<VmwareConnector> _logger;

    public VmwareConnector(
        IHttpClientFactory httpClientFactory,
        SourceFetchService fetchService,
        RawDocumentStorage rawDocumentStorage,
        IDocumentStore documentStore,
        IDtoStore dtoStore,
        IAdvisoryStore advisoryStore,
        ISourceStateRepository stateRepository,
        IPsirtFlagStore psirtFlagStore,
        IOptions<VmwareOptions> options,
        TimeProvider? timeProvider,
        VmwareDiagnostics diagnostics,
        ILogger<VmwareConnector> logger)
    {
        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
        _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));
        _psirtFlagStore = psirtFlagStore ?? throw new ArgumentNullException(nameof(psirtFlagStore));
        _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 => VmwareConnectorPlugin.SourceName;

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

        var cursor = await GetCursorAsync(cancellationToken).ConfigureAwait(false);
        var now = _timeProvider.GetUtcNow();
        var pendingDocuments = cursor.PendingDocuments.ToHashSet();
        var pendingMappings = cursor.PendingMappings.ToHashSet();
        var fetchCache = new Dictionary<string, VmwareFetchCacheEntry>(cursor.FetchCache, StringComparer.OrdinalIgnoreCase);
        var touchedResources = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var remainingCapacity = _options.MaxAdvisoriesPerFetch;

        IReadOnlyList<VmwareIndexItem> indexItems;
        try
        {
            indexItems = await FetchIndexAsync(cancellationToken).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            _diagnostics.FetchFailure();
            _logger.LogError(ex, "Failed to retrieve VMware advisory index");
            await _stateRepository.MarkFailureAsync(SourceName, now, TimeSpan.FromMinutes(10), ex.Message, cancellationToken).ConfigureAwait(false);
            throw;
        }

        if (indexItems.Count == 0)
        {
            return;
        }

        var orderedItems = indexItems
            .Where(static item => !string.IsNullOrWhiteSpace(item.Id) && !string.IsNullOrWhiteSpace(item.DetailUrl))
            .OrderBy(static item => item.Modified ?? DateTimeOffset.MinValue)
            .ThenBy(static item => item.Id, StringComparer.OrdinalIgnoreCase)
            .ToArray();

        var baseline = cursor.LastModified ?? now - _options.InitialBackfill;
        var resumeStart = baseline - _options.ModifiedTolerance;
        ProvenanceDiagnostics.ReportResumeWindow(SourceName, resumeStart, _logger);
        var processedIds = new HashSet<string>(cursor.ProcessedIds, StringComparer.OrdinalIgnoreCase);
        var maxModified = cursor.LastModified ?? DateTimeOffset.MinValue;
        var processedUpdated = false;

        foreach (var item in orderedItems)
        {
            if (remainingCapacity <= 0)
            {
                break;
            }

            cancellationToken.ThrowIfCancellationRequested();

            var modified = (item.Modified ?? DateTimeOffset.MinValue).ToUniversalTime();
            if (modified < baseline - _options.ModifiedTolerance)
            {
                continue;
            }

            if (cursor.LastModified.HasValue && modified < cursor.LastModified.Value - _options.ModifiedTolerance)
            {
                continue;
            }

            if (modified == cursor.LastModified && cursor.ProcessedIds.Contains(item.Id, StringComparer.OrdinalIgnoreCase))
            {
                continue;
            }

            if (!Uri.TryCreate(item.DetailUrl, UriKind.Absolute, out var detailUri))
            {
                _logger.LogWarning("VMware advisory {AdvisoryId} has invalid detail URL {Url}", item.Id, item.DetailUrl);
                continue;
            }

            var cacheKey = detailUri.AbsoluteUri;
            touchedResources.Add(cacheKey);

            var existing = await _documentStore.FindBySourceAndUriAsync(SourceName, cacheKey, cancellationToken).ConfigureAwait(false);
            var metadata = new Dictionary<string, string>(StringComparer.Ordinal)
            {
                ["vmware.id"] = item.Id,
                ["vmware.modified"] = modified.ToString("O"),
            };

            SourceFetchResult result;
            try
            {
                result = await _fetchService.FetchAsync(
                    new SourceFetchRequest(VmwareOptions.HttpClientName, SourceName, detailUri)
                    {
                        Metadata = metadata,
                        ETag = existing?.Etag,
                        LastModified = existing?.LastModified,
                        AcceptHeaders = new[] { "application/json" },
                    },
                    cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                _diagnostics.FetchFailure();
                _logger.LogError(ex, "Failed to fetch VMware advisory {AdvisoryId}", item.Id);
                await _stateRepository.MarkFailureAsync(SourceName, now, TimeSpan.FromMinutes(5), ex.Message, cancellationToken).ConfigureAwait(false);
                throw;
            }

            if (result.IsNotModified)
            {
                _diagnostics.FetchUnchanged();
                if (existing is not null)
                {
                    fetchCache[cacheKey] = VmwareFetchCacheEntry.FromDocument(existing);
                    pendingDocuments.Remove(existing.Id);
                    pendingMappings.Remove(existing.Id);
                    _logger.LogInformation("VMware advisory {AdvisoryId} returned 304 Not Modified", item.Id);
                }

                continue;
            }

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

            remainingCapacity--;

            if (modified > maxModified)
            {
                maxModified = modified;
                processedIds.Clear();
                processedUpdated = true;
            }

            if (modified == maxModified)
            {
                processedIds.Add(item.Id);
                processedUpdated = true;
            }

            var cacheEntry = VmwareFetchCacheEntry.FromDocument(result.Document);

            if (existing is not null
                && string.Equals(existing.Status, DocumentStatuses.Mapped, StringComparison.Ordinal)
                && cursor.TryGetFetchCache(cacheKey, out var cachedEntry)
                && cachedEntry.Matches(result.Document))
            {
                _diagnostics.FetchUnchanged();
                fetchCache[cacheKey] = cacheEntry;
                pendingDocuments.Remove(result.Document.Id);
                pendingMappings.Remove(result.Document.Id);
                await _documentStore.UpdateStatusAsync(result.Document.Id, existing.Status, cancellationToken).ConfigureAwait(false);
                _logger.LogInformation("VMware advisory {AdvisoryId} unchanged; skipping reprocessing", item.Id);
                continue;
            }

            _diagnostics.FetchItem();
            fetchCache[cacheKey] = cacheEntry;
            pendingDocuments.Add(result.Document.Id);
            _logger.LogInformation(
                "VMware advisory {AdvisoryId} fetched (documentId={DocumentId}, sha256={Sha})",
                item.Id,
                result.Document.Id,
                result.Document.Sha256);

            if (_options.RequestDelay > TimeSpan.Zero)
            {
                try
                {
                    await Task.Delay(_options.RequestDelay, cancellationToken).ConfigureAwait(false);
                }
                catch (TaskCanceledException)
                {
                    break;
                }
            }
        }

        if (fetchCache.Count > 0 && touchedResources.Count > 0)
        {
            var stale = fetchCache.Keys.Where(key => !touchedResources.Contains(key)).ToArray();
            foreach (var key in stale)
            {
                fetchCache.Remove(key);
            }
        }

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

        if (processedUpdated)
        {
            updatedCursor = updatedCursor.WithLastModified(maxModified, processedIds);
        }

        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 remaining = cursor.PendingDocuments.ToList();
        var pendingMappings = cursor.PendingMappings.ToList();

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

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

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

            byte[] bytes;
            try
            {
                bytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed downloading VMware document {DocumentId}", document.Id);
                throw;
            }

            VmwareDetailDto? detail;
            try
            {
                detail = JsonSerializer.Deserialize<VmwareDetailDto>(bytes, SerializerOptions);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Failed to deserialize VMware advisory {DocumentId}", document.Id);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                remaining.Remove(documentId);
                _diagnostics.ParseFailure();
                continue;
            }

            if (detail is null || string.IsNullOrWhiteSpace(detail.AdvisoryId))
            {
                _logger.LogWarning("VMware advisory document {DocumentId} contained empty payload", document.Id);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                remaining.Remove(documentId);
                _diagnostics.ParseFailure();
                continue;
            }

            var sanitized = JsonSerializer.Serialize(detail, SerializerOptions);
            var payload = MongoDB.Bson.BsonDocument.Parse(sanitized);
            var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, SourceName, "vmware.v1", payload, _timeProvider.GetUtcNow());

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

            remaining.Remove(documentId);
            if (!pendingMappings.Contains(documentId))
            {
                pendingMappings.Add(documentId);
            }
        }

        var updatedCursor = cursor
            .WithPendingDocuments(remaining)
            .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.ToList();

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

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

            var json = dto.Payload.ToJson(new JsonWriterSettings
            {
                OutputMode = JsonOutputMode.RelaxedExtendedJson,
            });

            VmwareDetailDto? detail;
            try
            {
                detail = JsonSerializer.Deserialize<VmwareDetailDto>(json, SerializerOptions);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to deserialize VMware DTO for document {DocumentId}", document.Id);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pendingMappings.Remove(documentId);
                continue;
            }

            if (detail is null || string.IsNullOrWhiteSpace(detail.AdvisoryId))
            {
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pendingMappings.Remove(documentId);
                continue;
            }

            var (advisory, flag) = VmwareMapper.Map(detail, document, dto);
            await _advisoryStore.UpsertAsync(advisory, cancellationToken).ConfigureAwait(false);
            await _psirtFlagStore.UpsertAsync(flag, cancellationToken).ConfigureAwait(false);
            await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);
            _diagnostics.MapAffectedCount(advisory.AffectedPackages.Length);
            _logger.LogInformation(
                "VMware advisory {AdvisoryId} mapped with {AffectedCount} affected packages",
                detail.AdvisoryId,
                advisory.AffectedPackages.Length);

            pendingMappings.Remove(documentId);
        }

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

    private async Task<IReadOnlyList<VmwareIndexItem>> FetchIndexAsync(CancellationToken cancellationToken)
    {
        var client = _httpClientFactory.CreateClient(VmwareOptions.HttpClientName);
        using var response = await client.GetAsync(_options.IndexUri, cancellationToken).ConfigureAwait(false);
        response.EnsureSuccessStatusCode();

        await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
        var items = await JsonSerializer.DeserializeAsync<IReadOnlyList<VmwareIndexItem>>(stream, SerializerOptions, cancellationToken).ConfigureAwait(false);
        return items ?? Array.Empty<VmwareIndexItem>();
    }

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

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