using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Feedser.Source.Vndr.Msrc.Configuration;

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

public interface IMsrcTokenProvider
{
    Task<string> GetAccessTokenAsync(CancellationToken cancellationToken);
}

public sealed class MsrcTokenProvider : IMsrcTokenProvider, IDisposable
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly MsrcOptions _options;
    private readonly TimeProvider _timeProvider;
    private readonly ILogger<MsrcTokenProvider> _logger;
    private readonly SemaphoreSlim _refreshLock = new(1, 1);

    private AccessToken? _currentToken;

    public MsrcTokenProvider(
        IHttpClientFactory httpClientFactory,
        IOptions<MsrcOptions> options,
        TimeProvider? timeProvider,
        ILogger<MsrcTokenProvider> logger)
    {
        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
        _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 async Task<string> GetAccessTokenAsync(CancellationToken cancellationToken)
    {
        var token = _currentToken;
        if (token is not null && !token.IsExpired(_timeProvider.GetUtcNow()))
        {
            return token.Token;
        }

        await _refreshLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            token = _currentToken;
            if (token is not null && !token.IsExpired(_timeProvider.GetUtcNow()))
            {
                return token.Token;
            }

            _logger.LogInformation("Requesting new MSRC access token");
            var client = _httpClientFactory.CreateClient(MsrcOptions.TokenClientName);
            var request = new HttpRequestMessage(HttpMethod.Post, BuildTokenUri())
            {
                Content = new FormUrlEncodedContent(new Dictionary<string, string>
                {
                    ["client_id"] = _options.ClientId,
                    ["client_secret"] = _options.ClientSecret,
                    ["grant_type"] = "client_credentials",
                    ["scope"] = _options.Scope,
                }),
            };

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

            var payload = await response.Content.ReadFromJsonAsync<TokenResponse>(cancellationToken: cancellationToken).ConfigureAwait(false)
                ?? throw new InvalidOperationException("AAD token response was null.");

            var expiresAt = _timeProvider.GetUtcNow().AddSeconds(payload.ExpiresIn - 60);
            _currentToken = new AccessToken(payload.AccessToken, expiresAt);
            return payload.AccessToken;
        }
        finally
        {
            _refreshLock.Release();
        }
    }

    private Uri BuildTokenUri()
        => new($"https://login.microsoftonline.com/{_options.TenantId}/oauth2/v2.0/token");

    public void Dispose() => _refreshLock.Dispose();

    private sealed record AccessToken(string Token, DateTimeOffset ExpiresAt)
    {
        public bool IsExpired(DateTimeOffset now) => now >= ExpiresAt;
    }

    private sealed record TokenResponse
    {
        [JsonPropertyName("access_token")]
        public string AccessToken { get; init; } = string.Empty;

        [JsonPropertyName("expires_in")]
        public int ExpiresIn { get; init; }
    }
}
