using System.Collections;
using System.Net;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Web;
using Managing.Application.Abstractions.Services;
using Managing.Core;
using Managing.Core.Exceptions;
using Managing.Domain.Accounts;
using Managing.Domain.Evm;
using Managing.Domain.Trades;
using Managing.Infrastructure.Evm.Models.Proxy;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Polly;
using static Managing.Common.Enums;

namespace Managing.Infrastructure.Evm.Services
{
    public class Web3ProxySettings
    {
        public string BaseUrl { get; set; } = "http://localhost:3000";
        public int MaxRetryAttempts { get; set; } = 3;
        public int RetryDelayMs { get; set; } = 1000;
        public int TimeoutSeconds { get; set; } = 30;
    }

    public class Web3ProxyService : IWeb3ProxyService
    {
        private readonly HttpClient _httpClient;
        private readonly Web3ProxySettings _settings;
        private readonly JsonSerializerOptions _jsonOptions;
        private readonly IAsyncPolicy<HttpResponseMessage> _retryPolicy;
        private readonly ILogger<Web3ProxyService> _logger;

        public Web3ProxyService(IOptions<Web3ProxySettings> options, ILogger<Web3ProxyService> logger)
        {
            _settings = options.Value;
            _logger = logger;

            _httpClient = new HttpClient();
            _httpClient.Timeout = TimeSpan.FromSeconds(_settings.TimeoutSeconds);

            _jsonOptions = new JsonSerializerOptions
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase
            };

