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

import type { RawOperation, StellarMemo } from '../types'
import type {
  AssetInfo,
  Block,
  BlockInfo,
  BlockOperation,
  BlockTransaction,
} from '@ledgerhq/coin-module-framework/api/index'
import BigNumber from 'bignumber.js'
import { fetchAllLedgerOperations, fetchLedgerRecord } from '../network'
import { decodeMemo, parseSequence } from '../network/serialization'
import { parseAPIValue } from './common'
import { assertUnreachable } from './utils'

const NATIVE_ASSET: AssetInfo = { type: 'native', name: 'XLM' }

/**
 * Horizon `type` strings that `getBlock` maps into `BlockOperation` entries.
 *
 * Same subset as user-facing history in `rawOperationsToOperations` (serialization);
 * other operation kinds are ignored until a later iteration expands coverage.
 */
const SUPPORTED_GETBLOCK_OP_TYPES = new Set<string>([
  'create_account',
  'payment',
  'path_payment_strict_send',
  'path_payment_strict_receive',
  'change_trust',
])

type RawPathStrictSendOp = Extract<RawOperation, { type: 'path_payment_strict_send' }>
type RawPathStrictReceiveOp = Extract<RawOperation, { type: 'path_payment_strict_receive' }> & {
  send_max?: string
}

/** Horizon ops mapped by `getBlock`; kept in sync with `SUPPORTED_GETBLOCK_OP_TYPES`. */
type SupportedGetBlockOperation = Extract<
  RawOperation,
  | { type: 'payment' }
  | { type: 'create_account' }
  | { type: 'change_trust' }
  | { type: 'path_payment_strict_send' }
  | { type: 'path_payment_strict_receive' }
>

function pathStrictDualLegTransfers(params: {
  fromAddr: string | undefined
  toAddr: string | undefined
  sourceAsset: AssetInfo
  destAsset: AssetInfo
  sourceStroops: bigint
  destStroops: bigint
}): BlockOperation[] {
  const { fromAddr, toAddr, sourceAsset, destAsset, sourceStroops, destStroops } = params
  const ops: BlockOperation[] = []
  if (fromAddr && sourceStroops !== 0n) {
    ops.push({
      type: 'transfer',
      address: fromAddr,
      ...(toAddr && { peer: toAddr }),
      asset: sourceAsset,
      amount: -sourceStroops,
    })
  }
  if (toAddr && destStroops !== 0n) {
    ops.push({
      type: 'transfer',
      address: toAddr,
      ...(fromAddr && { peer: fromAddr }),
      asset: destAsset,
      amount: destStroops,
    })
  }
  return ops
}

function toStroops(amountStr: string | undefined): bigint {
  if (!amountStr) {
    return 0n
  }
  return BigInt(parseAPIValue(amountStr).integerValue(BigNumber.ROUND_FLOOR).toFixed(0))
}

function assetFromHorizonFields(
  assetType: string | undefined,
  assetCode: string | undefined,
  assetIssuer: string | undefined
): AssetInfo {
  if (!assetType || assetType === 'native') {
    return NATIVE_ASSET
  }
  return {
    type: 'token',
    assetReference: assetCode ?? '',
    assetOwner: assetIssuer ?? '',
  }
}

function isSupportedGetBlockOperation(op: RawOperation): op is SupportedGetBlockOperation {
  return SUPPORTED_GETBLOCK_OP_TYPES.has(op.type)
}

function orderTransactionHashes(rawOps: RawOperation[]): string[] {
  const order: string[] = []
  const seen = new Set<string>()
  for (const op of rawOps) {
    const h = op.transaction_hash
    if (!seen.has(h)) {
      seen.add(h)
      order.push(h)
    }
  }
  return order
}

function groupOpsByHash(rawOps: RawOperation[]): Map<string, RawOperation[]> {
  const map = new Map<string, RawOperation[]>()
  for (const op of rawOps) {
    const h = op.transaction_hash
    const list = map.get(h)
    if (list) list.push(op)
    else map.set(h, [op])
  }
  return map
}

function mapPaymentLikeTransfer(
  fromAddr: string | undefined,
  toAddr: string | undefined,
  amountStr: string | undefined,
  asset: AssetInfo
): BlockOperation[] {
  const amount = toStroops(amountStr)
  if (amount === 0n) {
    return []
  }
  const ops: BlockOperation[] = []
  if (fromAddr) {
    ops.push({
      type: 'transfer',
      address: fromAddr,
      ...(toAddr && { peer: toAddr }),
      asset,
      amount: -amount,
    })
  }
  if (toAddr) {
    ops.push({
      type: 'transfer',
      address: toAddr,
      ...(fromAddr && { peer: fromAddr }),
      asset,
      amount,
    })
  }
  return ops
}

function blockOperationsPathStrictSend(op: RawPathStrictSendOp): BlockOperation[] {
  const destAsset = assetFromHorizonFields(op.asset_type, op.asset_code, op.asset_issuer)
  const sourceAsset = assetFromHorizonFields(
    op.source_asset_type,
    op.source_asset_code,
    op.source_asset_issuer
  )
  return pathStrictDualLegTransfers({
    fromAddr: op.from,
    toAddr: op.to,
    sourceAsset,
    destAsset,
    sourceStroops: toStroops(op.source_amount),
    destStroops: toStroops(op.amount),
  })
}

