/**
 * Synor IBC SDK Client
 *
 * Inter-Blockchain Communication (IBC) client for cross-chain interoperability.
 * Supports light client verification, connections, channels, packets, transfers, and atomic swaps.
 */

import type {
  IbcConfig,
  Height,
  ClientId,
  ClientState,
  ConsensusState,
  Header,
  ConnectionId,
  ConnectionEnd,
  PortId,
  ChannelId,
  Channel,
  ChannelOrder,
  Packet,
  Acknowledgement,
  Timeout,
  SwapId,
  SwapAsset,
  SwapState,
  AtomicSwap,
  Htlc,
  Hashlock,
  IbcEvent,
  CreateClientParams,
  UpdateClientParams,
  ConnOpenInitParams,
  ConnOpenTryParams,
  ChanOpenInitParams,
  ChanOpenTryParams,
  SendPacketParams,
  TransferParams,
  InitiateSwapParams,
  RespondSwapParams,
  ClaimSwapParams,
  FungibleTokenPacketData,
  CommitmentProof,
  Version,
} from './types';
import { IbcException } from './types';

const DEFAULT_ENDPOINT = 'https://ibc.synor.io/v1';
const DEFAULT_WS_ENDPOINT = 'wss://ibc.synor.io/v1/ws';
const DEFAULT_TIMEOUT = 30000;
const DEFAULT_RETRIES = 3;
const DEFAULT_CHAIN_ID = 'synor-1';

/** Subscription handle */
export interface Subscription {
  unsubscribe(): void;
}

/**
 * IBC Light Client sub-client
 */
export class LightClientClient {
  constructor(private ibc: SynorIbc) {}

  /** Create a new light client */
  async create(params: CreateClientParams): Promise<ClientId> {
    return this.ibc.post('/clients', params);
  }

  /** Update a light client with new header */
  async update(params: UpdateClientParams): Promise<Height> {
    return this.ibc.post(`/clients/${params.clientId.id}/update`, {
      header: params.header,
    });
  }

  /** Get client state */
  async getState(clientId: ClientId): Promise<ClientState> {
    return this.ibc.get(`/clients/${clientId.id}/state`);
  }

  /** Get consensus state at height */
  async getConsensusState(clientId: ClientId, height: Height): Promise<ConsensusState> {
    return this.ibc.get(
      `/clients/${clientId.id}/consensus/${height.revisionNumber}-${height.revisionHeight}`
    );
  }

  /** List all clients */
  async list(): Promise<Array<{ clientId: ClientId; clientState: ClientState }>> {
    return this.ibc.get('/clients');
  }

  /** Check if client is active */
  async isActive(clientId: ClientId): Promise<boolean> {
    const response = await this.ibc.get<{ active: boolean }>(`/clients/${clientId.id}/status`);
    return response.active;
  }
}

/**
 * IBC Connections sub-client
 */
export class ConnectionsClient {
  constructor(private ibc: SynorIbc) {}

  /** Initialize connection handshake */
  async openInit(params: ConnOpenInitParams): Promise<ConnectionId> {
    return this.ibc.post('/connections/init', params);
  }

  /** Try connection handshake (counterparty) */
  async openTry(params: ConnOpenTryParams): Promise<ConnectionId> {
    return this.ibc.post('/connections/try', params);
  }

  /** Acknowledge connection handshake */
  async openAck(
    connectionId: ConnectionId,
    counterpartyConnectionId: ConnectionId,
    version: Version,
    proofTry: Uint8Array,
    proofHeight: Height
  ): Promise<void> {
    return this.ibc.post(`/connections/${connectionId.id}/ack`, {
      counterpartyConnectionId,
      version,
      proofTry: Buffer.from(proofTry).toString('base64'),
      proofHeight,
    });
  }

  /** Confirm connection handshake */
  async openConfirm(
    connectionId: ConnectionId,
    proofAck: Uint8Array,
    proofHeight: Height
  ): Promise<void> {
    return this.ibc.post(`/connections/${connectionId.id}/confirm`, {
      proofAck: Buffer.from(proofAck).toString('base64'),
      proofHeight,
    });
  }

  /** Get connection by ID */
  async get(connectionId: ConnectionId): Promise<ConnectionEnd> {
    return this.ibc.get(`/connections/${connectionId.id}`);
  }

