import type {
  AssetInfo,
  Block,
  BlockOperation,
  BlockTransaction,
} from "@ledgerhq/coin-module-framework/api/types";
import { FINALITY_MS, HEDERA_TRANSACTION_NAMES } from "../constants";
import { apiClient } from "../network/api";
import { hgraphClient } from "../network/hgraph";
import { enrichERC20Transfers } from "../network/utils";
import type {
  ERC20TokenTransfer,
  HederaMirrorCoinTransfer,
  HederaMirrorTokenTransfer,
  HederaMirrorTransaction,
  MergedTransaction,
} from "../types";
import { getBlockInfo } from "./getBlockInfo";
import {
  getMemoFromBase64,
  analyzeStakingOperation,
  getDateRangeFromBlockHeight,
  mergeTransactionsFromDifferentSources,
  toEntityId,
  extractFeesPayer,
  millisToSeconds,
  secondsToNanos,
} from "./utils";

function isStakingTransactionType(
  item: MergedTransaction,
): item is Extract<MergedTransaction, { type: "mirror" }> {
  return item.type === "mirror" && item.data.name === HEDERA_TRANSACTION_NAMES.UpdateAccount;
}

function getMirrorTransaction(item: MergedTransaction): HederaMirrorTransaction {
  return item.type === "mirror" ? item.data : item.data.mirrorTransaction;
}

function createBlockOperationFromCoinTransfer({
  payerAccount,
  chargedFee,
  transfer,
  rewardTransfers,
}: {
  payerAccount: string;
  chargedFee: number;
  transfer: HederaMirrorCoinTransfer;
  rewardTransfers: HederaMirrorTransaction["staking_reward_transfers"];
}): BlockOperation {
  const address = transfer.account;
  const reward = rewardTransfers.find(r => r.account === address);
  const asset: AssetInfo = {
    type: "native",
  };

  // adjust the transfer amount:
  // - exclude fee from payer's operation amount (fees are accounted for separately, so operations must not represent fees)
  // - subtract staking rewards from the amount as they are represented as separate operations
  const feeAdjustment = payerAccount === address ? BigInt(chargedFee) : BigInt(0);
  const rewardAdjustment = BigInt(reward?.amount ?? 0);
  const amount = BigInt(transfer.amount) + feeAdjustment - rewardAdjustment;

  return {
    type: "transfer",
    address,
    asset,
    amount,
  };
}

function createBlockOperationFromHTSTokenTransfer({
  transfer,
}: {
  transfer: HederaMirrorTokenTransfer;
}): BlockOperation {
  const amount = BigInt(transfer.amount);
  const address = transfer.account;
  const asset: AssetInfo = {
    type: "hts",
    assetReference: transfer.token_id,
  };

  return {
    type: "transfer",
    address,
    asset,
    amount,
  };
}

function createBlockOperationFromERC20TokenTransfer({
  transfer,
}: {
  transfer: ERC20TokenTransfer;
}): BlockOperation[] {
  const amount = BigInt(transfer.amount);
  const recipient = transfer.receiver_account_id
    ? toEntityId({ num: transfer.receiver_account_id })
    : transfer.receiver_evm_address;
  const sender = transfer.sender_account_id
    ? toEntityId({ num: transfer.sender_account_id })
    : transfer.sender_evm_address;

  const asset: AssetInfo = {
    type: "erc20",
    assetReference: transfer.token_evm_address,
  };

  // if we don't have either sender or recipient info, we cannot create a meaningful operation, so we skip it
  if (!sender || !recipient) {
    return [];
  }

  return [
    {
      type: "transfer",
      address: recipient,
      asset,
      amount,
    },
    {
      type: "transfer",
      address: sender,
      asset,
      amount: -amount,
    },
  ];
}

function createStakingRewardOperations(tx: HederaMirrorTransaction): BlockOperation[] {
  return tx.staking_reward_transfers.map(rewardTransfer => ({
    type: "transfer",
    address: rewardTransfer.account,
    asset: { type: "native" },
    amount: BigInt(rewardTransfer.amount),
  }));
}

