package rpcclient

import (
	"errors"
	"fmt"
	"strings"
)

var (
	// ErrBackendVersion is returned when running against a bitcoind or
	// btcd that is older than the minimum version supported by the
	// rpcclient.
	ErrBackendVersion = errors.New("backend version too low")

	// ErrInvalidParam is returned when the caller provides an invalid
	// parameter to an RPC method.
	ErrInvalidParam = errors.New("invalid param")

	// ErrUndefined is used when an error returned is not recognized. We
	// should gradually increase our error types to avoid returning this
	// error.
	ErrUndefined = errors.New("undefined")
)

// BitcoindRPCErr represents an error returned by bitcoind's RPC server.
type BitcoindRPCErr uint32

// This section defines all possible errors or reject reasons returned from
// bitcoind's `sendrawtransaction` or `testmempoolaccept` RPC.
const (
	// ErrMissingInputsOrSpent is returned when calling
	// `sendrawtransaction` with missing inputs.
	ErrMissingInputsOrSpent BitcoindRPCErr = iota

	// ErrMaxBurnExceeded is returned when calling `sendrawtransaction`
	// with exceeding, falling short of, and equaling maxburnamount.
	ErrMaxBurnExceeded

	// ErrMaxFeeExceeded can happen when passing a signed tx to
	// `testmempoolaccept`, but the tx pays more fees than specified.
	ErrMaxFeeExceeded

	// ErrTxAlreadyKnown is used in the `reject-reason` field of
	// `testmempoolaccept` when a transaction is already in the blockchain.
	ErrTxAlreadyKnown

	// ErrTxAlreadyConfirmed is returned as an error from
	// `sendrawtransaction` when a transaction is already in the
	// blockchain.
	ErrTxAlreadyConfirmed

	// ErrMempoolConflict happens when RBF is not enabled yet the
	// transaction conflicts with an unconfirmed tx.  .
	//
	// NOTE: RBF rule 1.
	ErrMempoolConflict

	// ErrReplacementAddsUnconfirmed is returned when a transaction adds
	// new unconfirmed inputs.
	//
	// NOTE: RBF rule 2.
	ErrReplacementAddsUnconfirmed

	// ErrInsufficientFee is returned when fee rate used or fees paid
	// doesn't meet the requirements.
	//
	// NOTE: RBF rule 3 or 4.
	ErrInsufficientFee

	// ErrTooManyReplacements is returned when a transaction causes too
	// many transactions being replaced. This is set by
	// `MAX_REPLACEMENT_CANDIDATES` in `bitcoind` and defaults to 100.
	//
	// NOTE: RBF rule 5.
	ErrTooManyReplacements

	// ErrMempoolMinFeeNotMet is returned when the transaction doesn't meet
	// the minimum relay fee.
	ErrMempoolMinFeeNotMet

	// ErrConflictingTx is returned when a transaction that spends
	// conflicting tx outputs that are rejected.
	ErrConflictingTx

	// ErrEmptyOutput is returned when a transaction has no outputs.
	ErrEmptyOutput

	// ErrEmptyInput is returned when a transaction has no inputs.
	ErrEmptyInput

	// ErrTxTooSmall is returned when spending a tiny transaction(in
	// non-witness bytes) that is disallowed.
	//
	// NOTE: ErrTxTooLarge must be put after ErrTxTooSmall because it's a
	// subset of ErrTxTooSmall. Otherwise, if bitcoind returns
	// `tx-size-small`, it will be matched to ErrTxTooLarge.
	ErrTxTooSmall

	// ErrDuplicateInput is returned when a transaction has duplicate
	// inputs.
	ErrDuplicateInput

	// ErrEmptyPrevOut is returned when a non-coinbase transaction has
	// coinbase-like outpoint.
	ErrEmptyPrevOut

	// ErrBelowOutValue is returned when a transaction's output value is
	// greater than its input value.
	ErrBelowOutValue

	// ErrNegativeOutput is returned when a transaction has negative output
	// value.
	ErrNegativeOutput

	// ErrLargeOutput is returned when a transaction has too large output
	// value.
	ErrLargeOutput

	// ErrLargeTotalOutput is returned when a transaction has too large sum
	// of output values.
	ErrLargeTotalOutput

	// ErrScriptVerifyFlag is returned when there is invalid OP_IF
	// construction.
	ErrScriptVerifyFlag

	// ErrTooManySigOps is returned when a transaction has too many sigops.
	ErrTooManySigOps

	// ErrInvalidOpcode is returned when a transaction has invalid OP
	// codes.
	ErrInvalidOpcode

	// ErrTxAlreadyInMempool is returned when a transaction is in the
	// mempool.
	ErrTxAlreadyInMempool

	// ErrMissingInputs is returned when a transaction has missing inputs,
	// that never existed or only existed once in the past.
	ErrMissingInputs

	// ErrOversizeTx is returned when a transaction is too large.
	ErrOversizeTx

	// ErrCoinbaseTx is returned when the transaction is coinbase tx.
	ErrCoinbaseTx

	// ErrNonStandardVersion is returned when the transactions are not
	// standard - a version currently non-standard.
	ErrNonStandardVersion

	// ErrNonStandardScript is returned when the transactions are not
	// standard - non-standard script.
	ErrNonStandardScript

	// ErrBareMultiSig is returned when the transactions are not standard -
	// bare multisig script (2-of-3).
	ErrBareMultiSig

	// ErrScriptSigNotPushOnly is returned when the transactions are not
	// standard - not-pushonly scriptSig.
	ErrScriptSigNotPushOnly

	// ErrScriptSigSize is returned when the transactions are not standard
	// - too large scriptSig (>1650 bytes).
	ErrScriptSigSize

	// ErrTxTooLarge is returned when the transactions are not standard -
	// too large tx size.
	ErrTxTooLarge

	// ErrDust is returned when the transactions are not standard - output
	// too small.
	ErrDust

	// ErrMultiOpReturn is returned when the transactions are not standard
	// - muiltiple OP_RETURNs.
	ErrMultiOpReturn

	// ErrNonFinal is returned when spending a timelocked transaction that
	// hasn't expired yet.
	ErrNonFinal

	// ErrNonBIP68Final is returned when a transaction that is locked by
	// BIP68 sequence logic and not expired yet.
	ErrNonBIP68Final

	// ErrSameNonWitnessData is returned when another tx with the same
	// non-witness data is already in the mempool. For instance, these two
	// txns share the same `txid` but different `wtxid`.
	ErrSameNonWitnessData

	// ErrNonMandatoryScriptVerifyFlag is returned when passing a raw tx to
	// `testmempoolaccept`, which gives the error followed by (Witness
	// program hash mismatch).
	ErrNonMandatoryScriptVerifyFlag

	// errSentinel is used to indicate the end of the error list. This
	// should always be the last error code.
	errSentinel
)

