using System;
using System.Collections.Generic;
using System.IO.Abstractions;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Connectors.MSRC.CSAF.Configuration;

namespace StellaOps.Excititor.Connectors.MSRC.CSAF.Authentication;

public interface IMsrcTokenProvider
{
    ValueTask<MsrcAccessToken> GetAccessTokenAsync(CancellationToken cancellationToken);
}

public sealed class MsrcTokenProvider : IMsrcTokenProvider, IDisposable
{
    private const string CachePrefix = "StellaOps.Excititor.Connectors.MSRC.CSAF.Token";

    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IMemoryCache _cache;
    private readonly IFileSystem _fileSystem;
    private readonly ILogger<MsrcTokenProvider> _logger;
    private readonly TimeProvider _timeProvider;
    private readonly MsrcConnectorOptions _options;
    private readonly SemaphoreSlim _refreshLock = new(1, 1);

    public MsrcTokenProvider(
        IHttpClientFactory httpClientFactory,
        IMemoryCache cache,
        IFileSystem fileSystem,
        IOptions<MsrcConnectorOptions> options,
        ILogger<MsrcTokenProvider> logger,
        TimeProvider? timeProvider = null)
    {
        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
        _cache = cache ?? throw new ArgumentNullException(nameof(cache));
        _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
        ArgumentNullException.ThrowIfNull(options);
        _options = options.Value ?? throw new ArgumentNullException(nameof(options));
        _options.Validate(_fileSystem);
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _timeProvider = timeProvider ?? TimeProvider.System;
    }

    public async ValueTask<MsrcAccessToken> GetAccessTokenAsync(CancellationToken cancellationToken)
    {
        if (_options.PreferOfflineToken)
        {
            return LoadOfflineToken();
        }

        var cacheKey = CreateCacheKey();
        if (_cache.TryGetValue<MsrcAccessToken>(cacheKey, out var cachedToken) &&
            cachedToken is not null &&
            !cachedToken.IsExpired(_timeProvider.GetUtcNow()))
        {
            return cachedToken;
        }

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

            var token = await RequestTokenAsync(cancellationToken).ConfigureAwait(false);
            var absoluteExpiration = token.ExpiresAt == DateTimeOffset.MaxValue
                ? (DateTimeOffset?)null
                : token.ExpiresAt;

            var options = new MemoryCacheEntryOptions();
            if (absoluteExpiration.HasValue)
            {
                options.AbsoluteExpiration = absoluteExpiration.Value;
            }

            _cache.Set(cacheKey, token, options);
            return token;
        }
        finally
        {
            _refreshLock.Release();
        }
    }

    private MsrcAccessToken LoadOfflineToken()
    {
        if (!string.IsNullOrWhiteSpace(_options.StaticAccessToken))
        {
            return new MsrcAccessToken(_options.StaticAccessToken!, "Bearer", DateTimeOffset.MaxValue);
        }

        if (string.IsNullOrWhiteSpace(_options.OfflineTokenPath))
        {
            throw new InvalidOperationException("Offline token mode is enabled but no token was provided.");
        }

        if (!_fileSystem.File.Exists(_options.OfflineTokenPath))
        {
            throw new InvalidOperationException($"Offline token path '{_options.OfflineTokenPath}' does not exist.");
        }

        var token = _fileSystem.File.ReadAllText(_options.OfflineTokenPath).Trim();
        if (string.IsNullOrEmpty(token))
        {
            throw new InvalidOperationException("Offline token file was empty.");
        }

        return new MsrcAccessToken(token, "Bearer", DateTimeOffset.MaxValue);
    }

    private async Task<MsrcAccessToken> RequestTokenAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Fetching MSRC AAD access token for tenant {TenantId}.", _options.TenantId);

        var client = _httpClientFactory.CreateClient(MsrcConnectorOptions.TokenClientName);
        using 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"] = string.IsNullOrWhiteSpace(_options.Scope) ? MsrcConnectorOptions.DefaultScope : _options.Scope,
            }),
        };

        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 MSRC access token ({(int)response.StatusCode}). Response: {payload}");
        }

        var tokenResponse = await response.Content.ReadFromJsonAsync<TokenResponse>(cancellationToken: cancellationToken).ConfigureAwait(false)
            ?? throw new InvalidOperationException("Token endpoint returned an empty payload.");

        if (string.IsNullOrWhiteSpace(tokenResponse.AccessToken))
        {
            throw new InvalidOperationException("Token endpoint response did not include an access_token.");
        }

        var now = _timeProvider.GetUtcNow();
        var expiresAt = tokenResponse.ExpiresIn > _options.ExpiryLeewaySeconds
            ? now.AddSeconds(tokenResponse.ExpiresIn - _options.ExpiryLeewaySeconds)
            : now.AddMinutes(5);

        return new MsrcAccessToken(tokenResponse.AccessToken!, tokenResponse.TokenType ?? "Bearer", expiresAt);
    }

    private string CreateCacheKey()
        => $"{CachePrefix}:{_options.TenantId}:{_options.ClientId}:{_options.Scope}";

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

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

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

        [JsonPropertyName("token_type")]
        public string? TokenType { get; init; }

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

public sealed record MsrcAccessToken(string Value, string Type, DateTimeOffset ExpiresAt)
{
    public bool IsExpired(DateTimeOffset now) => now >= ExpiresAt;
}
