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

import type { TezosCoinConfig, TezosContext } from '../config'
import type { CoreAccountInfo, CoreTransactionInfo, EstimatedFees } from '../logic/estimateFees'
import type { TezosOperationMode } from '../types/model'
import type { TezosAccountInfo, TezosFeeEstimation } from './types'
import type {
  CraftedTransaction,
  Cursor,
  ListOperationsOptions,
  Page,
  Reward,
  Validator,
} from '@ledgerhq/coin-module-framework/api/index'
import type {
  BalanceOptions,
  CoinModuleApi,
  FeeEstimation,
  TransactionIntent,
} from '@ledgerhq/coin-module-framework/api/types'
import { rejectBalanceOptions } from '@ledgerhq/coin-module-framework/api/getBalance/rejectBalanceOptions'
import {
  IncorrectTypeError,
  notSupported,
  type Operation,
} from '@ledgerhq/coin-module-framework/api/index'
import { craftTransactionData } from '@ledgerhq/coin-module-framework/logic/craftTransactionData'
import { log } from '@ledgerhq/logs'
import { getRevealFee } from '@taquito/taquito'
import { getPkhfromPk, validatePublicKey, ValidationResult } from '@taquito/utils'
import {
  broadcast,
  combine,
  craftRawOperations,
  craftTransaction,
  estimateFees,
  getBalance,
  getBlock,
  getBlockInfo,
  getStakes,
  lastBlock,
  listOperations,
  rawEncode,
  validateIntent,
} from '../logic'
import { getTezosToolkit } from '../logic/tezosToolkit'
import { validateAddress } from '../logic/validateAddress'
import { hasManagerKey } from '../network/types'
import { createTzktApi } from '../network/tzkt'
import {
  DUST_MARGIN_MUTEZ,
  hasEmptyBalance,
  normalizePublicKeyForAddress,
  parseTezosTokenAsset,
  resolveTezosOperationMode,
} from '../utils'

// The caller builds the {@link TezosContext} (config + logger) and passes it to each method (ADR-019).
export function createApi(): CoinModuleApi<TezosCoinConfig> {
  return {
    broadcast: (context, tx) => broadcast(context, tx),
    async call() {
      throw new Error('call is not supported')
    },
    combine: (_context, tx, signature) => combine(tx, signature),
    craftTransaction: (context, transactionIntent, options) =>
      craft(context, transactionIntent, options?.customFees),
    craftRawTransaction: async (
      context,
      transaction: string,
      sender: string,
      publicKey: string,
      sequence: bigint
    ): Promise<CraftedTransaction> => {
      const tx = await craftRawOperations(context, transaction, sender, publicKey, sequence)
      return { transaction: tx }
    },
    estimateFees: (context, transactionIntent) => estimate(context, transactionIntent),
    getBalance: (context, address: string, options?: BalanceOptions) =>
      rejectBalanceOptions(() => getBalance(context, address), options),
    lastBlock: (context) => lastBlock(context),
    listOperations: (context, address, options) => operations(context, address, options),
    getStakes: (context, address, options) => getStakes(context, address, options?.cursor),
    validateIntent: (context, transactionIntent) => validateIntent(context, transactionIntent),
    getNextSequence: async (context, address: string) => {
      const config = await context.config()
      const accountInfo = await createTzktApi(config).getAccountByAddress(address)
      return hasManagerKey(accountInfo) ? BigInt(accountInfo.counter + 1) : 0n
    },
    getAccountInfo: async (context, address: string): Promise<TezosAccountInfo> => {
      const config = await context.config()
      const account = await createTzktApi(config).getAccountByAddress(address)
      // Manager-key accounts (plain wallets "user" and registered bakers "delegate") carry a
      // reveal state; empty / non-existent accounts are treated as unrevealed (no public key
      // published on-chain yet).
      return { type: 'tezos', revealed: hasManagerKey(account) ? account.revealed : false }
    },
    getBlock: (context, height) => getBlock(context, height),
    getBlockInfo: (context, height) => getBlockInfo(context, height),
    getRewards(_context, _address: string, _options?: { cursor?: Cursor }): Promise<Page<Reward>> {
      throw new Error('getRewards is not supported')
    },
    register: notSupported('register'),
    getValidators(_context, _options?: { cursor?: Cursor }): Promise<Page<Validator>> {
      throw new Error('getValidators is not supported')
    },
    validateAddress: (_context, address, parameters) => validateAddress(address, parameters),
    craftTransactionData: (_context, intent) => craftTransactionData(intent),
  }
}

