using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Security.Cryptography;
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.Distro.Ubuntu.Configuration;
using StellaOps.Feedser.Source.Distro.Ubuntu.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.Distro.Ubuntu;

public sealed class UbuntuConnector : IFeedConnector
{
    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 UbuntuOptions _options;
    private readonly TimeProvider _timeProvider;
    private readonly ILogger<UbuntuConnector> _logger;

    private static readonly Action<ILogger, string, int, Exception?> LogMapped =
        LoggerMessage.Define<string, int>(
            LogLevel.Information,
            new EventId(1, "UbuntuMapped"),
            "Ubuntu notice {NoticeId} mapped with {PackageCount} packages");

    public UbuntuConnector(
        SourceFetchService fetchService,
        RawDocumentStorage rawDocumentStorage,
        IDocumentStore documentStore,
        IDtoStore dtoStore,
        IAdvisoryStore advisoryStore,
        ISourceStateRepository stateRepository,
        IOptions<UbuntuOptions> options,
        TimeProvider? timeProvider,
        ILogger<UbuntuConnector> 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();
        _timeProvider = timeProvider ?? TimeProvider.System;
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public string SourceName => UbuntuConnectorPlugin.SourceName;

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

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

        var fetchCache = new Dictionary<string, UbuntuFetchCacheEntry>(cursor.FetchCache, StringComparer.OrdinalIgnoreCase);
        var pendingMappings = new HashSet<Guid>(cursor.PendingMappings);
        var processedIds = new HashSet<string>(cursor.ProcessedNoticeIds, StringComparer.OrdinalIgnoreCase);

        var indexResult = await FetchIndexAsync(cursor, fetchCache, now, cancellationToken).ConfigureAwait(false);

        if (indexResult.IsUnchanged)
        {
            await UpdateCursorAsync(cursor.WithFetchCache(fetchCache), cancellationToken).ConfigureAwait(false);
            return;
        }

        if (indexResult.Notices.Count == 0)
        {
            await UpdateCursorAsync(cursor.WithFetchCache(fetchCache), cancellationToken).ConfigureAwait(false);
            return;
        }

        var notices = indexResult.Notices;

        var baseline = (cursor.LastPublished ?? (now - _options.InitialBackfill)) - _options.ResumeOverlap;
        if (baseline < DateTimeOffset.UnixEpoch)
        {
            baseline = DateTimeOffset.UnixEpoch;
        }

        ProvenanceDiagnostics.ReportResumeWindow(SourceName, baseline, _logger);

        var candidates = notices
            .Where(notice => notice.Published >= baseline)
            .OrderBy(notice => notice.Published)
            .ThenBy(notice => notice.NoticeId, StringComparer.OrdinalIgnoreCase)
            .ToList();

        if (candidates.Count == 0)
        {
            candidates = notices
                .OrderByDescending(notice => notice.Published)
                .ThenBy(notice => notice.NoticeId, StringComparer.OrdinalIgnoreCase)
                .Take(_options.MaxNoticesPerFetch)
                .OrderBy(notice => notice.Published)
                .ThenBy(notice => notice.NoticeId, StringComparer.OrdinalIgnoreCase)
                .ToList();
        }
        else if (candidates.Count > _options.MaxNoticesPerFetch)
        {
            candidates = candidates
                .OrderByDescending(notice => notice.Published)
                .ThenBy(notice => notice.NoticeId, StringComparer.OrdinalIgnoreCase)
                .Take(_options.MaxNoticesPerFetch)
                .OrderBy(notice => notice.Published)
                .ThenBy(notice => notice.NoticeId, StringComparer.OrdinalIgnoreCase)
                .ToList();
        }

        var maxPublished = cursor.LastPublished ?? DateTimeOffset.MinValue;
        var processedWindow = new List<string>(candidates.Count);

        foreach (var notice in candidates)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var detailUri = new Uri(_options.NoticeDetailBaseUri, notice.NoticeId);
            var existing = await _documentStore.FindBySourceAndUriAsync(SourceName, detailUri.AbsoluteUri, cancellationToken).ConfigureAwait(false);

            var metadata = new Dictionary<string, string>(StringComparer.Ordinal)
            {
                ["ubuntu.id"] = notice.NoticeId,
                ["ubuntu.published"] = notice.Published.ToString("O")
            };

            var dtoDocument = ToBson(notice);
            var sha256 = ComputeNoticeHash(dtoDocument);

            var documentId = existing?.Id ?? Guid.NewGuid();
            var record = new DocumentRecord(
                documentId,
                SourceName,
                detailUri.AbsoluteUri,
                now,
                sha256,
                DocumentStatuses.PendingMap,
                "application/json",
                Headers: null,
                Metadata: metadata,
                Etag: existing?.Etag,
                LastModified: existing?.LastModified ?? notice.Published,
                GridFsId: null);

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

            var dtoRecord = new DtoRecord(Guid.NewGuid(), record.Id, SourceName, "ubuntu.notice.v1", dtoDocument, now);
            await _dtoStore.UpsertAsync(dtoRecord, cancellationToken).ConfigureAwait(false);

            pendingMappings.Add(record.Id);
            processedIds.Add(notice.NoticeId);
            processedWindow.Add(notice.NoticeId);

            if (notice.Published > maxPublished)
            {
                maxPublished = notice.Published;
            }
        }

