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

import type { TezosCoinConfig, TezosContext } from '../config'
import type { OperationContents } from '@taquito/rpc'
import { OpKind } from '@taquito/rpc'
import { UnsupportedTransactionMode } from '../types/errors'
import { createMockSigner } from '../utils'
import { estimateRevealLimits } from './estimateRevealLimits'
import { getTezosToolkit } from './tezosToolkit'

export type TransactionFee = {
  fees?: string
  gasLimit?: string
  storageLimit?: string
}
export type TransactionType =
  | 'OUT'
  | 'DELEGATE'
  | 'UNDELEGATE'
  | 'STAKE'
  | 'UNSTAKE'
  | 'FINALIZE_UNSTAKE'

export async function craftTransaction(
  context: TezosContext,
  account: {
    address: string
    counter?: number
  },
  transaction: {
    type:
      | 'send'
      | 'delegate'
      | 'undelegate'
      | 'send_token'
      | 'stake'
      | 'unstake'
      | 'finalize_unstake'
    recipient: string
    amount: bigint
    fee: TransactionFee
    contractAddress?: string
    tokenId?: number
  },
  publicKey?: {
    publicKey: string
    publicKeyHash: string
  }
): Promise<{ type: TransactionType; contents: OperationContents[] }> {
  const config = await context.config()
  const { address } = account

  const transactionFees = {
    fee: transaction.fee.fees ?? '0',
    gas_limit: transaction.fee.gasLimit ?? '0',
    storage_limit: transaction.fee.storageLimit ?? '0',
  }

  const tezosToolkit = getTezosToolkit(config)

  // Configure signer for Taquito operations (same as in estimateFees)
  if (publicKey) {
    tezosToolkit.setProvider({
      signer: createMockSigner(publicKey.publicKeyHash, publicKey.publicKey),
    })
  }

  const sourceData = await tezosToolkit.rpc.getContract(address)
  const counter = account.counter ?? Number(sourceData.counter)

  const contents: OperationContents[] = []

  if (publicKey !== undefined) {
    const feesConfig = config.fees
    const reveal = await estimateRevealLimits(tezosToolkit, address, feesConfig)

    contents.push({
      kind: OpKind.REVEAL,
      fee: reveal.fee.toString(),
      gas_limit: reveal.gasLimit.toString(),
      storage_limit: reveal.storageLimit.toString(),
      source: publicKey.publicKeyHash,
      counter: (counter + 1 + contents.length).toString(),
      public_key: publicKey.publicKey,
    })
  }

  let type: TransactionType
  switch (transaction.type) {
    case 'send': {
      type = 'OUT'
      contents.push({
        kind: OpKind.TRANSACTION,
        amount: transaction.amount.toString(),
        destination: transaction.recipient,
        source: address,
        counter: (counter + 1 + contents.length).toString(),
        ...transactionFees,
      })
      break
    }
    case 'send_token': {
      if (!transaction.contractAddress || transaction.tokenId === undefined) {
        throw new Error('FA2 transfer requires contractAddress and tokenId')
      }
      type = 'OUT'
      const tokenContract = await tezosToolkit.contract.at(transaction.contractAddress)
      const transferParams = tokenContract.methods
        .transfer([
          {
            from_: address,
            txs: [
              {
                to_: transaction.recipient,
                token_id: transaction.tokenId,
                amount: transaction.amount,
              },
            ],
          },
        ])
        .toTransferParams({ mutez: true })
      contents.push({
        kind: OpKind.TRANSACTION,
        source: address,
        destination: transaction.contractAddress,
        amount: '0',
        counter: (counter + 1 + contents.length).toString(),
        parameters: transferParams.parameter,
        ...transactionFees,
      })
      break
    }
    case 'delegate': {
      type = 'DELEGATE'
      contents.push({
        kind: OpKind.DELEGATION,
        source: address,
        counter: (counter + 1 + contents.length).toString(),
        delegate: transaction.recipient,
        ...transactionFees,
      })
      break
    }
    case 'undelegate': {
      // we undelegate as there's no "delegate" field
      // OpKind is still "DELEGATION"
      type = 'UNDELEGATE'
      contents.push({
        kind: OpKind.DELEGATION,
        source: address,
        counter: (counter + 1).toString(),
        ...transactionFees,
      })
      break
    }
    case 'stake':
    case 'unstake':
    case 'finalize_unstake': {
      const typeMap = {
        stake: 'STAKE',
        unstake: 'UNSTAKE',
        finalize_unstake: 'FINALIZE_UNSTAKE',
      } as const
      type = typeMap[transaction.type]
      contents.push({
        kind: OpKind.TRANSACTION,
        amount: transaction.type === 'finalize_unstake' ? '0' : transaction.amount.toString(),
        destination: address,
        source: address,
        counter: (counter + 1 + contents.length).toString(),
        parameters: { entrypoint: transaction.type, value: { prim: 'Unit' } },
        ...transactionFees,
      })
      break
    }
    default:
      throw new UnsupportedTransactionMode('unsupported mode', { mode: transaction.type })
  }

  return { type, contents }
}

/**
 * Return transaction in raw encoded format (i.e. hexa)
 */
export async function rawEncode(
  config: TezosCoinConfig,
  contents: OperationContents[]
): Promise<string> {
  const tezosToolkit = getTezosToolkit(config)

  const block = await tezosToolkit.rpc.getBlock()
  const forgedBytes = await tezosToolkit.rpc.forgeOperations({
    branch: block.hash,
    contents,
  })

  // 0x03 is a conventional prefix (aka a watermark) for tezos transactions
  return Buffer.concat([Buffer.from('03', 'hex'), Buffer.from(forgedBytes, 'hex')]).toString('hex')
}
