// SPDX-FileCopyrightText: © 2026 LEDGER SAS
// SPDX-License-Identifier: Apache-2.0

import type { TezosCoinConfig, TezosContext } from '../config'
import type { TezosOperationMode } from '../types/model'
import type { Estimate } from '@taquito/taquito'
import { log } from '@ledgerhq/logs'
import { COST_PER_BYTE, getRevealFee, ORIGINATION_SIZE } from '@taquito/taquito'
import { validatePublicKey, ValidationResult } from '@taquito/utils'
import { UnsupportedTransactionMode } from '../types/errors'
import {
  computeMaxStakeAmount,
  createFallbackEstimation,
  createMockSigner,
  DUST_MARGIN_MUTEZ,
  MIN_SUGGESTED_FEE_SMALL_TRANSFER,
  OP_SIZE_XTZ_TRANSFER,
  normalizePublicKeyForAddress,
  partitionNativeBalance,
} from '../utils'
import { getTezosToolkit } from './tezosToolkit'

export type CoreAccountInfo = {
  address: string
  balance: bigint
  stakedBalance?: bigint
  unstakedBalance?: bigint
  revealed: boolean
  xpub?: string
}
export type CoreTransactionInfo = {
  mode: TezosOperationMode
  recipient: string
  amount: bigint
  useAllAmount?: boolean
  /** FA2 token contract; required when `mode` is `send_token` */
  contractAddress?: string
  /** FA2 token id; required when `mode` is `send_token` */
  tokenId?: number
}

export type EstimatedFees = {
  fees: bigint
  gasLimit: bigint
  storageLimit: bigint
  estimatedFees: bigint
  amount?: bigint
  taquitoError?: string
}

/**
 * Fetch the transaction fees for a transaction
 *
 * @param {Account} account
 * @param {Transaction} transaction
 */