function isTezosTransactionType(
  type: string
): type is 'send' | 'delegate' | 'undelegate' | 'stake' | 'unstake' | 'finalize_unstake' {
  return ['send', 'delegate', 'undelegate', 'stake', 'unstake', 'finalize_unstake'].includes(type)
}
async function craft(
  context: TezosContext,
  transactionIntent: TransactionIntent,
  customFees?: FeeEstimation
): Promise<CraftedTransaction> {
  if (!isTezosTransactionType(transactionIntent.type)) {
    throw new IncorrectTypeError(transactionIntent.type)
  }

  const config = await context.config()
  const api = createTzktApi(config)

  // Always estimate to get gasLimit/storageLimit
  const estimation = await estimate(context, transactionIntent)
  const fee = {
    fees: (customFees?.value ?? estimation.value).toString(),
    gasLimit: estimation.parameters?.gasLimit?.toString(),
    storageLimit: estimation.parameters?.storageLimit?.toString(),
  }

  const tezosMode = resolveTezosOperationMode(transactionIntent.type, transactionIntent.asset)
  const mappedType: TezosOperationMode =
    tezosMode === 'send_token' ? 'send_token' : (transactionIntent.type as TezosOperationMode)
  const tokenCraftInfo =
    tezosMode === 'send_token' ? parseTezosTokenAsset(transactionIntent.asset)! : undefined

  let amountToUse = tezosMode === 'finalize_unstake' ? 0n : transactionIntent.amount
  if (tezosMode === 'send' && transactionIntent.useAllAmount) {
    const senderInfo = await api.getAccountByAddress(transactionIntent.sender)
    if (hasManagerKey(senderInfo)) {
      // Use the amount calculated by the estimation which includes proper buffers and adjustments
      if (estimation.parameters?.amount !== undefined) {
        amountToUse = estimation.parameters.amount
      } else {
        // Fallback to the original calculation if estimation doesn't provide amount
        const bal = BigInt(senderInfo.balance)
        const feeBI = BigInt(fee.fees || '0')
        const dustMargin = BigInt(DUST_MARGIN_MUTEZ)
        const totalToDeduct = feeBI + dustMargin
        amountToUse = bal > totalToDeduct ? bal - totalToDeduct : 0n
      }
    } else {
      amountToUse = 0n
    }
  }

  const accountForCraft = {
    address: transactionIntent.sender,
  }
  const senderApiAcc = await api.getAccountByAddress(transactionIntent.sender)
  const needsReveal = hasManagerKey(senderApiAcc) && !senderApiAcc.revealed
  const totalFee = Number(fee.fees || '0')

  const feesConfig = config.fees
  const revealFeeForSplit = needsReveal
    ? Math.max(feesConfig.minFees ?? 0, getRevealFee(transactionIntent.sender))
    : 0

  let txFee: number
  if (customFees) {
    txFee = needsReveal ? Math.max(totalFee - revealFeeForSplit, 0) : totalFee
  } else if (estimation.parameters?.txFee !== undefined) {
    txFee = Number(estimation.parameters.txFee)
  } else {
    txFee = needsReveal ? Math.max(totalFee - revealFeeForSplit, 0) : totalFee
  }

  const txForCraft = {
    type: mappedType,
    recipient: transactionIntent.recipient,
    amount: amountToUse,
    fee: { ...fee, fees: txFee.toString() },
    ...(tokenCraftInfo && {
      contractAddress: tokenCraftInfo.contractAddress,
      tokenId: tokenCraftInfo.tokenId,
    }),
  }
  const publicKeyForCraft =
    needsReveal && transactionIntent.senderPublicKey
      ? (() => {
          // Accept either base58 or hex from device, and map curve using sender address
          let pk = transactionIntent.senderPublicKey
          if (validatePublicKey(pk) !== ValidationResult.VALID) {
            pk = normalizePublicKeyForAddress(pk, transactionIntent.sender) || pk
          }
          // Verify the public key matches the sender address to avoid inconsistent_hash
          let isPublicKeyValid = false
          try {
            const derived = getPkhfromPk(pk)
            isPublicKeyValid = derived === transactionIntent.sender
          } catch {
            // getPkhfromPk failed = will fallback to basic validation below
            isPublicKeyValid = false
          }

          if (!isPublicKeyValid) {
            // If derivation failed/doesn't match, check if the key is atleast valid format
            if (validatePublicKey(pk) !== ValidationResult.VALID) {
              throw new Error('Unable to normalize sender public key')
            }
          }
          return { publicKey: pk, publicKeyHash: transactionIntent.sender }
        })()
      : undefined
  const { contents } = await craftTransaction(
    context,
    accountForCraft,
    txForCraft,
    publicKeyForCraft
  )
  const tx = await rawEncode(config, contents)
  return { transaction: tx }
}

