using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Feedser.Source.Vndr.Cisco.Configuration;

namespace StellaOps.Feedser.Source.Vndr.Cisco.Internal;

internal sealed class CiscoAccessTokenProvider : IDisposable
{
    private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
    {
        PropertyNameCaseInsensitive = true,
    };

    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IOptionsMonitor<CiscoOptions> _options;
    private readonly TimeProvider _timeProvider;
    private readonly ILogger<CiscoAccessTokenProvider> _logger;
    private readonly SemaphoreSlim _refreshLock = new(1, 1);

    private volatile AccessToken? _cached;
    private bool _disposed;

    public CiscoAccessTokenProvider(
        IHttpClientFactory httpClientFactory,
        IOptionsMonitor<CiscoOptions> options,
        TimeProvider? timeProvider,
        ILogger<CiscoAccessTokenProvider> logger)
    {
        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
        _options = options ?? throw new ArgumentNullException(nameof(options));
        _timeProvider = timeProvider ?? TimeProvider.System;
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<string> GetTokenAsync(CancellationToken cancellationToken)
        => await GetTokenInternalAsync(forceRefresh: false, cancellationToken).ConfigureAwait(false);

    public void Invalidate()
        => _cached = null;

    private async Task<string> GetTokenInternalAsync(bool forceRefresh, CancellationToken cancellationToken)
    {
        ThrowIfDisposed();

        var options = _options.CurrentValue;
        var now = _timeProvider.GetUtcNow();
        var cached = _cached;
        if (!forceRefresh && cached is not null && now < cached.ExpiresAt - options.TokenRefreshSkew)
        {
            return cached.Value;
        }

        await _refreshLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            cached = _cached;
            now = _timeProvider.GetUtcNow();
            if (!forceRefresh && cached is not null && now < cached.ExpiresAt - options.TokenRefreshSkew)
            {
                return cached.Value;
            }

            var fresh = await RequestTokenAsync(options, cancellationToken).ConfigureAwait(false);
            _cached = fresh;
            return fresh.Value;
        }
        finally
        {
            _refreshLock.Release();
        }
    }

    private async Task<AccessToken> RequestTokenAsync(CiscoOptions options, CancellationToken cancellationToken)
    {
        var client = _httpClientFactory.CreateClient(CiscoOptions.AuthHttpClientName);
        client.Timeout = options.RequestTimeout;

        using var request = new HttpRequestMessage(HttpMethod.Post, options.TokenEndpoint);
        request.Headers.Accept.Clear();
        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        var content = new FormUrlEncodedContent(new Dictionary<string, string>
        {
            ["grant_type"] = "client_credentials",
            ["client_id"] = options.ClientId,
            ["client_secret"] = options.ClientSecret,
        });

        request.Content = content;

        using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
        if (!response.IsSuccessStatusCode)
        {
            var preview = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
            var message = $"Cisco OAuth token request failed with status {(int)response.StatusCode} {response.StatusCode}.";
            _logger.LogError("Cisco openVuln token request failed: {Message}; response={Preview}", message, preview);
            throw new HttpRequestException(message);
        }

        await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
        var payload = await JsonSerializer.DeserializeAsync<TokenResponse>(stream, SerializerOptions, cancellationToken).ConfigureAwait(false);
        if (payload is null || string.IsNullOrWhiteSpace(payload.AccessToken))
        {
            throw new InvalidOperationException("Cisco OAuth token response did not include an access token.");
        }

        var expiresIn = payload.ExpiresIn > 0 ? TimeSpan.FromSeconds(payload.ExpiresIn) : TimeSpan.FromHours(1);
        var now = _timeProvider.GetUtcNow();
        var expiresAt = now + expiresIn;
        _logger.LogInformation("Cisco openVuln token issued; expires in {ExpiresIn}", expiresIn);
        return new AccessToken(payload.AccessToken, expiresAt);
    }

    public async Task<string> RefreshAsync(CancellationToken cancellationToken)
        => await GetTokenInternalAsync(forceRefresh: true, cancellationToken).ConfigureAwait(false);

    private void ThrowIfDisposed()
    {
        if (_disposed)
        {
            throw new ObjectDisposedException(nameof(CiscoAccessTokenProvider));
        }
    }

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _refreshLock.Dispose();
        _disposed = true;
    }

    private sealed record AccessToken(string Value, DateTimeOffset ExpiresAt);

    private sealed record TokenResponse(
        [property: JsonPropertyName("access_token")] string AccessToken,
        [property: JsonPropertyName("expires_in")] int ExpiresIn,
        [property: JsonPropertyName("token_type")] string? TokenType);
}
