using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO.Abstractions;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Authentication;
using StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Configuration;
using StellaOps.Excititor.Core;

namespace StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Metadata;

public sealed class RancherHubMetadataLoader
{
    public const string CachePrefix = "StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Metadata";

    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IMemoryCache _memoryCache;
    private readonly RancherHubTokenProvider _tokenProvider;
    private readonly IFileSystem _fileSystem;
    private readonly ILogger<RancherHubMetadataLoader> _logger;
    private readonly SemaphoreSlim _semaphore = new(1, 1);
    private readonly JsonDocumentOptions _documentOptions;

    public RancherHubMetadataLoader(
        IHttpClientFactory httpClientFactory,
        IMemoryCache memoryCache,
        RancherHubTokenProvider tokenProvider,
        IFileSystem fileSystem,
        ILogger<RancherHubMetadataLoader> logger)
    {
        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
        _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
        _tokenProvider = tokenProvider ?? throw new ArgumentNullException(nameof(tokenProvider));
        _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _documentOptions = new JsonDocumentOptions
        {
            CommentHandling = JsonCommentHandling.Skip,
            AllowTrailingCommas = true,
        };
    }

    public async Task<RancherHubMetadataResult> LoadAsync(RancherHubConnectorOptions options, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(options);

        var cacheKey = CreateCacheKey(options);
        if (_memoryCache.TryGetValue<CacheEntry>(cacheKey, out var cached) && cached is not null && !cached.IsExpired())
        {
            _logger.LogDebug("Returning cached Rancher hub metadata (expires {Expires}).", cached.ExpiresAt);
            return new RancherHubMetadataResult(cached.Metadata, cached.FetchedAt, FromCache: true, cached.FromOfflineSnapshot);
        }

        await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            if (_memoryCache.TryGetValue<CacheEntry>(cacheKey, out cached) && cached is not null && !cached.IsExpired())
            {
                return new RancherHubMetadataResult(cached.Metadata, cached.FetchedAt, FromCache: true, cached.FromOfflineSnapshot);
            }

            CacheEntry? previous = cached;
            CacheEntry? entry = null;

            if (options.PreferOfflineSnapshot)
            {
                entry = TryLoadFromOffline(options);
                if (entry is null)
                {
                    throw new InvalidOperationException("PreferOfflineSnapshot is enabled but no offline discovery snapshot was found.");
                }
            }
            else
            {
                entry = await TryFetchFromNetworkAsync(options, previous, cancellationToken).ConfigureAwait(false);
                if (entry is null)
                {
                    entry = TryLoadFromOffline(options);
                }
            }

            if (entry is null)
            {
                throw new InvalidOperationException("Unable to load Rancher hub discovery metadata from network or offline snapshot.");
            }

            _memoryCache.Set(cacheKey, entry, new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = options.MetadataCacheDuration,
            });