async function estimate(
  context: TezosContext,
  transactionIntent: TransactionIntent
): Promise<TezosFeeEstimation> {
  // avoid taquito error when estimating a 0-amount transfer during input
  const config = await context.config()
  const api = createTzktApi(config)
  const tezosModeForEstimate = resolveTezosOperationMode(
    transactionIntent.type,
    transactionIntent.asset
  )
  if (
    (tezosModeForEstimate === 'send' || tezosModeForEstimate === 'send_token') &&
    transactionIntent.amount === 0n &&
    !transactionIntent.useAllAmount
  ) {
    return {
      value: BigInt(DUST_MARGIN_MUTEZ),
      parameters: {
        gasLimit: 10000n,
        storageLimit: 300n,
        amount: 0n,
        txFee: BigInt(DUST_MARGIN_MUTEZ),
      },
    }
  }
  const senderAccountInfo = await api.getAccountByAddress(transactionIntent.sender)
  // If the sender is not a manager-key account (user or delegate), return default estimation values
  if (!hasManagerKey(senderAccountInfo)) {
    return {
      value: BigInt(DUST_MARGIN_MUTEZ),
      parameters: {
        gasLimit: 10000n,
        storageLimit: 300n,
        amount: 0n,
        txFee: BigInt(DUST_MARGIN_MUTEZ),
      },
    }
  }

  const accountBase: CoreAccountInfo = {
    address: transactionIntent.sender,
    revealed: senderAccountInfo.revealed,
    balance: BigInt(senderAccountInfo.balance),
    stakedBalance: BigInt(senderAccountInfo.stakedBalance ?? 0),
    unstakedBalance: BigInt(senderAccountInfo.unstakedBalance ?? 0),
  }

  const tokenEstimationInfo =
    tezosModeForEstimate === 'send_token'
      ? parseTezosTokenAsset(transactionIntent.asset)!
      : undefined

  const transaction: CoreTransactionInfo = {
    mode: tezosModeForEstimate,
    recipient: transactionIntent.recipient,
    amount: tezosModeForEstimate === 'finalize_unstake' ? 0n : transactionIntent.amount,
    useAllAmount: !!transactionIntent.useAllAmount,
    ...(tokenEstimationInfo && {
      contractAddress: tokenEstimationInfo.contractAddress,
      tokenId: tokenEstimationInfo.tokenId,
    }),
  }

  async function logicEstimate(xpub?: string): Promise<EstimatedFees> {
    // needed by the compiler (it can assume it's a manager-key account with respective fields)
    if (!hasManagerKey(senderAccountInfo)) throw new Error('unexpected account type')
    const account = xpub ? { ...accountBase, xpub } : accountBase
    return await estimateFees(context, { account, transaction })
  }
  const xpub = transactionIntent.senderPublicKey ?? senderAccountInfo.publicKey

  try {
    // try intent public key first and fallback to tzkt public key
    let estimation
    try {
      estimation = await logicEstimate(xpub)
    } catch (error) {
      // for some unknown reason, on some address the estimation fails with that error:
      // {"kind":"permanent","id":"proto.023-PtSeouLo.contract.manager.inconsistent_hash","public_key":"sppk7aMmdpDZc9KHjJBWac53NVoK4kfYbTC39EbmEzpZizjENonbHQD","expected_hash":"tz2BHzkaizWwCmhYswwTQCycgT8mXFH8QTL5","provided_hash":"tz2R3ynJBBzFZYtbx1Ywmvd8n6z2ZH3rXAQ6"}
      // it's not clear why this happens, it couldn't be further investigated
      // so we fallback to make an estimation without the public key
      // there is a test that covers this, see "fallback to an estimation without the public key" index-mainnet.integ.test.ts
      log('estimate-error', 'error estimating fees, trying without pubkey', { error })
      estimation = await logicEstimate()
    }

    if (
      estimation.taquitoError &&
      !estimation.taquitoError.includes('delegate.unchanged') &&
      !estimation.taquitoError.includes('subtraction_underflow') &&
      !estimation.taquitoError.includes('balance_too_low') &&
      !estimation.taquitoError.includes('script_rejected') &&
      !estimation.taquitoError.includes(
        'cannot_stake_with_unfinalizable_unstake_requests_to_another_delegate'
      )
    ) {
      throw new Error(`Fees estimation failed: ${estimation.taquitoError}`)
    }

    return {
      value: estimation.estimatedFees,
      parameters: {
        gasLimit: estimation.gasLimit,
        storageLimit: estimation.storageLimit,
        amount: estimation.amount,
        txFee: estimation.fees,
      },
    }
  } catch (error: any) {
    // Handle PublicKeyNotFoundError
    if (error?.message?.includes('Public key not found')) {
      const apiAccount = await api.getAccountByAddress(transactionIntent.recipient)
      const storageLimit =
        !hasEmptyBalance(apiAccount) || transactionIntent.type === 'stake' ? 0n : 277n

      // Check if account needs reveal for proper fee calculation
      const senderApiAcc = await api.getAccountByAddress(transactionIntent.sender)
      const needsReveal = hasManagerKey(senderApiAcc) && !senderApiAcc.revealed

      let baseTxFee: bigint
      let txGasLimit: bigint

      try {
        const toolkit = getTezosToolkit(config)
        const simpleEstimate = await toolkit.estimate.transfer({
          to: transactionIntent.recipient,
          amount: Number(transactionIntent.amount),
          mutez: true,
          source: transactionIntent.sender,
        })
        // Use Taquito estimation, respecting minFees from config
        baseTxFee = BigInt(Math.max(config.fees.minFees, simpleEstimate.suggestedFeeMutez))
        txGasLimit = BigInt(simpleEstimate.gasLimit)
      } catch {
        // When estimation fails because the sender is unrevealed (PublicKeyNotFoundError),
        // fallback to a conservative gas value suitable for typical new-account XTZ transfers.
        // This buffer (~2500 gas) is more than enough for a standard transfer that actually uses ~1420 gas.
        // The fee is computed according to Taquito's calculation so it will satisfy the Tezos prefilter rule:
        //   total_fees >= ceil(100 + 0.1*total_gas + op_size)
        // We use a base of 120 instead of 100 to mimic Taquito and minimize rejected low-fee ops.
        const SAFE_FALLBACK_GAS = 2500 // covers typical new-account transfer (~1420) with buffer
        const FALLBACK_OP_SIZE_BYTES = 154 // typical forged size for a simple XTZ transfer
        txGasLimit = BigInt(SAFE_FALLBACK_GAS)
        baseTxFee = BigInt(
          Math.max(
            config.fees.minFees,
            Math.ceil(120 + 0.1 * SAFE_FALLBACK_GAS + FALLBACK_OP_SIZE_BYTES)
          )
        )
      }

      const revealFee = needsReveal
        ? BigInt(Math.max(config.fees.minFees ?? 0, getRevealFee(transactionIntent.sender)))
        : 0n
      const totalFee = baseTxFee + revealFee

      return {
        value: totalFee,
        parameters: {
          gasLimit: txGasLimit,
          storageLimit,
          amount: 0n,
          txFee: baseTxFee,
        },
      }
    } else {
      // Re-throw other errors
      throw error
    }
  }
}

async function operations(
  context: TezosContext,
  address: string,
  { minHeight = 0, cursor, order = 'asc' }: ListOperationsOptions
): Promise<Page<Operation>> {
  // FIXME This wrapper hard-codes limit: 1000 and ignores any caller-provided limit from ListOperationsOptions. Either
  //  forward options.limit (as a soft/capped limit) or throw a "not supported" error when limit is set to match the
  //  ListOperationsOptions contract.
  const [items, newNextCursor] = await listOperations(context, address, {
    limit: 1000, // Increased limit to 1000 to ensure delegation information is available when displaying account details (temporary fix until proper pagination is implemented).
    token: cursor,
    sort: order === 'asc' ? 'Ascending' : 'Descending',
    minHeight: minHeight,
  })

  return { items, next: newNextCursor || undefined }
}
