using System.Net.Http.Headers;
using System.Threading;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using NexusReader.Application.Abstractions.Services;
using NexusReader.UI.Shared.Services;

namespace NexusReader.Maui.Infrastructure.Identity;

/// <summary>
/// A secure HTTP message delegating handler for MAUI that automatically appends JWT tokens
/// to trusted origin requests and transparently refreshes expired tokens in a thread-safe manner.
/// </summary>
public class MobileAuthenticationHeaderHandler : DelegatingHandler
{
    private readonly INativeStorageService _storageService;
    private readonly IServiceProvider _serviceProvider;
    private const string TokenKey = "nexus_auth_token";
    private static readonly SemaphoreSlim _refreshSemaphore = new(1, 1);

    public MobileAuthenticationHeaderHandler(INativeStorageService storageService, IServiceProvider serviceProvider)
    {
        _storageService = storageService;
        _serviceProvider = serviceProvider;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var path = request.RequestUri?.AbsolutePath ?? "";
        bool isAuthEndpoint = path.Contains("identity/login") || 
                             path.Contains("identity/register") || 
                             path.Contains("identity/refresh");

        // Resolve configured API host dynamically to avoid hardcoded IP addresses
        var config = _serviceProvider.GetRequiredService<IConfiguration>();
        var apiBaseUrlString = config["ApiSettings:BaseUrl"];
        string? apiHost = null;
        if (!string.IsNullOrEmpty(apiBaseUrlString) && Uri.TryCreate(apiBaseUrlString, UriKind.Absolute, out var apiUri))
        {
            apiHost = apiUri.Host;
        }

        // In MAUI, since we only call our own local or staging APIs, we trust local IP/localhost/configured API host.
        // We ensure we don't accidentally leak tokens to third-party endpoints.
        bool isTrustedHost = request.RequestUri != null && 
                             (request.RequestUri.Host == "localhost" ||
                              request.RequestUri.Host == "127.0.0.1" ||
                              (apiHost != null && request.RequestUri.Host == apiHost) ||
                              request.RequestUri.Host.EndsWith("nexusreader.com")); // Or staging domains

        string? originalToken = null;

        if (!isAuthEndpoint && isTrustedHost)
        {
            var tokenResult = await _storageService.GetSecureString(TokenKey);
            if (tokenResult.IsSuccess && !string.IsNullOrEmpty(tokenResult.Value))
            {
                originalToken = tokenResult.Value;
                
                // Only attach the Bearer token if it is not expired
                if (!JwtTokenValidator.IsExpired(originalToken))
                {
                    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", originalToken);
                }
            }
        }

        var response = await base.SendAsync(request, cancellationToken);

        // Transparent JWT Auto-Refresh on 401 Unauthorized
        if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized && !isAuthEndpoint)
        {
            await _refreshSemaphore.WaitAsync(cancellationToken);
            try
            {
                // Re-read token to verify if another concurrent request already refreshed it
                var tokenResult = await _storageService.GetSecureString(TokenKey);
                var currentToken = tokenResult.IsSuccess ? tokenResult.Value : null;

                bool refreshed = false;

                if (!string.IsNullOrEmpty(currentToken) && currentToken != originalToken)
                {
                    refreshed = true;
                }
                else
                {
                    using var scope = _serviceProvider.CreateScope();
                    var identityService = scope.ServiceProvider.GetRequiredService<IIdentityService>();
                    var refreshResult = await identityService.RefreshTokenAsync();
                    if (refreshResult.IsSuccess)
                    {
                        var newTokenResult = await _storageService.GetSecureString(TokenKey);
                        currentToken = newTokenResult.IsSuccess ? newTokenResult.Value : null;
                        refreshed = !string.IsNullOrEmpty(currentToken);
                    }
                    else
                    {
                        await identityService.LogoutAsync();
                    }
                }

                if (refreshed && !string.IsNullOrEmpty(currentToken))
                {
                    var newRequest = await CloneHttpRequestMessageAsync(request);
                    newRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", currentToken);
                    return await base.SendAsync(newRequest, cancellationToken);
                }
            }
            catch (Exception ex)
            {
                Serilog.Log.Error(ex, "[MobileAuthHandler] Automated token renewal failed");
            }
            finally
            {
                _refreshSemaphore.Release();
            }
        }

        return response;
    }

    private async Task<HttpRequestMessage> CloneHttpRequestMessageAsync(HttpRequestMessage req)
    {
        var clone = new HttpRequestMessage(req.Method, req.RequestUri)
        {
            Version = req.Version
        };

        if (req.Content != null)
        {
            var ms = new System.IO.MemoryStream();
            await req.Content.CopyToAsync(ms);
            ms.Position = 0;
            clone.Content = new StreamContent(ms);

            foreach (var h in req.Content.Headers)
            {
                clone.Content.Headers.TryAddWithoutValidation(h.Key, h.Value);
            }
        }

        foreach (var h in req.Headers)
        {
            clone.Headers.TryAddWithoutValidation(h.Key, h.Value);
        }

        return clone;
    }
}
