// Balance arithmetic for the transaction confirmation screen.
//
// Pure: no DOM, no network, no state. Everything is exact integer math on
// 18-decimal fixed point (wei for ETH), so it can be unit tested directly
// instead of through the confirmation view. The caller maps the returned
// codes to the reserved message elements on the screen.
//
// Human decimal strings ("1.25") are scaled to 18 decimals for comparison.
// That scale is independent of a token's own decimals: both the amount and
// the token balance arrive as human decimal strings, so comparing them at a
// common scale is exact.

const { parseUnits } = require("ethers");

const SCALE_DECIMALS = 18;

// Whether the asynchronous fee estimate has arrived yet.
const FEE_PENDING = "pending";
const FEE_KNOWN = "known";
const FEE_UNAVAILABLE = "unavailable";

const CODES = {
    // The amount is not a non-negative number we can do exact arithmetic on.
    AMOUNT_INVALID: "amount-invalid",
    // ERC-20: the token amount exceeds the token balance.
    INSUFFICIENT_TOKEN: "insufficient-token",
    // ETH: the amount alone already exceeds the ETH balance.
    INSUFFICIENT_ETH: "insufficient-eth",
    // ETH: the amount fits, the amount plus the network fee does not.
    INSUFFICIENT_ETH_WITH_FEE: "insufficient-eth-with-fee",
    // ERC-20: the token balance covers the transfer, the ETH balance does
    // not cover the network fee it costs.
    INSUFFICIENT_ETH_FOR_FEE: "insufficient-eth-for-fee",
    // The fee estimate has not arrived yet.
    FEE_PENDING: "fee-pending",
    // The fee estimate failed. Unknown is never treated as zero.
    FEE_UNAVAILABLE: "fee-unavailable",
};

// The fee that must be reserved for a transaction, in wei: the amount the
// node will require, not the amount the transaction is expected to cost.
//
// A send that pins no fee fields is populated by ethers as a type-2
// (EIP-1559) transaction, and a node validates that against
// `value + gasLimit * maxFeePerGas`. ethers derives maxFeePerGas as
// `baseFeePerGas * 2 + maxPriorityFeePerGas`, so reserving `gasPrice`
// (roughly `baseFee + tip`) under-reserves by about `gasLimit * baseFee` and
// lets through a transaction the node then rejects with "insufficient funds
// for gas * price + value". gasPrice is the fallback only for a network that
// offers no type-2 pricing at all.
//
// Returns null when no usable price is available, which the caller must treat
// as a failed estimate rather than as a free transaction.
function feeReserveWei(gasLimit, feeData) {
    if (typeof gasLimit !== "bigint" || gasLimit < 0n) return null;
    const price = feeData?.maxFeePerGas ?? feeData?.gasPrice;
    if (typeof price !== "bigint" || price < 0n) return null;
    return gasLimit * price;
}

// What the transaction is expected to actually cost, in wei — not what must
// be reserved for it. A type-2 transaction is charged `baseFee + tip` per gas
// and refunded the rest of the cap, and `eth_gasPrice` reports roughly that,
// so gasPrice is the estimate and maxFeePerGas is the reserve. On a network
// with no type-2 pricing the two are the same number.
//
// Display only: nothing gates on this. Returns null on the same unusable
// inputs as feeReserveWei().
function feeEstimateWei(gasLimit, feeData) {
    if (typeof gasLimit !== "bigint" || gasLimit < 0n) return null;
    const price = feeData?.gasPrice ?? feeData?.maxFeePerGas;
    if (typeof price !== "bigint" || price < 0n) return null;
    return gasLimit * price;
}

// Scale a human decimal string to 18-decimal fixed point. Returns null when
// the value is not a decimal number or carries more precision than the scale
// can hold, which the caller must treat as unusable rather than as zero.
function toFixedPoint(value) {
    if (typeof value !== "string" && typeof value !== "number") return null;
    const text = String(value).trim();
    if (text === "") return null;
    try {
        return parseUnits(text, SCALE_DECIMALS);
    } catch {
        return null;
    }
}

// Validate a pending transfer against the balances that must cover it.
//
//   isErc20      — token transfer rather than a native ETH transfer
//   amount       — human decimal string being sent, non-negative. Anything
//                  else, a negative value included, is an unusable amount
//                  rather than an amount that passes every comparison.
//   ethBalance   — human decimal string, the sender's ETH balance
//   tokenBalance — human decimal string, the sender's token balance
//   feeStatus    — FEE_PENDING, FEE_KNOWN or FEE_UNAVAILABLE. Anything else
//                  is treated as FEE_UNAVAILABLE.
//   feeWei       — the fee reserve in wei from feeReserveWei(), as a
//                  non-negative bigint, when FEE_KNOWN. Any other value makes
//                  the fee unavailable rather than zero.
//
// Returns { canSend, codes }. Every code blocks sending: canSend is true
// only when nothing was found.
function validateTransfer({
    isErc20 = false,
    amount,
    ethBalance,
    tokenBalance,
    feeStatus = FEE_PENDING,
    feeWei = null,
} = {}) {
    const codes = [];

    const amountFp = toFixedPoint(amount);
    const ethFp = toFixedPoint(ethBalance) ?? 0n;

    // A negative amount parses to a valid bigint, so every comparison below
    // is trivially false and the send clears the screen — then dies at encode
    // time in parseEther(). Unusable, on the same footing as a malformed fee.
    if (amountFp === null || amountFp < 0n) {
        codes.push(CODES.AMOUNT_INVALID);
        return { canSend: false, codes };
    }

    // Fail closed. Anything that is not a usable fee under a recognised
    // status — a malformed feeWei, or a status this module does not know —
    // is an unavailable estimate, never a fee of zero. Every such input errs
    // in the direction that lets money out, so none of them is trusted.
    const known =
        feeStatus === FEE_KNOWN && typeof feeWei === "bigint" && feeWei >= 0n;
    let status = feeStatus;
    if (feeStatus === FEE_KNOWN && !known) status = FEE_UNAVAILABLE;
    if (status !== FEE_KNOWN && status !== FEE_PENDING) {
        status = FEE_UNAVAILABLE;
    }

    const feeFp = known ? feeWei : null;

    if (isErc20) {
        const tokenFp = toFixedPoint(tokenBalance) ?? 0n;
        if (amountFp > tokenFp) codes.push(CODES.INSUFFICIENT_TOKEN);
        if (feeFp !== null && feeFp > ethFp) {
            codes.push(CODES.INSUFFICIENT_ETH_FOR_FEE);
        }
    } else if (amountFp > ethFp) {
        codes.push(CODES.INSUFFICIENT_ETH);
    } else if (feeFp !== null && amountFp + feeFp > ethFp) {
        codes.push(CODES.INSUFFICIENT_ETH_WITH_FEE);
    }

    // An unknown fee is never assumed to be zero: sending stays blocked
    // until the estimate arrives, and stays blocked if it never does.
    if (status === FEE_PENDING) codes.push(CODES.FEE_PENDING);
    if (status === FEE_UNAVAILABLE) codes.push(CODES.FEE_UNAVAILABLE);

    return { canSend: codes.length === 0, codes };
}

module.exports = {
    CODES,
    FEE_PENDING,
    FEE_KNOWN,
    FEE_UNAVAILABLE,
    SCALE_DECIMALS,
    feeReserveWei,
    feeEstimateWei,
    toFixedPoint,
    validateTransfer,
};
