using Dapper;
using KArtSell.BuildingBlocks.Time;
using Microsoft.Extensions.Logging;
using Npgsql;
using Polly;
using Polly.CircuitBreaker;

namespace KArtSell.Host.Infrastructure;

/// <summary>
/// Circuit breaker policy for external API calls.
/// Trips after 3 consecutive 429 errors, opens for 5 minutes, auto-recovers.
/// Classifies failures: transient (retry) vs permanent (fail-fast) vs dq (quarantine).
/// </summary>
public class CircuitBreakerPolicyFactory
{
    private readonly NpgsqlDataSource _dataSource;
    private readonly IClock _clock;
    private readonly ILogger<CircuitBreakerPolicyFactory> _logger;

    private static readonly Dictionary<string, IAsyncPolicy<HttpResponseMessage>> Policies = new();

    private static readonly Action<ILogger, string, Exception?> LogCircuitOpened =
        LoggerMessage.Define<string>(
            LogLevel.Error,
            new EventId(1, nameof(LogCircuitOpened)),
            "Circuit breaker OPENED for {ApiName} (3 failures in 5 min)");

    private static readonly Action<ILogger, string, Exception?> LogCircuitClosed =
        LoggerMessage.Define<string>(
            LogLevel.Information,
            new EventId(2, nameof(LogCircuitClosed)),
            "Circuit breaker CLOSED for {ApiName} (recovery successful)");

    public CircuitBreakerPolicyFactory(NpgsqlDataSource dataSource, IClock clock, ILogger<CircuitBreakerPolicyFactory> logger)
    {
        _dataSource = dataSource;
        _clock = clock;
        _logger = logger;
    }

    /// <summary>
    /// Get or create circuit breaker policy for the given API.
    /// </summary>
    public IAsyncPolicy<HttpResponseMessage> GetPolicy(string apiName)
    {
        if (Policies.TryGetValue(apiName, out var policy))
            return policy;

        var newPolicy = CreatePolicy(apiName);
        Policies[apiName] = newPolicy;
        return newPolicy;
    }

    private IAsyncPolicy<HttpResponseMessage> CreatePolicy(string apiName)
    {
        // Circuit breaker: trip after 3 consecutive failures, open for 5 min
        var breakPolicy = Policy
            .Handle<HttpRequestException>()
            .Or<TaskCanceledException>()
            .OrResult<HttpResponseMessage>(r => r.StatusCode == System.Net.HttpStatusCode.TooManyRequests)  // 429
            .CircuitBreakerAsync<HttpResponseMessage>(
                handledEventsAllowedBeforeBreaking: 3,
                durationOfBreak: TimeSpan.FromMinutes(5),
                onBreak: (outcome, timespan) =>
                {
                    LogCircuitOpened(_logger, apiName, null);
                    LogStateChangeAsync(apiName, "opened", null).GetAwaiter().GetResult();
                },
                onReset: () =>
                {
                    LogCircuitClosed(_logger, apiName, null);
                    LogStateChangeAsync(apiName, "closed", null).GetAwaiter().GetResult();
                });

        // Retry policy (transient errors): exponential backoff
        var retryPolicy = Policy
            .Handle<HttpRequestException>()
            .Or<TaskCanceledException>()
            .OrResult<HttpResponseMessage>(r =>
                r.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable ||
                r.StatusCode == System.Net.HttpStatusCode.GatewayTimeout ||
                r.StatusCode == System.Net.HttpStatusCode.RequestTimeout)
            .WaitAndRetryAsync<HttpResponseMessage>(
                retryCount: 3,
                sleepDurationProvider: attempt => TimeSpan.FromMilliseconds(Math.Pow(2, attempt) * 100),
                onRetry: (outcome, timespan, retryCount, context) =>
                {
                    _logger.LogWarning("Transient error for {ApiName}, retry {RetryCount} after {Delay}ms",
                        apiName, retryCount, timespan.TotalMilliseconds);
                });

        // Combine: retry THEN circuit breaker
        return Policy.WrapAsync(retryPolicy, breakPolicy);
    }

    /// <summary>
    /// Classify failure type for logging and retry strategy.
    /// </summary>
    public static FailureClassification Classify(Exception ex, System.Net.HttpStatusCode? statusCode)
    {
        return statusCode switch
        {
            System.Net.HttpStatusCode.TooManyRequests => FailureClassification.Transient,  // 429
            System.Net.HttpStatusCode.ServiceUnavailable => FailureClassification.Transient,  // 503
            System.Net.HttpStatusCode.GatewayTimeout => FailureClassification.Transient,  // 504
            System.Net.HttpStatusCode.BadRequest => FailureClassification.Permanent,  // 400
            System.Net.HttpStatusCode.Unauthorized => FailureClassification.Permanent,  // 401
            System.Net.HttpStatusCode.Forbidden => FailureClassification.Permanent,  // 403
            System.Net.HttpStatusCode.NotFound => FailureClassification.Permanent,  // 404
            _ when ex is TaskCanceledException => FailureClassification.Transient,
            _ when ex is HttpRequestException => FailureClassification.Transient,
            _ => FailureClassification.DataQuality  // Unknown: quarantine for manual review
        };
    }

    private async Task LogStateChangeAsync(string apiName, string state, string? reason)
    {
        const string sql = """
            INSERT INTO infrastructure.circuit_breaker_events (api_name, state_change, reason, executed_at, published_at)
            VALUES (@apiName, @state, @reason, @now, @now)
            """;

        try
        {
            var now = _clock.UtcNow.UtcDateTime;
            await using var connection = await _dataSource.OpenConnectionAsync();
            await connection.ExecuteAsync(
                sql,
                new { apiName, state, reason, now },
                commandTimeout: 5);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to log circuit breaker state change");
        }
    }
}

public enum FailureClassification
{
    Transient = 0,      // Retry immediately (rate limit, timeout, etc)
    Permanent = 1,      // Fail fast (bad request, auth error, etc)
    DataQuality = 2     // Quarantine for manual review
}

/// <summary>
/// Helper: Wrap HTTP client calls with circuit breaker + error classification.
/// </summary>
public class ResilientHttpClient
{
    private readonly HttpClient _httpClient;
    private readonly CircuitBreakerPolicyFactory _policyFactory;
    private readonly ILogger<ResilientHttpClient> _logger;

    public ResilientHttpClient(
        HttpClient httpClient,
        CircuitBreakerPolicyFactory policyFactory,
        ILogger<ResilientHttpClient> logger)
    {
        _httpClient = httpClient;
        _policyFactory = policyFactory;
        _logger = logger;
    }

    public async Task<HttpResponseMessage> GetAsync(string apiName, string url, CancellationToken cancellationToken = default)
    {
        var policy = _policyFactory.GetPolicy(apiName);

        try
        {
            return await policy.ExecuteAsync(ct => _httpClient.GetAsync(url, ct), cancellationToken);
        }
        catch (BrokenCircuitException ex)
        {
            _logger.LogError(ex, "Circuit breaker is open for {ApiName}", apiName);
            throw;
        }
    }
}
