using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Common.Fetch;
using StellaOps.Feedser.Source.Vndr.Cisco.Configuration;
using StellaOps.Feedser.Source.Vndr.Cisco.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.Cisco;

public sealed class CiscoConnector : IFeedConnector
{
    private const string DtoSchemaVersion = "cisco.dto.v1";

    private static readonly JsonSerializerOptions RawSerializerOptions = new(JsonSerializerDefaults.Web)
    {
        PropertyNameCaseInsensitive = true,
    };

    private static readonly JsonSerializerOptions DtoSerializerOptions = new(JsonSerializerDefaults.Web)
    {
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        PropertyNameCaseInsensitive = true,
    };

    private static readonly IReadOnlyDictionary<string, string> DefaultHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
    {
        ["content-type"] = "application/json",
    };

    private readonly CiscoOpenVulnClient _openVulnClient;
    private readonly RawDocumentStorage _rawDocumentStorage;
    private readonly IDocumentStore _documentStore;
    private readonly IDtoStore _dtoStore;
    private readonly IAdvisoryStore _advisoryStore;
    private readonly ISourceStateRepository _stateRepository;
    private readonly CiscoDtoFactory _dtoFactory;
    private readonly CiscoDiagnostics _diagnostics;
    private readonly IOptions<CiscoOptions> _options;
    private readonly TimeProvider _timeProvider;
    private readonly ILogger<CiscoConnector> _logger;