  /** List all connections */
  async list(): Promise<Array<{ connectionId: ConnectionId; connection: ConnectionEnd }>> {
    return this.ibc.get('/connections');
  }

  /** Get connections for a client */
  async getByClient(clientId: ClientId): Promise<ConnectionId[]> {
    return this.ibc.get(`/clients/${clientId.id}/connections`);
  }
}

/**
 * IBC Channels sub-client
 */
export class ChannelsClient {
  constructor(private ibc: SynorIbc) {}

  /** Bind a port */
  async bindPort(portId: PortId, module: string): Promise<void> {
    return this.ibc.post('/ports/bind', { portId, module });
  }

  /** Release port binding */
  async releasePort(portId: PortId): Promise<void> {
    return this.ibc.post(`/ports/${portId.id}/release`, {});
  }

  /** Initialize channel handshake */
  async openInit(params: ChanOpenInitParams): Promise<ChannelId> {
    return this.ibc.post('/channels/init', params);
  }

  /** Try channel handshake (counterparty) */
  async openTry(params: ChanOpenTryParams): Promise<ChannelId> {
    return this.ibc.post('/channels/try', params);
  }

  /** Acknowledge channel handshake */
  async openAck(
    portId: PortId,
    channelId: ChannelId,
    counterpartyChannelId: ChannelId,
    counterpartyVersion: string,
    proofTry: Uint8Array,
    proofHeight: Height
  ): Promise<void> {
    return this.ibc.post(`/channels/${portId.id}/${channelId.id}/ack`, {
      counterpartyChannelId,
      counterpartyVersion,
      proofTry: Buffer.from(proofTry).toString('base64'),
      proofHeight,
    });
  }

  /** Confirm channel handshake */
  async openConfirm(
    portId: PortId,
    channelId: ChannelId,
    proofAck: Uint8Array,
    proofHeight: Height
  ): Promise<void> {
    return this.ibc.post(`/channels/${portId.id}/${channelId.id}/confirm`, {
      proofAck: Buffer.from(proofAck).toString('base64'),
      proofHeight,
    });
  }

  /** Close channel init */
  async closeInit(portId: PortId, channelId: ChannelId): Promise<void> {
    return this.ibc.post(`/channels/${portId.id}/${channelId.id}/close`, {});
  }

  /** Close channel confirm */
  async closeConfirm(
    portId: PortId,
    channelId: ChannelId,
    proofInit: Uint8Array,
    proofHeight: Height
  ): Promise<void> {
    return this.ibc.post(`/channels/${portId.id}/${channelId.id}/close/confirm`, {
      proofInit: Buffer.from(proofInit).toString('base64'),
      proofHeight,
    });
  }

  /** Get channel */
  async get(portId: PortId, channelId: ChannelId): Promise<Channel> {
    return this.ibc.get(`/channels/${portId.id}/${channelId.id}`);
  }

  /** List channels for a port */
  async listByPort(portId: PortId): Promise<Array<{ channelId: ChannelId; channel: Channel }>> {
    return this.ibc.get(`/ports/${portId.id}/channels`);
  }

  /** List all channels */
  async list(): Promise<Array<{ portId: PortId; channelId: ChannelId; channel: Channel }>> {
    return this.ibc.get('/channels');
  }
}

/**
 * IBC Packets sub-client
 */
export class PacketsClient {
  constructor(private ibc: SynorIbc) {}

  /** Send a packet */
  async send(params: SendPacketParams): Promise<{ sequence: bigint; packet: Packet }> {
    return this.ibc.post('/packets/send', {
      ...params,
      data: Buffer.from(params.data).toString('base64'),
    });
  }

  /** Receive a packet (called by relayer) */
  async recv(
    packet: Packet,
    proof: Uint8Array,
    proofHeight: Height
  ): Promise<Acknowledgement> {
    return this.ibc.post('/packets/recv', {
      packet: {
        ...packet,
        data: Buffer.from(packet.data).toString('base64'),
      },
      proof: Buffer.from(proof).toString('base64'),
      proofHeight,
    });
  }

  /** Acknowledge a packet */
  async ack(
    packet: Packet,
    acknowledgement: Acknowledgement,
    proof: Uint8Array,
    proofHeight: Height
  ): Promise<void> {
    return this.ibc.post('/packets/ack', {
      packet: {
        ...packet,
        data: Buffer.from(packet.data).toString('base64'),
      },
      acknowledgement,
      proof: Buffer.from(proof).toString('base64'),
      proofHeight,
    });
  }

