using System.Collections.Immutable;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Connectors.RedHat.CSAF.Configuration;
using StellaOps.Excititor.Core;
using System.IO.Abstractions;

namespace StellaOps.Excititor.Connectors.RedHat.CSAF.Metadata;

public sealed class RedHatProviderMetadataLoader
{
    public const string CacheKey = "StellaOps.Excititor.Connectors.RedHat.CSAF.Metadata";

    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IMemoryCache _cache;
    private readonly ILogger<RedHatProviderMetadataLoader> _logger;
    private readonly RedHatConnectorOptions _options;
    private readonly IFileSystem _fileSystem;
    private readonly JsonSerializerOptions _serializerOptions;
    private readonly SemaphoreSlim _refreshSemaphore = new(1, 1);

    public RedHatProviderMetadataLoader(
        IHttpClientFactory httpClientFactory,
        IMemoryCache memoryCache,
        IOptions<RedHatConnectorOptions> options,
        ILogger<RedHatProviderMetadataLoader> logger,
        IFileSystem? fileSystem = null)
    {
        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
        _cache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _options = (options ?? throw new ArgumentNullException(nameof(options))).Value ?? throw new ArgumentNullException(nameof(options));
        _fileSystem = fileSystem ?? new FileSystem();
        _serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
        {
            PropertyNameCaseInsensitive = true,
            ReadCommentHandling = JsonCommentHandling.Skip,
        };
    }