        var updatedCursor = cursor
            .WithFetchCache(fetchCache)
            .WithPendingDocuments(Array.Empty<Guid>())
            .WithPendingMappings(pendingMappings)
            .WithProcessed(maxPublished, processedWindow);

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

    public Task ParseAsync(IServiceProvider services, CancellationToken cancellationToken)
        => Task.CompletedTask;

    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 pending = 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)
            {
                pending.Remove(documentId);
                continue;
            }

            UbuntuNoticeDto notice;
            try
            {
                notice = FromBson(dto.Payload);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to deserialize Ubuntu notice DTO for document {DocumentId}", documentId);
                await _documentStore.UpdateStatusAsync(documentId, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
                pending.Remove(documentId);
                continue;
            }

            var advisory = UbuntuMapper.Map(notice, document, _timeProvider.GetUtcNow());
            await _advisoryStore.UpsertAsync(advisory, cancellationToken).ConfigureAwait(false);
            await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);
            pending.Remove(documentId);

            LogMapped(_logger, notice.NoticeId, advisory.AffectedPackages.Length, null);
        }

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

    private async Task<UbuntuIndexFetchResult> FetchIndexAsync(
        UbuntuCursor cursor,
        IDictionary<string, UbuntuFetchCacheEntry> fetchCache,
        DateTimeOffset now,
        CancellationToken cancellationToken)
    {
        var pageSize = Math.Clamp(_options.IndexPageSize, 1, UbuntuOptions.MaxPageSize);
        var maxNotices = Math.Clamp(_options.MaxNoticesPerFetch, 1, 200);
        var maxPages = Math.Max(1, (int)Math.Ceiling(maxNotices / (double)pageSize));
        var aggregated = new List<UbuntuNoticeDto>(Math.Min(maxNotices, pageSize * maxPages));
        var seenNoticeIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        var offset = 0;
        var totalResults = int.MaxValue;

        for (var pageIndex = 0; pageIndex < maxPages && offset < totalResults; pageIndex++)
        {
            var pageUri = BuildIndexUri(_options.NoticesEndpoint, offset, pageSize);
            var cacheKey = pageUri.ToString();

            cursor.TryGetCache(cacheKey, out var cachedEntry);

            var metadata = new Dictionary<string, string>(StringComparer.Ordinal)
            {
                ["ubuntu.type"] = "index",
                ["ubuntu.offset"] = offset.ToString(CultureInfo.InvariantCulture),
                ["ubuntu.limit"] = pageSize.ToString(CultureInfo.InvariantCulture)
            };

            var indexRequest = new SourceFetchRequest(UbuntuOptions.HttpClientName, SourceName, pageUri)
            {
                Metadata = metadata,
                ETag = cachedEntry?.ETag,
                LastModified = cachedEntry?.LastModified,
                TimeoutOverride = _options.FetchTimeout,
                AcceptHeaders = new[] { "application/json" }
            };

            SourceFetchResult fetchResult;
            try
            {
                fetchResult = await _fetchService.FetchAsync(indexRequest, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Ubuntu notices index fetch failed for {Uri}", pageUri);
                await _stateRepository.MarkFailureAsync(SourceName, now, TimeSpan.FromMinutes(5), ex.Message, cancellationToken).ConfigureAwait(false);
                throw;
            }

            byte[] payload;

            if (fetchResult.IsNotModified)
            {
                if (pageIndex == 0)
                {
                    if (cursor.FetchCache.TryGetValue(cacheKey, out var existingCache))
                    {
                        fetchCache[cacheKey] = existingCache;
                    }

                    return UbuntuIndexFetchResult.Unchanged();
                }

                if (!cursor.FetchCache.TryGetValue(cacheKey, out var cachedEntryForPage))
                {
                    break;
                }

                fetchCache[cacheKey] = cachedEntryForPage;

                var existingDocument = await _documentStore.FindBySourceAndUriAsync(SourceName, cacheKey, cancellationToken).ConfigureAwait(false);
                if (existingDocument is null || !existingDocument.GridFsId.HasValue)
                {
                    break;
                }

                payload = await _rawDocumentStorage.DownloadAsync(existingDocument.GridFsId.Value, cancellationToken).ConfigureAwait(false);
            }
            else
            {
                if (!fetchResult.IsSuccess || fetchResult.Document is null)
                {
                    continue;
                }

                fetchCache[cacheKey] = UbuntuFetchCacheEntry.FromDocument(fetchResult.Document);

                if (!fetchResult.Document.GridFsId.HasValue)
                {
                    _logger.LogWarning("Ubuntu index document {DocumentId} missing GridFS payload", fetchResult.Document.Id);
                    continue;
                }

                payload = await _rawDocumentStorage.DownloadAsync(fetchResult.Document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
            }

            var page = UbuntuNoticeParser.ParseIndex(Encoding.UTF8.GetString(payload));

            if (page.TotalResults > 0)
            {
                totalResults = page.TotalResults;
            }

            foreach (var notice in page.Notices)
            {
                if (!seenNoticeIds.Add(notice.NoticeId))
                {
                    continue;
                }

                aggregated.Add(notice);
                if (aggregated.Count >= maxNotices)
                {
                    break;
                }
            }

            if (aggregated.Count >= maxNotices)
            {
                break;
            }

            if (page.Notices.Count < pageSize)
            {
                break;
            }

            offset += pageSize;
        }

        return new UbuntuIndexFetchResult(false, aggregated);
    }

    private static Uri BuildIndexUri(Uri endpoint, int offset, int limit)
    {
        var builder = new UriBuilder(endpoint);
        var queryBuilder = new StringBuilder();

        if (!string.IsNullOrEmpty(builder.Query))
        {
            var existing = builder.Query.TrimStart('?');
            if (!string.IsNullOrEmpty(existing))
            {
                queryBuilder.Append(existing);
                if (existing[^1] != '&')
                {
                    queryBuilder.Append('&');
                }
            }
        }

        queryBuilder.Append("offset=");
        queryBuilder.Append(offset.ToString(CultureInfo.InvariantCulture));
        queryBuilder.Append("&limit=");
        queryBuilder.Append(limit.ToString(CultureInfo.InvariantCulture));

        builder.Query = queryBuilder.ToString();
        return builder.Uri;
    }

    private sealed record UbuntuIndexFetchResult(bool IsUnchanged, IReadOnlyList<UbuntuNoticeDto> Notices)
    {
        public static UbuntuIndexFetchResult Unchanged()
            => new(true, Array.Empty<UbuntuNoticeDto>());
    }

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

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

    private static string ComputeNoticeHash(BsonDocument document)
    {
        var bytes = document.ToBson();
        var hash = SHA256.HashData(bytes);
        return Convert.ToHexString(hash).ToLowerInvariant();
    }

    private static BsonDocument ToBson(UbuntuNoticeDto notice)
    {
        var packages = new BsonArray();
        foreach (var package in notice.Packages)
        {
            packages.Add(new BsonDocument
            {
                ["release"] = package.Release,
                ["package"] = package.Package,
                ["version"] = package.Version,
                ["pocket"] = package.Pocket,
                ["isSource"] = package.IsSource
            });
        }

        var references = new BsonArray();
        foreach (var reference in notice.References)
        {
            var doc = new BsonDocument
            {
                ["url"] = reference.Url
            };

            if (!string.IsNullOrWhiteSpace(reference.Kind))
            {
                doc["kind"] = reference.Kind;
            }

            if (!string.IsNullOrWhiteSpace(reference.Title))
            {
                doc["title"] = reference.Title;
            }

            references.Add(doc);
        }

        return new BsonDocument
        {
            ["noticeId"] = notice.NoticeId,
            ["published"] = notice.Published.UtcDateTime,
            ["title"] = notice.Title,
            ["summary"] = notice.Summary,
            ["cves"] = new BsonArray(notice.CveIds ?? Array.Empty<string>()),
            ["packages"] = packages,
            ["references"] = references
        };
    }

    private static UbuntuNoticeDto FromBson(BsonDocument document)
    {
        var noticeId = document.GetValue("noticeId", string.Empty).AsString;
        var published = document.TryGetValue("published", out var publishedValue)
            ? publishedValue.BsonType switch
            {
                BsonType.DateTime => DateTime.SpecifyKind(publishedValue.ToUniversalTime(), DateTimeKind.Utc),
                BsonType.String when DateTimeOffset.TryParse(publishedValue.AsString, out var parsed) => parsed.ToUniversalTime(),
                _ => DateTimeOffset.UtcNow
            }
            : DateTimeOffset.UtcNow;

        var title = document.GetValue("title", noticeId).AsString;
        var summary = document.GetValue("summary", string.Empty).AsString;

        var cves = document.TryGetValue("cves", out var cveArray) && cveArray is BsonArray cveBson
            ? cveBson.OfType<BsonValue>()
                .Select(static value => value?.ToString())
                .Where(static value => !string.IsNullOrWhiteSpace(value))
                .Select(static value => value!)
                .ToArray()
            : Array.Empty<string>();

        var packages = new List<UbuntuReleasePackageDto>();
        if (document.TryGetValue("packages", out var packageArray) && packageArray is BsonArray packageBson)
        {
            foreach (var element in packageBson.OfType<BsonDocument>())
            {
                packages.Add(new UbuntuReleasePackageDto(
                    Release: element.GetValue("release", string.Empty).AsString,
                    Package: element.GetValue("package", string.Empty).AsString,
                    Version: element.GetValue("version", string.Empty).AsString,
                    Pocket: element.GetValue("pocket", string.Empty).AsString,
                    IsSource: element.TryGetValue("isSource", out var sourceValue) && sourceValue.AsBoolean));
            }
        }

        var references = new List<UbuntuReferenceDto>();
        if (document.TryGetValue("references", out var referenceArray) && referenceArray is BsonArray referenceBson)
        {
            foreach (var element in referenceBson.OfType<BsonDocument>())
            {
                var url = element.GetValue("url", string.Empty).AsString;
                if (string.IsNullOrWhiteSpace(url))
                {
                    continue;
                }

                references.Add(new UbuntuReferenceDto(
                    url,
                    element.TryGetValue("kind", out var kindValue) ? kindValue.AsString : null,
                    element.TryGetValue("title", out var titleValue) ? titleValue.AsString : null));
            }
        }

        return new UbuntuNoticeDto(
            noticeId,
            published,
            title,
            summary,
            cves,
            packages,
            references);
    }
}