// Error implements the error interface. It returns the error message defined
// in `bitcoind`.

// Some of the dashes used in the original error string is removed, e.g.
// "missing-inputs" is now "missing inputs". This is ok since we will normalize
// the errors before matching.
//
// references:
// - https://github.com/bitcoin/bitcoin/blob/master/test/functional/rpc_rawtransaction.py#L342
// - https://github.com/bitcoin/bitcoin/blob/master/test/functional/data/invalid_txs.py
// - https://github.com/bitcoin/bitcoin/blob/master/test/functional/mempool_accept.py
// - https://github.com/bitcoin/bitcoin/blob/master/test/functional/mempool_accept_wtxid.py
// - https://github.com/bitcoin/bitcoin/blob/master/test/functional/mempool_dust.py
// - https://github.com/bitcoin/bitcoin/blob/master/test/functional/mempool_limit.py
// - https://github.com/bitcoin/bitcoin/blob/master/src/validation.cpp
func (r BitcoindRPCErr) Error() string {
	switch r {
	case ErrMissingInputsOrSpent:
		return "bad-txns-inputs-missingorspent"

	case ErrMaxBurnExceeded:
		return "Unspendable output exceeds maximum configured by user (maxburnamount)"

	case ErrMaxFeeExceeded:
		return "max-fee-exceeded"

	case ErrTxAlreadyKnown:
		return "txn-already-known"

	case ErrTxAlreadyConfirmed:
		return "Transaction already in block chain"

	case ErrMempoolConflict:
		return "txn mempool conflict"

	case ErrReplacementAddsUnconfirmed:
		return "replacement adds unconfirmed"

	case ErrInsufficientFee:
		return "insufficient fee"

	case ErrTooManyReplacements:
		return "too many potential replacements"

	case ErrMempoolMinFeeNotMet:
		return "mempool min fee not met"

	case ErrConflictingTx:
		return "bad txns spends conflicting tx"

	case ErrEmptyOutput:
		return "bad txns vout empty"

	case ErrEmptyInput:
		return "bad txns vin empty"

	case ErrTxTooSmall:
		return "tx size small"

	case ErrDuplicateInput:
		return "bad txns inputs duplicate"

	case ErrEmptyPrevOut:
		return "bad txns prevout null"

	case ErrBelowOutValue:
		return "bad txns in belowout"

	case ErrNegativeOutput:
		return "bad txns vout negative"

	case ErrLargeOutput:
		return "bad txns vout toolarge"

	case ErrLargeTotalOutput:
		return "bad txns txouttotal toolarge"

	case ErrScriptVerifyFlag:
		return "mandatory script verify flag failed"

	case ErrTooManySigOps:
		return "bad txns too many sigops"

	case ErrInvalidOpcode:
		return "disabled opcode"

	case ErrTxAlreadyInMempool:
		return "txn already in mempool"

	case ErrMissingInputs:
		return "missing inputs"

	case ErrOversizeTx:
		return "bad txns oversize"

	case ErrCoinbaseTx:
		return "coinbase"

	case ErrNonStandardVersion:
		return "version"

	case ErrNonStandardScript:
		return "scriptpubkey"

	case ErrBareMultiSig:
		return "bare multisig"

	case ErrScriptSigNotPushOnly:
		return "scriptsig not pushonly"

	case ErrScriptSigSize:
		return "scriptsig size"

	case ErrTxTooLarge:
		return "tx size"

	case ErrDust:
		return "dust"

	case ErrMultiOpReturn:
		return "multi op return"

	case ErrNonFinal:
		return "non final"

	case ErrNonBIP68Final:
		return "non BIP68 final"

	case ErrSameNonWitnessData:
		return "txn-same-nonwitness-data-in-mempool"

	case ErrNonMandatoryScriptVerifyFlag:
		return "non-mandatory-script-verify-flag"
	}

	return "unknown error"
}

