// Transaction confirmation view with inline password.
// Shows transaction details, warnings, errors. On Sign & Send,
// reads inline password, decrypts secret, signs and broadcasts.

const { parseEther, parseUnits, formatEther, Contract } = require("ethers");
const {
    $,
    showError,
    hideError,
    showView,
    addressTitle,
    escapeHtml,
    displaySymbol,
    renderAddressHtml,
    attachCopyHandlers,
    goBack,
    onViewLeave,
} = require("./helpers");
const { state } = require("../../shared/state");
const { getSignerForAddress } = require("../../shared/wallet");
const { decryptWithPassword } = require("../../shared/vault");
const { formatUsd, getPrice } = require("../../shared/prices");
const { getProvider } = require("../../shared/balances");
const {
    getLocalWarnings,
    getFullWarnings,
} = require("../../shared/addressWarnings");
const { ERC20_ABI, isBurnAddress } = require("../../shared/constants");
const {
    displayedDecimals,
    transferAmountUnits,
} = require("../../shared/transferAmount");
const {
    CODES,
    FEE_PENDING,
    FEE_KNOWN,
    FEE_UNAVAILABLE,
    feeReserveWei,
    feeEstimateWei,
    validateTransfer,
} = require("../../shared/txValidation");
const { log } = require("../../shared/log");
const makeBlockie = require("ethereum-blockies-base64");
const txStatus = require("./txStatus");

let pendingTx = null;
// Network fee for the transaction currently on screen. Reset by show() and
// filled in by estimateGas() when the estimate resolves or fails.
let feeStatus = FEE_PENDING;
let feeWei = null;

function restore() {
    const d = state.viewData;
    if (d && d.pendingTx) {
        show(d.pendingTx);
    }
}

