/**
 * Synor Bridge SDK Client
 *
 * Cross-chain asset transfers with lock-mint and burn-unlock patterns.
 */

import {
  BridgeConfig,
  BridgeError,
  BridgeErrorCode,
  Chain,
  ChainId,
  Asset,
  WrappedAsset,
  Transfer,
  TransferFilter,
  TransferStatus,
  LockReceipt,
  LockProof,
  LockOptions,
  BurnReceipt,
  BurnProof,
  BurnOptions,
  MintOptions,
  UnlockOptions,
  FeeEstimate,
  ExchangeRate,
  SignedTransaction,
  DEFAULT_BRIDGE_ENDPOINT,
} from './types';

/** Synor Bridge Client */
export class SynorBridge {
  private readonly config: Required<BridgeConfig>;
  private closed = false;

  constructor(config: BridgeConfig) {
    this.config = {
      apiKey: config.apiKey,
      endpoint: config.endpoint || DEFAULT_BRIDGE_ENDPOINT,
      timeout: config.timeout || 60000,
      retries: config.retries || 3,
      debug: config.debug || false,
    };
  }

  // ==================== Chain Operations ====================

  /** Get all supported chains */
  async getSupportedChains(): Promise<Chain[]> {
    const response = await this.request<{ chains: Chain[] }>('GET', '/chains');
    return response.chains;
  }

  /** Get chain by ID */
  async getChain(chainId: ChainId): Promise<Chain> {
    return this.request<Chain>('GET', `/chains/${chainId}`);
  }

  /** Check if chain is supported */
  async isChainSupported(chainId: ChainId): Promise<boolean> {
    try {
      const chain = await this.getChain(chainId);
      return chain.supported;
    } catch {
      return false;
    }
  }

  // ==================== Asset Operations ====================

  /** Get supported assets for a chain */
  async getSupportedAssets(chainId: ChainId): Promise<Asset[]> {
    const response = await this.request<{ assets: Asset[] }>(
      'GET',
      `/chains/${chainId}/assets`
    );
    return response.assets;
  }

  /** Get asset by ID */
  async getAsset(assetId: string): Promise<Asset> {
    return this.request<Asset>('GET', `/assets/${assetId}`);
  }

  /** Get wrapped asset mapping */
  async getWrappedAsset(
    originalAssetId: string,
    targetChain: ChainId
  ): Promise<WrappedAsset> {
    return this.request<WrappedAsset>(
      'GET',
      `/assets/${originalAssetId}/wrapped/${targetChain}`
    );
  }

  /** Get all wrapped assets for a chain */
  async getWrappedAssets(chainId: ChainId): Promise<WrappedAsset[]> {
    const response = await this.request<{ assets: WrappedAsset[] }>(
      'GET',
      `/chains/${chainId}/wrapped`
    );
    return response.assets;
  }

  // ==================== Fee & Rate Operations ====================

  /** Estimate bridge fee */
  async estimateFee(
    asset: string,
    amount: string,
    sourceChain: ChainId,
    targetChain: ChainId
  ): Promise<FeeEstimate> {
    return this.request<FeeEstimate>('POST', '/fees/estimate', {
      asset,
      amount,
      sourceChain,
      targetChain,
    });
  }

  /** Get exchange rate between assets */
  async getExchangeRate(fromAsset: string, toAsset: string): Promise<ExchangeRate> {
    return this.request<ExchangeRate>(
      'GET',
      `/rates/${encodeURIComponent(fromAsset)}/${encodeURIComponent(toAsset)}`
    );
  }

  // ==================== Lock-Mint Flow ====================

  /**
   * Lock assets on source chain for cross-chain transfer
   * Step 1 of lock-mint flow
   */
  async lock(
    asset: string,
    amount: string,
    targetChain: ChainId,
    options?: LockOptions
  ): Promise<LockReceipt> {
    return this.request<LockReceipt>('POST', '/transfers/lock', {
      asset,
      amount,
      targetChain,
      recipient: options?.recipient,
      deadline: options?.deadline,
      slippage: options?.slippage,
    });
  }

  /**
   * Get lock proof for minting
   * Step 2 of lock-mint flow (wait for confirmations)
   */
  async getLockProof(lockReceiptId: string): Promise<LockProof> {
    return this.request<LockProof>('GET', `/transfers/lock/${lockReceiptId}/proof`);
  }

