using System.Net.Http.Json;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;

namespace Synor.Sdk.Rpc;

/// <summary>
/// Synor RPC SDK client for C#.
///
/// Provides blockchain data queries, transaction submission,
/// and real-time subscriptions via WebSocket.
/// </summary>
/// <example>
/// <code>
/// var rpc = new SynorRpc(new RpcConfig { ApiKey = "your-api-key" });
///
/// // Get latest block
/// var block = await rpc.GetLatestBlockAsync();
/// Console.WriteLine($"Block height: {block.Height}");
///
/// // Subscribe to new blocks
/// var subscription = await rpc.SubscribeBlocksAsync(block => {
///     Console.WriteLine($"New block: {block.Height}");
/// });
///
/// // Later: cancel subscription
/// subscription.Cancel();
/// rpc.Dispose();
/// </code>
/// </example>
public class SynorRpc : IDisposable
{
    private readonly RpcConfig _config;
    private readonly HttpClient _httpClient;
    private readonly JsonSerializerOptions _jsonOptions;
    private ClientWebSocket? _webSocket;
    private CancellationTokenSource? _wsCancellation;
    private readonly Dictionary<string, Action<JsonElement>> _subscriptionCallbacks = new();
    private bool _disposed;

    public SynorRpc(RpcConfig config)
    {
        _config = config;
        _httpClient = new HttpClient
        {
            BaseAddress = new Uri(config.Endpoint),
            Timeout = config.Timeout
        };
        _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}");

