import { getTransactionReceipt, readContract } from '@wagmi/core';
import axios from 'axios';
import { Buffer } from 'buffer';
import { type Address, getAddress, type Hash, type Hex, numberToHex, type TransactionReceipt } from 'viem';

import { bridgeAbi } from '$abi';
import { routingContractsMap } from '$bridgeConfig';
import { apiService } from '$config';
import type { BridgeTransaction, MessageStatus } from '$libs/bridge';
import { isSupportedChain } from '$libs/chain';
import { TokenType } from '$libs/token';
import { getLogger } from '$libs/util/logger';
import { config } from '$libs/wagmi';

import {
  type APIRequestParams,
  type APIResponse,
  type APIResponseTransaction,
  type Fee,
  type FeeType,
  type GetAllByAddressResponse,
  type PaginationInfo,
  type PaginationParams,
  type ProcessingFeeApiResponse,
  type RelayerBlockInfo,
  RelayerEventType,
} from './types';

const log = getLogger('RelayerAPIService');

const relayerMessageIntegerFields = ['Fee', 'Value', 'Id', 'SrcChainId', 'DestChainId'];
const relayerMessageIntegerPattern = new RegExp(`("(${relayerMessageIntegerFields.join('|')})"\\s*:\\s*)(\\d+)`, 'g');

export function preserveMessageIntegerPrecision(rawResponse: string): string {
  return rawResponse.replace(relayerMessageIntegerPattern, '$1"$3"');
}

export function parseRelayerApiResponse(rawResponse: string): APIResponse {
  return JSON.parse(preserveMessageIntegerPrecision(rawResponse));
}

export function parseApiBigInt(value: unknown): bigint {
  if (typeof value === 'bigint' || typeof value === 'string') {
    return BigInt(value);
  }
  if (typeof value === 'number') {
    if (!Number.isSafeInteger(value)) {
      throw new TypeError('Unsafe integer value from relayer API');
    }
    return BigInt(value);
  }
  throw new TypeError('Invalid integer value from relayer API');
}

export class RelayerAPIService {
  constructor(baseUrl: string) {
    log('relayer service instantiated');
    // There is a chance that by accident the env var
    // does (or does not) have trailing slash for
    // this baseURL. Normalize it, preventing errors
    this.baseUrl = baseUrl.replace(/\/$/, '');
  }

  //Todo: duplicate code in BridgeTxService
  private static async _getTransactionReceipt(chainId: number, hash: Hash) {
    try {
      return await getTransactionReceipt(config, { chainId, hash });
    } catch (error) {
      log(`Error getting transaction receipt for ${hash}: ${error}`);
      return null;
    }
  }

  private static _filterDuplicateAndWrongBridge(items: APIResponseTransaction[]): APIResponseTransaction[] {
    const uniqueHashes = new Set<string>();
    const filteredItems: APIResponseTransaction[] = [];
    for (const item of items) {
      const { Message, Raw } = item.data || {};

      // If no data is present, we skip this item
      if (!Message || !Raw) {
        continue;
      }

      const { DestChainId: destChainId, SrcChainId: srcChainId } = Message;
      const { bridgeAddress } = routingContractsMap[Number(srcChainId)][Number(destChainId)];
      const { transactionHash, address } = Raw;

      // Check all conditions
      const isTransactionHashPresent = Boolean(transactionHash);
      const isAddressPresent = Boolean(address);
      const isUniqueHash = !uniqueHashes.has(transactionHash);
      const isCorrectBridgeAddress = address?.toLowerCase() === bridgeAddress?.toLowerCase();
      const areChainsSupported = isSupportedChain(Number(destChainId)) && isSupportedChain(Number(srcChainId));

      // If the transaction hash is unique, add it to the set for future checks
      if (isUniqueHash) uniqueHashes.add(transactionHash);

      // All these conditions must be true
      const satisfiesAllConditions = [
        isTransactionHashPresent,
        isAddressPresent,
        isUniqueHash,
        isCorrectBridgeAddress,
        areChainsSupported,
      ].every(Boolean);

      // If all conditions are satisfied, add the item to the filtered list
      if (satisfiesAllConditions) filteredItems.push(item);
    }
    return filteredItems;
  }