  /** Timeout a packet */
  async timeout(
    packet: Packet,
    proof: Uint8Array,
    proofHeight: Height,
    nextSequenceRecv: bigint
  ): Promise<void> {
    return this.ibc.post('/packets/timeout', {
      packet: {
        ...packet,
        data: Buffer.from(packet.data).toString('base64'),
      },
      proof: Buffer.from(proof).toString('base64'),
      proofHeight,
      nextSequenceRecv: nextSequenceRecv.toString(),
    });
  }

  /** Get packet commitment */
  async getCommitment(
    portId: PortId,
    channelId: ChannelId,
    sequence: bigint
  ): Promise<Uint8Array | null> {
    const response = await this.ibc.get<{ commitment: string | null }>(
      `/packets/${portId.id}/${channelId.id}/${sequence}/commitment`
    );
    return response.commitment ? Buffer.from(response.commitment, 'base64') : null;
  }

  /** Get packet acknowledgement */
  async getAcknowledgement(
    portId: PortId,
    channelId: ChannelId,
    sequence: bigint
  ): Promise<Acknowledgement | null> {
    return this.ibc.get(`/packets/${portId.id}/${channelId.id}/${sequence}/ack`);
  }

  /** List unreceived packets */
  async listUnreceived(
    portId: PortId,
    channelId: ChannelId,
    sequences: bigint[]
  ): Promise<bigint[]> {
    return this.ibc.post(`/packets/${portId.id}/${channelId.id}/unreceived`, {
      sequences: sequences.map(s => s.toString()),
    });
  }

  /** List unacknowledged packets */
  async listUnacknowledged(
    portId: PortId,
    channelId: ChannelId,
    sequences: bigint[]
  ): Promise<bigint[]> {
    return this.ibc.post(`/packets/${portId.id}/${channelId.id}/unacked`, {
      sequences: sequences.map(s => s.toString()),
    });
  }
}

/**
 * IBC Transfer sub-client (ICS-20)
 */
export class TransferClient {
  constructor(private ibc: SynorIbc) {}

  /** Transfer tokens to another chain */
  async transfer(params: TransferParams): Promise<{ sequence: bigint; txHash: string }> {
    return this.ibc.post('/transfer', params);
  }

  /** Get denom trace for an IBC token */
  async getDenomTrace(ibcDenom: string): Promise<{ path: string; baseDenom: string }> {
    return this.ibc.get(`/transfer/denom_trace/${ibcDenom}`);
  }

  /** Get all denom traces */
  async listDenomTraces(): Promise<Array<{ path: string; baseDenom: string }>> {
    return this.ibc.get('/transfer/denom_traces');
  }

  /** Get escrow address for a channel */
  async getEscrowAddress(portId: PortId, channelId: ChannelId): Promise<string> {
    const response = await this.ibc.get<{ address: string }>(
      `/transfer/escrow/${portId.id}/${channelId.id}`
    );
    return response.address;
  }

  /** Get total escrow for a denom */
  async getTotalEscrow(denom: string): Promise<string> {
    const response = await this.ibc.get<{ amount: string }>(
      `/transfer/escrow/total/${encodeURIComponent(denom)}`
    );
    return response.amount;
  }
}

/**
 * IBC Atomic Swap sub-client (HTLC)
 */
export class SwapsClient {
  constructor(private ibc: SynorIbc) {}

  /** Initiate an atomic swap */
  async initiate(params: InitiateSwapParams): Promise<{ swapId: SwapId; hashlock: Hashlock }> {
    return this.ibc.post('/swaps/initiate', params);
  }

  /** Lock initiator's tokens */
  async lock(swapId: SwapId): Promise<void> {
    return this.ibc.post(`/swaps/${swapId.id}/lock`, {});
  }

  /** Respond to a swap (lock responder's tokens) */
  async respond(params: RespondSwapParams): Promise<Htlc> {
    return this.ibc.post(`/swaps/${params.swapId.id}/respond`, { asset: params.asset });
  }

  /** Claim tokens with secret */
  async claim(params: ClaimSwapParams): Promise<{ txHash: string; revealedSecret: Uint8Array }> {
    return this.ibc.post(`/swaps/${params.swapId.id}/claim`, {
      secret: Buffer.from(params.secret).toString('base64'),
    });
  }

