/**
 * Synor RPC Client
 */

import type {
  RpcConfig,
  Block,
  BlockHeader,
  Transaction,
  FeeEstimate,
  ChainInfo,
  MempoolInfo,
  UTXO,
  Subscription,
  SubscriptionType,
  Priority,
  Notification,
} from './types';

const DEFAULT_ENDPOINT = 'https://rpc.synor.cc/api/v1';
const DEFAULT_WS_ENDPOINT = 'wss://rpc.synor.cc/ws';

/**
 * Synor RPC SDK error.
 */
export class RpcError extends Error {
  constructor(
    message: string,
    public statusCode?: number,
    public code?: string
  ) {
    super(message);
    this.name = 'RpcError';
  }
}

/**
 * Main Synor RPC client.
 *
 * @example
 * ```ts
 * const rpc = new SynorRpc({ apiKey: 'sk_...' });
 *
 * // Get latest block
 * const block = await rpc.getLatestBlock();
 * console.log('Height:', block.height);
 *
 * // Subscribe to new blocks
 * const sub = await rpc.subscribeBlocks((block) => {
 *   console.log('New block:', block.height);
 * });
 * ```
 */
export class SynorRpc {
  private config: Required<RpcConfig>;
  private ws: WebSocket | null = null;
  private subscriptions: Map<string, (data: Notification) => void> = new Map();
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 5;

  constructor(config: RpcConfig) {
    this.config = {
      apiKey: config.apiKey,
      endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
      wsEndpoint: config.wsEndpoint ?? DEFAULT_WS_ENDPOINT,
      network: config.network ?? 'mainnet',
      timeout: config.timeout ?? 30000,
      debug: config.debug ?? false,
    };
  }

  // ==================== Block Operations ====================

  /**
   * Get block by hash or height.
   */
  async getBlock(hashOrHeight: string | number): Promise<Block> {
    const path = typeof hashOrHeight === 'number'
      ? `/blocks/height/${hashOrHeight}`
      : `/blocks/${hashOrHeight}`;
    return this.request('GET', path);
  }

  /**
   * Get latest block.
   */
  async getLatestBlock(): Promise<Block> {
    return this.request('GET', '/blocks/latest');
  }

  /**
   * Get block header only.
   */
  async getBlockHeader(hashOrHeight: string | number): Promise<BlockHeader> {
    const path = typeof hashOrHeight === 'number'
      ? `/blocks/height/${hashOrHeight}/header`
      : `/blocks/${hashOrHeight}/header`;
    return this.request('GET', path);
  }

  /**
   * Get blocks in a range.
   */
  async getBlocks(startHeight: number, endHeight: number): Promise<Block[]> {
    const response = await this.request('GET', '/blocks', undefined, {
      start: startHeight.toString(),
      end: endHeight.toString(),
    });
    return response.blocks;
  }

  // ==================== Transaction Operations ====================

  /**
   * Get transaction by hash.
   */
  async getTransaction(txid: string): Promise<Transaction> {
    return this.request('GET', `/transactions/${txid}`);
  }

  /**
   * Get raw transaction hex.
   */
  async getRawTransaction(txid: string): Promise<string> {
    const response = await this.request('GET', `/transactions/${txid}/raw`);
    return response.raw;
  }

  /**
   * Send a raw transaction.
   */
  async sendRawTransaction(rawTx: string): Promise<string> {
    const response = await this.request('POST', '/transactions', { raw: rawTx });
    return response.txid;
  }

  /**
   * Get transactions for an address.
   */
  async getAddressTransactions(
    address: string,
    options?: { limit?: number; offset?: number }
  ): Promise<Transaction[]> {
    const params: Record<string, string> = {};
    if (options?.limit) params.limit = options.limit.toString();
    if (options?.offset) params.offset = options.offset.toString();

    const response = await this.request(
      'GET',
      `/addresses/${address}/transactions`,
      undefined,
      params
    );
    return response.transactions;
  }

  // ==================== Fee Estimation ====================

  /**
   * Estimate fee for a priority level.
   */
  async estimateFee(priority: Priority = 'medium'): Promise<FeeEstimate> {
    return this.request('GET', '/fees/estimate', undefined, { priority });
  }

  /**
   * Get all fee estimates.
   */
  async getAllFeeEstimates(): Promise<FeeEstimate[]> {
    const response = await this.request('GET', '/fees/estimate/all');
    return response.estimates;
  }

  // ==================== Chain Information ====================

  /**
   * Get chain information.
   */
  async getChainInfo(): Promise<ChainInfo> {
    return this.request('GET', '/chain');
  }

  /**
   * Get mempool information.
   */
  async getMempoolInfo(): Promise<MempoolInfo> {
    return this.request('GET', '/mempool');
  }