  private static async _getBridgeMessageStatus({
    msgHash,
    srcChainId,
    destChainId,
  }: {
    msgHash: Hash;
    srcChainId: number;
    destChainId: number;
  }) {
    const { bridgeAddress } = routingContractsMap[Number(destChainId)][Number(srcChainId)];

    const result = await readContract(config, {
      address: bridgeAddress,
      abi: bridgeAbi,
      chainId: Number(destChainId),
      functionName: 'messageStatus',
      args: [msgHash],
    });
    return result as MessageStatus;
  }

  private readonly baseUrl: string;

  async getTransactionsFromAPI(params: APIRequestParams): Promise<APIResponse> {
    const requestURL = `${this.baseUrl}/events`;

    try {
      log('Fetching events from API with params', params);

      const response = await axios.get<APIResponse | string>(requestURL, {
        params,
        timeout: apiService.timeout,
        transformResponse: [(data) => data],
      });

      if (!response || response.status >= 400) throw response;

      const data = typeof response.data === 'string' ? parseRelayerApiResponse(response.data) : response.data;

      log('Events form API', data);

      return data;
    } catch (error) {
      console.error(error);
      throw new Error('could not fetch transactions from API', {
        cause: error,
      });
    }
  }

  async getAllBridgeTransactionByAddress(
    address: Address,
    paginationParams: PaginationParams,
    chainId?: number,
  ): Promise<GetAllByAddressResponse> {
    let params;
    if (chainId) {
      params = {
        address,
        chainID: chainId,
        event: 'MessageSent',
        ...paginationParams,
      };
    } else {
      params = {
        address,
        event: 'MessageSent',
        ...paginationParams,
      };
    }

    const apiTxs: APIResponse = await this.getTransactionsFromAPI(params);

    const { page, size, total, total_pages, first, last, max_page } = apiTxs;

    // TODO: we cannot rely on these values, because the API might return duplicates
    //       and we need to filter them out in the Frontend side. We should fix this
    //       in the API side.
    const paginationInfo: PaginationInfo = {
      page,
      size,
      total,
      total_pages,
      first,
      last,
      max_page,
    };

    if (apiTxs.items?.length === 0) {
      return { txs: [], paginationInfo };
    }

    const items = RelayerAPIService._filterDuplicateAndWrongBridge(apiTxs.items);

    const txs: BridgeTransaction[] = items.map((tx: APIResponseTransaction) => {
      let data: string | Hex = tx.data.Message.Data;
      if (data === '') {
        data = '0x' as Hex;
      } else if (data !== '0x') {
        const buffer = Buffer.from(data, 'base64');
        data = `0x${buffer.toString('hex')}`;
      }

      const tokenType: TokenType = _eventToTokenType(tx.eventType);

      const messageFee = parseApiBigInt(tx.data.Message.Fee);
      const messageValue = parseApiBigInt(tx.data.Message.Value);
      const messageId = parseApiBigInt(tx.data.Message.Id);
      const srcChainId = parseApiBigInt(tx.data.Message.SrcChainId);
      const destChainId = parseApiBigInt(tx.data.Message.DestChainId);

      const transformedTx = {
        status: tx.status,
        amount: BigInt(tx.amount),
        symbol: tx.canonicalTokenSymbol || 'ETH',
        decimals: tx.canonicalTokenDecimals,
        srcTxHash: tx.data.Raw.transactionHash,
        destTxHash: tx.processedTxHash,
        from: getAddress(tx.messageOwner),
        srcChainId,
        destChainId,
        msgHash: tx.msgHash,
        tokenType: tokenType,
        blockNumber: tx.data.Raw.blockNumber,
        canonicalTokenAddress: tx.canonicalTokenAddress,
        processingFee: messageFee,
        claimedBy: tx.claimedBy ? getAddress(tx.claimedBy) : undefined,
        fee: tx.fee ? BigInt(tx.fee) : undefined,
        message: {
          id: messageId,
          to: getAddress(tx.data.Message.To),
          destOwner: getAddress(tx.data.Message.DestOwner),
          data: data as Hex,
          srcOwner: getAddress(tx.data.Message.SrcOwner),
          from: getAddress(tx.data.Message.From),
          gasLimit: tx.data.Message.GasLimit,
          value: messageValue,
          srcChainId,
          destChainId,
          fee: messageFee,
        },
      } satisfies BridgeTransaction;

      return transformedTx;
    });

    const txsPromises = txs.map(async (bridgeTx) => {
      if (!bridgeTx) return;

      const senderMatch = getAddress(bridgeTx.from) === getAddress(address);
      const receiverMatch = bridgeTx.message && getAddress(bridgeTx.message.destOwner) === getAddress(address);

      if (!senderMatch && !receiverMatch) return;

      const { destChainId, srcChainId, srcTxHash, msgHash } = bridgeTx;

      // Returns the transaction receipt for hash or null
      // if the transaction has not been mined.
      const receipt = await RelayerAPIService._getTransactionReceipt(Number(srcChainId), srcTxHash);

      // TODO: do we want to show these transactions?
      if (!receipt || receipt === null) {
        log('Transaction not mined yet', { srcTxHash, srcChainId });
      }

      bridgeTx.receipt = receipt as TransactionReceipt;

      // Populate blockNumber from receipt if not provided by the relayer API
      if (!bridgeTx.blockNumber && receipt) {
        bridgeTx.blockNumber = numberToHex(receipt.blockNumber);
      }

      if (!msgHash) return; //todo: handle this case

      const msgStatus = await RelayerAPIService._getBridgeMessageStatus({
        msgHash,
        srcChainId: Number(srcChainId),
        destChainId: Number(destChainId),
      });

      // Update the status
      bridgeTx.msgStatus = msgStatus;

      return bridgeTx;
    });

    const bridgeTxs: BridgeTransaction[] = (await Promise.all(txsPromises)).filter((tx): tx is BridgeTransaction =>
      Boolean(tx),
    ); // Removes undefined values

    // Spreading to preserve original txs in case of array mutation
    log('Enhanced transactions', [...bridgeTxs]);

    return { txs: bridgeTxs, paginationInfo };
  }

