/// Synor RPC SDK client for Flutter/Dart.
library synor_rpc;

import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:web_socket_channel/web_socket_channel.dart';
import 'types.dart';

export 'types.dart';

/// Synor RPC SDK client for Flutter/Dart.
///
/// Provides blockchain queries, transaction submission, and real-time
/// subscriptions via WebSocket.
///
/// Example:
/// ```dart
/// final rpc = SynorRpc(RpcConfig(apiKey: 'your-api-key'));
///
/// // Get latest block
/// final block = await rpc.getLatestBlock();
/// print('Latest block: ${block.height}');
///
/// // Subscribe to new blocks
/// final subscription = rpc.subscribeBlocks((block) {
///   print('New block: ${block.height}');
/// });
///
/// // Later: cancel subscription
/// subscription.cancel();
/// rpc.close();
/// ```
class SynorRpc {
  final RpcConfig config;
  final http.Client _client;
  WebSocketChannel? _wsChannel;
  StreamSubscription? _wsSubscription;
  final Map<String, void Function(String)> _subscriptions = {};

  SynorRpc(this.config) : _client = http.Client();

  // Block operations

  /// Get the latest block.
  Future<Block> getLatestBlock() async {
    return _get('/blocks/latest', Block.fromJson);
  }

  /// Get a block by hash or height.
  Future<Block> getBlock(String hashOrHeight) async {
    return _get('/blocks/$hashOrHeight', Block.fromJson);
  }

  /// Get a block header by hash or height.
  Future<BlockHeader> getBlockHeader(String hashOrHeight) async {
    return _get('/blocks/$hashOrHeight/header', BlockHeader.fromJson);
  }

  /// Get blocks in a range.
  Future<List<Block>> getBlocks(int startHeight, int endHeight) async {
    return _getList(
      '/blocks?start=$startHeight&end=$endHeight',
      Block.fromJson,
    );
  }

  // Transaction operations

  /// Get a transaction by ID.
  Future<RpcTransaction> getTransaction(String txid) async {
    return _get('/transactions/$txid', RpcTransaction.fromJson);
  }

  /// Get raw transaction hex.
  Future<String> getRawTransaction(String txid) async {
    final response = await _get<Map<String, dynamic>>(
      '/transactions/$txid/raw',
      (json) => json,
    );
    return response['hex'] as String;
  }

  /// Send a raw transaction.
  Future<SubmitResult> sendRawTransaction(String hex) async {
    return _post('/transactions/send', {'hex': hex}, SubmitResult.fromJson);
  }

  /// Decode a raw transaction without broadcasting.
  Future<RpcTransaction> decodeRawTransaction(String hex) async {
    return _post('/transactions/decode', {'hex': hex}, RpcTransaction.fromJson);
  }

  /// Get transactions for an address.
  Future<List<RpcTransaction>> getAddressTransactions(
    String address, {
    int limit = 50,
    int offset = 0,
  }) async {
    return _getList(
      '/addresses/$address/transactions?limit=$limit&offset=$offset',
      RpcTransaction.fromJson,
    );
  }

  // Fee estimation

  /// Estimate fee for a given priority.
  Future<FeeEstimate> estimateFee({RpcPriority priority = RpcPriority.medium}) async {
    return _get(
      '/fees/estimate?priority=${priority.toJson()}',
      FeeEstimate.fromJson,
    );
  }

  /// Get all fee estimates.
  Future<Map<RpcPriority, FeeEstimate>> getAllFeeEstimates() async {
    final estimates = await _getList('/fees/estimates', FeeEstimate.fromJson);
    return {for (var e in estimates) e.priority: e};
  }

  // Chain information

  /// Get chain information.
  Future<ChainInfo> getChainInfo() async {
    return _get('/chain/info', ChainInfo.fromJson);
  }

  /// Get mempool information.
  Future<MempoolInfo> getMempoolInfo() async {
    return _get('/mempool/info', MempoolInfo.fromJson);
  }

  /// Get mempool transactions.
  Future<List<String>> getMempoolTransactions({int limit = 100}) async {
    return _getList('/mempool/transactions?limit=$limit', (json) => json['txid'] as String);
  }

  // WebSocket subscriptions

  /// Subscribe to new blocks.
  Subscription subscribeBlocks(void Function(Block) callback) {
    return _subscribe('blocks', null, (data) {
      final block = Block.fromJson(jsonDecode(data) as Map<String, dynamic>);
      callback(block);
    });
  }

  /// Subscribe to transactions for a specific address.
  Subscription subscribeAddress(String address, void Function(RpcTransaction) callback) {
    return _subscribe('address', address, (data) {
      final tx = RpcTransaction.fromJson(jsonDecode(data) as Map<String, dynamic>);
      callback(tx);
    });
  }

