import type {
  Block,
  BlockInfo,
  BlockOperation,
  BlockTransaction,
} from "@ledgerhq/coin-module-framework/api/index";
import { log } from "@ledgerhq/logs";
import BigNumber from "bignumber.js";
import {
  getBlock as networkGetBlock,
  getBlockWithTransactions,
  getTransactionInfoByBlockNum,
} from "../network";
import { encode58Check } from "../network/format";
import { inferAssetInfo } from "../network/trongrid/trongrid-adapters";
import type { BlockTransactionAPI, TransactionInfoByBlockNumAPI } from "../network/types";
import { abiDecodeTrc20Transfer } from "../network/utils";
import type { TrongridTxInfo, TrongridTxType } from "../types";

type BlockTxInfo = TrongridTxInfo;

export async function getBlockInfo(height: number): Promise<BlockInfo> {
  if (!Number.isSafeInteger(height) || height <= 0) {
    throw new Error(`Invalid block height: ${height}`);
  }

  const block = await networkGetBlock(height);
  return {
    height: block.height,
    hash: block.hash,
    time: block.time ?? new Date(0),
  };
}

export async function getBlock(height: number): Promise<Block> {
  if (!Number.isSafeInteger(height) || height <= 0) {
    throw new Error(`Invalid block height: ${height}`);
  }

  const [data, txInfos] = await Promise.all([
    getBlockWithTransactions(height),
    getTransactionInfoByBlockNum(height).catch(error => {
      log("tron/getBlock", "Failed to fetch transaction info, falling back to ret fees", {
        height,
        error,
      });
      return [];
    }),
  ]);
  const header = data.block_header.raw_data;
  const blockTimestamp = header.timestamp ?? 0;

  const info: BlockInfo = {
    height: header.number ?? height,
    hash: data.blockID,
    time: blockTimestamp ? new Date(blockTimestamp) : new Date(0),
  };

  if (header.parentHash && info.height > 1) {
    info.parent = { height: info.height - 1, hash: header.parentHash };
  }

  const rawTxs = data.transactions ?? [];
  const txInfoById = buildTxInfoMap(txInfos);

  const transactions: BlockTransaction[] = rawTxs
    .map(tx => toBlockTransaction(tx, blockTimestamp, info.height, txInfoById))
    .filter((tx): tx is BlockTransaction => tx !== null);

  return { info, transactions };
}

function buildTxInfoMap(
  txInfos: TransactionInfoByBlockNumAPI[],
): Map<string, TransactionInfoByBlockNumAPI> {
  return new Map(txInfos.map(tx => [tx.id, tx]));
}

function toBlockTransaction(
  tx: BlockTransactionAPI,
  blockTimestamp: number,
  blockHeight: number,
  txInfoById: Map<string, TransactionInfoByBlockNumAPI>,
): BlockTransaction | null {
  const txInfo = formatBlockTransaction(tx, blockTimestamp, blockHeight);
  if (!txInfo) return null;

  const txDetail = txInfoById.get(tx.txID);
  const fee = txDetail?.fee ?? tx.ret?.[0]?.fee ?? 0;

  return {
    hash: txInfo.txID,
    failed: txInfo.hasFailed,
    fees: BigInt(fee),
    feesPayer: txInfo.from,
    operations: txInfo.hasFailed ? [] : toBlockOperations(txInfo),
  };
}

function formatBlockTransaction(
  tx: BlockTransactionAPI,
  blockTimestamp: number,
  blockHeight: number,
): BlockTxInfo | null {
  try {
    const contract = tx.raw_data.contract[0];
    if (!contract) return null;

    const type = contract.type as TrongridTxType;
    const params = contract.parameter.value;
    const ownerAddress = params.owner_address;
    if (!ownerAddress) return null;

    const from = encode58Check(ownerAddress);
    const contractRet = tx.ret?.[0]?.contractRet ?? "SUCCESS";
    const hasFailed = contractRet !== "SUCCESS";

    const isTrc20 = type === "TriggerSmartContract" && params.contract_address;
    const isTrc10 = type === "TransferAssetContract";
    const tokenType = isTrc10 ? "trc10" : isTrc20 ? "trc20" : undefined;

    let to: string | undefined;
    let value: BigNumber;

    if (isTrc20 && params.data) {
      const decoded = abiDecodeTrc20Transfer(params.data);
      if (decoded) {
        to = encode58Check(decoded.to);
        value = decoded.amount;
      } else {
        value = new BigNumber(0);
      }
    } else {
      to = params.to_address ? encode58Check(params.to_address) : undefined;
      value = params.amount ? new BigNumber(params.amount) : new BigNumber(0);
    }

    const tokenId = isTrc10
      ? decodeHexAssetName(params.asset_name)
      : isTrc20 && params.contract_address
        ? encode58Check(params.contract_address)
        : undefined;

    return {
      txID: tx.txID,
      date: new Date(blockTimestamp),
      type,
      tokenId,
      tokenType,
      tokenAddress:
        isTrc20 && params.contract_address ? encode58Check(params.contract_address) : undefined,
      from,
      to,
      value,
      blockHeight,
      hasFailed,
    };
  } catch (error) {
    log("tron/getBlock", "formatBlockTransaction error", {
      txId: tx.txID,
      error,
    });
    return null;
  }
}

function toBlockOperations(txInfo: BlockTxInfo): BlockOperation[] {
  if (isTransfer(txInfo) && txInfo.to && txInfo.value && !txInfo.value.isZero()) {
    const asset = inferAssetInfo(txInfo);
    const value = txInfo.value;
    if (value.isNaN() || !value.isFinite()) {
      return [{ type: "other", operationType: "NONE", contractType: txInfo.type }];
    }
    const amount = BigInt(value.integerValue().toFixed(0));
    return [
      { type: "transfer", address: txInfo.from, peer: txInfo.to, asset, amount: -amount },
      { type: "transfer", address: txInfo.to, peer: txInfo.from, asset, amount },
    ];
  }

  const operationType = getOperationType(txInfo.type);
  return [{ type: "other", operationType: operationType, contractType: txInfo.type }];
}

function isTransfer(txInfo: TrongridTxInfo): boolean {
  return (
    txInfo.type === "TransferContract" ||
    txInfo.type === "TransferAssetContract" ||
    (txInfo.type === "TriggerSmartContract" && txInfo.tokenType === "trc20")
  );
}

function getOperationType(contractType: string): string {
  switch (contractType) {
    case "ContractApproval":
      return "APPROVE";
    case "ExchangeTransactionContract":
      return "OUT";
    case "VoteWitnessContract":
      return "VOTE";
    case "WithdrawBalanceContract":
      return "REWARD";
    case "FreezeBalanceContract":
    case "FreezeBalanceV2Contract":
      return "FREEZE";
    case "UnfreezeBalanceV2Contract":
      return "UNFREEZE";
    case "WithdrawExpireUnfreezeContract":
      return "WITHDRAW_EXPIRE_UNFREEZE";
    case "UnDelegateResourceContract":
      return "UNDELEGATE_RESOURCE";
    case "UnfreezeBalanceContract":
      return "LEGACY_UNFREEZE";
    default:
      return "NONE";
  }
}

function decodeHexAssetName(hexAssetName: string | undefined): string | undefined {
  if (!hexAssetName) return undefined;
  return Buffer.from(hexAssetName, "hex").toString("utf8");
}