            return new RancherHubMetadataResult(entry.Metadata, entry.FetchedAt, FromCache: false, entry.FromOfflineSnapshot);
        }
        finally
        {
            _semaphore.Release();
        }
    }

    private async Task<CacheEntry?> TryFetchFromNetworkAsync(RancherHubConnectorOptions options, CacheEntry? previous, CancellationToken cancellationToken)
    {
        try
        {
            var client = _httpClientFactory.CreateClient(RancherHubConnectorOptions.HttpClientName);
            using var request = new HttpRequestMessage(HttpMethod.Get, options.DiscoveryUri);
            request.Headers.Accept.ParseAdd("application/json");

            if (!string.IsNullOrWhiteSpace(previous?.ETag) && EntityTagHeaderValue.TryParse(previous.ETag, out var etag))
            {
                request.Headers.IfNoneMatch.Add(etag);
            }

            var token = await _tokenProvider.GetAccessTokenAsync(options, cancellationToken).ConfigureAwait(false);
            if (token is not null)
            {
                var scheme = string.IsNullOrWhiteSpace(token.TokenType) ? "Bearer" : token.TokenType;
                request.Headers.Authorization = new AuthenticationHeaderValue(scheme, token.Value);
            }

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

            if (response.StatusCode == HttpStatusCode.NotModified && previous is not null)
            {
                _logger.LogDebug("Rancher hub discovery document not modified (etag {ETag}).", previous.ETag);
                return previous with
                {
                    FetchedAt = DateTimeOffset.UtcNow,
                    ExpiresAt = DateTimeOffset.UtcNow + options.MetadataCacheDuration,
                    FromOfflineSnapshot = false,
                };
            }

            response.EnsureSuccessStatusCode();
            var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
            var metadata = ParseMetadata(payload, options);
            var entry = new CacheEntry(
                metadata,
                DateTimeOffset.UtcNow,
                DateTimeOffset.UtcNow + options.MetadataCacheDuration,
                response.Headers.ETag?.ToString(),
                FromOfflineSnapshot: false,
                Payload: payload);

            PersistOfflineSnapshot(options, payload);
            return entry;
        }
        catch (Exception ex) when (ex is not OperationCanceledException && !options.PreferOfflineSnapshot)
        {
            _logger.LogWarning(ex, "Failed to fetch Rancher hub discovery document; attempting offline snapshot fallback.");
            return null;
        }
    }

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

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

        try
        {
            var payload = _fileSystem.File.ReadAllText(options.OfflineSnapshotPath);
            var metadata = ParseMetadata(payload, options);
            return new CacheEntry(
                metadata,
                DateTimeOffset.UtcNow,
                DateTimeOffset.UtcNow + options.MetadataCacheDuration,
                ETag: null,
                FromOfflineSnapshot: true,
                Payload: payload);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to load Rancher hub discovery metadata from offline snapshot {Path}.", options.OfflineSnapshotPath);
            return null;
        }
    }

    private void PersistOfflineSnapshot(RancherHubConnectorOptions options, string payload)
    {
        if (!options.PersistOfflineSnapshot || string.IsNullOrWhiteSpace(options.OfflineSnapshotPath))
        {
            return;
        }

        try
        {
            var directory = _fileSystem.Path.GetDirectoryName(options.OfflineSnapshotPath);
            if (!string.IsNullOrWhiteSpace(directory))
            {
                _fileSystem.Directory.CreateDirectory(directory);
            }

            _fileSystem.File.WriteAllText(options.OfflineSnapshotPath, payload);
            _logger.LogDebug("Persisted Rancher hub discovery snapshot to {Path}.", options.OfflineSnapshotPath);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to persist Rancher hub discovery snapshot to {Path}.", options.OfflineSnapshotPath);
        }
    }

    private RancherHubMetadata ParseMetadata(string payload, RancherHubConnectorOptions options)
    {
        if (string.IsNullOrWhiteSpace(payload))
        {
            throw new InvalidOperationException("Rancher hub discovery payload was empty.");
        }

        try
        {
            using var document = JsonDocument.Parse(payload, _documentOptions);
            var root = document.RootElement;

            var hubId = ReadString(root, "hubId") ?? "excititor:suse:rancher";
            var title = ReadString(root, "title") ?? ReadString(root, "displayName") ?? "SUSE Rancher VEX Hub";
            var baseUri = ReadUri(root, "baseUri");

            var subscriptionElement = TryGetProperty(root, "subscription");
            if (!subscriptionElement.HasValue)
            {
                throw new InvalidOperationException("Discovery payload missing subscription section.");
            }

            var subscription = subscriptionElement.Value;
            var eventsUri = ReadRequiredUri(subscription, "eventsUri", "eventsUrl", "eventsEndpoint");
            var checkpointUri = ReadUri(subscription, "checkpointUri", "checkpointUrl", "checkpointEndpoint");
            var channels = ReadStringArray(subscription, "channels", "defaultChannels", "products");
            var scopes = ReadStringArray(subscription, "scopes", "defaultScopes");
            var requiresAuth = ReadBoolean(subscription, "requiresAuthentication", defaultValue: options.TokenEndpoint is not null);

            var authenticationElement = TryGetProperty(root, "authentication");
            var tokenEndpointFromMetadata = authenticationElement.HasValue
                ? ReadUri(authenticationElement.Value, "tokenUri", "tokenEndpoint") ?? options.TokenEndpoint
                : options.TokenEndpoint;
            var audience = authenticationElement.HasValue
                ? ReadString(authenticationElement.Value, "audience", "aud") ?? options.Audience
                : options.Audience;

            var offlineElement = TryGetProperty(root, "offline", "snapshot");
            var offlineSnapshot = offlineElement.HasValue
                ? BuildOfflineSnapshot(offlineElement.Value, options)
                : null;

            var provider = BuildProvider(hubId, title, baseUri, eventsUri, options);
            var subscriptionMetadata = new RancherHubSubscriptionMetadata(eventsUri, checkpointUri, channels, scopes, requiresAuth);
            var authenticationMetadata = new RancherHubAuthenticationMetadata(tokenEndpointFromMetadata, audience);

            return new RancherHubMetadata(provider, subscriptionMetadata, authenticationMetadata, offlineSnapshot);
        }
        catch (JsonException ex)
        {
            throw new InvalidOperationException("Failed to parse Rancher hub discovery payload.", ex);
        }
    }

    private RancherHubOfflineSnapshotMetadata? BuildOfflineSnapshot(JsonElement element, RancherHubConnectorOptions options)
    {
        var snapshotUri = ReadUri(element, "snapshotUri", "uri", "url");
        if (snapshotUri is null)
        {
            return null;
        }

        var checksum = ReadString(element, "sha256", "checksum", "digest");
        DateTimeOffset? updatedAt = null;
        var updatedString = ReadString(element, "updated", "lastModified", "timestamp");
        if (!string.IsNullOrWhiteSpace(updatedString) && DateTimeOffset.TryParse(updatedString, out var parsed))
        {
            updatedAt = parsed;
        }

        return new RancherHubOfflineSnapshotMetadata(snapshotUri, checksum, updatedAt);
    }

    private VexProvider BuildProvider(string hubId, string title, Uri? baseUri, Uri eventsUri, RancherHubConnectorOptions options)
    {
        var baseUris = new List<Uri>();
        if (baseUri is not null)
        {
            baseUris.Add(baseUri);
        }
        baseUris.Add(eventsUri);

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

        var trust = new VexProviderTrust(options.TrustWeight, cosign, options.PgpFingerprints);
        return new VexProvider(hubId, title, VexProviderKind.Hub, baseUris, new VexProviderDiscovery(options.DiscoveryUri, null), trust);
    }

    private static string CreateCacheKey(RancherHubConnectorOptions options)
        => $"{CachePrefix}:{options.DiscoveryUri}";

    private static JsonElement? TryGetProperty(JsonElement element, params string[] propertyNames)
    {
        foreach (var name in propertyNames)
        {
            if (element.TryGetProperty(name, out var value) && value.ValueKind is not JsonValueKind.Null and not JsonValueKind.Undefined)
            {
                return value;
            }
        }

        return null;
    }

    private static string? ReadString(JsonElement element, params string[] propertyNames)
    {
        var property = TryGetProperty(element, propertyNames);
        if (property is null || property.Value.ValueKind is not JsonValueKind.String)
        {
            return null;
        }

        var value = property.Value.GetString();
        return string.IsNullOrWhiteSpace(value) ? null : value;
    }

    private static bool ReadBoolean(JsonElement element, string propertyName, bool defaultValue)
    {
        if (!element.TryGetProperty(propertyName, out var property))
        {
            return defaultValue;
        }

        return property.ValueKind switch
        {
            JsonValueKind.True => true,
            JsonValueKind.False => false,
            JsonValueKind.String when bool.TryParse(property.GetString(), out var parsed) => parsed,
            _ => defaultValue,
        };
    }

    private static ImmutableArray<string> ReadStringArray(JsonElement element, params string[] propertyNames)
    {
        var property = TryGetProperty(element, propertyNames);
        if (property is null)
        {
            return ImmutableArray<string>.Empty;
        }

        if (property.Value.ValueKind is JsonValueKind.Array)
        {
            var builder = ImmutableArray.CreateBuilder<string>();
            foreach (var item in property.Value.EnumerateArray())
            {
                if (item.ValueKind is JsonValueKind.String)
                {
                    var value = item.GetString();
                    if (!string.IsNullOrWhiteSpace(value))
                    {
                        builder.Add(value!);
                    }
                }
            }

            return builder.Count == 0 ? ImmutableArray<string>.Empty : builder.ToImmutable();
        }

        if (property.Value.ValueKind is JsonValueKind.String)
        {
            var single = property.Value.GetString();
            return string.IsNullOrWhiteSpace(single)
                ? ImmutableArray<string>.Empty
                : ImmutableArray.Create(single!);
        }

        return ImmutableArray<string>.Empty;
    }

    private static Uri? ReadUri(JsonElement element, params string[] propertyNames)
    {
        var value = ReadString(element, propertyNames);
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        if (!Uri.TryCreate(value, UriKind.Absolute, out var uri) || uri.Scheme is not ("http" or "https"))
        {
            throw new InvalidOperationException($"Discovery field '{string.Join("/", propertyNames)}' must be an absolute HTTP(S) URI.");
        }

        return uri;
    }

    private static Uri ReadRequiredUri(JsonElement element, params string[] propertyNames)
    {
        var uri = ReadUri(element, propertyNames);
        if (uri is null)
        {
            throw new InvalidOperationException($"Discovery payload missing required URI field '{string.Join("/", propertyNames)}'.");
        }

        return uri;
    }

    private sealed record CacheEntry(
        RancherHubMetadata Metadata,
        DateTimeOffset FetchedAt,
        DateTimeOffset ExpiresAt,
        string? ETag,
        bool FromOfflineSnapshot,
        string? Payload)
    {
        public bool IsExpired() => DateTimeOffset.UtcNow >= ExpiresAt;
    }
}

public sealed record RancherHubMetadata(
    VexProvider Provider,
    RancherHubSubscriptionMetadata Subscription,
    RancherHubAuthenticationMetadata Authentication,
    RancherHubOfflineSnapshotMetadata? OfflineSnapshot);

public sealed record RancherHubSubscriptionMetadata(
    Uri EventsUri,
    Uri? CheckpointUri,
    ImmutableArray<string> Channels,
    ImmutableArray<string> Scopes,
    bool RequiresAuthentication);

public sealed record RancherHubAuthenticationMetadata(
    Uri? TokenEndpoint,
    string? Audience);

public sealed record RancherHubOfflineSnapshotMetadata(
    Uri SnapshotUri,
    string? Sha256,
    DateTimeOffset? UpdatedAt);

public sealed record RancherHubMetadataResult(
    RancherHubMetadata Metadata,
    DateTimeOffset FetchedAt,
    bool FromCache,
    bool FromOfflineSnapshot);
