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

import { BadResponseError } from '@stellar/stellar-sdk'

/** Horizon RFC 7807 problem+json body (subset used by coin-stellar). */
export type HorizonErrorBody = {
  type?: string
  title?: string
  status?: number
  detail?: string
  extras?: {
    envelope_xdr?: string
    result_xdr?: string
    result_codes?: {
      transaction?: string
      operations?: string[]
    }
    invalid_field?: string
    reason?: string
  }
}

const GENERIC_TRANSACTION_FAILED_DETAIL =
  /The transaction failed when submitted to the Stellar network/i

/** Horizon `extras.result_codes.transaction` → one-line description (Stellar docs). */
const HORIZON_TRANSACTION_RESULT_CODE_DOCUMENTATION: Record<string, string> = {
  tx_success: 'The transaction succeeded.',
  tx_failed: 'One of the operations failed (none were applied).',
  tx_too_early: 'The ledger closeTime was before the minTime.',
  tx_too_late: 'The ledger closeTime was after the maxTime.',
  tx_missing_operation: 'No operation was specified.',
  tx_bad_seq: 'Sequence number does not match source account.',
  tx_bad_auth: 'Too few valid signatures / wrong network.',
  tx_insufficient_balance: 'Fee would bring account below reserve.',
  tx_no_source_account: 'Source account not found.',
  tx_insufficient_fee: 'Fee is too small.',
  tx_bad_auth_extra: 'Unused signatures attached to transaction.',
  tx_internal_error: 'An unknown error occurred.',
  tx_fee_bump_inner_success: 'Fee bump inner transaction succeeded.',
  tx_fee_bump_inner_failed: 'Fee bump inner transaction failed.',
  tx_not_supported: 'Transaction type not supported.',
  tx_bad_sponsorship: 'Sponsorship is invalid.',
  tx_bad_min_seq_age_or_gap: 'Minimum sequence age or gap precondition failed.',
  tx_malformed: 'Transaction is malformed.',
  tx_soroban_invalid: 'Soroban transaction is invalid.',
}

/** Horizon `extras.result_codes.operations[]` → one-line description (Stellar docs). */
const HORIZON_OPERATION_RESULT_CODE_DOCUMENTATION: Record<string, string> = {
  op_inner: 'The inner object result is valid and the operation was a success.',
  op_bad_auth:
    'There are too few valid signatures, or the transaction was submitted to the wrong network.',
  op_no_source_account: 'The source account was not found.',
  op_no_account: 'The source account was not found.',
  op_not_supported: 'The operation is not supported at this time.',
  op_too_many_subentries: 'Max number of subentries (1000) already reached.',
  op_exceeded_work_limit: 'Operation did too much work.',
  op_malformed: 'The operation input is invalid.',
  op_underfunded:
    'The source account does not have enough balance to send the payment amount while maintaining its minimum reserve.',
  op_src_no_trust:
    'The source account does not have a trustline for the asset it is trying to send.',
  op_src_not_authorized: 'The source account is not authorized to send this asset.',
  op_no_destination: 'The destination account does not exist.',
  op_no_trust: 'The destination account does not have a trustline for the asset being sent.',
  op_not_authorized: 'The destination account is not authorized to hold this asset.',
  op_line_full:
    'The destination account does not have sufficient limits to receive the amount and still satisfy its buying liabilities.',
  op_no_issuer: 'The issuer of the asset does not exist.',
}

/** Operation result codes that mark success for a prior op in a multi-op failure list. */
const HORIZON_OPERATION_SUCCESS_RESULT_CODES = new Set<string>(['op_inner'])

function failingOperationCode(operations: string[] | undefined): string | undefined {
  return operations?.find((code) => !HORIZON_OPERATION_SUCCESS_RESULT_CODES.has(code))
}

export function isHorizonErrorBody(data: unknown): data is HorizonErrorBody {
  return !!(
    data &&
    typeof data === 'object' &&
    'title' in data &&
    typeof data.title === 'string' &&
    'status' in data &&
    typeof data.status === 'number'
  )
}

/**
 * Horizon failures are usually {@link BadResponseError} with `response` = problem+json body.
 * Axios rejects first; the SDK forwards it unchanged, so the body lives on `error.response.data`.
 */
export function getHorizonErrorBody(error: unknown): HorizonErrorBody | null {
  if (error instanceof BadResponseError && isHorizonErrorBody(error.response)) {
    return error.response
  }
  if (error && typeof error === 'object' && 'response' in error) {
    const data = (error as { response?: { data?: unknown } }).response?.data
    if (isHorizonErrorBody(data)) {
      return data
    }
  }
  return null
}

function lookupOperationDocumentation(operationCode: string | undefined): string | undefined {
  if (!operationCode) {
    return undefined
  }
  return HORIZON_OPERATION_RESULT_CODE_DOCUMENTATION[operationCode]
}

function lookupTransactionDocumentation(transactionCode: string | undefined): string | undefined {
  if (!transactionCode) {
    return undefined
  }
  return HORIZON_TRANSACTION_RESULT_CODE_DOCUMENTATION[transactionCode]
}

function detailDocumentation(body: HorizonErrorBody): string | undefined {
  const detail = body.detail?.trim()
  if (!detail || GENERIC_TRANSACTION_FAILED_DETAIL.test(detail)) {
    return undefined
  }
  return detail
}

function extrasReasonDocumentation(body: HorizonErrorBody): string | undefined {
  const reason = body.extras?.reason?.trim()
  if (!reason) {
    return undefined
  }
  const invalidField = body.extras?.invalid_field?.trim()
  return invalidField ? `${reason} (field: ${invalidField})` : reason
}

/**
 * Best-effort human-readable summary for logs (`errorExtras.documentationSummary`).
 * Prefers operation result codes, then transaction codes, then Horizon `detail` / `extras.reason`.
 */
export function documentationSummaryFromHorizonBody(body: HorizonErrorBody): string {
  const transactionCode = body.extras?.result_codes?.transaction
  const operationCode = failingOperationCode(body.extras?.result_codes?.operations)

  const operationSummary = lookupOperationDocumentation(operationCode)
  if (operationSummary) {
    return operationSummary
  }

  const transactionSummary = lookupTransactionDocumentation(transactionCode)
  if (transactionSummary && transactionCode !== 'tx_failed') {
    return transactionSummary
  }

  const detail = detailDocumentation(body)
  if (detail) {
    return detail
  }

  const extrasReason = extrasReasonDocumentation(body)
  if (extrasReason) {
    return extrasReason
  }

  if (transactionSummary) {
    return transactionSummary
  }

  const title = body.title?.trim()
  if (title) {
    return title
  }

  return 'Unknown Horizon error.'
}
