using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
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.Configuration;

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

public sealed class RancherHubTokenProvider
{
    private const string CachePrefix = "StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Token";

    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IMemoryCache _cache;
    private readonly ILogger<RancherHubTokenProvider> _logger;
    private readonly SemaphoreSlim _semaphore = new(1, 1);

    public RancherHubTokenProvider(IHttpClientFactory httpClientFactory, IMemoryCache cache, ILogger<RancherHubTokenProvider> logger)
    {
        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
        _cache = cache ?? throw new ArgumentNullException(nameof(cache));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async ValueTask<RancherHubAccessToken?> GetAccessTokenAsync(RancherHubConnectorOptions options, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(options);

        if (options.PreferOfflineSnapshot)
        {
            _logger.LogDebug("Skipping token request because PreferOfflineSnapshot is enabled.");
            return null;
        }

        var hasCredentials = !string.IsNullOrWhiteSpace(options.ClientId) &&
                              !string.IsNullOrWhiteSpace(options.ClientSecret) &&
                              options.TokenEndpoint is not null;

        if (!hasCredentials)
        {
            if (!options.AllowAnonymousDiscovery)
            {
                _logger.LogDebug("No Rancher hub credentials configured; proceeding without Authorization header.");
            }

            return null;
        }

        var cacheKey = $"{CachePrefix}:{options.ClientId}";
        if (_cache.TryGetValue<RancherHubAccessToken>(cacheKey, out var cachedToken) && cachedToken is not null && !cachedToken.IsExpired())
        {
            return cachedToken;
        }

        await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            if (_cache.TryGetValue<RancherHubAccessToken>(cacheKey, out cachedToken) && cachedToken is not null && !cachedToken.IsExpired())
            {
                return cachedToken;
            }

            var token = await RequestTokenAsync(options, cancellationToken).ConfigureAwait(false);
            if (token is not null)
            {
                var lifetime = token.ExpiresAt - DateTimeOffset.UtcNow;
                if (lifetime <= TimeSpan.Zero)
                {
                    lifetime = TimeSpan.FromMinutes(5);
                }

                var absoluteExpiration = lifetime > TimeSpan.FromSeconds(30)
                    ? DateTimeOffset.UtcNow + lifetime - TimeSpan.FromSeconds(30)
                    : DateTimeOffset.UtcNow + TimeSpan.FromSeconds(10);

                _cache.Set(cacheKey, token, new MemoryCacheEntryOptions
                {
                    AbsoluteExpiration = absoluteExpiration,
                });
            }

            return token;
        }
        finally
        {
            _semaphore.Release();
        }
    }

    private async Task<RancherHubAccessToken?> RequestTokenAsync(RancherHubConnectorOptions options, CancellationToken cancellationToken)
    {
        using var request = new HttpRequestMessage(HttpMethod.Post, options.TokenEndpoint);
        request.Headers.Accept.ParseAdd("application/json");

        var parameters = new Dictionary<string, string>
        {
            ["grant_type"] = "client_credentials",
        };

        if (options.Scopes.Count > 0)
        {
            parameters["scope"] = string.Join(' ', options.Scopes);
        }

        if (!string.IsNullOrWhiteSpace(options.Audience))
        {
            parameters["audience"] = options.Audience!;
        }

        if (string.Equals(options.ClientAuthenticationScheme, "client_secret_basic", StringComparison.Ordinal))
        {
            var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{options.ClientId}:{options.ClientSecret}"));
            request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials);
        }
        else
        {
            parameters["client_id"] = options.ClientId!;
            parameters["client_secret"] = options.ClientSecret!;
        }

        request.Content = new FormUrlEncodedContent(parameters);

        var client = _httpClientFactory.CreateClient(RancherHubConnectorOptions.HttpClientName);
        using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
        if (!response.IsSuccessStatusCode)
        {
            var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
            throw new InvalidOperationException($"Failed to acquire Rancher hub access token ({response.StatusCode}): {payload}");
        }

        await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
        using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
        var root = document.RootElement;

        if (!root.TryGetProperty("access_token", out var accessTokenProperty) || accessTokenProperty.ValueKind is not JsonValueKind.String)
        {
            throw new InvalidOperationException("Token endpoint response missing access_token.");
        }

        var token = accessTokenProperty.GetString();
        if (string.IsNullOrWhiteSpace(token))
        {
            throw new InvalidOperationException("Token endpoint response contained an empty access_token.");
        }

        var tokenType = root.TryGetProperty("token_type", out var tokenTypeElement) && tokenTypeElement.ValueKind is JsonValueKind.String
            ? tokenTypeElement.GetString() ?? "Bearer"
            : "Bearer";

        var expires = root.TryGetProperty("expires_in", out var expiresElement) &&
                      expiresElement.ValueKind is JsonValueKind.Number &&
                      expiresElement.TryGetInt32(out var expiresSeconds)
            ? DateTimeOffset.UtcNow + TimeSpan.FromSeconds(Math.Max(30, expiresSeconds))
            : DateTimeOffset.UtcNow + TimeSpan.FromMinutes(30);

        _logger.LogDebug("Acquired Rancher hub access token (expires {Expires}).", expires);

        return new RancherHubAccessToken(token, tokenType, expires);
    }
}

public sealed record RancherHubAccessToken(string Value, string TokenType, DateTimeOffset ExpiresAt)
{
    public bool IsExpired() => DateTimeOffset.UtcNow >= ExpiresAt - TimeSpan.FromMinutes(1);
}