            // Configure retry policy
            _retryPolicy = Policy
                .Handle<HttpRequestException>()
                .Or<TaskCanceledException>()
                .Or<TimeoutException>()
                .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode && IsRetryableStatusCode(r.StatusCode))
                .WaitAndRetryAsync(
                    retryCount: _settings.MaxRetryAttempts,
                    sleepDurationProvider: retryAttempt => TimeSpan.FromMilliseconds(
                        _settings.RetryDelayMs * Math.Pow(2, retryAttempt - 1) + // Exponential backoff
                        new Random().Next(0, _settings.RetryDelayMs / 4) // Add jitter
                    ),
                    onRetry: (outcome, timespan, retryCount, context) =>
                    {
                        var exception = outcome.Exception;
                        var response = outcome.Result;
                        var errorMessage = exception?.Message ?? $"HTTP {response?.StatusCode}";

                        _logger.LogWarning(
                            "Web3Proxy request failed (attempt {RetryCount}/{MaxRetries}): {Error}. Retrying in {Delay}ms",
                            retryCount, _settings.MaxRetryAttempts + 1, errorMessage, timespan.TotalMilliseconds);
                    });
        }

        private static bool IsRetryableStatusCode(HttpStatusCode statusCode)
        {
            return statusCode == HttpStatusCode.RequestTimeout ||
                   statusCode == HttpStatusCode.TooManyRequests ||
                   statusCode == HttpStatusCode.InternalServerError ||
                   statusCode == HttpStatusCode.BadGateway ||
                   statusCode == HttpStatusCode.ServiceUnavailable ||
                   statusCode == HttpStatusCode.GatewayTimeout;
        }

        /// <summary>
        /// Checks if an error message indicates insufficient funds or allowance that should not be retried
        /// </summary>
        /// <param name="errorMessage">The error message to check</param>
        /// <returns>True if this is a non-retryable insufficient funds error</returns>
        private static bool IsInsufficientFundsError(string errorMessage)
        {
            if (string.IsNullOrEmpty(errorMessage))
                return false;

            var lowerError = errorMessage.ToLowerInvariant();

            // Check for common insufficient funds/allowance error patterns
            return lowerError.Contains("erc20: transfer amount exceeds allowance") ||
                   lowerError.Contains("insufficient funds") ||
                   lowerError.Contains("insufficient balance") ||
                   lowerError.Contains("execution reverted") &&
                   (lowerError.Contains("allowance") || lowerError.Contains("balance")) ||
                   lowerError.Contains("gas required exceeds allowance") ||
                   lowerError.Contains("out of gas") ||
                   lowerError.Contains("insufficient eth") ||
                   lowerError.Contains("insufficient token");
        }

        /// <summary>
        /// Determines the type of insufficient funds error based on the error message
        /// </summary>
        /// <param name="errorMessage">The error message to analyze</param>
        /// <returns>The type of insufficient funds error</returns>
        private static InsufficientFundsType GetInsufficientFundsType(string errorMessage)
        {
            if (string.IsNullOrEmpty(errorMessage))
                return InsufficientFundsType.General;

            var lowerError = errorMessage.ToLowerInvariant();

            if (lowerError.Contains("erc20: transfer amount exceeds allowance") ||
                lowerError.Contains("allowance"))
            {
                return InsufficientFundsType.InsufficientAllowance;
            }

            if (lowerError.Contains("gas required exceeds allowance") ||
                lowerError.Contains("out of gas") ||
                lowerError.Contains("insufficient eth"))
            {
                return InsufficientFundsType.InsufficientEth;
            }

            if (lowerError.Contains("insufficient balance") ||
                lowerError.Contains("insufficient token"))
            {
                return InsufficientFundsType.InsufficientBalance;
            }

            return InsufficientFundsType.General;
        }

        private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<HttpResponseMessage>> httpCall, string operationName)
        {
            try
            {
                var response = await _retryPolicy.ExecuteAsync(httpCall);

                if (!response.IsSuccessStatusCode)
                {
                    await HandleErrorResponse(response);
                }

                var result = await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
                return result ?? throw new Web3ProxyException($"Failed to deserialize response for {operationName}");
            }
            catch (InsufficientFundsException)
            {
                // Re-throw insufficient funds exceptions immediately without retrying
                throw;
            }
            catch (Exception ex) when (!(ex is Web3ProxyException))
            {
                _logger.LogError(ex, "Operation {OperationName} failed after all retry attempts", operationName);
                SentrySdk.CaptureException(ex);
                throw new Web3ProxyException($"Failed to execute {operationName}: {ex.Message}");
            }
        }

        private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<HttpResponseMessage>> httpCall, string operationName,
            string idempotencyKey)
        {
            try
            {
                var response = await _retryPolicy.ExecuteAsync(httpCall);

                if (!response.IsSuccessStatusCode)
                {
                    await HandleErrorResponse(response);
                }

                var result = await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
                return result ?? throw new Web3ProxyException($"Failed to deserialize response for {operationName}");
            }
            catch (InsufficientFundsException)
            {
                // Re-throw insufficient funds exceptions immediately without retrying
                throw;
            }
            catch (Exception ex) when (!(ex is Web3ProxyException))
            {
                _logger.LogError(ex,
                    "Operation {OperationName} failed after all retry attempts (IdempotencyKey: {IdempotencyKey})",
                    operationName, idempotencyKey);
                SentrySdk.CaptureException(ex);
                throw new Web3ProxyException($"Failed to execute {operationName}: {ex.Message}");
            }
        }

        public async Task<T> CallPrivyServiceAsync<T>(string endpoint, object payload)
        {
            if (!endpoint.StartsWith("/"))
            {
                endpoint = $"/{endpoint}";
            }

            var url = $"{_settings.BaseUrl}/api/privy{endpoint}";
            var idempotencyKey = Guid.NewGuid().ToString();

            return await ExecuteWithRetryAsync<T>(
                () =>
                {
                    var request = new HttpRequestMessage(HttpMethod.Post, url)
                    {
                        Content = JsonContent.Create(payload, options: _jsonOptions)
                    };
                    request.Headers.Add("Idempotency-Key", idempotencyKey);
                    return _httpClient.SendAsync(request);
                },
                $"CallPrivyServiceAsync({endpoint})",
                idempotencyKey);
        }

        public async Task<T> GetPrivyServiceAsync<T>(string endpoint, object? payload = null)
        {
            if (!endpoint.StartsWith("/"))
            {
                endpoint = $"/{endpoint}";
            }

            var url = $"{_settings.BaseUrl}/api/privy{endpoint}";

            if (payload != null)
            {
                url += BuildQueryString(payload);
            }

            return await ExecuteWithRetryAsync<T>(
                () => _httpClient.GetAsync(url),
                $"GetPrivyServiceAsync({endpoint})");
        }

        public async Task<T> CallGmxServiceAsync<T>(string endpoint, object payload)
        {
            if (!endpoint.StartsWith("/"))
            {
                endpoint = $"/{endpoint}";
            }

            var url = $"{_settings.BaseUrl}/api/gmx{endpoint}";
            var idempotencyKey = Guid.NewGuid().ToString();

            return await ExecuteWithRetryAsync<T>(
                () =>
                {
                    var request = new HttpRequestMessage(HttpMethod.Post, url)
                    {
                        Content = JsonContent.Create(payload, options: _jsonOptions)
                    };
                    request.Headers.Add("Idempotency-Key", idempotencyKey);
                    return _httpClient.SendAsync(request);
                },
                $"CallGmxServiceAsync({endpoint})",
                idempotencyKey);
        }

        public async Task<T> GetGmxServiceAsync<T>(string endpoint, object? payload = null)
        {
            if (!endpoint.StartsWith("/"))
            {
                endpoint = $"/{endpoint}";
            }

            var url = $"{_settings.BaseUrl}/api/gmx{endpoint}";

            if (payload != null)
            {
                url += BuildQueryString(payload);
            }

            return await ExecuteWithRetryAsync<T>(
                () => _httpClient.GetAsync(url),
                $"GetGmxServiceAsync({endpoint})");
        }

        public async Task<GmxClaimableSummary> GetGmxClaimableSummaryAsync(string account)
        {
            var payload = new { account };
            var response = await GetGmxServiceAsync<ClaimingFeesResponse>("/claimable-summary", payload);


            if (response.Data == null)
            {
                throw new Web3ProxyException("GMX claimable summary data is null");
            }

            // Map from Web3Proxy response model to domain model
            return new GmxClaimableSummary
            {
                ClaimableFundingFees = new FundingFeesData
                {
                    TotalUsdc = response.Data.ClaimableFundingFees.TotalUsdc
                },
                ClaimableUiFees = new UiFeesData
                {
                    TotalUsdc = response.Data.ClaimableUiFees.TotalUsdc
                },
                RebateStats = new RebateStatsData
                {
                    TotalRebateUsdc = response.Data.RebateStats.TotalRebateUsdc,
                    DiscountUsdc = response.Data.RebateStats.DiscountUsdc,
                    RebateFactor = response.Data.RebateStats.RebateFactor,
                    DiscountFactor = response.Data.RebateStats.DiscountFactor
                }
            };
        }

        public async Task<SwapInfos> SwapGmxTokensAsync(string account, Ticker fromTicker, Ticker toTicker,
            double amount, string orderType = "market", double? triggerRatio = null, double allowedSlippage = 0.5)
        {
            var payload = new
            {
                account,
                fromTicker = fromTicker.ToString(),
                toTicker = toTicker.ToString(),
                amount,
                orderType,
                triggerRatio,
                allowedSlippage
            };

            var response = await CallGmxServiceAsync<GmxSwapResponse>("/swap-tokens", payload);

            if (response == null)
            {
                throw new Web3ProxyException("GMX swap response is null");
            }

            // Map from infrastructure model to domain model
            return new SwapInfos
            {
                Success = response.Success,
                Hash = response.Hash,
                Message = response.Message,
                Error = null, // GmxSwapResponse doesn't have Error property
                ErrorType = response.ErrorType,
                Suggestion = response.Suggestion
            };
        }

        public async Task<SwapInfos> SendTokenAsync(string senderAddress, string recipientAddress, Ticker ticker,
            decimal amount, int? chainId = null)
        {
            var payload = new
            {
                senderAddress,
                recipientAddress,
                ticker = ticker.ToString(),
                amount = amount.ToString(), // Convert decimal to string for bigint compatibility
                chainId
            };

            var response = await CallPrivyServiceAsync<Web3ProxyTokenSendResponse>("/send-token", payload);

            if (response == null)
            {
                throw new Web3ProxyException("Token send response is null");
            }

            // Map from infrastructure model to domain model
            return new SwapInfos
            {
                Success = response.Success,
                Hash = response.Hash,
                Message = null, // Web3ProxyTokenSendResponse doesn't have Message property
                Error = response.Error,
                ErrorType = null, // Web3ProxyTokenSendResponse doesn't have ErrorType property
                Suggestion = null // Web3ProxyTokenSendResponse doesn't have Suggestion property
            };
        }

        public async Task<List<Balance>> GetWalletBalanceAsync(string address, Ticker[] assets, string[] chains)
        {
            var payload = new
            {
                address,
                asset = assets,
                chain = chains
            };

            var response = await GetPrivyServiceAsync<Web3ProxyBalanceResponse>("/wallet-balance", payload);

            if (response == null)
            {
                throw new Web3ProxyException("Wallet balance response is null");
            }

            if (!response.Success)
            {
                throw new Web3ProxyException($"Wallet balance request failed: {response.Error}");
            }

            return response.Balances ?? new List<Balance>();
        }

        public async Task<decimal> GetEstimatedGasFeeUsdAsync()
        {
            var response = await GetGmxServiceAsync<GasFeeResponse>("/gas-fee", null);

            if (response == null)
            {
                throw new Web3ProxyException("Gas fee response is null");
            }

            if (!response.Success)
            {
                throw new Web3ProxyException($"Gas fee request failed: {response.Error}");
            }

            if (response.Data is null)
            {
                throw new Web3ProxyException("Gas fee data is null");
            }

            return (decimal)(response.Data.EstimatedGasFeeUsd ?? 0);
        }

        public async Task<GasFeeData> GetGasFeeDataAsync()
        {
            var response = await GetGmxServiceAsync<GasFeeResponse>("/gas-fee", null);

            if (response == null)
            {
                throw new Web3ProxyException("Gas fee response is null");
            }

            if (!response.Success)
            {
                throw new Web3ProxyException($"Gas fee request failed: {response.Error}");
            }

            if (response.Data is null)
            {
                throw new Web3ProxyException("Gas fee data is null");
            }

            return response.Data;
        }

        private async Task HandleErrorResponse(HttpResponseMessage response)
        {
            var statusCode = (int)response.StatusCode;

            try
            {
                // Try to parse as the Web3Proxy error format (success: false, error: string)
                var content = await response.Content.ReadAsStringAsync();
                var errorResponse = await response.Content.ReadFromJsonAsync<Web3ProxyResponse>(_jsonOptions);

                if (errorResponse != null && !errorResponse.Success && !string.IsNullOrEmpty(errorResponse.Error))
                {
                    // Check if this is an insufficient funds error that should not be retried
                    if (IsInsufficientFundsError(errorResponse.Error))
                    {
                        var errorType = GetInsufficientFundsType(errorResponse.Error);
                        throw new InsufficientFundsException(errorResponse.Error, errorType);
                    }

                    // Handle the standard Web3Proxy error format
                    throw new Web3ProxyException(errorResponse.Error);
                }

                // Fallback for other error formats
                try
                {
                    // Try to parse as structured error if it doesn't match the simple format
                    var structuredErrorResponse =
                        await response.Content.ReadFromJsonAsync<Web3ProxyErrorResponse>(_jsonOptions);

                    if (structuredErrorResponse?.ErrorDetails != null)
                    {
                        structuredErrorResponse.ErrorDetails.StatusCode = statusCode;

                        // Check if this is an insufficient funds error that should not be retried
                        if (IsInsufficientFundsError(structuredErrorResponse.ErrorDetails.Message))
                        {
                            var errorType = GetInsufficientFundsType(structuredErrorResponse.ErrorDetails.Message);
                            throw new InsufficientFundsException(structuredErrorResponse.ErrorDetails.Message,
                                errorType);
                        }

                        throw new Web3ProxyException(structuredErrorResponse.ErrorDetails);
                    }
                }
                catch (Exception ex) when (ex is InsufficientFundsException)
                {
                    // Re-throw insufficient funds exceptions as-is
                    throw;
                }
                catch
                {
                    // Check if the raw content contains insufficient funds errors
                    if (IsInsufficientFundsError(content))
                    {
                        var errorType = GetInsufficientFundsType(content);
                        throw new InsufficientFundsException(content, errorType);
                    }

                    // If we couldn't parse as structured error, use the simple error or fallback
                    throw new Web3ProxyException($"HTTP error {statusCode}: {content}");
                }
            }
            catch (Exception ex) when (ex is InsufficientFundsException)
            {
                // Re-throw insufficient funds exceptions as-is
                throw;
            }
            catch (Exception ex) when (!(ex is Web3ProxyException))
            {
                SentrySdk.CaptureException(ex);
                // If we couldn't parse the error as JSON or another issue occurred
                var content = await response.Content.ReadAsStringAsync();

                // Check if the raw content contains insufficient funds errors
                if (IsInsufficientFundsError(content))
                {
                    var errorType = GetInsufficientFundsType(content);
                    throw new InsufficientFundsException(content, errorType);
                }

                throw new Web3ProxyException($"HTTP error {statusCode}: {content}");
            }
        }

        private string BuildQueryString(object payload)
        {
            var properties = payload.GetType().GetProperties();
            if (properties.Length == 0)
            {
                return string.Empty;
            }

            var queryString = new StringBuilder("?");
            bool isFirst = true;

            foreach (var prop in properties)
            {
                var value = prop.GetValue(payload);
                if (value != null)
                {
                    var paramName = prop.Name;
                    // Apply camelCase to match JSON property naming
                    paramName = char.ToLowerInvariant(paramName[0]) + paramName.Substring(1);

                    // Check if the value is an array or enumerable (but not string)
                    if (value is IEnumerable enumerable && !(value is string))
                    {
                        // Handle arrays by creating multiple query parameters with the same name
                        foreach (var item in enumerable)
                        {
                            if (!isFirst)
                            {
                                queryString.Append("&");
                            }

                            queryString.Append(HttpUtility.UrlEncode(paramName));
                            queryString.Append("=");
                            queryString.Append(HttpUtility.UrlEncode(item?.ToString() ?? ""));

                            isFirst = false;
                        }
                    }
                    else
                    {
                        // Handle single values
                        if (!isFirst)
                        {
                            queryString.Append("&");
                        }

                        queryString.Append(HttpUtility.UrlEncode(paramName));
                        queryString.Append("=");
                        queryString.Append(HttpUtility.UrlEncode(value.ToString()));

                        isFirst = false;
                    }
                }
            }

            return queryString.ToString();
        }

        public async Task<List<Position>> GetGmxPositionHistoryAsync(string account, int pageIndex = 0,
            int pageSize = 20, string? ticker = null)
        {
            var payload = new
            {
                account,
                pageIndex,
                pageSize,
                ticker
            };

            var response = await GetGmxServiceAsync<GetGmxPositionHistoryResponse>("/position-history", payload);

            if (response == null)
            {
                throw new Web3ProxyException("Position history response is null");
            }

            if (!response.Success)
            {
                throw new Web3ProxyException($"Position history request failed: {response.Error}");
            }

            // Map to domain Positions manually
            var positions = new List<Position>();
            if (response.Positions != null)
            {
                foreach (var g in response.Positions)
                {
                    try
                    {
                        var tickerEnum = MiscExtensions.ParseEnum<Ticker>(g.Ticker);
                        var directionEnum = MiscExtensions.ParseEnum<TradeDirection>(g.Direction);

                        var position = new Position(
                            Guid.NewGuid(),
                            0, // AccountId unknown at this layer
                            directionEnum,
                            tickerEnum,
                            null, // MoneyManagement not available here
                            PositionInitiator.Bot,
                            g.Date,
                            null // User not available here
                        );

                        position.Status = MiscExtensions.ParseEnum<PositionStatus>(g.Status);

                        // Open trade represents the closing event from GMX history
                        if (g.Open != null)
                        {
                            position.Open = new Trade(
                                g.Open.Date,
                                MiscExtensions.ParseEnum<TradeDirection>(g.Open.Direction),
                                MiscExtensions.ParseEnum<TradeStatus>(g.Open.Status),
                                TradeType.Market,
                                tickerEnum,
                                (decimal)g.Open.Quantity,
                                (decimal)g.Open.Price,
                                (decimal)g.Open.Leverage,
                                g.Open.ExchangeOrderId,
                                string.Empty
                            );
                        }
                        else
                        {
                            position.Open = new Trade(
                                g.Date,
                                directionEnum,
                                TradeStatus.Filled,
                                TradeType.Market,
                                tickerEnum,
                                (decimal)g.Quantity,
                                (decimal)g.Price,
                                (decimal)g.Leverage,
                                g.ExchangeOrderId,
                                "GMX Position Close"
                            );
                        }

                        position.ProfitAndLoss = new ProfitAndLoss
                        {
                            Realized = g.ProfitAndLoss?.Realized != null
                                ? (decimal)g.ProfitAndLoss.Realized
                                : (decimal)g.Pnl,
                            Net = g.ProfitAndLoss?.Net != null ? (decimal)g.ProfitAndLoss.Net : (decimal)g.Pnl
                        };

                        position.UiFees = g.UiFees.HasValue ? (decimal)g.UiFees.Value : 0;
                        position.GasFees = g.GasFees.HasValue ? (decimal)g.GasFees.Value : 0;

                        positions.Add(position);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogWarning(ex, "Failed to map GMX position history entry");
                    }
                }
            }

            return positions;
        }
    }
}