function blockOperationsPathStrictReceive(op: RawPathStrictReceiveOp): BlockOperation[] {
  const destAsset = assetFromHorizonFields(op.asset_type, op.asset_code, op.asset_issuer)
  const sourceAsset = assetFromHorizonFields(
    op.source_asset_type,
    op.source_asset_code,
    op.source_asset_issuer
  )
  // Horizon reports actual source debit in `source_amount`; `send_max` is only the envelope cap.
  const sourceStroops = toStroops(op.source_amount ?? op.send_max)
  return pathStrictDualLegTransfers({
    fromAddr: op.from,
    toAddr: op.to,
    sourceAsset,
    destAsset,
    sourceStroops,
    destStroops: toStroops(op.amount),
  })
}

type TxContext = {
  memo: StellarMemo | undefined
  sequence: string | undefined
  feeCharged: string
}

function mapSupportedOperationToBlockOperations(
  op: SupportedGetBlockOperation,
  txCtx: TxContext
): BlockOperation[] {
  if (op.type === 'payment') {
    const asset = assetFromHorizonFields(op.asset_type, op.asset_code, op.asset_issuer)
    const toAddr = op.to_muxed || op.to
    return mapPaymentLikeTransfer(op.from, toAddr, op.amount, asset)
  }
  if (op.type === 'create_account') {
    return mapPaymentLikeTransfer(op.funder, op.account, op.starting_balance, NATIVE_ASSET)
  }
  if (op.type === 'change_trust') {
    const isOptOut = new BigNumber(op.limit || '0').eq(0)
    // Align with listOperations: assetAmount = fee_charged (not the trustline limit)
    return [
      {
        type: 'other',
        ledgerOpType: isOptOut ? 'OPT_OUT' : 'OPT_IN',
        assetAmount: op.asset_code ? txCtx.feeCharged : undefined,
        ...(txCtx.memo && { memo: txCtx.memo }),
        ...(txCtx.sequence && { sequence: txCtx.sequence }),
      },
    ]
  }
  if (op.type === 'path_payment_strict_send') {
    return blockOperationsPathStrictSend(op)
  }
  if (op.type === 'path_payment_strict_receive') {
    return blockOperationsPathStrictReceive(op)
  }
  return assertUnreachable(op)
}

async function blockTransactionForHash(
  hash: string,
  ops: RawOperation[]
): Promise<BlockTransaction | null> {
  if (ops.length === 0) return null
  const failed = !ops[0].transaction_successful

  const tx = await ops[0].transaction()
  const txCtx: TxContext = {
    memo: decodeMemo(tx),
    sequence: parseSequence(tx.source_account_sequence),
    feeCharged: new BigNumber(tx.fee_charged || '0').toString(),
  }

  let blockOperations: BlockOperation[] = []
  if (!failed) {
    for (const op of ops) {
      if (isSupportedGetBlockOperation(op)) {
        blockOperations = blockOperations.concat(mapSupportedOperationToBlockOperations(op, txCtx))
      }
    }
  }

  const fees = BigInt(tx.fee_charged || '0')
  const feesPayer = tx.fee_account || tx.source_account

  return {
    hash,
    failed,
    fees,
    ...(feesPayer ? { feesPayer } : {}),
    operations: blockOperations,
  }
}

async function buildBlockTransactions(rawOps: RawOperation[]): Promise<BlockTransaction[]> {
  const order = orderTransactionHashes(rawOps)
  const byHash = groupOpsByHash(rawOps)
  const out: BlockTransaction[] = []

  for (const hash of order) {
    // `order` and `byHash` are built from the same `rawOps`; every ordered hash has a non-empty op list.
    const ops = byHash.get(hash)!
    const row = await blockTransactionForHash(hash, ops)
    if (row) {
      out.push(row)
    }
  }

  return out
}

/**
 * Returns the Stellar closed ledger at `height` (ledger sequence) with mapped
 * `BlockTransaction` entries (see `SUPPORTED_GETBLOCK_OP_TYPES` for which Horizon
 * operations are included).
 *
 * Only supported payment / path / trust / create-account operations contribute
 * balance operations; other Horizon operation types are skipped for successful
 * transactions. Failed transactions still appear with `failed: true`, fees
 * populated, and empty `operations`.
 */
export async function getBlock(height: number): Promise<Block> {
  if (!Number.isSafeInteger(height) || height <= 0) {
    throw new Error(`getBlock: height must be a positive integer, got ${height}`)
  }

  const [ledger, rawOps] = await Promise.all([
    fetchLedgerRecord(height),
    fetchAllLedgerOperations(height),
  ])

  let parent: BlockInfo['parent']
  if (height > 1) {
    const prevHash = (ledger as { prev_hash?: string }).prev_hash
    if (prevHash) {
      parent = { height: height - 1, hash: prevHash }
    } else {
      const parentLedger = await fetchLedgerRecord(height - 1)
      parent = { height: parentLedger.sequence, hash: parentLedger.hash }
    }
  }

  const info: BlockInfo = {
    height: ledger.sequence,
    hash: ledger.hash,
    time: new Date(ledger.closed_at),
    ...(parent && { parent }),
  }

  const transactions = await buildBlockTransactions(rawOps)
  return { info, transactions }
}