function blockieHtml(address) {
    const src = makeBlockie(address);
    return `<img src="${escapeHtml(src)}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
}

function confirmAddressHtml(address, ensName, title) {
    const blockie = blockieHtml(address);
    return (
        `<div class="mb-1">${blockie}</div>` +
        renderAddressHtml(address, { title, ensName })
    );
}

function valueWithUsd(text, usdAmount) {
    if (usdAmount !== null && usdAmount !== undefined && !isNaN(usdAmount)) {
        return text + " (" + formatUsd(usdAmount) + ")";
    }
    return text;
}

function show(txInfo) {
    pendingTx = txInfo;
    feeStatus = FEE_PENDING;
    feeWei = null;

    const isErc20 = txInfo.token !== "ETH";
    // The raw symbol is the price-table key; the capped one is what the
    // screen says. Truncating before the lookup would silently drop the
    // price of any token whose symbol is long enough to be capped.
    const rawSymbol = isErc20 ? txInfo.tokenSymbol || "?" : "ETH";
    const symbol = displaySymbol(rawSymbol);

    // Transaction type
    if (isErc20) {
        $("confirm-type").textContent =
            "ERC-20 token transfer (" + symbol + ")";
    } else {
        $("confirm-type").textContent = "Native ETH transfer";
    }

    // Token contract section (ERC-20 only)
    const tokenSection = $("confirm-token-section");
    if (isErc20) {
        $("confirm-token-contract").innerHTML = renderAddressHtml(
            txInfo.token,
            {},
        );
        tokenSection.classList.remove("hidden");
        attachCopyHandlers(tokenSection);
    } else {
        tokenSection.classList.add("hidden");
    }

    // From (with blockie)
    const fromTitle = addressTitle(txInfo.from, state.wallets);
    $("confirm-from").innerHTML = confirmAddressHtml(
        txInfo.from,
        null,
        fromTitle,
    );

    // To (with blockie)
    const toTitle = addressTitle(txInfo.to, state.wallets);
    $("confirm-to").innerHTML = confirmAddressHtml(
        txInfo.to,
        txInfo.ensName,
        toTitle,
    );
    $("confirm-to-ens").classList.add("hidden");

    // Amount (with inline USD)
    const ethPrice = getPrice("ETH");
    const tokenPrice = getPrice(rawSymbol);
    const amountNum = parseFloat(txInfo.amount);
    const price = isErc20 ? tokenPrice : ethPrice;
    const amountUsd = price ? amountNum * price : null;
    $("confirm-amount").textContent = valueWithUsd(
        txInfo.amount + " " + symbol,
        amountUsd,
    );

    // Balance (with inline USD)
    if (isErc20) {
        // null is a balance whose scale nothing knows, not a balance of zero
        // (https://git.eeqj.de/sneak/AutistMask/issues/349). The send is
        // refused at encode time for the same missing scale; what this line
        // must not do is state a quantity nobody established.
        const bal = txInfo.tokenBalance;
        const balUsd =
            tokenPrice && bal != null ? parseFloat(bal) * tokenPrice : null;
        $("confirm-balance").textContent =
            bal == null
                ? "unknown (" + symbol + ")"
                : valueWithUsd(bal + " " + symbol, balUsd);
    } else {
        const bal = txInfo.balance || "0";
        const balUsd = ethPrice ? parseFloat(bal) * ethPrice : null;
        $("confirm-balance").textContent = valueWithUsd(bal + " ETH", balUsd);
    }

    // Check for warnings (synchronous local checks)
    const localWarnings = getLocalWarnings(txInfo.to, {
        fromAddress: txInfo.from,
    });

    const warningsEl = $("confirm-warnings");
    if (localWarnings.length > 0) {
        warningsEl.innerHTML = localWarnings
            .map(
                (w) =>
                    // Only the three hardcoded strings in
                    // src/shared/addressWarnings.js reach this today, but
                    // src/shared/etherscanLabels.js already builds a
                    // `warning` out of scraped explorer markup, so this is
                    // one wiring change away from carrying remote text.
                    `<div class="border border-border border-dashed p-2 mb-1 text-xs font-bold">WARNING: ${escapeHtml(w.message)}</div>`,
            )
            .join("");
        warningsEl.style.visibility = "visible";
    } else {
        warningsEl.innerHTML = "";
        warningsEl.style.visibility = "hidden";
    }

    // The two fee messages are mutually exclusive per transaction type, and
    // the type is known here, before the first paint. Drop the one that can
    // never apply and reserve the space of the one that can, so the async
    // estimate landing later never moves anything.
    $("confirm-amount-fee-error").classList.toggle("hidden", isErc20);
    $("confirm-gas-error").classList.toggle("hidden", !isErc20);

    renderValidation(txInfo);

    // Reset password field and error
    $("confirm-tx-password").value = "";
    hideError("confirm-tx-password-error");

    // Gas estimate — show placeholder then fetch async
    $("confirm-fee").style.visibility = "visible";
    $("confirm-fee-amount").textContent = "Estimating...";
    setVisible("confirm-fee-reserve", false);
    state.viewData = { pendingTx: txInfo };
    showView("confirm-tx");
    attachCopyHandlers("view-confirm-tx");

    // Reset async warnings to hidden (space always reserved, no layout shift)
    $("confirm-recipient-warning").style.visibility = "hidden";
    $("confirm-contract-warning").style.visibility = "hidden";
    $("confirm-burn-warning").style.visibility = "hidden";
    $("confirm-etherscan-warning").style.visibility = "hidden";

    // Show burn warning via reserved element (in addition to inline warning)
    if (isBurnAddress(txInfo.to)) {
        $("confirm-burn-warning").style.visibility = "visible";
    }

    estimateGas(txInfo);
    checkRecipientHistory(txInfo);
}

// Render the balance check for the transaction on screen. Called once during
// show() and again when the fee estimate resolves or fails. Every element it
// touches already occupies its space, so re-running it never moves anything.
function renderValidation(txInfo) {
    const isErc20 = txInfo.token !== "ETH";
    const symbol = isErc20 ? displaySymbol(txInfo.tokenSymbol || "?") : "ETH";

    const { canSend, codes } = validateTransfer({
        isErc20,
        amount: txInfo.amount,
        ethBalance: txInfo.balance,
        tokenBalance: txInfo.tokenBalance,
        feeStatus,
        feeWei,
    });

    // Messages carrying the user's own numbers are built here; the fixed
    // sentences live in the reserved elements in index.html.
    const messages = [];
    if (codes.includes(CODES.AMOUNT_INVALID)) {
        messages.push("Please enter a valid amount to send.");
    }
    if (codes.includes(CODES.INSUFFICIENT_TOKEN)) {
        messages.push(
            txInfo.tokenBalance == null
                ? "This token's balance is unknown, because nothing this" +
                      " wallet can consult reports how many decimal places it" +
                      " uses, so the amount you are trying to send cannot be" +
                      " checked against it."
                : "Insufficient " +
                      symbol +
                      " balance. You have " +
                      txInfo.tokenBalance +
                      " " +
                      symbol +
                      " but are trying to send " +
                      txInfo.amount +
                      " " +
                      symbol +
                      ".",
        );
    }
    if (codes.includes(CODES.INSUFFICIENT_ETH)) {
        messages.push(
            "Insufficient balance. You have " +
                txInfo.balance +
                " ETH but are trying to send " +
                txInfo.amount +
                " ETH.",
        );
    }

    const errorsEl = $("confirm-errors");
    if (messages.length > 0) {
        errorsEl.innerHTML = messages
            .map((m) => `<div class="text-xs">${escapeHtml(m)}</div>`)
            .join("");
        errorsEl.style.visibility = "visible";
    } else {
        errorsEl.innerHTML = "";
        errorsEl.style.visibility = "hidden";
    }

    setVisible(
        "confirm-amount-fee-error",
        codes.includes(CODES.INSUFFICIENT_ETH_WITH_FEE),
    );
    setVisible(
        "confirm-gas-error",
        codes.includes(CODES.INSUFFICIENT_ETH_FOR_FEE),
    );
    setVisible(
        "confirm-fee-unknown-error",
        codes.includes(CODES.FEE_UNAVAILABLE),
    );

    // While the estimate is in flight there is no error to show — the fee
    // line already reads "Estimating..." — but sending stays blocked so a
    // transaction the fee would break cannot be signed in the meantime.
    const sendBtn = $("btn-confirm-send");
    sendBtn.disabled = !canSend;
    sendBtn.classList.toggle("text-muted", !canSend);
}

function setVisible(id, visible) {
    $(id).style.visibility = visible ? "visible" : "hidden";
}

// A fee in wei as an ETH string, truncated to 6 decimal places.
function formatFeeEth(wei) {
    const parts = formatEther(wei).split(".");
    const dec =
        parts.length > 1 ? parts[1].slice(0, 6).replace(/0+$/, "") || "0" : "0";
    return parts[0] + "." + dec + " ETH";
}

async function estimateGas(txInfo) {
    try {
        const provider = getProvider(state.rpcUrl, state.networkId);
        const feeData = await provider.getFeeData();
        let gasLimit;

        if (txInfo.token === "ETH") {
            gasLimit = await provider.estimateGas({
                from: txInfo.from,
                to: txInfo.to,
                value: parseEther(txInfo.amount),
            });
        } else {
            const contract = new Contract(txInfo.token, ERC20_ABI, provider);
            // The scale the screen is rendering with, not the contract's own
            // answer: the estimate has to be for the transfer that would be
            // signed, and that one is encoded from what was displayed. See
            // transferAmount.js. A pending transaction that carries no usable
            // scale throws here, which reports the fee as unknown and leaves
            // Send blocked — an amount that cannot be checked against the
            // screen is never estimated for, let alone sent.
            const amount = parseUnits(
                txInfo.amount,
                displayedDecimals(txInfo.tokenDecimals),
            );
            gasLimit = await contract.transfer.estimateGas(txInfo.to, amount, {
                from: txInfo.from,
            });
        }

        // What the node will require to be reserved, which is what the gate
        // must be: the send pins no fee fields, so it is broadcast as a
        // type-2 transaction priced at maxFeePerGas.
        const gasCostWei = feeReserveWei(gasLimit, feeData);
        if (gasCostWei === null) {
            throw new Error("no usable gas price from the provider");
        }
        // What the transaction is expected to cost, which is a different and
        // usually much smaller number. Both are shown: quoting only the
        // reserve overstates the typical cost by roughly double on mainnet,
        // and quoting only the estimate contradicts the balance check.
        const estimateWei = feeEstimateWei(gasLimit, feeData);
        // The user may have left this transaction while the estimate was in
        // flight; a stale fee must not reach the screen or the balance check.
        if (pendingTx !== txInfo) return;

        const ethPrice = getPrice("ETH");
        const usd = (wei) =>
            ethPrice ? parseFloat(formatEther(wei)) * ethPrice : null;

        if (estimateWei !== null && estimateWei < gasCostWei) {
            $("confirm-fee-amount").textContent = valueWithUsd(
                "~" + formatFeeEth(estimateWei),
                usd(estimateWei),
            );
            $("confirm-fee-reserve").textContent =
                "up to " + formatFeeEth(gasCostWei) + " reserved";
            setVisible("confirm-fee-reserve", true);
        } else {
            // No spread to report: either there is no estimate, or the node
            // quotes a gas price at or above maxFeePerGas, so the expected
            // cost is not below the reserve. Show the reserve alone.
            $("confirm-fee-amount").textContent = valueWithUsd(
                formatFeeEth(gasCostWei),
                usd(gasCostWei),
            );
            setVisible("confirm-fee-reserve", false);
        }
        feeStatus = FEE_KNOWN;
        feeWei = gasCostWei;
        renderValidation(txInfo);
    } catch (e) {
        log.errorf("gas estimation failed:", e.message);
        if (pendingTx !== txInfo) return;
        $("confirm-fee-amount").textContent = "Unable to estimate";
        setVisible("confirm-fee-reserve", false);
        feeStatus = FEE_UNAVAILABLE;
        feeWei = null;
        renderValidation(txInfo);
    }
}

async function checkRecipientHistory(txInfo) {
    try {
        const provider = getProvider(state.rpcUrl, state.networkId);
        const asyncWarnings = await getFullWarnings(txInfo.to, provider, {
            fromAddress: txInfo.from,
        });
        for (const w of asyncWarnings) {
            if (w.type === "contract") {
                $("confirm-contract-warning").style.visibility = "visible";
            }
            if (w.type === "new-address") {
                $("confirm-recipient-warning").style.visibility = "visible";
            }
            if (w.type === "etherscan-phishing") {
                $("confirm-etherscan-warning").style.visibility = "visible";
            }
        }
    } catch (e) {
        log.errorf("recipient history check failed:", e.message);
    }
}

// Drop the password from the DOM. Registered as the view-leave handler so
// it does not sit in the hidden view once the screen navigates on — to the
// wait screen after a send, or anywhere else the user goes.
function clearPassword() {
    $("confirm-tx-password").value = "";
    hideError("confirm-tx-password-error");
}

function init(_ctx) {
    onViewLeave("confirm-tx", clearPassword);

    $("btn-confirm-send").addEventListener("click", async () => {
        const password = $("confirm-tx-password").value;
        if (!password) {
            showError(
                "confirm-tx-password-error",
                "Please enter your password.",
            );
            return;
        }

        const wallet = state.wallets[state.selectedWallet];
        let decryptedSecret;
        hideError("confirm-tx-password-error");

        try {
            decryptedSecret = await decryptWithPassword(
                wallet.encryptedSecret,
                password,
            );
        } catch {
            showError(
                "confirm-tx-password-error",
                "That password is incorrect. Please try again.",
            );
            return;
        }

        $("btn-confirm-send").disabled = true;
        $("btn-confirm-send").classList.add("text-muted");

        let tx;
        try {
            const signer = getSignerForAddress(
                wallet,
                state.selectedAddress,
                decryptedSecret,
            );
            const provider = getProvider(state.rpcUrl, state.networkId);
            const connectedSigner = signer.connect(provider);

            if (pendingTx.token === "ETH") {
                tx = await connectedSigner.sendTransaction({
                    to: pendingTx.to,
                    value: parseEther(pendingTx.amount),
                });
            } else {
                const contract = new Contract(
                    pendingTx.token,
                    ERC20_ABI,
                    connectedSigner,
                );
                // The contract's decimals() is read to be COMPARED with the
                // scale the screen rendered this amount at, not to encode with:
                // encoding from it signs whatever the contract answers now,
                // which is not what the user read. A disagreement throws and is
                // reported on the error screen. See transferAmount.js.
                const amount = transferAmountUnits(
                    pendingTx.amount,
                    pendingTx.tokenDecimals,
                    await contract.decimals(),
                );
                tx = await contract.transfer(pendingTx.to, amount);
            }

            // Best-effort: clear decrypted secret after use.
            // Note: JS strings are immutable; this nulls the reference but
            // the original string may persist in memory until GC.
            decryptedSecret = null;
            txStatus.showWait(pendingTx, tx.hash);
        } catch (e) {
            decryptedSecret = null;
            const hash = tx ? tx.hash : null;
            txStatus.showError(pendingTx, hash, e.shortMessage || e.message);
        } finally {
            $("btn-confirm-send").disabled = false;
            $("btn-confirm-send").classList.remove("text-muted");
        }
    });

    $("btn-confirm-back").addEventListener("click", () => {
        goBack();
    });
}

module.exports = { init, show, restore };