        _jsonOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
            PropertyNameCaseInsensitive = true
        };
    }

    /// <summary>
    /// Get the latest block.
    /// </summary>
    public async Task<Block> GetLatestBlockAsync(CancellationToken cancellationToken = default)
    {
        return await GetAsync<Block>("/blocks/latest", cancellationToken);
    }

    /// <summary>
    /// Get a block by hash or height.
    /// </summary>
    public async Task<Block> GetBlockAsync(
        string hashOrHeight,
        CancellationToken cancellationToken = default)
    {
        return await GetAsync<Block>($"/blocks/{hashOrHeight}", cancellationToken);
    }

    /// <summary>
    /// Get a block header by hash or height.
    /// </summary>
    public async Task<BlockHeader> GetBlockHeaderAsync(
        string hashOrHeight,
        CancellationToken cancellationToken = default)
    {
        return await GetAsync<BlockHeader>($"/blocks/{hashOrHeight}/header", cancellationToken);
    }

    /// <summary>
    /// Get a range of blocks.
    /// </summary>
    public async Task<List<Block>> GetBlocksAsync(
        long startHeight,
        long endHeight,
        CancellationToken cancellationToken = default)
    {
        return await GetAsync<List<Block>>(
            $"/blocks?start={startHeight}&end={endHeight}",
            cancellationToken);
    }

    /// <summary>
    /// Get a transaction by ID.
    /// </summary>
    public async Task<RpcTransaction> GetTransactionAsync(
        string txid,
        CancellationToken cancellationToken = default)
    {
        return await GetAsync<RpcTransaction>($"/transactions/{txid}", cancellationToken);
    }

    /// <summary>
    /// Get raw transaction hex.
    /// </summary>
    public async Task<string> GetRawTransactionAsync(
        string txid,
        CancellationToken cancellationToken = default)
    {
        var response = await GetAsync<JsonElement>($"/transactions/{txid}/raw", cancellationToken);
        return response.GetProperty("hex").GetString() ?? throw new RpcException("Invalid response");
    }

    /// <summary>
    /// Send a raw transaction.
    /// </summary>
    public async Task<SubmitResult> SendRawTransactionAsync(
        string hex,
        CancellationToken cancellationToken = default)
    {
        var request = new { hex };
        return await PostAsync<SubmitResult>("/transactions/send", request, cancellationToken);
    }

    /// <summary>
    /// Decode a raw transaction.
    /// </summary>
    public async Task<RpcTransaction> DecodeRawTransactionAsync(
        string hex,
        CancellationToken cancellationToken = default)
    {
        var request = new { hex };
        return await PostAsync<RpcTransaction>("/transactions/decode", request, cancellationToken);
    }

    /// <summary>
    /// Get transactions for an address.
    /// </summary>
    public async Task<List<RpcTransaction>> GetAddressTransactionsAsync(
        string address,
        int limit = 20,
        int offset = 0,
        CancellationToken cancellationToken = default)
    {
        return await GetAsync<List<RpcTransaction>>(
            $"/addresses/{address}/transactions?limit={limit}&offset={offset}",
            cancellationToken);
    }

    /// <summary>
    /// Estimate transaction fee.
    /// </summary>
    public async Task<FeeEstimate> EstimateFeeAsync(
        RpcPriority priority = RpcPriority.Medium,
        CancellationToken cancellationToken = default)
    {
        return await GetAsync<FeeEstimate>(
            $"/fees/estimate?priority={priority.ToString().ToLower()}",
            cancellationToken);
    }

    /// <summary>
    /// Get all fee estimates.
    /// </summary>
    public async Task<Dictionary<RpcPriority, FeeEstimate>> GetAllFeeEstimatesAsync(
        CancellationToken cancellationToken = default)
    {
        var estimates = await GetAsync<List<FeeEstimate>>("/fees/estimates", cancellationToken);
        return estimates.ToDictionary(e => e.Priority, e => e);
    }

    /// <summary>
    /// Get chain information.
    /// </summary>
    public async Task<ChainInfo> GetChainInfoAsync(CancellationToken cancellationToken = default)
    {
        return await GetAsync<ChainInfo>("/chain/info", cancellationToken);
    }

    /// <summary>
    /// Get mempool information.
    /// </summary>
    public async Task<MempoolInfo> GetMempoolInfoAsync(CancellationToken cancellationToken = default)
    {
        return await GetAsync<MempoolInfo>("/mempool/info", cancellationToken);
    }

    /// <summary>
    /// Get mempool transaction IDs.
    /// </summary>
    public async Task<List<string>> GetMempoolTransactionsAsync(
        int limit = 100,
        CancellationToken cancellationToken = default)
    {
        return await GetAsync<List<string>>($"/mempool/transactions?limit={limit}", cancellationToken);
    }

    /// <summary>
    /// Subscribe to new blocks.
    /// </summary>
    public async Task<Subscription> SubscribeBlocksAsync(
        Action<Block> callback,
        CancellationToken cancellationToken = default)
    {
        await EnsureWebSocketConnectedAsync(cancellationToken);

        var subscriptionId = Guid.NewGuid().ToString();
        _subscriptionCallbacks[subscriptionId] = element =>
        {
            var block = element.Deserialize<Block>(_jsonOptions);
            if (block != null) callback(block);
        };

        var subscribeMessage = JsonSerializer.Serialize(new
        {
            type = "subscribe",
            channel = "blocks",
            subscription_id = subscriptionId
        });

        await SendWebSocketMessageAsync(subscribeMessage, cancellationToken);

        return new Subscription(subscriptionId, "blocks", () =>
        {
            _subscriptionCallbacks.Remove(subscriptionId);
            var unsubscribeMessage = JsonSerializer.Serialize(new
            {
                type = "unsubscribe",
                subscription_id = subscriptionId
            });
            _ = SendWebSocketMessageAsync(unsubscribeMessage, CancellationToken.None);
        });
    }

    /// <summary>
    /// Subscribe to address transactions.
    /// </summary>
    public async Task<Subscription> SubscribeAddressAsync(
        string address,
        Action<RpcTransaction> callback,
        CancellationToken cancellationToken = default)
    {
        await EnsureWebSocketConnectedAsync(cancellationToken);

        var subscriptionId = Guid.NewGuid().ToString();
        _subscriptionCallbacks[subscriptionId] = element =>
        {
            var tx = element.Deserialize<RpcTransaction>(_jsonOptions);
            if (tx != null) callback(tx);
        };

        var subscribeMessage = JsonSerializer.Serialize(new
        {
            type = "subscribe",
            channel = "address",
            address,
            subscription_id = subscriptionId
        });

        await SendWebSocketMessageAsync(subscribeMessage, cancellationToken);

        return new Subscription(subscriptionId, $"address:{address}", () =>
        {
            _subscriptionCallbacks.Remove(subscriptionId);
            var unsubscribeMessage = JsonSerializer.Serialize(new
            {
                type = "unsubscribe",
                subscription_id = subscriptionId
            });
            _ = SendWebSocketMessageAsync(unsubscribeMessage, CancellationToken.None);
        });
    }

    /// <summary>
    /// Subscribe to mempool transactions.
    /// </summary>
    public async Task<Subscription> SubscribeMempoolAsync(
        Action<RpcTransaction> callback,
        CancellationToken cancellationToken = default)
    {
        await EnsureWebSocketConnectedAsync(cancellationToken);

        var subscriptionId = Guid.NewGuid().ToString();
        _subscriptionCallbacks[subscriptionId] = element =>
        {
            var tx = element.Deserialize<RpcTransaction>(_jsonOptions);
            if (tx != null) callback(tx);
        };

        var subscribeMessage = JsonSerializer.Serialize(new
        {
            type = "subscribe",
            channel = "mempool",
            subscription_id = subscriptionId
        });

        await SendWebSocketMessageAsync(subscribeMessage, cancellationToken);

        return new Subscription(subscriptionId, "mempool", () =>
        {
            _subscriptionCallbacks.Remove(subscriptionId);
            var unsubscribeMessage = JsonSerializer.Serialize(new
            {
                type = "unsubscribe",
                subscription_id = subscriptionId
            });
            _ = SendWebSocketMessageAsync(unsubscribeMessage, CancellationToken.None);
        });
    }

    private async Task EnsureWebSocketConnectedAsync(CancellationToken cancellationToken)
    {
        if (_webSocket?.State == WebSocketState.Open)
            return;

        _webSocket?.Dispose();
        _wsCancellation?.Cancel();
        _wsCancellation?.Dispose();

        _webSocket = new ClientWebSocket();
        _webSocket.Options.SetRequestHeader("Authorization", $"Bearer {_config.ApiKey}");
        _wsCancellation = new CancellationTokenSource();

        await _webSocket.ConnectAsync(new Uri(_config.WsEndpoint), cancellationToken);
        _ = ReceiveWebSocketMessagesAsync(_wsCancellation.Token);
    }

    private async Task ReceiveWebSocketMessagesAsync(CancellationToken cancellationToken)
    {
        var buffer = new byte[8192];

        try
        {
            while (_webSocket?.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
            {
                var result = await _webSocket.ReceiveAsync(buffer, cancellationToken);

                if (result.MessageType == WebSocketMessageType.Close)
                    break;

                if (result.MessageType == WebSocketMessageType.Text)
                {
                    var json = Encoding.UTF8.GetString(buffer, 0, result.Count);
                    ProcessWebSocketMessage(json);
                }
            }
        }
        catch (OperationCanceledException)
        {
            // Expected when cancelling
        }
        catch (Exception ex)
        {
            if (_config.Debug)
            {
                Console.WriteLine($"WebSocket error: {ex.Message}");
            }
        }
    }

    private void ProcessWebSocketMessage(string json)
    {
        try
        {
            var doc = JsonDocument.Parse(json);
            var root = doc.RootElement;

            if (root.TryGetProperty("subscription_id", out var subIdElement))
            {
                var subscriptionId = subIdElement.GetString();
                if (subscriptionId != null && _subscriptionCallbacks.TryGetValue(subscriptionId, out var callback))
                {
                    if (root.TryGetProperty("data", out var data))
                    {
                        callback(data);
                    }
                }
            }
        }
        catch (Exception ex)
        {
            if (_config.Debug)
            {
                Console.WriteLine($"Error processing WebSocket message: {ex.Message}");
            }
        }
    }

    private async Task SendWebSocketMessageAsync(string message, CancellationToken cancellationToken)
    {
        if (_webSocket?.State == WebSocketState.Open)
        {
            var bytes = Encoding.UTF8.GetBytes(message);
            await _webSocket.SendAsync(bytes, WebSocketMessageType.Text, true, cancellationToken);
        }
    }

    private async Task<T> GetAsync<T>(string path, CancellationToken cancellationToken)
    {
        return await ExecuteWithRetryAsync(async () =>
        {
            var response = await _httpClient.GetAsync(path, cancellationToken);
            await EnsureSuccessAsync(response);
            return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken)
                ?? throw new RpcException("Invalid response");
        });
    }

    private async Task<T> PostAsync<T>(string path, object body, CancellationToken cancellationToken)
    {
        return await ExecuteWithRetryAsync(async () =>
        {
            var response = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, cancellationToken);
            await EnsureSuccessAsync(response);
            return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken)
                ?? throw new RpcException("Invalid response");
        });
    }

    private async Task EnsureSuccessAsync(HttpResponseMessage response)
    {
        if (!response.IsSuccessStatusCode)
        {
            var content = await response.Content.ReadAsStringAsync();
            throw new RpcException(
                $"HTTP error: {content}",
                statusCode: (int)response.StatusCode);
        }
    }

    private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation)
    {
        Exception? lastException = null;

        for (int attempt = 0; attempt < _config.Retries; attempt++)
        {
            try
            {
                return await operation();
            }
            catch (Exception ex)
            {
                lastException = ex;
                if (_config.Debug)
                {
                    Console.WriteLine($"Attempt {attempt + 1} failed: {ex.Message}");
                }
                if (attempt < _config.Retries - 1)
                {
                    await Task.Delay(TimeSpan.FromSeconds(attempt + 1));
                }
            }
        }

        throw lastException ?? new RpcException("Unknown error after retries");
    }

    public void Dispose()
    {
        if (!_disposed)
        {
            _wsCancellation?.Cancel();
            _wsCancellation?.Dispose();
            _webSocket?.Dispose();
            _httpClient.Dispose();
            _disposed = true;
        }
        GC.SuppressFinalize(this);
    }
}
