using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
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.Source.Common;
using StellaOps.Feedser.Source.Common.Fetch;
using StellaOps.Feedser.Source.Vndr.Apple.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.Apple;

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

    private readonly SourceFetchService _fetchService;
    private readonly RawDocumentStorage _rawDocumentStorage;
    private readonly IDocumentStore _documentStore;
    private readonly IDtoStore _dtoStore;
    private readonly IAdvisoryStore _advisoryStore;
    private readonly IPsirtFlagStore _psirtFlagStore;
    private readonly ISourceStateRepository _stateRepository;
    private readonly AppleOptions _options;
    private readonly AppleDiagnostics _diagnostics;
    private readonly TimeProvider _timeProvider;
    private readonly ILogger<AppleConnector> _logger;

    public AppleConnector(
        SourceFetchService fetchService,
        RawDocumentStorage rawDocumentStorage,
        IDocumentStore documentStore,
        IDtoStore dtoStore,
        IAdvisoryStore advisoryStore,
        IPsirtFlagStore psirtFlagStore,
        ISourceStateRepository stateRepository,
        AppleDiagnostics diagnostics,
        IOptions<AppleOptions> options,
        TimeProvider? timeProvider,
        ILogger<AppleConnector> 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));
        _psirtFlagStore = psirtFlagStore ?? throw new ArgumentNullException(nameof(psirtFlagStore));
        _stateRepository = stateRepository ?? throw new ArgumentNullException(nameof(stateRepository));
        _diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
        _options = (options ?? throw new ArgumentNullException(nameof(options))).Value ?? throw new ArgumentNullException(nameof(options));
        _options.Validate();
        _timeProvider = timeProvider ?? TimeProvider.System;
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public string SourceName => VndrAppleConnectorPlugin.SourceName;

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

        var cursor = await GetCursorAsync(cancellationToken).ConfigureAwait(false);
        var pendingDocuments = cursor.PendingDocuments.ToHashSet();
        var pendingMappings = cursor.PendingMappings.ToHashSet();
        var processedIds = cursor.ProcessedIds.ToHashSet(StringComparer.OrdinalIgnoreCase);
        var maxPosted = cursor.LastPosted ?? DateTimeOffset.MinValue;
        var baseline = cursor.LastPosted?.Add(-_options.ModifiedTolerance) ?? _timeProvider.GetUtcNow().Add(-_options.InitialBackfill);

        SourceFetchContentResult indexResult;
        try
        {
            var request = new SourceFetchRequest(AppleOptions.HttpClientName, SourceName, _options.SoftwareLookupUri!)
            {
                AcceptHeaders = new[] { "application/json", "application/vnd.apple.security+json;q=0.9" },
            };

            indexResult = await _fetchService.FetchContentAsync(request, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            _diagnostics.FetchFailure();
            _logger.LogError(ex, "Apple software index fetch failed from {Uri}", _options.SoftwareLookupUri);
            await _stateRepository.MarkFailureAsync(SourceName, _timeProvider.GetUtcNow(), TimeSpan.FromMinutes(10), ex.Message, cancellationToken).ConfigureAwait(false);
            throw;
        }

        if (!indexResult.IsSuccess || indexResult.Content is null)
        {
            if (indexResult.IsNotModified)
            {
                _diagnostics.FetchUnchanged();
            }

            await UpdateCursorAsync(cursor, cancellationToken).ConfigureAwait(false);
            return;
        }

        var indexEntries = AppleIndexParser.Parse(indexResult.Content, _options.AdvisoryBaseUri!);
        if (indexEntries.Count == 0)
        {
            await UpdateCursorAsync(cursor, cancellationToken).ConfigureAwait(false);
            return;
        }

        var allowlist = _options.AdvisoryAllowlist;
        var blocklist = _options.AdvisoryBlocklist;

        var ordered = indexEntries
            .Where(entry => ShouldInclude(entry, allowlist, blocklist))
            .OrderBy(entry => entry.PostingDate)
            .ThenBy(entry => entry.ArticleId, StringComparer.OrdinalIgnoreCase)
            .ToArray();

        foreach (var entry in ordered)
        {
            cancellationToken.ThrowIfCancellationRequested();

            if (entry.PostingDate < baseline)
            {
                continue;
            }

            if (cursor.LastPosted.HasValue
                && entry.PostingDate <= cursor.LastPosted.Value
                && processedIds.Contains(entry.UpdateId))
            {
                continue;
            }

            var metadata = BuildMetadata(entry);
            var existing = await _documentStore.FindBySourceAndUriAsync(SourceName, entry.DetailUri.ToString(), cancellationToken).ConfigureAwait(false);

            SourceFetchResult result;
            try
            {
                result = await _fetchService.FetchAsync(
                    new SourceFetchRequest(AppleOptions.HttpClientName, SourceName, entry.DetailUri)
                    {
                        Metadata = metadata,
                        ETag = existing?.Etag,
                        LastModified = existing?.LastModified,
                        AcceptHeaders = new[]
                        {
                            "text/html",
                            "application/xhtml+xml",
                            "text/plain;q=0.5"
                        },
                    },
                    cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                _diagnostics.FetchFailure();
                _logger.LogError(ex, "Apple advisory fetch failed for {Uri}", entry.DetailUri);
                await _stateRepository.MarkFailureAsync(SourceName, _timeProvider.GetUtcNow(), TimeSpan.FromMinutes(5), ex.Message, cancellationToken).ConfigureAwait(false);
                throw;
            }

            if (result.StatusCode == HttpStatusCode.NotModified)
            {
                _diagnostics.FetchUnchanged();
            }

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

            _diagnostics.FetchItem();

            pendingDocuments.Add(result.Document.Id);
            processedIds.Add(entry.UpdateId);

            if (entry.PostingDate > maxPosted)
            {
                maxPosted = entry.PostingDate;
            }
        }

        var updated = cursor
            .WithPendingDocuments(pendingDocuments)
            .WithPendingMappings(pendingMappings)
            .WithLastPosted(maxPosted == DateTimeOffset.MinValue ? cursor.LastPosted ?? DateTimeOffset.MinValue : maxPosted, processedIds);

        await UpdateCursorAsync(updated, 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();

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

            AppleDetailDto dto;
            try
            {
                var content = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
                var html = System.Text.Encoding.UTF8.GetString(content);
                var entry = RehydrateIndexEntry(document);
                dto = AppleDetailParser.Parse(html, entry);
            }
            catch (Exception ex)
            {
                _diagnostics.ParseFailure();
                _logger.LogError(ex, "Apple parse failed for document {DocumentId}", document.Id);
                await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                remainingDocuments.Remove(documentId);
                pendingMappings.Remove(documentId);
                continue;
            }

            var json = JsonSerializer.Serialize(dto, SerializerOptions);
            var payload = BsonDocument.Parse(json);
            var validatedAt = _timeProvider.GetUtcNow();

            var existingDto = await _dtoStore.FindByDocumentIdAsync(document.Id, cancellationToken).ConfigureAwait(false);
            var dtoRecord = existingDto is null
                ? new DtoRecord(Guid.NewGuid(), document.Id, SourceName, "apple.security.update.v1", payload, validatedAt)
                : existingDto with
                {
                    Payload = payload,
                    SchemaVersion = "apple.security.update.v1",
                    ValidatedAt = validatedAt,
                };

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

            remainingDocuments.Remove(documentId);
            pendingMappings.Add(document.Id);
        }

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

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

            var (advisory, flag) = AppleMapper.Map(dto, document, dtoRecord);
            _diagnostics.MapAffectedCount(advisory.AffectedPackages.Length);

            await _advisoryStore.UpsertAsync(advisory, cancellationToken).ConfigureAwait(false);
            await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);

            if (flag is not null)
            {
                await _psirtFlagStore.UpsertAsync(flag, cancellationToken).ConfigureAwait(false);
            }

            pendingMappings.Remove(documentId);
        }

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

    private AppleIndexEntry RehydrateIndexEntry(DocumentRecord document)
    {
        var metadata = document.Metadata ?? new Dictionary<string, string>(StringComparer.Ordinal);
        metadata.TryGetValue("apple.articleId", out var articleId);
        metadata.TryGetValue("apple.updateId", out var updateId);
        metadata.TryGetValue("apple.title", out var title);
        metadata.TryGetValue("apple.postingDate", out var postingDateRaw);
        metadata.TryGetValue("apple.detailUri", out var detailUriRaw);
        metadata.TryGetValue("apple.rapidResponse", out var rapidRaw);
        metadata.TryGetValue("apple.products", out var productsJson);

        if (!DateTimeOffset.TryParse(postingDateRaw, out var postingDate))
        {
            postingDate = document.FetchedAt;
        }

        var detailUri = !string.IsNullOrWhiteSpace(detailUriRaw) && Uri.TryCreate(detailUriRaw, UriKind.Absolute, out var parsedUri)
            ? parsedUri
            : new Uri(_options.AdvisoryBaseUri!, articleId ?? document.Uri);

        var rapid = string.Equals(rapidRaw, "true", StringComparison.OrdinalIgnoreCase);
        var products = DeserializeProducts(productsJson);

        return new AppleIndexEntry(
            UpdateId: string.IsNullOrWhiteSpace(updateId) ? articleId ?? document.Uri : updateId,
            ArticleId: articleId ?? document.Uri,
            Title: title ?? document.Metadata?["apple.originalTitle"] ?? "Apple Security Update",
            PostingDate: postingDate.ToUniversalTime(),
            DetailUri: detailUri,
            Products: products,
            IsRapidSecurityResponse: rapid);
    }

    private static IReadOnlyList<AppleIndexProduct> DeserializeProducts(string? json)
    {
        if (string.IsNullOrWhiteSpace(json))
        {
            return Array.Empty<AppleIndexProduct>();
        }

        try
        {
            var products = JsonSerializer.Deserialize<List<AppleIndexProduct>>(json, SerializerOptions);
            return products is { Count: > 0 } ? products : Array.Empty<AppleIndexProduct>();
        }
        catch (JsonException)
        {
            return Array.Empty<AppleIndexProduct>();
        }
    }

    private static Dictionary<string, string> BuildMetadata(AppleIndexEntry entry)
    {
        var metadata = new Dictionary<string, string>(StringComparer.Ordinal)
        {
            ["apple.articleId"] = entry.ArticleId,
            ["apple.updateId"] = entry.UpdateId,
            ["apple.title"] = entry.Title,
            ["apple.postingDate"] = entry.PostingDate.ToString("O"),
            ["apple.detailUri"] = entry.DetailUri.ToString(),
            ["apple.rapidResponse"] = entry.IsRapidSecurityResponse ? "true" : "false",
            ["apple.products"] = JsonSerializer.Serialize(entry.Products, SerializerOptions),
        };

        return metadata;
    }

    private static bool ShouldInclude(AppleIndexEntry entry, IReadOnlyCollection<string> allowlist, IReadOnlyCollection<string> blocklist)
    {
        if (allowlist.Count > 0 && !allowlist.Contains(entry.ArticleId))
        {
            return false;
        }

        if (blocklist.Count > 0 && blocklist.Contains(entry.ArticleId))
        {
            return false;
        }

        return true;
    }

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

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