// BitcoindErrMap is a map of additional errors bitcoind can throw that are
// version dependent (e.g. versions up to v29 return the error as specified in
// `Error()` above, while versions v30 and beyond return the error as mapped
// here. We add a new map for errors that were simply renamed but have the same
// semantic meaning. New errors should be added above as new error constants.
var BitcoindErrMap = map[string]error{
	// The error message was changed in
	// https://github.com/bitcoin/bitcoin/pull/33050 which will be included
	// in bitcoind v30.0 and beyond.
	"mempool script verify flag failed": ErrNonMandatoryScriptVerifyFlag,

	// The error message was changed in
	// https://github.com/bitcoin/bitcoin/pull/33183 which will also be
	// included in bitcoind v30.0 and beyond.
	"block script verify flag failed": ErrScriptVerifyFlag,
}

// BtcdErrMap takes the errors returned from btcd's `testmempoolaccept` and
// `sendrawtransaction` RPCs and map them to the errors defined above, which
// are results from calling either `testmempoolaccept` or `sendrawtransaction`
// in `bitcoind`.
//
// Errors not mapped in `btcd`:
//   - deployment error from `validateSegWitDeployment`.
//   - the error when total inputs is higher than max allowed value from
//     `CheckTransactionInputs`.
//   - the error when total outputs is higher than total inputs from
//     `CheckTransactionInputs`.
//   - errors from `CalcSequenceLock`.
//
// NOTE: This is not an exhaustive list of errors, but it covers the
// usage case of LND.
//
//nolint:lll
var BtcdErrMap = map[string]error{
	// BIP125 related errors.
	//
	// When fee rate used or fees paid doesn't meet the requirements.
	"replacement transaction has an insufficient fee rate":     ErrInsufficientFee,
	"replacement transaction has an insufficient absolute fee": ErrInsufficientFee,

	// When a transaction causes too many transactions being replaced. This
	// is set by `MAX_REPLACEMENT_CANDIDATES` in `bitcoind` and defaults to
	// 100.
	"replacement transaction evicts more transactions than permitted": ErrTooManyReplacements,

	// When a transaction adds new unconfirmed inputs.
	"replacement transaction spends new unconfirmed input": ErrReplacementAddsUnconfirmed,

	// A transaction that spends conflicting tx outputs that are rejected.
	"replacement transaction spends parent transaction": ErrConflictingTx,

	// A transaction that conflicts with an unconfirmed tx. Happens when
	// RBF is not enabled.
	"output already spent in mempool": ErrMempoolConflict,

	// A transaction with no outputs.
	"transaction has no outputs": ErrEmptyOutput,

	// A transaction with no inputs.
	"transaction has no inputs": ErrEmptyInput,

	// A transaction with duplicate inputs.
	"transaction contains duplicate inputs": ErrDuplicateInput,

	// A non-coinbase transaction with coinbase-like outpoint.
	"transaction input refers to previous output that is null": ErrEmptyPrevOut,

	// A transaction pays too little fee.
	"fees which is under the required amount":               ErrMempoolMinFeeNotMet,
	"has insufficient priority":                             ErrInsufficientFee,
	"has been rejected by the rate limiter due to low fees": ErrInsufficientFee,

	// A transaction with negative output value.
	"transaction output has negative value": ErrNegativeOutput,

	// A transaction with too large output value.
	"transaction output value is higher than max allowed value": ErrLargeOutput,

	// A transaction with too large sum of output values.
	"total value of all transaction outputs exceeds max allowed value": ErrLargeTotalOutput,

	// A transaction with too many sigops.
	"sigop cost is too hight": ErrTooManySigOps,

	// A transaction already in the blockchain.
	"database contains entry for spent tx output": ErrTxAlreadyKnown,
	"transaction already exists in blockchain":    ErrTxAlreadyConfirmed,

	// A transaction in the mempool.
	//
	// NOTE: For btcd v0.24.2 and beyond, the error message is "already
	// have transaction in mempool".
	"already have transaction": ErrTxAlreadyInMempool,

	// A transaction with missing inputs, that never existed or only
	// existed once in the past.
	"either does not exist or has already been spent": ErrMissingInputs,
	"orphan transaction":                              ErrMissingInputs,

	// A really large transaction.
	"serialized transaction is too big": ErrOversizeTx,

	// A coinbase transaction.
	"transaction is an invalid coinbase": ErrCoinbaseTx,

	// Some nonstandard transactions - a version currently non-standard.
	"transaction version": ErrNonStandardVersion,

	// Some nonstandard transactions - non-standard script.
	"non-standard script form": ErrNonStandardScript,
	"has a non-standard input": ErrNonStandardScript,

	// Some nonstandard transactions - bare multisig script
	// (2-of-3).
	"milti-signature script": ErrBareMultiSig,

	// Some nonstandard transactions - not-pushonly scriptSig.
	"signature script is not push only": ErrScriptSigNotPushOnly,

	// Some nonstandard transactions - too large scriptSig (>1650
	// bytes).
	"signature script size is larger than max allowed": ErrScriptSigSize,

	// Some nonstandard transactions - too large tx size.
	"weight of transaction is larger than max allowed": ErrTxTooLarge,

	// Some nonstandard transactions - output too small.
	"payment is dust": ErrDust,

	// Some nonstandard transactions - muiltiple OP_RETURNs.
	"more than one transaction output in a nulldata script": ErrMultiOpReturn,

	// A timelocked transaction.
	"transaction is not finalized":               ErrNonFinal,
	"tried to spend coinbase transaction output": ErrNonFinal,

	// A transaction that is locked by BIP68 sequence logic.
	"transaction's sequence locks on inputs not met": ErrNonBIP68Final,

	// TODO(yy): find/return the following errors in `btcd`.
	//
	// A tiny transaction(in non-witness bytes) that is disallowed.
	// "unmatched btcd error 1": ErrTxTooSmall,
	// "unmatched btcd error 2": ErrScriptVerifyFlag,
	// // A transaction with invalid OP codes.
	// "unmatched btcd error 3": ErrInvalidOpcode,
	// // Minimally-small transaction(in non-witness bytes) that is
	// // allowed.
	// "unmatched btcd error 4": ErrSameNonWitnessData,
}