    public async Task<RedHatProviderMetadataResult> LoadAsync(CancellationToken cancellationToken)
    {
        if (_cache.TryGetValue<CacheEntry>(CacheKey, out var cached) && cached is { } cachedEntry && !cachedEntry.IsExpired())
        {
            _logger.LogDebug("Returning cached Red Hat provider metadata (expires {Expires}).", cachedEntry.ExpiresAt);
            return new RedHatProviderMetadataResult(cachedEntry.Provider, cachedEntry.FetchedAt, true, cachedEntry.FromOffline);
        }

        await _refreshSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            if (_cache.TryGetValue<CacheEntry>(CacheKey, out cached) && cached is { } cachedAfterLock && !cachedAfterLock.IsExpired())
            {
                return new RedHatProviderMetadataResult(cachedAfterLock.Provider, cachedAfterLock.FetchedAt, true, cachedAfterLock.FromOffline);
            }

            CacheEntry? previous = cached;

            // Attempt live fetch unless offline preferred.
            if (!_options.PreferOfflineSnapshot)
            {
                var httpResult = await TryFetchFromNetworkAsync(previous, cancellationToken).ConfigureAwait(false);
                if (httpResult is not null)
                {
                    StoreCache(httpResult);
                    return new RedHatProviderMetadataResult(httpResult.Provider, httpResult.FetchedAt, false, false);
                }
            }

            var offlineResult = TryLoadFromOffline();
            if (offlineResult is not null)
            {
                var offlineEntry = offlineResult with
                {
                    FetchedAt = DateTimeOffset.UtcNow,
                    ExpiresAt = DateTimeOffset.UtcNow + _options.MetadataCacheDuration,
                    FromOffline = true,
                };
                StoreCache(offlineEntry);
                return new RedHatProviderMetadataResult(offlineEntry.Provider, offlineEntry.FetchedAt, false, true);
            }

            throw new InvalidOperationException("Unable to load Red Hat CSAF provider metadata from network or offline snapshot.");
        }
        finally
        {
            _refreshSemaphore.Release();
        }
    }

    private void StoreCache(CacheEntry entry)
    {
        var cacheEntryOptions = new MemoryCacheEntryOptions
        {
            AbsoluteExpiration = entry.ExpiresAt,
        };
        _cache.Set(CacheKey, entry, cacheEntryOptions);
    }

    private async Task<CacheEntry?> TryFetchFromNetworkAsync(CacheEntry? previous, CancellationToken cancellationToken)
    {
        try
        {
            var client = _httpClientFactory.CreateClient(RedHatConnectorOptions.HttpClientName);
            using var request = new HttpRequestMessage(HttpMethod.Get, _options.MetadataUri);
            if (!string.IsNullOrWhiteSpace(previous?.ETag))
            {
                if (EntityTagHeaderValue.TryParse(previous.ETag, out var etag))
                {
                    request.Headers.IfNoneMatch.Add(etag);
                }
            }

            using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);

            if (response.StatusCode == HttpStatusCode.NotModified && previous is not null)
            {
                _logger.LogDebug("Red Hat provider metadata not modified (etag {ETag}).", previous.ETag);
                return previous with
                {
                    FetchedAt = DateTimeOffset.UtcNow,
                    ExpiresAt = DateTimeOffset.UtcNow + _options.MetadataCacheDuration,
                };
            }

            response.EnsureSuccessStatusCode();
            var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);

            var provider = ParseAndValidate(payload);
            var etagHeader = response.Headers.ETag?.ToString();

            if (_options.PersistOfflineSnapshot && !string.IsNullOrWhiteSpace(_options.OfflineSnapshotPath))
            {
                try
                {
                    _fileSystem.File.WriteAllText(_options.OfflineSnapshotPath, payload);
                    _logger.LogDebug("Persisted Red Hat metadata snapshot to {Path}.", _options.OfflineSnapshotPath);
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Failed to persist Red Hat metadata snapshot to {Path}.", _options.OfflineSnapshotPath);
                }
            }

            return new CacheEntry(
                provider,
                DateTimeOffset.UtcNow,
                DateTimeOffset.UtcNow + _options.MetadataCacheDuration,
                etagHeader,
                FromOffline: false);
        }
        catch (Exception ex) when (ex is not OperationCanceledException && !_options.PreferOfflineSnapshot)
        {
            _logger.LogWarning(ex, "Failed to fetch Red Hat provider metadata from {Uri}, will attempt offline snapshot.", _options.MetadataUri);
            return null;
        }
    }

    private CacheEntry? TryLoadFromOffline()
    {
        if (string.IsNullOrWhiteSpace(_options.OfflineSnapshotPath))
        {
            return null;
        }

        if (!_fileSystem.File.Exists(_options.OfflineSnapshotPath))
        {
            _logger.LogWarning("Offline snapshot path {Path} does not exist.", _options.OfflineSnapshotPath);
            return null;
        }

        try
        {
            var payload = _fileSystem.File.ReadAllText(_options.OfflineSnapshotPath);
            var provider = ParseAndValidate(payload);
            return new CacheEntry(provider, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow + _options.MetadataCacheDuration, ETag: null, FromOffline: true);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to load Red Hat provider metadata from offline snapshot {Path}.", _options.OfflineSnapshotPath);
            return null;
        }
    }

    private VexProvider ParseAndValidate(string payload)
    {
        if (string.IsNullOrWhiteSpace(payload))
        {
            throw new InvalidOperationException("Provider metadata payload was empty.");
        }

        ProviderMetadataDocument? document;
        try
        {
            document = JsonSerializer.Deserialize<ProviderMetadataDocument>(payload, _serializerOptions);
        }
        catch (JsonException ex)
        {
            throw new InvalidOperationException("Provider metadata payload could not be parsed.", ex);
        }

        if (document is null)
        {
            throw new InvalidOperationException("Provider metadata payload was null after parsing.");
        }

        if (document.Metadata?.Provider?.Name is null)
        {
            throw new InvalidOperationException("Provider metadata missing provider name.");
        }

        var distributions = document.Distributions?
            .Select(static d => d.Directory)
            .Where(static s => !string.IsNullOrWhiteSpace(s))
            .Select(static s => CreateUri(s!, nameof(ProviderMetadataDistribution.Directory)))
            .ToImmutableArray() ?? ImmutableArray<Uri>.Empty;

        if (distributions.IsDefaultOrEmpty)
        {
            throw new InvalidOperationException("Provider metadata did not include any valid distribution directories.");
        }

        Uri? rolieFeed = null;
        if (document.Rolie?.Feeds is not null)
        {
            foreach (var feed in document.Rolie.Feeds)
            {
                if (!string.IsNullOrWhiteSpace(feed.Url))
                {
                    rolieFeed = CreateUri(feed.Url, "rolie.feeds[].url");
                    break;
                }
            }
        }

        var trust = BuildTrust();
        return new VexProvider(
            id: "excititor:redhat",
            displayName: document.Metadata.Provider.Name,
            kind: VexProviderKind.Distro,
            baseUris: distributions,
            discovery: new VexProviderDiscovery(_options.MetadataUri, rolieFeed),
            trust: trust);
    }

    private VexProviderTrust BuildTrust()
    {
        VexCosignTrust? cosign = null;
        if (!string.IsNullOrWhiteSpace(_options.CosignIssuer) && !string.IsNullOrWhiteSpace(_options.CosignIdentityPattern))
        {
            cosign = new VexCosignTrust(_options.CosignIssuer!, _options.CosignIdentityPattern!);
        }

        return new VexProviderTrust(
            _options.TrustWeight,
            cosign,
            _options.PgpFingerprints);
    }

    private static Uri CreateUri(string value, string propertyName)
    {
        if (!Uri.TryCreate(value, UriKind.Absolute, out var uri) || uri.Scheme is not ("http" or "https"))
        {
            throw new InvalidOperationException($"Provider metadata field '{propertyName}' must be an absolute HTTP(S) URI.");
        }

        return uri;
    }

    private sealed record ProviderMetadataDocument(
        [property: JsonPropertyName("metadata")] ProviderMetadata? Metadata,
        [property: JsonPropertyName("distributions")] IReadOnlyList<ProviderMetadataDistribution>? Distributions,
        [property: JsonPropertyName("rolie")] ProviderMetadataRolie? Rolie);

    private sealed record ProviderMetadata(
        [property: JsonPropertyName("provider")] ProviderMetadataProvider? Provider);

    private sealed record ProviderMetadataProvider(
        [property: JsonPropertyName("name")] string? Name);

    private sealed record ProviderMetadataDistribution(
        [property: JsonPropertyName("directory")] string? Directory);

    private sealed record ProviderMetadataRolie(
        [property: JsonPropertyName("feeds")] IReadOnlyList<ProviderMetadataRolieFeed>? Feeds);

    private sealed record ProviderMetadataRolieFeed(
        [property: JsonPropertyName("url")] string? Url);

    private sealed record CacheEntry(
        VexProvider Provider,
        DateTimeOffset FetchedAt,
        DateTimeOffset ExpiresAt,
        string? ETag,
        bool FromOffline)
    {
        public bool IsExpired() => DateTimeOffset.UtcNow >= ExpiresAt;
    }
}

public sealed record RedHatProviderMetadataResult(
    VexProvider Provider,
    DateTimeOffset FetchedAt,
    bool FromCache,
    bool FromOfflineSnapshot);