  /**
   * Get mempool transactions.
   */
  async getMempoolTransactions(limit: number = 100): Promise<string[]> {
    const response = await this.request('GET', '/mempool/transactions', undefined, {
      limit: limit.toString(),
    });
    return response.txids;
  }

  // ==================== UTXO Operations ====================

  /**
   * Get UTXOs for an address.
   */
  async getUtxos(address: string): Promise<UTXO[]> {
    const response = await this.request('GET', `/addresses/${address}/utxos`);
    return response.utxos;
  }

  /**
   * Get balance for an address.
   */
  async getBalance(address: string): Promise<{ confirmed: string; unconfirmed: string; total: string }> {
    return this.request('GET', `/addresses/${address}/balance`);
  }

  // ==================== Subscriptions ====================

  /**
   * Subscribe to new blocks.
   */
  async subscribeBlocks(callback: (block: Block) => void): Promise<Subscription> {
    return this.subscribe('blocks', (notification) => {
      if (notification.type === 'block') {
        callback(notification.block);
      }
    });
  }

  /**
   * Subscribe to address transactions.
   */
  async subscribeAddress(
    address: string,
    callback: (tx: Transaction) => void
  ): Promise<Subscription> {
    return this.subscribe('address', (notification) => {
      if (notification.type === 'transaction') {
        callback(notification.transaction);
      }
    }, { address });
  }

  /**
   * Subscribe to mempool transactions.
   */
  async subscribeMempool(callback: (tx: Transaction) => void): Promise<Subscription> {
    return this.subscribe('mempool', (notification) => {
      if (notification.type === 'transaction') {
        callback(notification.transaction);
      }
    });
  }

  private async subscribe(
    type: SubscriptionType,
    callback: (notification: Notification) => void,
    params?: Record<string, string>
  ): Promise<Subscription> {
    await this.ensureWebSocket();

    const id = `${type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;

    this.subscriptions.set(id, callback);

    this.ws!.send(JSON.stringify({
      type: 'subscribe',
      id,
      subscription: type,
      ...params,
    }));

    return {
      id,
      type,
      createdAt: Date.now(),
      unsubscribe: () => this.unsubscribe(id),
    };
  }

  private unsubscribe(id: string): void {
    this.subscriptions.delete(id);

    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({
        type: 'unsubscribe',
        id,
      }));
    }
  }

  private async ensureWebSocket(): Promise<void> {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      return;
    }

    return new Promise((resolve, reject) => {
      const wsUrl = `${this.config.wsEndpoint}?apiKey=${this.config.apiKey}&network=${this.config.network}`;

      this.ws = new WebSocket(wsUrl);

      this.ws.onopen = () => {
        this.reconnectAttempts = 0;
        if (this.config.debug) {
          console.log('[SynorRpc] WebSocket connected');
        }
        resolve();
      };

      this.ws.onmessage = (event) => {
        try {
          const message = JSON.parse(event.data);
          if (message.subscriptionId && this.subscriptions.has(message.subscriptionId)) {
            this.subscriptions.get(message.subscriptionId)!(message.data);
          }
        } catch {
          console.error('[SynorRpc] Failed to parse WebSocket message');
        }
      };

      this.ws.onerror = (error) => {
        console.error('[SynorRpc] WebSocket error:', error);
        reject(new RpcError('WebSocket connection failed'));
      };

      this.ws.onclose = () => {
        if (this.config.debug) {
          console.log('[SynorRpc] WebSocket closed');
        }
        this.attemptReconnect();
      };
    });
  }

  private attemptReconnect(): void {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('[SynorRpc] Max reconnect attempts reached');
      return;
    }

    this.reconnectAttempts++;
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);

    setTimeout(() => {
      if (this.subscriptions.size > 0) {
        this.ensureWebSocket().catch(console.error);
      }
    }, delay);
  }

  /**
   * Close the client and all connections.
   */
  close(): void {
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
    this.subscriptions.clear();
  }

  // ==================== HTTP Request ====================

  private async request(
    method: string,
    path: string,
    body?: unknown,
    params?: Record<string, string>
  ): Promise<any> {
    let url = `${this.config.endpoint}${path}`;

    if (params && Object.keys(params).length > 0) {
      const searchParams = new URLSearchParams(params);
      url += `?${searchParams.toString()}`;
    }

    if (this.config.debug) {
      console.log(`[SynorRpc] ${method} ${url}`);
    }

    const response = await fetch(url, {
      method,
      headers: {
        'Authorization': `Bearer ${this.config.apiKey}`,
        'Content-Type': 'application/json',
        'X-Network': this.config.network,
      },
      body: body ? JSON.stringify(body) : undefined,
      signal: AbortSignal.timeout(this.config.timeout),
    });

    if (!response.ok) {
      const error = await response.json().catch(() => ({ message: response.statusText }));
      throw new RpcError(
        error.message || 'Request failed',
        response.status,
        error.code
      );
    }

    return response.json();
  }
}