// MapRPCErr takes an error returned from calling RPC methods from various
// chain backend and map it to an defined error here. It uses the `BtcdErrMap`
// defined above, whose keys are btcd error strings and values are errors made
// from bitcoind error strings.
//
// NOTE: we assume neutrino shares the same error strings as btcd.
func MapRPCErr(rpcErr error) error {
	// Iterate the btcd error map and find the matching error.
	for btcdErr, err := range BtcdErrMap {
		// Match it against btcd's error first.
		if matchErrStr(rpcErr, btcdErr) {
			return err
		}
	}

	// Also check the bitcoind error map, which is used for bitcoind version
	// dependent errors.
	for bitcoindErr, err := range BitcoindErrMap {
		// Match it against bitcoind's error.
		if matchErrStr(rpcErr, bitcoindErr) {
			return err
		}
	}

	// If not found, try to match it against bitcoind's error.
	for i := uint32(0); i < uint32(errSentinel); i++ {
		err := BitcoindRPCErr(i)
		if matchErrStr(rpcErr, err.Error()) {
			return err
		}
	}

	// If not matched, return the original error wrapped.
	return fmt.Errorf("%w: %v", ErrUndefined, rpcErr)
}

// matchErrStr takes an error returned from RPC client and matches it against
// the specified string. If the expected string pattern is found in the error
// passed, return true. Both the error strings are normalized before matching.
func matchErrStr(err error, s string) bool {
	// Replace all dashes found in the error string with spaces.
	strippedErrStr := strings.ReplaceAll(err.Error(), "-", " ")

	// Replace all dashes found in the error string with spaces.
	strippedMatchStr := strings.ReplaceAll(s, "-", " ")

	// Match against the lowercase.
	return strings.Contains(
		strings.ToLower(strippedErrStr),
		strings.ToLower(strippedMatchStr),
	)
}