  /// Subscribe to mempool transactions.
  Subscription subscribeMempool(void Function(RpcTransaction) callback) {
    return _subscribe('mempool', null, (data) {
      final tx = RpcTransaction.fromJson(jsonDecode(data) as Map<String, dynamic>);
      callback(tx);
    });
  }

  Subscription _subscribe(
    String channel,
    String? filter,
    void Function(String) callback,
  ) {
    _ensureWebSocketConnection();

    final subscriptionId = DateTime.now().millisecondsSinceEpoch.toString();
    _subscriptions[subscriptionId] = callback;

    final message = jsonEncode({
      'type': 'subscribe',
      'channel': channel,
      'filter': filter,
    });
    _wsChannel?.sink.add(message);

    return Subscription(
      id: subscriptionId,
      channel: channel,
      onCancel: () {
        _subscriptions.remove(subscriptionId);
        final unsubMessage = jsonEncode({
          'type': 'unsubscribe',
          'channel': channel,
        });
        _wsChannel?.sink.add(unsubMessage);
      },
    );
  }

  void _ensureWebSocketConnection() {
    if (_wsChannel != null) return;

    final uri = Uri.parse('${config.wsEndpoint}?token=${config.apiKey}');
    _wsChannel = WebSocketChannel.connect(uri);

    _wsSubscription = _wsChannel!.stream.listen(
      (message) {
        try {
          final data = jsonDecode(message as String) as Map<String, dynamic>;
          if (data['type'] == 'data' && data['data'] != null) {
            final messageData = data['data'] as String;
            for (final callback in _subscriptions.values) {
              callback(messageData);
            }
          }
        } catch (e) {
          if (config.debug) {
            print('Failed to parse WS message: $e');
          }
        }
      },
      onError: (error) {
        if (config.debug) {
          print('WebSocket error: $error');
        }
      },
      onDone: () {
        _wsChannel = null;
        _wsSubscription = null;
      },
    );
  }

  // HTTP helper methods

  Future<T> _get<T>(String path, T Function(Map<String, dynamic>) fromJson) async {
    return _executeWithRetry(() async {
      final response = await _client
          .get(
            Uri.parse('${config.endpoint}$path'),
            headers: _headers,
          )
          .timeout(config.timeout);

      return _handleResponse(response, fromJson);
    });
  }

  Future<List<T>> _getList<T>(
    String path,
    T Function(Map<String, dynamic>) fromJson,
  ) async {
    return _executeWithRetry(() async {
      final response = await _client
          .get(
            Uri.parse('${config.endpoint}$path'),
            headers: _headers,
          )
          .timeout(config.timeout);

      if (response.statusCode >= 200 && response.statusCode < 300) {
        final List<dynamic> jsonList = jsonDecode(response.body);
        return jsonList
            .map((e) => fromJson(e as Map<String, dynamic>))
            .toList();
      }

      throw RpcException(
        'HTTP error: ${response.body}',
        statusCode: response.statusCode,
      );
    });
  }

  Future<T> _post<T>(
    String path,
    Map<String, dynamic> body,
    T Function(Map<String, dynamic>) fromJson,
  ) async {
    return _executeWithRetry(() async {
      final response = await _client
          .post(
            Uri.parse('${config.endpoint}$path'),
            headers: _headers,
            body: jsonEncode(body),
          )
          .timeout(config.timeout);

      return _handleResponse(response, fromJson);
    });
  }

  Map<String, String> get _headers => {
        'Authorization': 'Bearer ${config.apiKey}',
        'Content-Type': 'application/json',
      };

  T _handleResponse<T>(
    http.Response response,
    T Function(Map<String, dynamic>) fromJson,
  ) {
    if (response.statusCode >= 200 && response.statusCode < 300) {
      final json = jsonDecode(response.body) as Map<String, dynamic>;
      return fromJson(json);
    }

    throw RpcException(
      'HTTP error: ${response.body}',
      statusCode: response.statusCode,
    );
  }

  Future<T> _executeWithRetry<T>(Future<T> Function() operation) async {
    Exception? lastError;

    for (var attempt = 0; attempt < config.retries; attempt++) {
      try {
        return await operation();
      } catch (e) {
        lastError = e as Exception;
        if (config.debug) {
          print('Attempt ${attempt + 1} failed: $e');
        }
        if (attempt < config.retries - 1) {
          await Future.delayed(Duration(seconds: attempt + 1));
        }
      }
    }

    throw lastError ?? RpcException('Unknown error after ${config.retries} retries');
  }

  /// Close the client and release resources.
  void close() {
    _wsSubscription?.cancel();
    _wsChannel?.sink.close();
    _client.close();
  }
}