export async function getBlockV2(height: number): Promise<Block> {
  const { start, end } = getDateRangeFromBlockHeight(height);

  // block data should be immutable: do not allow querying blocks on non-finalized time range
  if (end.getTime() > Date.now() - FINALITY_MS) {
    throw new Error(`Block ${height} is not available yet`);
  }

  const latestHgraphIndexedTimestampNs = await hgraphClient.getLatestIndexedConsensusTimestamp();
  const startSeconds = millisToSeconds(start.getTime());
  const endSeconds = millisToSeconds(end.getTime());
  const endNanos = secondsToNanos(endSeconds);
  const limit = 100;
  const order = "desc";

  // do not allow querying blocks if hgraph is not fully synced up to the end of the block time range
  if (latestHgraphIndexedTimestampNs.lt(endNanos)) {
    throw new Error(`Block ${height} has no ERC20 synced yet (${latestHgraphIndexedTimestampNs})`);
  }

  const [blockInfo, mirrorTransactions, enrichedERC20Transfers] = await Promise.all([
    getBlockInfo(height),
    apiClient.getTransactionsByTimestampRange({
      startTimestamp: `gte:${startSeconds}`,
      endTimestamp: `lt:${endSeconds}`,
      limit,
      order,
    }),
    hgraphClient
      .getERC20TransfersByTimestampRange({
        startTimestamp: startSeconds.toFixed(9),
        endTimestamp: endSeconds.toFixed(9),
        limit,
        order,
      })
      .then(erc20Transfers => enrichERC20Transfers(erc20Transfers)),
  ]);

  const mergeResult = mergeTransactionsFromDifferentSources({
    mirrorTransactions,
    enrichedERC20Transfers,
    order,
    limit,
    latestHgraphIndexedTimestampNs,
    fetchAllPages: true,
  });

  // analyze CRYPTOUPDATEACCOUNT transactions to distinguish staking operations from regular account updates.
  // this creates a map of transaction_hash -> StakingAnalysis to avoid repeated lookups.
  const stakingAnalyses = await Promise.all(
    mergeResult.merged.filter(isStakingTransactionType).map(async item => {
      const payerAccount = extractFeesPayer(item.data);
      const analysis = await analyzeStakingOperation(payerAccount, item.data);

      return [item.data.transaction_hash, analysis] as const;
    }),
  );
  const stakingAnalysisMap = new Map(stakingAnalyses);

  const blockTransactions: BlockTransaction[] = mergeResult.merged.map(item => {
    const mirrorTx = getMirrorTransaction(item);
    const payerAccount = extractFeesPayer(mirrorTx);
    const stakingAnalysis = stakingAnalysisMap.get(mirrorTx.transaction_hash);

    let operations: BlockOperation[];

    if (stakingAnalysis) {
      operations = [
        {
          type: "other",
          operationType: stakingAnalysis.operationType,
          stakedNodeId: stakingAnalysis.targetStakingNodeId,
          previousStakedNodeId: stakingAnalysis.previousStakingNodeId,
          stakedAmount: stakingAnalysis.stakedAmount,
        },
      ];
    } else {
      const allTransfers: (
        | HederaMirrorCoinTransfer
        | HederaMirrorTokenTransfer
        | ERC20TokenTransfer
      )[] = [
        ...mirrorTx.transfers,
        ...mirrorTx.token_transfers,
        ...(item.type === "erc20" ? item.data.transfers : []),
      ];

      operations = allTransfers.flatMap(transfer => {
        if ("token_evm_address" in transfer) {
          return createBlockOperationFromERC20TokenTransfer({ transfer });
        } else if ("token_id" in transfer) {
          return createBlockOperationFromHTSTokenTransfer({ transfer });
        } else {
          return createBlockOperationFromCoinTransfer({
            payerAccount,
            transfer,
            chargedFee: mirrorTx.charged_tx_fee,
            rewardTransfers: mirrorTx.staking_reward_transfers,
          });
        }
      });
    }

    // add staking reward operations if present (can occur on any transaction type)
    const rewardOperations = createStakingRewardOperations(mirrorTx);
    operations.push(...rewardOperations);

    return {
      hash: mirrorTx.transaction_hash,
      failed: mirrorTx.result !== "SUCCESS",
      operations,
      fees: BigInt(mirrorTx.charged_tx_fee),
      feesPayer: payerAccount,
      details: { memo: getMemoFromBase64(mirrorTx.memo_base64) },
    };
  });

  return {
    info: blockInfo,
    transactions: blockTransactions,
  };
}