  /**
   * Wait for lock proof to be ready
   * Polls until confirmations are sufficient
   */
  async waitForLockProof(
    lockReceiptId: string,
    pollInterval = 5000,
    maxWait = 600000
  ): Promise<LockProof> {
    const startTime = Date.now();

    while (Date.now() - startTime < maxWait) {
      try {
        const proof = await this.getLockProof(lockReceiptId);
        return proof;
      } catch (error) {
        if (
          error instanceof BridgeError &&
          error.code === 'CONFIRMATIONS_PENDING'
        ) {
          await this.sleep(pollInterval);
          continue;
        }
        throw error;
      }
    }

    throw new BridgeError(
      'Timeout waiting for lock proof',
      'CONFIRMATIONS_PENDING'
    );
  }

  /**
   * Mint wrapped tokens on target chain
   * Step 3 of lock-mint flow
   */
  async mint(
    proof: LockProof,
    targetAddress: string,
    options?: MintOptions
  ): Promise<SignedTransaction> {
    return this.request<SignedTransaction>('POST', '/transfers/mint', {
      proof,
      targetAddress,
      gasLimit: options?.gasLimit,
      maxFeePerGas: options?.maxFeePerGas,
      maxPriorityFeePerGas: options?.maxPriorityFeePerGas,
    });
  }

  // ==================== Burn-Unlock Flow ====================

  /**
   * Burn wrapped tokens on current chain
   * Step 1 of burn-unlock flow
   */
  async burn(
    wrappedAsset: string,
    amount: string,
    options?: BurnOptions
  ): Promise<BurnReceipt> {
    return this.request<BurnReceipt>('POST', '/transfers/burn', {
      wrappedAsset,
      amount,
      recipient: options?.recipient,
      deadline: options?.deadline,
    });
  }

  /**
   * Get burn proof for unlocking
   * Step 2 of burn-unlock flow (wait for confirmations)
   */
  async getBurnProof(burnReceiptId: string): Promise<BurnProof> {
    return this.request<BurnProof>('GET', `/transfers/burn/${burnReceiptId}/proof`);
  }

  /**
   * Wait for burn proof to be ready
   * Polls until confirmations are sufficient
   */
  async waitForBurnProof(
    burnReceiptId: string,
    pollInterval = 5000,
    maxWait = 600000
  ): Promise<BurnProof> {
    const startTime = Date.now();

    while (Date.now() - startTime < maxWait) {
      try {
        const proof = await this.getBurnProof(burnReceiptId);
        return proof;
      } catch (error) {
        if (
          error instanceof BridgeError &&
          error.code === 'CONFIRMATIONS_PENDING'
        ) {
          await this.sleep(pollInterval);
          continue;
        }
        throw error;
      }
    }

    throw new BridgeError(
      'Timeout waiting for burn proof',
      'CONFIRMATIONS_PENDING'
    );
  }

  /**
   * Unlock original tokens on source chain
   * Step 3 of burn-unlock flow
   */
  async unlock(proof: BurnProof, options?: UnlockOptions): Promise<SignedTransaction> {
    return this.request<SignedTransaction>('POST', '/transfers/unlock', {
      proof,
      gasLimit: options?.gasLimit,
      gasPrice: options?.gasPrice,
    });
  }

  // ==================== Transfer Management ====================

  /** Get transfer by ID */
  async getTransfer(transferId: string): Promise<Transfer> {
    return this.request<Transfer>('GET', `/transfers/${transferId}`);
  }

  /** Get transfer status */
  async getTransferStatus(transferId: string): Promise<TransferStatus> {
    const transfer = await this.getTransfer(transferId);
    return transfer.status;
  }

  /** List transfers with optional filters */
  async listTransfers(filter?: TransferFilter): Promise<Transfer[]> {
    const params = new URLSearchParams();
    if (filter) {
      if (filter.status) params.set('status', filter.status);
      if (filter.sourceChain) params.set('sourceChain', filter.sourceChain);
      if (filter.targetChain) params.set('targetChain', filter.targetChain);
      if (filter.asset) params.set('asset', filter.asset);
      if (filter.sender) params.set('sender', filter.sender);
      if (filter.recipient) params.set('recipient', filter.recipient);
      if (filter.fromDate) params.set('fromDate', filter.fromDate.toString());
      if (filter.toDate) params.set('toDate', filter.toDate.toString());
      if (filter.limit) params.set('limit', filter.limit.toString());
      if (filter.offset) params.set('offset', filter.offset.toString());
    }

    const query = params.toString();
    const path = query ? `/transfers?${query}` : '/transfers';

    const response = await this.request<{ transfers: Transfer[] }>('GET', path);
    return response.transfers;
  }