  async getBlockInfo(): Promise<Record<number, RelayerBlockInfo>> {
    const requestURL = `${this.baseUrl}/blockInfo`;
    const blockInfoRecord: Record<number, RelayerBlockInfo> = {};

    try {
      const response = await axios.get<{ data: RelayerBlockInfo[] }>(requestURL);

      if (response.status >= 400) throw response;

      const { data } = response;

      if (data?.data.length > 0) {
        data.data.forEach((blockInfo: RelayerBlockInfo) => (blockInfoRecord[blockInfo.chainID] = blockInfo));
      }
    } catch (error) {
      console.error(error);
      throw new Error('Failed to fetch block info', { cause: error });
    }

    return blockInfoRecord;
  }

  async getSpecificBlockInfo({
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    srcChainId,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    destChainId,
  }: {
    srcChainId: number;
    destChainId: number;
  }): Promise<Record<number, RelayerBlockInfo>> {
    throw new Error('Not implemented');
  }

  async recommendedProcessingFees({
    typeFilter,
    destChainIDFilter,
  }: {
    typeFilter?: FeeType;
    destChainIDFilter?: number;
  }): Promise<Fee[]> {
    const requestURL = `${this.baseUrl}/recommendedProcessingFees`;

    try {
      const response = await axios.get<ProcessingFeeApiResponse>(requestURL);

      if (response.status >= 400) throw new Error('HTTP error', { cause: response });

      let { fees } = response.data;

      if (typeFilter) {
        fees = fees.filter((fee) => fee.type === typeFilter);
      }

      if (destChainIDFilter !== undefined) {
        fees = fees.filter((fee) => fee.destChainID === destChainIDFilter);
      }

      return fees;
    } catch (error) {
      console.error(error);
      throw new Error('Failed to fetch recommended processing fees', {
        cause: error instanceof Error ? error : undefined,
      });
    }
  }
}

const _eventToTokenType = (eventType: RelayerEventType): TokenType => {
  switch (eventType) {
    case RelayerEventType.ERC20:
      return TokenType.ERC20;
    case RelayerEventType.ERC721:
      return TokenType.ERC721;
    case RelayerEventType.ERC1155:
      return TokenType.ERC1155;
    default:
      return TokenType.ETH;
  }
};