  /** Refund expired swap */
  async refund(swapId: SwapId): Promise<{ txHash: string }> {
    return this.ibc.post(`/swaps/${swapId.id}/refund`, {});
  }

  /** Cancel pending swap */
  async cancel(swapId: SwapId): Promise<void> {
    return this.ibc.post(`/swaps/${swapId.id}/cancel`, {});
  }

  /** Get swap by ID */
  async get(swapId: SwapId): Promise<AtomicSwap> {
    return this.ibc.get(`/swaps/${swapId.id}`);
  }

  /** Get HTLC by ID */
  async getHtlc(swapId: SwapId): Promise<Htlc> {
    return this.ibc.get(`/swaps/${swapId.id}/htlc`);
  }

  /** List active swaps */
  async listActive(): Promise<AtomicSwap[]> {
    return this.ibc.get('/swaps/active');
  }

  /** List swaps by participant */
  async listByParticipant(address: string): Promise<AtomicSwap[]> {
    return this.ibc.get(`/swaps/participant/${address}`);
  }

  /** Get swap status */
  async getStatus(swapId: SwapId): Promise<{
    state: SwapState;
    remainingSeconds: number;
    initiatorLocked: boolean;
    responderLocked: boolean;
  }> {
    return this.ibc.get(`/swaps/${swapId.id}/status`);
  }

  /** Verify hashlock with secret */
  verifySecret(hashlock: Hashlock, secret: Uint8Array): boolean {
    // SHA256 hash verification
    const crypto = require('crypto');
    const hash = crypto.createHash('sha256').update(secret).digest();
    return Buffer.from(hashlock.hash).equals(hash);
  }
}

/**
 * Synor IBC SDK Client
 *
 * Main client for Inter-Blockchain Communication operations.
 */
export class SynorIbc {
  private readonly config: Required<IbcConfig>;
  private closed = false;
  private ws?: WebSocket;

  /** Light client operations */
  readonly clients: LightClientClient;
  /** Connection operations */
  readonly connections: ConnectionsClient;
  /** Channel operations */
  readonly channels: ChannelsClient;
  /** Packet operations */
  readonly packets: PacketsClient;
  /** Token transfer operations (ICS-20) */
  readonly transfer: TransferClient;
  /** Atomic swap operations (HTLC) */
  readonly swaps: SwapsClient;

  constructor(config: IbcConfig) {
    this.config = {
      apiKey: config.apiKey,
      endpoint: config.endpoint || DEFAULT_ENDPOINT,
      wsEndpoint: config.wsEndpoint || DEFAULT_WS_ENDPOINT,
      timeout: config.timeout || DEFAULT_TIMEOUT,
      retries: config.retries || DEFAULT_RETRIES,
      chainId: config.chainId || DEFAULT_CHAIN_ID,
      debug: config.debug || false,
    };

    this.clients = new LightClientClient(this);
    this.connections = new ConnectionsClient(this);
    this.channels = new ChannelsClient(this);
    this.packets = new PacketsClient(this);
    this.transfer = new TransferClient(this);
    this.swaps = new SwapsClient(this);
  }

  // =========================================================================
  // Chain Info
  // =========================================================================

  /** Get chain ID */
  get chainId(): string {
    return this.config.chainId;
  }

  /** Get chain info */
  async getChainInfo(): Promise<{
    chainId: string;
    height: Height;
    timestamp: bigint;
    ibcVersion: string;
  }> {
    return this.get('/chain');
  }

  /** Get current height */
  async getHeight(): Promise<Height> {
    return this.get('/chain/height');
  }

  // =========================================================================
  // Proofs
  // =========================================================================

  /** Get client state proof */
  async getClientStateProof(clientId: ClientId): Promise<CommitmentProof> {
    return this.get(`/proofs/client_state/${clientId.id}`);
  }

  /** Get consensus state proof */
  async getConsensusStateProof(clientId: ClientId, height: Height): Promise<CommitmentProof> {
    return this.get(
      `/proofs/consensus_state/${clientId.id}/${height.revisionNumber}-${height.revisionHeight}`
    );
  }

  /** Get connection proof */
  async getConnectionProof(connectionId: ConnectionId): Promise<CommitmentProof> {
    return this.get(`/proofs/connection/${connectionId.id}`);
  }