    public CiscoConnector(
        CiscoOpenVulnClient openVulnClient,
        RawDocumentStorage rawDocumentStorage,
        IDocumentStore documentStore,
        IDtoStore dtoStore,
        IAdvisoryStore advisoryStore,
        ISourceStateRepository stateRepository,
        CiscoDtoFactory dtoFactory,
        CiscoDiagnostics diagnostics,
        IOptions<CiscoOptions> options,
        TimeProvider? timeProvider,
        ILogger<CiscoConnector> logger)
    {
        _openVulnClient = openVulnClient ?? throw new ArgumentNullException(nameof(openVulnClient));
        _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));
        _dtoFactory = dtoFactory ?? throw new ArgumentNullException(nameof(dtoFactory));
        _diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
        _options = options ?? throw new ArgumentNullException(nameof(options));
        _timeProvider = timeProvider ?? TimeProvider.System;
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public string SourceName => VndrCiscoConnectorPlugin.SourceName;

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

        var now = _timeProvider.GetUtcNow();
        var options = _options.Value;
        var cursor = await GetCursorAsync(cancellationToken).ConfigureAwait(false);
        var pendingDocuments = cursor.PendingDocuments.ToHashSet();
        var pendingMappings = cursor.PendingMappings.ToHashSet();

        var latestModified = cursor.LastModified;
        var latestAdvisoryId = cursor.LastAdvisoryId;

        var startDate = DetermineStartDate(cursor, now, options);
        var endDate = DateOnly.FromDateTime(now.UtcDateTime.Date);

        var added = 0;
        var pagesFetched = 0;

        try
        {
            for (var date = startDate; date <= endDate; date = date.AddDays(1))
            {
                cancellationToken.ThrowIfCancellationRequested();

                for (var pageIndex = 1; pageIndex <= options.MaxPagesPerFetch; pageIndex++)
                {
                    cancellationToken.ThrowIfCancellationRequested();

                    var page = await _openVulnClient.FetchAsync(date, pageIndex, cancellationToken).ConfigureAwait(false);
                    pagesFetched++;

                    if (page is null || page.Advisories.Count == 0)
                    {
                        break;
                    }

                    foreach (var advisory in page.Advisories
                                 .OrderBy(static a => a.LastUpdated ?? DateTimeOffset.MinValue)
                                 .ThenBy(static a => a.AdvisoryId, StringComparer.OrdinalIgnoreCase))
                    {
                        cancellationToken.ThrowIfCancellationRequested();

                        if (!ShouldProcess(advisory, latestModified, latestAdvisoryId))
                        {
                            continue;
                        }

                        var documentUri = BuildDocumentUri(advisory.AdvisoryId);
                        var payload = advisory.GetRawBytes();
                        var sha = ComputeSha256(payload);

                        var existing = await _documentStore.FindBySourceAndUriAsync(SourceName, documentUri, cancellationToken).ConfigureAwait(false);
                        if (existing is not null && string.Equals(existing.Sha256, sha, StringComparison.OrdinalIgnoreCase))
                        {
                            _diagnostics.FetchUnchanged();
                            continue;
                        }

                        ObjectId gridFsId;
                        try
                        {
                            gridFsId = await _rawDocumentStorage.UploadAsync(SourceName, documentUri, payload, "application/json", cancellationToken).ConfigureAwait(false);
                        }
                        catch (MongoWriteException ex)
                        {
                            _diagnostics.FetchFailure();
                            _logger.LogError(ex, "Failed to upload Cisco advisory {AdvisoryId} to GridFS", advisory.AdvisoryId);
                            throw;
                        }

                        var recordId = existing?.Id ?? Guid.NewGuid();
                        var record = new DocumentRecord(
                            recordId,
                            SourceName,
                            documentUri,
                            now,
                            sha,
                            DocumentStatuses.PendingParse,
                            "application/json",
                            DefaultHeaders,
                            BuildMetadata(advisory),
                            Etag: null,
                            LastModified: advisory.LastUpdated ?? advisory.FirstPublished ?? now,
                            GridFsId: gridFsId,
                            ExpiresAt: null);

                        var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
                        pendingDocuments.Add(upserted.Id);
                        pendingMappings.Remove(upserted.Id);
                        added++;
                        _diagnostics.FetchDocument();

                        if (advisory.LastUpdated.HasValue)
                        {
                            latestModified = advisory.LastUpdated;
                            latestAdvisoryId = advisory.AdvisoryId;
                        }

                        if (added >= options.MaxAdvisoriesPerFetch)
                        {
                            break;
                        }
                    }

                    if (added >= options.MaxAdvisoriesPerFetch)
                    {
                        break;
                    }

                    if (!page.HasMore)
                    {
                        break;
                    }

                    await DelayAsync(options.RequestDelay, cancellationToken).ConfigureAwait(false);
                }

                if (added >= options.MaxAdvisoriesPerFetch)
                {
                    break;
                }
            }

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

            if (latestModified.HasValue)
            {
                updatedCursor = updatedCursor.WithCheckpoint(latestModified.Value, latestAdvisoryId ?? string.Empty);
            }

            await UpdateCursorAsync(updatedCursor, cancellationToken).ConfigureAwait(false);
            _logger.LogInformation(
                "Cisco fetch completed startDate={StartDate} pages={PagesFetched} added={Added} lastUpdated={LastUpdated} lastAdvisoryId={LastAdvisoryId}",
                startDate,
                pagesFetched,
                added,
                latestModified,
                latestAdvisoryId);
        }
        catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or JsonException or MongoException)
        {
            _diagnostics.FetchFailure();
            _logger.LogError(ex, "Cisco fetch failed");
            await _stateRepository.MarkFailureAsync(SourceName, now, options.FailureBackoff, 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 pendingDocuments = cursor.PendingDocuments.ToList();
        var pendingMappings = cursor.PendingMappings.ToList();
        var parsed = 0;
        var failures = 0;

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

            var document = await _documentStore.FindAsync(documentId, cancellationToken).ConfigureAwait(false);
            if (document is null)
            {
                pendingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
                _diagnostics.ParseFailure();
                failures++;
                continue;
            }

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

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

            CiscoRawAdvisory? raw;
            try
            {
                raw = JsonSerializer.Deserialize<CiscoRawAdvisory>(payload, RawSerializerOptions);
            }
            catch (Exception ex)
            {
                _diagnostics.ParseFailure();
                _logger.LogWarning(ex, "Cisco failed to deserialize raw document {DocumentId}", documentId);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pendingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
                failures++;
                continue;
            }

            if (raw is null)
            {
                _diagnostics.ParseFailure();
                _logger.LogWarning("Cisco raw document {DocumentId} produced null payload", documentId);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pendingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
                failures++;
                continue;
            }

            CiscoAdvisoryDto dto;
            try
            {
                dto = await _dtoFactory.CreateAsync(raw, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                _diagnostics.ParseFailure();
                _logger.LogWarning(ex, "Cisco failed to build DTO for {DocumentId}", documentId);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pendingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
                failures++;
                continue;
            }

            try
            {
                var dtoJson = JsonSerializer.Serialize(dto, DtoSerializerOptions);
                var dtoBson = BsonDocument.Parse(dtoJson);
                var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, SourceName, DtoSchemaVersion, dtoBson, _timeProvider.GetUtcNow());
                await _dtoStore.UpsertAsync(dtoRecord, cancellationToken).ConfigureAwait(false);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.PendingMap, cancellationToken).ConfigureAwait(false);
                pendingDocuments.Remove(documentId);
                if (!pendingMappings.Contains(documentId))
                {
                    pendingMappings.Add(documentId);
                }

                _diagnostics.ParseSuccess();
                parsed++;
            }
            catch (Exception ex)
            {
                _diagnostics.ParseFailure();
                _logger.LogError(ex, "Cisco failed to persist DTO for {DocumentId}", documentId);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pendingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
                failures++;
            }
        }

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

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

        if (parsed > 0 || failures > 0)
        {
            _logger.LogInformation(
                "Cisco parse completed parsed={Parsed} failures={Failures} pendingDocuments={PendingDocuments} pendingMappings={PendingMappings}",
                parsed,
                failures,
                pendingDocuments.Count,
                pendingMappings.Count);
        }
    }

    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();
        var mapped = 0;
        var failures = 0;

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

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

            var dtoRecord = await _dtoStore.FindByDocumentIdAsync(documentId, cancellationToken).ConfigureAwait(false);
            if (dtoRecord is null)
            {
                _diagnostics.MapFailure();
                _logger.LogWarning("Cisco document {DocumentId} missing DTO payload", documentId);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pendingMappings.Remove(documentId);
                failures++;
                continue;
            }

            CiscoAdvisoryDto? dto;
            try
            {
                var json = dtoRecord.Payload.ToJson();
                dto = JsonSerializer.Deserialize<CiscoAdvisoryDto>(json, DtoSerializerOptions);
            }
            catch (Exception ex)
            {
                _diagnostics.MapFailure();
                _logger.LogWarning(ex, "Cisco failed to deserialize DTO for document {DocumentId}", documentId);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pendingMappings.Remove(documentId);
                failures++;
                continue;
            }

            if (dto is null)
            {
                _diagnostics.MapFailure();
                _logger.LogWarning("Cisco DTO for document {DocumentId} evaluated to null", documentId);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pendingMappings.Remove(documentId);
                failures++;
                continue;
            }

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

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

        if (mapped > 0 || failures > 0)
        {
            _logger.LogInformation(
                "Cisco map completed mapped={Mapped} failures={Failures} pendingMappings={PendingMappings}",
                mapped,
                failures,
                pendingMappings.Count);
        }
    }

    private static string ComputeSha256(byte[] payload)
    {
        Span<byte> hash = stackalloc byte[32];
        SHA256.HashData(payload, hash);
        return Convert.ToHexString(hash).ToLowerInvariant();
    }

    private static bool ShouldProcess(CiscoAdvisoryItem advisory, DateTimeOffset? checkpoint, string? checkpointId)
    {
        if (checkpoint is null || advisory.LastUpdated is null)
        {
            return true;
        }

        var comparison = advisory.LastUpdated.Value.CompareTo(checkpoint.Value);
        if (comparison > 0)
        {
            return true;
        }

        if (comparison < 0)
        {
            return false;
        }

        if (string.IsNullOrWhiteSpace(checkpointId))
        {
            return true;
        }

        return string.Compare(advisory.AdvisoryId, checkpointId, StringComparison.OrdinalIgnoreCase) > 0;
    }

    private static IReadOnlyDictionary<string, string> BuildMetadata(CiscoAdvisoryItem advisory)
    {
        var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            ["cisco.advisoryId"] = advisory.AdvisoryId,
        };

        if (advisory.LastUpdated.HasValue)
        {
            metadata["cisco.lastUpdated"] = advisory.LastUpdated.Value.ToString("O", CultureInfo.InvariantCulture);
        }

        if (advisory.FirstPublished.HasValue)
        {
            metadata["cisco.firstPublished"] = advisory.FirstPublished.Value.ToString("O", CultureInfo.InvariantCulture);
        }

        if (!string.IsNullOrWhiteSpace(advisory.Severity))
        {
            metadata["cisco.severity"] = advisory.Severity!;
        }

        if (!string.IsNullOrWhiteSpace(advisory.CsafUrl))
        {
            metadata["cisco.csafUrl"] = advisory.CsafUrl!;
        }

        if (!string.IsNullOrWhiteSpace(advisory.CvrfUrl))
        {
            metadata["cisco.cvrfUrl"] = advisory.CvrfUrl!;
        }

        if (!string.IsNullOrWhiteSpace(advisory.PublicationUrl))
        {
            metadata["cisco.publicationUrl"] = advisory.PublicationUrl!;
        }

        if (!string.IsNullOrWhiteSpace(advisory.CvssBaseScore))
        {
            metadata["cisco.cvssBaseScore"] = advisory.CvssBaseScore!;
        }

        if (advisory.Cves.Count > 0)
        {
            metadata["cisco.cves"] = string.Join(",", advisory.Cves);
        }

        if (advisory.BugIds.Count > 0)
        {
            metadata["cisco.bugIds"] = string.Join(",", advisory.BugIds);
        }

        if (advisory.ProductNames.Count > 0)
        {
            metadata["cisco.productNames"] = string.Join(";", advisory.ProductNames.Take(10));
        }

        return metadata;
    }

    private static DateOnly DetermineStartDate(CiscoCursor cursor, DateTimeOffset now, CiscoOptions options)
    {
        if (cursor.LastModified.HasValue)
        {
            return DateOnly.FromDateTime(cursor.LastModified.Value.UtcDateTime.Date);
        }

        var baseline = now - options.InitialBackfillWindow;
        return DateOnly.FromDateTime(baseline.UtcDateTime.Date);
    }

    private string BuildDocumentUri(string advisoryId)
    {
        var baseUri = _options.Value.BaseUri;
        var relative = $"advisories/{Uri.EscapeDataString(advisoryId)}";
        return new Uri(baseUri, relative).ToString();
    }

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

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

    private static Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken)
    {
        if (delay <= TimeSpan.Zero)
        {
            return Task.CompletedTask;
        }

        return Task.Delay(delay, cancellationToken);
    }
}