export async function estimateFees(
  context: TezosContext,
  {
    account,
    transaction,
  }: {
    account: CoreAccountInfo
    transaction: CoreTransactionInfo
  }
): Promise<EstimatedFees> {
  const config = await context.config()
  // Normalize public key (hex -> base58) when provided (may be undefined for unrevealed accounts)
  // before the device is connected
  const encodedPubKey = account.xpub
    ? normalizePublicKeyForAddress(account.xpub, account.address)
    : undefined

  const tezosToolkit = getTezosToolkit(config)
  if (encodedPubKey && validatePublicKey(encodedPubKey) === ValidationResult.VALID) {
    tezosToolkit.setProvider({ signer: createMockSigner(account.address, encodedPubKey) })
  } else {
    tezosToolkit.setProvider({ signer: createMockSigner(account.address, '') })
  }

  const estimation: EstimatedFees = {
    fees: 0n,
    gasLimit: 0n,
    storageLimit: 0n,
    estimatedFees: 0n,
  }

  // For legacy compatibility
  if (account.balance === 0n) {
    return transaction.useAllAmount ? { ...estimation, amount: 0n } : estimation
  }

  const spendableForMax = Number(
    partitionNativeBalance(
      account.balance,
      account.stakedBalance ?? 0n,
      account.unstakedBalance ?? 0n
    ).spendable
  )

  let amount = transaction.amount
  const coerceMinAmountForEstimation =
    (transaction.useAllAmount &&
      (transaction.mode === 'send' ||
        transaction.mode === 'stake' ||
        transaction.mode === 'unstake')) ||
    (amount === 0n && transaction.mode !== 'send_token')
  if (coerceMinAmountForEstimation) {
    amount = 1n // send/stake max or zero-amount pre-estimation (taquito refuses 0); not used for FA2 send_token
  }

  try {
    if (
      (transaction.mode === 'send' || transaction.mode === 'send_token') &&
      !transaction.recipient
    ) {
      return {
        ...estimation,
        ...createFallbackEstimation(config),
      }
    }
    let estimate: Estimate
    switch (transaction.mode) {
      case 'send':
        estimate = await tezosToolkit.estimate.transfer({
          mutez: true,
          to: transaction.recipient,
          amount: Number(amount),
          source: account.address, // avoid requiring signer for estimation
          storageLimit: ORIGINATION_SIZE, // https://github.com/TezTech/eztz/blob/master/PROTO_003_FEES.md for originating an account
        })
        break
      case 'delegate':
        estimate = await tezosToolkit.estimate.setDelegate({
          source: account.address,
          delegate: transaction.recipient,
        })
        break
      case 'undelegate':
        estimate = await tezosToolkit.estimate.setDelegate({
          source: account.address,
        })
        break
      case 'stake':
        estimate = await tezosToolkit.estimate.stake({
          amount: Number(amount),
          mutez: true,
        })
        break
      case 'unstake':
        estimate = await tezosToolkit.estimate.unstake({
          amount: Number(amount),
          mutez: true,
        })
        break
      case 'finalize_unstake':
        estimate = await tezosToolkit.estimate.finalizeUnstake({})
        break
      case 'send_token': {
        if (!transaction.contractAddress || transaction.tokenId === undefined) {
          throw new Error('FA2 transfer requires contractAddress and tokenId')
        }
        const tokenContract = await tezosToolkit.contract.at(transaction.contractAddress)
        const transferParams = tokenContract.methods
          .transfer([
            {
              from_: account.address,
              txs: [
                {
                  to_: transaction.recipient,
                  token_id: transaction.tokenId,
                  amount: amount,
                },
              ],
            },
          ])
          .toTransferParams({ mutez: true })
        estimate = await tezosToolkit.estimate.transfer({
          ...transferParams,
          source: account.address,
        })
        break
      }
      default:
        throw new UnsupportedTransactionMode('unsupported mode', { mode: transaction.mode })
    }

    const minFees = config.fees.minFees ?? 0
    const mainOpFee = Math.max(minFees, estimate.suggestedFeeMutez)
    const revealFee = account.revealed
      ? 0n
      : BigInt(getRevealFeeForEstimation(config, account.address))

    // NOTE: send-max only applies to native XTZ transfer, not FA2
    if (transaction.useAllAmount && transaction.mode === 'send') {
      // Reserve `mainOpFee`, not the raw taquito suggestion: `estimatedFees` below reports the
      // `minFees` floor, so subtracting the (lower) suggestion here yields an amount whose
      // `amount + estimatedFees` exceeds the spendable balance, and validateIntent then rejects
      // the very max it was handed (LIVE-28506).
      // NOTE: from https://github.com/ecadlabs/taquito/blob/master/integration-tests/__tests__/contract/empty-implicit-account-into-new-implicit-account.spec.ts#L37
      const totalFees =
        estimate.burnFeeMutez > 0
          ? mainOpFee + estimate.burnFeeMutez - 20 * COST_PER_BYTE // 20 is storage buffer
          : mainOpFee
      const maxAmount = spendableForMax - (totalFees + Number(revealFee))
      // NOTE: from https://github.com/ecadlabs/taquito/blob/a70c64c4b105381bb9f1d04c9c70e8ef26e9241c/integration-tests/contract-empty-implicit-account-into-new-implicit-account.spec.ts#L33
      // Temporary fix, see https://gitlab.com/tezos/tezos/-/issues/1754
      // we need to increase the gasLimit and fee returned by the estimation
      const MINIMAL_FEE_PER_GAS_MUTEZ = 0.1
      const incr = DUST_MARGIN_MUTEZ * MINIMAL_FEE_PER_GAS_MUTEZ + Number(estimate.opSize)
      const maxMinusBuff = maxAmount - (DUST_MARGIN_MUTEZ - incr)
      estimation.amount = maxMinusBuff > 0 ? BigInt(maxMinusBuff) : 0n
      log('tezos-send-max', 'send-max fee inputs', {
        minFees,
        mainOpFee,
        suggestedFeeMutez: estimate.suggestedFeeMutez,
        burnFeeMutez: estimate.burnFeeMutez,
        opSize: Number(estimate.opSize),
        revealFee: Number(revealFee),
      })
    } else if (transaction.useAllAmount && transaction.mode === 'stake') {
      estimation.amount = computeMaxStakeAmount(
        BigInt(account.balance),
        account.stakedBalance ?? 0n,
        account.unstakedBalance ?? 0n,
        BigInt(mainOpFee) + revealFee
      )
    } else if (transaction.useAllAmount && transaction.mode === 'unstake') {
      // unstake-max draws from the staked balance, not the spendable balance
      estimation.amount = account.stakedBalance ?? 0n
    } else {
      estimation.amount = transaction.amount
    }

    estimation.fees = BigInt(mainOpFee)
    estimation.gasLimit = BigInt(estimate.gasLimit)
    estimation.storageLimit = BigInt(estimate.storageLimit)
    estimation.estimatedFees = estimation.fees + revealFee
  } catch (e) {
    if (typeof e !== 'object' || !e) throw e
    if ('id' in e) {
      estimation.taquitoError = (e as { id: string }).id
      log('taquito-error', 'taquito got error ' + e.id)
    } else if ('status' in e) {
      const errorMessage = String((e as unknown as { message: string }).message || '')
      if (
        errorMessage.includes('Public key not found') ||
        errorMessage.includes('wallet or contract API')
      ) {
        log(
          'taquito-network-error',
          'Recipient address not found (new account), using default fees',
          {
            transaction: transaction,
          }
        )
        const fallback = createFallbackEstimation(config)
        estimation.fees = fallback.fees
        estimation.gasLimit = fallback.gasLimit
        estimation.storageLimit = fallback.storageLimit
        estimation.estimatedFees = fallback.fees
        if (!account.revealed) {
          estimation.estimatedFees =
            estimation.estimatedFees + BigInt(getRevealFeeForEstimation(config, account.address))
        }
        // Handle useAllAmount also for send mode when estimation falls back
        if (transaction.useAllAmount && transaction.mode === 'send') {
          // Approximate Taquito behavior for send-max using stable constants
          const suggestedFee =
            transaction.mode === 'send' ? MIN_SUGGESTED_FEE_SMALL_TRANSFER : Number(estimation.fees)

          // For display consistency in tests, align fees to suggestedFee in send-max
          if (transaction.mode === 'send') {
            estimation.fees = BigInt(suggestedFee)
            estimation.estimatedFees = BigInt(suggestedFee)
            if (!account.revealed) {
              estimation.estimatedFees =
                estimation.estimatedFees +
                BigInt(getRevealFeeForEstimation(config, account.address))
            }
          }

          const burnFeeMutez = Number(estimation.storageLimit) * COST_PER_BYTE
          const totalFees =
            suggestedFee + (burnFeeMutez > 0 ? burnFeeMutez - 20 * COST_PER_BYTE : 0)

          const revealFee = account.revealed
            ? 0
            : getRevealFeeForEstimation(config, account.address)
          const maxAmount = spendableForMax - (totalFees + revealFee)

          const MINIMAL_FEE_PER_GAS_MUTEZ = 0.1
          const incr = OP_SIZE_XTZ_TRANSFER + DUST_MARGIN_MUTEZ * MINIMAL_FEE_PER_GAS_MUTEZ
          const maxMinusBuff = maxAmount - (DUST_MARGIN_MUTEZ - incr)
          estimation.amount = maxMinusBuff > 0 ? BigInt(Math.floor(maxMinusBuff)) : 0n
        } else {
          // preserve input amount in fallback for readability/tests
          estimation.amount = transaction.amount
        }
      } else {
        log('taquito-network-error', errorMessage, {
          transaction: transaction,
        })
        throw e
      }
    } else {
      const msg = String((e as any).message || '')
      if (msg.includes('No signer has been configured')) {
        const fallback = createFallbackEstimation(config)
        estimation.fees = fallback.fees
        estimation.gasLimit = fallback.gasLimit
        estimation.storageLimit = fallback.storageLimit
        estimation.estimatedFees = fallback.estimatedFees
        if (!account.revealed) {
          estimation.estimatedFees =
            estimation.estimatedFees + BigInt(getRevealFeeForEstimation(config, account.address))
        }
        if (transaction.useAllAmount && transaction.mode === 'send') {
          const suggestedFee =
            transaction.mode === 'send' ? MIN_SUGGESTED_FEE_SMALL_TRANSFER : Number(estimation.fees)

          if (transaction.mode === 'send') {
            estimation.fees = BigInt(suggestedFee)
            estimation.estimatedFees = BigInt(suggestedFee)
            if (!account.revealed) {
              estimation.estimatedFees =
                estimation.estimatedFees +
                BigInt(getRevealFeeForEstimation(config, account.address))
            }
          }

          const burnFeeMutez = Number(estimation.storageLimit) * COST_PER_BYTE
          const totalFees =
            suggestedFee + (burnFeeMutez > 0 ? burnFeeMutez - 20 * COST_PER_BYTE : 0)
          const revealFee = account.revealed
            ? 0
            : getRevealFeeForEstimation(config, account.address)
          const maxAmount = spendableForMax - (totalFees + revealFee)
          const MINIMAL_FEE_PER_GAS_MUTEZ = 0.1
          const incr = OP_SIZE_XTZ_TRANSFER + DUST_MARGIN_MUTEZ * MINIMAL_FEE_PER_GAS_MUTEZ
          const maxMinusBuff = maxAmount - (DUST_MARGIN_MUTEZ - incr)
          estimation.amount = maxMinusBuff > 0 ? BigInt(Math.floor(maxMinusBuff)) : 0n
        } else {
          // preserve input amount in fallback for readability/tests
          estimation.amount = transaction.amount
        }
      } else {
        throw e
      }
    }
  }
  return estimation
}

function getRevealFeeForEstimation(config: TezosCoinConfig, address: string): number {
  const minFees = config.fees.minFees ?? 0
  return Math.max(minFees, getRevealFee(address))
}