  /** Get channel proof */
  async getChannelProof(portId: PortId, channelId: ChannelId): Promise<CommitmentProof> {
    return this.get(`/proofs/channel/${portId.id}/${channelId.id}`);
  }

  /** Get packet commitment proof */
  async getPacketCommitmentProof(
    portId: PortId,
    channelId: ChannelId,
    sequence: bigint
  ): Promise<CommitmentProof> {
    return this.get(`/proofs/packet_commitment/${portId.id}/${channelId.id}/${sequence}`);
  }

  /** Get packet acknowledgement proof */
  async getPacketAckProof(
    portId: PortId,
    channelId: ChannelId,
    sequence: bigint
  ): Promise<CommitmentProof> {
    return this.get(`/proofs/packet_ack/${portId.id}/${channelId.id}/${sequence}`);
  }

  // =========================================================================
  // WebSocket Subscriptions
  // =========================================================================

  /** Subscribe to IBC events */
  subscribeEvents(callback: (event: IbcEvent) => void): Subscription {
    return this.subscribe('events', callback);
  }

  /** Subscribe to specific client updates */
  subscribeClient(clientId: ClientId, callback: (event: IbcEvent) => void): Subscription {
    return this.subscribe(`client/${clientId.id}`, callback);
  }

  /** Subscribe to channel events */
  subscribeChannel(
    portId: PortId,
    channelId: ChannelId,
    callback: (event: IbcEvent) => void
  ): Subscription {
    return this.subscribe(`channel/${portId.id}/${channelId.id}`, callback);
  }

  /** Subscribe to swap updates */
  subscribeSwap(swapId: SwapId, callback: (event: IbcEvent) => void): Subscription {
    return this.subscribe(`swap/${swapId.id}`, callback);
  }

  private subscribe(topic: string, callback: (event: IbcEvent) => void): Subscription {
    this.ensureWebSocket();

    const message = JSON.stringify({ type: 'subscribe', topic });
    this.ws!.send(message);

    const handler = (event: MessageEvent) => {
      try {
        const data = JSON.parse(event.data);
        if (data.topic === topic) {
          callback(data.event);
        }
      } catch (error) {
        if (this.config.debug) {
          console.error('IBC WebSocket message parse error:', error);
        }
      }
    };

    this.ws!.addEventListener('message', handler);

    return {
      unsubscribe: () => {
        this.ws?.removeEventListener('message', handler);
        this.ws?.send(JSON.stringify({ type: 'unsubscribe', topic }));
      },
    };
  }

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

    this.ws = new WebSocket(this.config.wsEndpoint);
    this.ws.onopen = () => {
      this.ws!.send(
        JSON.stringify({
          type: 'auth',
          apiKey: this.config.apiKey,
        })
      );
    };
    this.ws.onerror = (error) => {
      if (this.config.debug) {
        console.error('IBC WebSocket error:', error);
      }
    };
  }

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

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

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

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

  // =========================================================================
  // HTTP Methods (internal)
  // =========================================================================

  async get<T>(path: string): Promise<T> {
    return this.request<T>('GET', path);
  }

  async post<T>(path: string, body: unknown): Promise<T> {
    return this.request<T>('POST', path, body);
  }

  async delete<T>(path: string): Promise<T> {
    return this.request<T>('DELETE', path);
  }

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

    const url = `${this.config.endpoint}${path}`;
    let lastError: Error | undefined;

    for (let attempt = 0; attempt <= this.config.retries; attempt++) {
      try {
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), this.config.timeout);

        const response = await fetch(url, {
          method,
          headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${this.config.apiKey}`,
            'X-SDK-Version': 'js/0.1.0',
            'X-Chain-Id': this.config.chainId,
          },
          body: body ? JSON.stringify(body) : undefined,
          signal: controller.signal,
        });

        clearTimeout(timeout);

        if (!response.ok) {
          const error = await response.json().catch(() => ({}));
          throw new IbcException(
            error.message || `HTTP ${response.status}`,
            error.code,
            response.status
          );
        }

        return response.json();
      } catch (error) {
        lastError = error as Error;
        if (error instanceof IbcException) {
          throw error;
        }
        if (attempt < this.config.retries) {
          await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 100));
        }
      }
    }

    throw lastError || new IbcException('Request failed', 'NETWORK_ERROR');
  }
}

export default SynorIbc;