  /** Wait for transfer to complete */
  async waitForTransfer(
    transferId: string,
    pollInterval = 10000,
    maxWait = 1800000
  ): Promise<Transfer> {
    const startTime = Date.now();
    const finalStatuses: TransferStatus[] = ['completed', 'failed', 'refunded'];

    while (Date.now() - startTime < maxWait) {
      const transfer = await this.getTransfer(transferId);

      if (finalStatuses.includes(transfer.status)) {
        return transfer;
      }

      await this.sleep(pollInterval);
    }

    throw new BridgeError('Timeout waiting for transfer completion');
  }

  // ==================== Convenience Methods ====================

  /**
   * Execute complete lock-mint transfer
   * Combines lock, wait for proof, and mint into single operation
   */
  async bridgeTo(
    asset: string,
    amount: string,
    targetChain: ChainId,
    targetAddress: string,
    options?: LockOptions & MintOptions
  ): Promise<Transfer> {
    // Lock on source chain
    const lockReceipt = await this.lock(asset, amount, targetChain, options);

    if (this.config.debug) {
      console.log(`Locked: ${lockReceipt.id}, waiting for confirmations...`);
    }

    // Wait for proof
    const proof = await this.waitForLockProof(lockReceipt.id);

    if (this.config.debug) {
      console.log(`Proof ready, minting on ${targetChain}...`);
    }

    // Mint on target chain
    await this.mint(proof, targetAddress, options);

    // Return final transfer status
    return this.waitForTransfer(lockReceipt.id);
  }

  /**
   * Execute complete burn-unlock transfer
   * Combines burn, wait for proof, and unlock into single operation
   */
  async bridgeBack(
    wrappedAsset: string,
    amount: string,
    options?: BurnOptions & UnlockOptions
  ): Promise<Transfer> {
    // Burn wrapped tokens
    const burnReceipt = await this.burn(wrappedAsset, amount, options);

    if (this.config.debug) {
      console.log(`Burned: ${burnReceipt.id}, waiting for confirmations...`);
    }

    // Wait for proof
    const proof = await this.waitForBurnProof(burnReceipt.id);

    if (this.config.debug) {
      console.log(`Proof ready, unlocking on ${burnReceipt.targetChain}...`);
    }

    // Unlock on original chain
    await this.unlock(proof, options);

    // Return final transfer status
    return this.waitForTransfer(burnReceipt.id);
  }

  // ==================== Lifecycle ====================

  /** Close the client */
  close(): void {
    this.closed = true;
  }

  /** Check if client is closed */
  isClosed(): boolean {
    return this.closed;
  }

  /** Health check */
  async healthCheck(): Promise<boolean> {
    try {
      const response = await this.request<{ status: string }>('GET', '/health');
      return response.status === 'healthy';
    } catch {
      return false;
    }
  }

  // ==================== Private Methods ====================

  private async request<T>(
    method: string,
    path: string,
    body?: Record<string, unknown>
  ): Promise<T> {
    if (this.closed) {
      throw new BridgeError('Client has been closed');
    }

    let lastError: Error | undefined;

    for (let attempt = 0; attempt < this.config.retries; attempt++) {
      try {
        return await this.doRequest<T>(method, path, body);
      } catch (error) {
        if (this.config.debug) {
          console.error(`Attempt ${attempt + 1} failed:`, error);
        }
        lastError = error as Error;

        if (attempt < this.config.retries - 1) {
          await this.sleep(Math.pow(2, attempt) * 1000);
        }
      }
    }

    throw lastError || new BridgeError('Unknown error after retries');
  }

  private async doRequest<T>(
    method: string,
    path: string,
    body?: Record<string, unknown>
  ): Promise<T> {
    const url = `${this.config.endpoint}${path}`;

    const headers: Record<string, string> = {
      Authorization: `Bearer ${this.config.apiKey}`,
      'Content-Type': 'application/json',
      'X-SDK-Version': 'js/0.1.0',
    };

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);

    try {
      const response = await fetch(url, {
        method,
        headers,
        body: body ? JSON.stringify(body) : undefined,
        signal: controller.signal,
      });

      if (!response.ok) {
        const errorBody = await response.json().catch(() => ({}));
        const message =
          errorBody.message || errorBody.error || `HTTP ${response.status}`;
        const code = errorBody.code as BridgeErrorCode | undefined;

        throw new BridgeError(message, code, response.status, errorBody);
      }

      return response.json();
    } finally {
      clearTimeout(timeoutId);
    }
  }

  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}
