using System.Collections.Concurrent;
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using Polly;
using Polly.Retry;
using Refit;

namespace OTSSignsOrchestrator.Server.Clients;

/// <summary>
/// Creates per-instance <see cref="IXiboApiClient"/> Refit proxies with
/// OAuth2 bearer-token caching, auto-refresh on 401, and Polly retry.
/// Registered as a singleton.
/// </summary>
public sealed class XiboClientFactory
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ConcurrentDictionary<string, TokenEntry> _tokenCache = new();
    private readonly SemaphoreSlim _tokenLock = new(1, 1);

    private static readonly TimeSpan TokenCacheTtl = TimeSpan.FromMinutes(5);

    public XiboClientFactory(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    /// <summary>
    /// Build a Refit client targeting <paramref name="instanceBaseUrl"/>/api.
    /// Tokens are cached per base URL for 5 minutes and auto-refreshed on 401.
    /// </summary>
    public async Task<IXiboApiClient> CreateAsync(
        string instanceBaseUrl,
        string clientId,
        string clientSecret)
    {
        // Ensure we have a valid token up-front
        var token = await GetOrRefreshTokenAsync(instanceBaseUrl, clientId, clientSecret);

        var retryPipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
            .AddRetry(new RetryStrategyOptions<HttpResponseMessage>
            {
                MaxRetryAttempts = 3,
                BackoffType = DelayBackoffType.Exponential,
                Delay = TimeSpan.FromSeconds(1),
                ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
                    .HandleResult(r =>
                        r.StatusCode is HttpStatusCode.RequestTimeout
                            or HttpStatusCode.TooManyRequests
                            or >= HttpStatusCode.InternalServerError),
            })
            .Build();

        var handler = new XiboDelegatingHandler(
            this, instanceBaseUrl, clientId, clientSecret, token, retryPipeline)
        {
            InnerHandler = new HttpClientHandler(),
        };

        var httpClient = new HttpClient(handler)
        {
            BaseAddress = new Uri(instanceBaseUrl.TrimEnd('/') + "/api"),
        };

        return RestService.For<IXiboApiClient>(httpClient);
    }

    // ── Token management ────────────────────────────────────────────────────

    internal async Task<string> GetOrRefreshTokenAsync(
        string instanceBaseUrl,
        string clientId,
        string clientSecret,
        bool forceRefresh = false)
    {
        var key = instanceBaseUrl.TrimEnd('/').ToLowerInvariant();

        if (!forceRefresh
            && _tokenCache.TryGetValue(key, out var cached)
            && cached.ExpiresAt > DateTimeOffset.UtcNow)
        {
            return cached.AccessToken;
        }

        await _tokenLock.WaitAsync();
        try
        {
            // Double-check after acquiring lock
            if (!forceRefresh
                && _tokenCache.TryGetValue(key, out cached)
                && cached.ExpiresAt > DateTimeOffset.UtcNow)
            {
                return cached.AccessToken;
            }

            var token = await RequestTokenAsync(instanceBaseUrl, clientId, clientSecret);
            _tokenCache[key] = new TokenEntry(token, DateTimeOffset.UtcNow.Add(TokenCacheTtl));
            return token;
        }
        finally
        {
            _tokenLock.Release();
        }
    }

    private static async Task<string> RequestTokenAsync(
        string instanceBaseUrl,
        string clientId,
        string clientSecret)
    {
        using var http = new HttpClient();

        var content = new FormUrlEncodedContent(new Dictionary<string, string>
        {
            ["grant_type"] = "client_credentials",
            ["client_id"] = clientId,
            ["client_secret"] = clientSecret,
        });

        var response = await http.PostAsync(
            $"{instanceBaseUrl.TrimEnd('/')}/api/authorize/access_token",
            content);

        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        using var doc = JsonDocument.Parse(json);
        return doc.RootElement.GetProperty("access_token").GetString()
            ?? throw new InvalidOperationException("Token response missing access_token.");
    }

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

    // ── Delegating handler ──────────────────────────────────────────────────

    private sealed class XiboDelegatingHandler : DelegatingHandler
    {
        private readonly XiboClientFactory _factory;
        private readonly string _instanceBaseUrl;
        private readonly string _clientId;
        private readonly string _clientSecret;
        private readonly ResiliencePipeline<HttpResponseMessage> _retryPipeline;
        private string _accessToken;

        public XiboDelegatingHandler(
            XiboClientFactory factory,
            string instanceBaseUrl,
            string clientId,
            string clientSecret,
            string accessToken,
            ResiliencePipeline<HttpResponseMessage> retryPipeline)
        {
            _factory = factory;
            _instanceBaseUrl = instanceBaseUrl;
            _clientId = clientId;
            _clientSecret = clientSecret;
            _accessToken = accessToken;
            _retryPipeline = retryPipeline;
        }

        protected override async Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            return await _retryPipeline.ExecuteAsync(async ct =>
            {
                // Clone the request for retries (original may already be disposed)
                using var clone = await CloneRequestAsync(request);
                clone.Headers.Authorization =
                    new AuthenticationHeaderValue("Bearer", _accessToken);

                var response = await base.SendAsync(clone, ct);

                if (response.StatusCode == HttpStatusCode.Unauthorized)
                {
                    // Force-refresh the token and retry once
                    _accessToken = await _factory.GetOrRefreshTokenAsync(
                        _instanceBaseUrl, _clientId, _clientSecret, forceRefresh: true);

                    using var retry = await CloneRequestAsync(request);
                    retry.Headers.Authorization =
                        new AuthenticationHeaderValue("Bearer", _accessToken);

                    response = await base.SendAsync(retry, ct);
                }

                return response;
            }, cancellationToken);
        }

        private static async Task<HttpRequestMessage> CloneRequestAsync(
            HttpRequestMessage original)
        {
            var clone = new HttpRequestMessage(original.Method, original.RequestUri);

            if (original.Content != null)
            {
                var body = await original.Content.ReadAsByteArrayAsync();
                clone.Content = new ByteArrayContent(body);
                foreach (var header in original.Content.Headers)
                    clone.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
            }

            foreach (var header in original.Headers)
                clone.Headers.TryAddWithoutValidation(header.Key, header.Value);

            return clone;
        }
    }
}
