const {
    $,
    addressTitle,
    escapeHtml,
    showView,
    showError,
    hideError,
    renderAddressHtml,
    attachCopyHandlers,
    onViewLeave,
} = require("./helpers");
const { state, saveState } = require("../../shared/state");
const { networkByChainId } = require("../../shared/networks");
const {
    formatEther,
    formatUnits,
    getBytes,
    Interface,
    toUtf8String,
} = require("ethers");
const { getPrice, formatUsd } = require("../../shared/prices");
const { ERC20_ABI } = require("../../shared/constants");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
const {
    resolveTokenDecimals,
    unknownDecimalsAmount,
} = require("../../shared/approvalAmount");
// Four decimals, with the nonzero floor these screens hold: every amount this
// view renders — the ERC-20 line, the ETH value, the max fee — and every one
// it carries forward to the wait/success/error screens goes through it.
const {
    truncateAmountNeverZero: formatTxValue,
} = require("../../shared/amountDisplay");
const { decryptWithPassword } = require("../../shared/vault");
const { getSignerForAddress } = require("../../shared/wallet");
const { walletDefect } = require("../../shared/walletDefects");
const { describeSigningFailure } = require("../../shared/approvalVerify");
const txStatus = require("./txStatus");
const uniswap = require("../../shared/uniswap");
const { notify, runtimeApi, sendMessage } = require("../../shared/browserApi");

const erc20Iface = new Interface(ERC20_ABI);

function approvalAddressHtml(address) {
    const title = addressTitle(address, state.wallets);
    return renderAddressHtml(address, { title });
}

// The amount line for a decoded ERC-20 call. With a known scale it is the
// token quantity; with `decimals` null it is the base-unit integer with the
// unknown scale stated, because formatting it with an assumed scale is what
// showed a 5,000-token transfer as `0.0000`. `raw` is what the status screens
// carry, `display` is what the approval screen shows.
function tokenAmountText(rawAmount, decimals, symbol) {
    if (decimals === null) {
        const unknown = unknownDecimalsAmount(rawAmount);
        return { raw: unknown, display: unknown };
    }
    const formatted = formatTxValue(formatUnits(rawAmount, decimals));
    return {
        raw: formatted,
        display: formatted + (symbol ? " " + symbol : ""),
    };
}

function tokenLabel(address) {
    const t = TOKEN_BY_ADDRESS.get(address.toLowerCase());
    return t ? t.symbol : null;
}

// Try to decode calldata using known ABIs.
// Returns { name, description, details } or null.
function decodeCalldata(data, toAddress) {
    if (!data || data === "0x" || data.length < 10) return null;

    // Where a token's scale is looked for, for every decoder below: the ERC-20
    // amount line and the swap's Amount and Min. received lines resolve it the
    // same way, and refuse to format the same way when it is nowhere.
    const decimalsSources = {
        trackedTokens: state.trackedTokens,
        wallets: state.wallets,
    };

    // Try ERC-20 (approve / transfer)
    try {
        const parsed = erc20Iface.parseTransaction({ data });
        if (parsed) {
            const token = TOKEN_BY_ADDRESS.get(toAddress.toLowerCase());
            const tokenSymbol = token ? token.symbol : null;
            // null when no source knows this token's scale. It is not
            // defaulted to 18: an amount formatted with a guessed scale is
            // the wrong number, and for a token with fewer decimals than the
            // guess it is the wrong number in the direction that reads as
            // zero. See tokenAmountText().
            const tokenDecimals = resolveTokenDecimals(
                toAddress,
                decimalsSources,
            );
            const contractLabel = tokenSymbol
                ? tokenSymbol + " (" + toAddress + ")"
                : toAddress;

            if (parsed.name === "approve") {
                const spender = parsed.args[0];
                const rawAmount = parsed.args[1];
                const maxUint = BigInt(
                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
                );
                const isUnlimited = rawAmount === maxUint;
                // An unbounded allowance needs no scale to describe, so it is
                // still named rather than refused.
                const amount = isUnlimited
                    ? { raw: "Unlimited", display: "Unlimited" }
                    : tokenAmountText(rawAmount, tokenDecimals, tokenSymbol);

                return {
                    name: "Token Approval",
                    description: tokenSymbol
                        ? "Approve spending of your " + tokenSymbol
                        : "Approve spending of an ERC-20 token",
                    details: [
                        {
                            label: "Token",
                            value: contractLabel,
                            address: toAddress,
                            isToken: true,
                        },
                        {
                            label: "Spender",
                            value: spender,
                            address: spender,
                        },
                        {
                            label: "Amount",
                            value: amount.display,
                            rawValue: amount.raw,
                        },
                    ],
                };
            }

            if (parsed.name === "transfer") {
                const to = parsed.args[0];
                const rawAmount = parsed.args[1];
                const amount = tokenAmountText(
                    rawAmount,
                    tokenDecimals,
                    tokenSymbol,
                );

                return {
                    name: "Token Transfer",
                    description: tokenSymbol
                        ? "Transfer " + tokenSymbol
                        : "Transfer ERC-20 token",
                    details: [
                        {
                            label: "Token",
                            value: contractLabel,
                            address: toAddress,
                            isToken: true,
                        },
                        { label: "Recipient", value: to, address: to },
                        {
                            label: "Amount",
                            value: amount.display,
                            rawValue: amount.raw,
                        },
                    ],
                };
            }
        }
    } catch {
        // Not ERC-20 — fall through
    }

    // Try Uniswap Universal Router
    const routerResult = uniswap.decode(data, toAddress, decimalsSources);
    if (routerResult) return routerResult;

    return null;
}

function showPhishingWarning(elementId, isPhishing) {
    const el = $(elementId);
    if (!el) return;
    // The background script performs the authoritative phishing domain check
    // and passes the result via the isPhishingDomain flag.
    if (isPhishing) {
        el.classList.remove("hidden");
    } else {
        el.classList.add("hidden");
    }
}

// The fields of the approved transaction the value and recipient lines do not
// already carry: network, gas limit, fee per gas, the most the fee can come to,
// and the nonce. The background compares every one of them against the signed
// artifact, so every one of them has to be on the screen — a number that is
// verified but never displayed is verified against nothing the user agreed to.
function showTxFee(approvedTx, ethPrice) {
    const network = networkByChainId(approvedTx.chainId);
    $("approve-tx-network").textContent = network
        ? network.name
        : "Unknown network (chain id " + BigInt(approvedTx.chainId) + ")";

    const gasLimit = BigInt(approvedTx.gasLimit);
    const feePerGas = BigInt(approvedTx.maxFeePerGas || approvedTx.gasPrice);
    const maxFeeEth = formatTxValue(formatEther(gasLimit * feePerGas));
    const usdStr = formatUsd(
        ethPrice ? parseFloat(maxFeeEth) * ethPrice : null,
    );
    $("approve-tx-fee").textContent =
        maxFeeEth + " ETH" + (usdStr ? " (" + usdStr + ")" : "");

    let detail =
        gasLimit.toString() +
        " gas at up to " +
        formatUnits(feePerGas, 9) +
        " gwei";
    if (approvedTx.maxPriorityFeePerGas) {
        detail +=
            ", " +
            formatUnits(approvedTx.maxPriorityFeePerGas, 9) +
            " gwei priority";
    }
    $("approve-tx-fee-detail").textContent = detail;
    $("approve-tx-nonce").textContent = BigInt(approvedTx.nonce).toString();
}

function showTxApproval(details) {
    showPhishingWarning(
        "approve-tx-phishing-warning",
        details.isPhishingDomain,
    );

    // The transaction the background populated. It is displayed as it stands,
    // signed as it stands, and verified against as it stands — the popup fills
    // nothing in, so there is no number on this screen that the background
    // cannot compare with the artifact it gets back.
    pendingTxParams = details.approvedTx;
    const approvedTx = details.approvedTx;

    const toAddr = approvedTx.to;
    const token = toAddr ? TOKEN_BY_ADDRESS.get(toAddr.toLowerCase()) : null;
    const ethValue = formatEther(approvedTx.value || "0");

    // Build txInfo for status screens
    pendingTxDetails = {
        from: details.approvedFrom,
        to: toAddr || "",
        amount: formatTxValue(ethValue),
        token: "ETH",
        tokenSymbol: token ? token.symbol : null,
    };

    // If this is an ERC-20 call, try to extract the real recipient and amount
    const decoded = decodeCalldata(approvedTx.data, toAddr || "");
    if (decoded && decoded.details) {
        let decodedTokenAddr = null;
        let decodedTokenSymbol = null;
        for (const d of decoded.details) {
            if (d.label === "Recipient" && d.address) {
                pendingTxDetails.to = d.address;
            }
            if (d.label === "Amount") {
                pendingTxDetails.amount = d.rawValue || d.value;
            }
            if (d.label === "Token In" && d.isToken && d.address) {
                const t = TOKEN_BY_ADDRESS.get(d.address.toLowerCase());
                if (t) {
                    decodedTokenAddr = d.address;
                    decodedTokenSymbol = t.symbol;
                }
            }
        }
        if (token) {
            pendingTxDetails.token = toAddr;
            pendingTxDetails.tokenSymbol = token.symbol;
        } else if (decodedTokenAddr) {
            pendingTxDetails.token = decodedTokenAddr;
            pendingTxDetails.tokenSymbol = decodedTokenSymbol;
        }
    }

    // Carry decoded calldata info through to success/error views
    if (decoded) {
        pendingTxDetails.decoded = {
            name: decoded.name,
            description: decoded.description,
            details: decoded.details,
        };
    }

    $("approve-tx-hostname").textContent = details.hostname;
    $("approve-tx-from").innerHTML = approvalAddressHtml(details.approvedFrom);

    // Show token symbol next to contract address if known
    const symbol = toAddr ? tokenLabel(toAddr) : null;
    if (toAddr) {
        let toHtml = "";
        if (symbol) {
            toHtml += `<div class="font-bold mb-1">${escapeHtml(symbol)}</div>`;
        }
        toHtml += approvalAddressHtml(toAddr);
        $("approve-tx-to").innerHTML = toHtml;
    } else {
        $("approve-tx-to").innerHTML = escapeHtml("(contract creation)");
    }

    const ethValueFormatted = formatTxValue(
        formatEther(approvedTx.value || "0"),
    );
    const ethPrice = getPrice("ETH");
    const ethUsd = ethPrice ? parseFloat(ethValueFormatted) * ethPrice : null;
    const usdStr = formatUsd(ethUsd);
    $("approve-tx-value").textContent =
        ethValueFormatted + " ETH" + (usdStr ? " (" + usdStr + ")" : "");

    showTxFee(approvedTx, ethPrice);

    // Decode calldata (reuse decoded from above)
    const decodedEl = $("approve-tx-decoded");
    if (decoded) {
        $("approve-tx-action").textContent = decoded.name;
        let detailsHtml = "";
        if (decoded.description) {
            detailsHtml += `<div class="mb-2">${escapeHtml(decoded.description)}</div>`;
        }
        for (const d of decoded.details) {
            detailsHtml += `<div class="mb-2">`;
            detailsHtml += `<div class="text-muted">${escapeHtml(d.label)}</div>`;
            if (d.address) {
                if (d.isToken) {
                    detailsHtml += `<div class="font-bold">${escapeHtml(tokenLabel(d.address) || "Unknown token")}</div>`;
                }
                detailsHtml += approvalAddressHtml(d.address);
            } else {
                detailsHtml += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
            }
            detailsHtml += `</div>`;
        }
        $("approve-tx-decoded-details").innerHTML = detailsHtml;
        decodedEl.classList.remove("hidden");
    } else {
        decodedEl.classList.add("hidden");
    }

    // Always show raw data when present
    if (approvedTx.data && approvedTx.data !== "0x") {
        $("approve-tx-data").textContent = approvedTx.data;
        $("approve-tx-data-section").classList.remove("hidden");
    } else {
        $("approve-tx-data-section").classList.add("hidden");
    }

    $("approve-tx-password").value = "";
    hideError("approve-tx-error");

    showView("approve-tx");
    attachCopyHandlers("view-approve-tx");
    gateOnWalletDefect(
        "approve-tx-error",
        "btn-approve-tx",
        details.approvedFrom,
    );
}

function decodeHexMessage(hex) {
    try {
        const bytes = Uint8Array.from(
            hex
                .slice(2)
                .match(/.{1,2}/g)
                .map((b) => parseInt(b, 16)),
        );
        return toUtf8String(bytes);
    } catch {
        return null;
    }
}

function formatTypedDataHtml(jsonStr) {
    try {
        const data = JSON.parse(jsonStr);
        let html = "";

        if (data.domain) {
            html += `<div class="mb-2"><div class="text-muted">Domain</div>`;
            for (const [key, val] of Object.entries(data.domain)) {
                html += `<div><span class="text-muted">${escapeHtml(key)}:</span> ${escapeHtml(String(val))}</div>`;
            }
            html += `</div>`;
        }

        if (data.primaryType) {
            html += `<div class="mb-2"><div class="text-muted">Primary type</div>`;
            html += `<div class="font-bold">${escapeHtml(data.primaryType)}</div></div>`;
        }

        if (data.message) {
            html += `<div class="mb-2"><div class="text-muted">Message</div>`;
            for (const [key, val] of Object.entries(data.message)) {
                const display =
                    typeof val === "object" ? JSON.stringify(val) : String(val);
                html += `<div><span class="text-muted">${escapeHtml(key)}:</span> <span class="break-all">${escapeHtml(display)}</span></div>`;
            }
            html += `</div>`;
        }

        return html;
    } catch {
        return `<div class="break-all">${escapeHtml(jsonStr)}</div>`;
    }
}

function showSignApproval(details) {
    showPhishingWarning(
        "approve-sign-phishing-warning",
        details.isPhishingDomain,
    );

    const sp = details.signParams;
    pendingSignParams = sp;
    pendingSignFrom = details.approvedFrom;

    $("approve-sign-hostname").textContent = details.hostname;
    $("approve-sign-from").innerHTML = approvalAddressHtml(
        details.approvedFrom,
    );

    const isTyped =
        sp.method === "eth_signTypedData_v4" ||
        sp.method === "eth_signTypedData";
    $("approve-sign-type").textContent = isTyped
        ? "Typed data (EIP-712)"
        : "Personal message";

    if (isTyped) {
        $("approve-sign-message").innerHTML = formatTypedDataHtml(sp.typedData);
    } else {
        const decoded = decodeHexMessage(sp.message);
        if (decoded !== null) {
            $("approve-sign-message").textContent = decoded;
        } else {
            $("approve-sign-message").textContent = sp.message;
        }
    }

    // Display danger warning for eth_sign (raw hash signing)
    const warningEl = $("approve-sign-danger-warning");
    if (warningEl) {
        if (sp.dangerWarning) {
            warningEl.textContent = sp.dangerWarning;
            warningEl.style.visibility = "visible";
        } else {
            warningEl.textContent = "";
            warningEl.style.visibility = "hidden";
        }
    }

    $("approve-sign-password").value = "";
    hideError("approve-sign-error");
    $("btn-approve-sign").disabled = false;
    $("btn-approve-sign").classList.remove("text-muted");

    showView("approve-sign");
    attachCopyHandlers("view-approve-sign");
    gateOnWalletDefect(
        "approve-sign-error",
        "btn-approve-sign",
        details.approvedFrom,
    );
}

// Awaited by nobody: the popup entry point calls this and moves on. It
// therefore has to absorb its own failure, and a background that cannot
// describe the approval is the same outcome as an approval that is gone.
async function show(id) {
    approvalId = id;
    approvalPort = runtimeApi().connect({ name: "approval:" + id });

    let details = null;
    try {
        details = await sendMessage({ type: "AUTISTMASK_GET_APPROVAL", id });
    } catch {
        details = null;
    }

    if (!details) {
        window.close();
        return;
    }
    if (details.type === "tx") {
        showTxApproval(details);
        return;
    }
    if (details.type === "sign") {
        showSignApproval(details);
        return;
    }
    // Site connection approval
    showPhishingWarning(
        "approve-site-phishing-warning",
        details.isPhishingDomain,
    );
    $("approve-hostname").textContent = details.hostname;
    $("approve-address").innerHTML = approvalAddressHtml(state.activeAddress);
    attachCopyHandlers("view-approve-site");
    $("approve-remember").checked = state.rememberSiteChoice;
}

let approvalId = null;
// The port this approval was opened on. Closing this window disconnects it,
// and the background treats that disconnect as "closed without deciding" for a
// site connection — so the decision goes out on this same port and not as a
// one-off message. One channel is ordered: a message posted on it is delivered
// before its own disconnect, however immediately the close follows. Two
// channels were not, and the close won, reporting a user who approved as
// having refused.
let approvalPort = null;
let pendingTxDetails = null;
// The exact objects shown to the user, kept so the popup signs what it
// displayed rather than re-fetching or re-populating anything at approval
// time. All are repopulated by show() when the popup is closed and reopened.
let pendingTxParams = null;
let pendingSignParams = null;
// The address the approval was raised for. Signing uses this rather than the
// active address, so that an address switch since the approval fails here
// instead of producing a signature from an account the screen never named.
let pendingSignFrom = null;

// Approve buttons stay disabled and muted while the popup derives the key and
// signs, which is slow enough (Argon2id) that a double click is likely.
function setTxButtonBusy(busy) {
    $("btn-approve-tx").disabled = busy;
    $("btn-approve-tx").classList.toggle("text-muted", busy);
}

function setSignButtonBusy(busy) {
    $("btn-approve-sign").disabled = busy;
    $("btn-approve-sign").classList.toggle("text-muted", busy);
}

// Say so on the approval screen itself, and disable the approve button, when
// the address the approval was raised for belongs to a wallet whose keys
// cannot be derived. Without this the screen would take a password and fail
// after deriving it. Reject stays available; the wallet is not touched.
// Returns true when it gated.
function gateOnWalletDefect(errorId, buttonId, address) {
    const owner = findWalletFor(address);
    const defect = owner ? walletDefect(owner.wallet) : null;
    if (!defect) return false;
    showError(errorId, defect.shortMessage);
    $(buttonId).disabled = true;
    $(buttonId).classList.add("text-muted");
    return true;
}

// Locate the wallet and the address index owning an address. Returns null when
// no wallet holds it. Approvals look up the address they were raised for, not
// whichever address is active now: the approval named one account, and signing
// with another is what verification refuses.
function findWalletFor(address) {
    for (const wallet of state.wallets) {
        for (let i = 0; i < wallet.addresses.length; i++) {
            if (wallet.addresses[i].address === address) {
                return { wallet, addrIndex: i };
            }
        }
    }
    return null;
}

// Drop the password from the DOM when either approval screen is left. The
// approval window navigates on after a signature — approve-tx goes to the
// wait screen — and the password must not sit in the hidden view for the
// life of that window.
function clearTxPassword() {
    $("approve-tx-password").value = "";
    hideError("approve-tx-error");
}

function clearSignPassword() {
    $("approve-sign-password").value = "";
    hideError("approve-sign-error");
}

// Answer a site-connection approval and close. The decision goes out on the
// approval port — see approvalPort above for why — and carries no approval id,
// because the port name already names the approval the background will settle.
// The post is guarded because a throw must not cost the close: posting on a
// port whose background worker has been torn down throws, and the approval it
// would have settled died with that worker, so the only thing left to do is
// what the user asked for — go away.
function decideSite(approved) {
    if (approvalPort) {
        try {
            approvalPort.postMessage({
                type: "AUTISTMASK_APPROVAL_DECISION",
                approved,
                remember: $("approve-remember").checked,
            });
        } catch {
            // Nothing to report it to; the window closes either way.
        }
    }
    window.close();
}

function init(_ctx) {
    onViewLeave("approve-tx", clearTxPassword);
    onViewLeave("approve-sign", clearSignPassword);

    $("approve-remember").addEventListener("change", async () => {
        state.rememberSiteChoice = $("approve-remember").checked;
        await saveState();
    });

    $("btn-approve").addEventListener("click", () => {
        decideSite(true);
    });

    $("btn-reject").addEventListener("click", () => {
        decideSite(false);
    });

    $("btn-approve-tx").addEventListener("click", async () => {
        let password = $("approve-tx-password").value;
        if (!password) {
            showError("approve-tx-error", "Please enter your password.");
            return;
        }
        hideError("approve-tx-error");
        setTxButtonBusy(true);

        const active = findWalletFor(pendingTxParams.from);
        if (!active) {
            password = null;
            showError(
                "approve-tx-error",
                "No wallet was found for the address this transaction was approved for.",
            );
            setTxButtonBusy(false);
            return;
        }

        const defect = walletDefect(active.wallet);
        if (defect) {
            password = null;
            showError("approve-tx-error", defect.shortMessage);
            setTxButtonBusy(false);
            return;
        }

        // Decrypt here, in the popup. The password must never cross the
        // extension messaging boundary; only the signed transaction does.
        let decryptedSecret;
        try {
            decryptedSecret = await decryptWithPassword(
                active.wallet.encryptedSecret,
                password,
            );
        } catch {
            showError(
                "approve-tx-error",
                "That password is incorrect. Please try again.",
            );
            setTxButtonBusy(false);
            return;
        } finally {
            // Best-effort: drop the password as soon as the key derivation
            // is done. Note that JS strings are immutable; this clears the
            // reference but the original string may persist until GC.
            password = null;
        }

        const payload = {
            type: "AUTISTMASK_TX_RESPONSE",
            id: approvalId,
            approved: true,
        };
        try {
            const signer = getSignerForAddress(
                active.wallet,
                active.addrIndex,
                decryptedSecret,
            );
            // Sign the approved transaction exactly as it was displayed. The
            // background populated it before this screen was drawn and checks
            // the artifact against it field for field, so there is nothing to
            // fill in here and no provider to fill it in from. The copy is
            // because ethers may strip `from` off what it is handed, and the
            // approval has to survive a retry intact; keeping `from` on it
            // makes ethers refuse a key that is not the approved address.
            payload.rawSignedTx = await signer.signTransaction({
                ...pendingTxParams,
            });
        } catch (e) {
            payload.error =
                e.shortMessage || e.message || "Transaction signing failed.";
        } finally {
            // Best-effort: clear the decrypted secret after use, with the
            // same immutability caveat as the password above.
            decryptedSecret = null;
        }

        // A send that never reaches the background is reported to the user
        // the same way a background that refused it is: describeSigningFailure
        // turns a null response into the generic message below.
        let response = null;
        try {
            response = await sendMessage(payload);
        } catch {
            response = null;
        }

        if (response && response.txHash) {
            txStatus.showWait(pendingTxDetails, response.txHash);
            return;
        }
        // A retryable failure leaves the approval pending in the
        // background, so stay on this screen with a live button rather
        // than sending the user to a dead end.
        const outcome = describeSigningFailure(
            response,
            "The transaction could not be sent.",
        );
        if (outcome.retryable) {
            showError("approve-tx-error", outcome.message);
            setTxButtonBusy(false);
        } else {
            txStatus.showError(pendingTxDetails, null, outcome.message);
        }
    });

    $("btn-reject-tx").addEventListener("click", () => {
        notify({
            type: "AUTISTMASK_TX_RESPONSE",
            id: approvalId,
            approved: false,
        });
        window.close();
    });

    $("btn-approve-sign").addEventListener("click", async () => {
        let password = $("approve-sign-password").value;
        if (!password) {
            showError("approve-sign-error", "Please enter your password.");
            return;
        }
        hideError("approve-sign-error");
        setSignButtonBusy(true);

        const active = findWalletFor(pendingSignFrom);
        if (!active) {
            password = null;
            showError(
                "approve-sign-error",
                "No wallet was found for the address this request was approved for.",
            );
            setSignButtonBusy(false);
            return;
        }

        const defect = walletDefect(active.wallet);
        if (defect) {
            password = null;
            showError("approve-sign-error", defect.shortMessage);
            setSignButtonBusy(false);
            return;
        }

        // Decrypt here, in the popup. The password must never cross the
        // extension messaging boundary; only the signature does.
        let decryptedSecret;
        try {
            decryptedSecret = await decryptWithPassword(
                active.wallet.encryptedSecret,
                password,
            );
        } catch {
            showError(
                "approve-sign-error",
                "That password is incorrect. Please try again.",
            );
            setSignButtonBusy(false);
            return;
        } finally {
            // Best-effort: drop the password as soon as the key derivation
            // is done. Note that JS strings are immutable; this clears the
            // reference but the original string may persist until GC.
            password = null;
        }

        const payload = {
            type: "AUTISTMASK_SIGN_RESPONSE",
            id: approvalId,
            approved: true,
        };
        try {
            const signer = getSignerForAddress(
                active.wallet,
                active.addrIndex,
                decryptedSecret,
            );
            const sp = pendingSignParams;
            if (sp.method === "personal_sign" || sp.method === "eth_sign") {
                payload.signature = await signer.signMessage(
                    getBytes(sp.message),
                );
            } else {
                // eth_signTypedData_v4 / eth_signTypedData
                const typedData = JSON.parse(sp.typedData);
                const { domain, types, message } = typedData;
                // ethers handles EIP712Domain internally
                delete types.EIP712Domain;
                payload.signature = await signer.signTypedData(
                    domain,
                    types,
                    message,
                );
            }
        } catch (e) {
            payload.error = e.shortMessage || e.message || "Signing failed.";
        } finally {
            // Best-effort: clear the decrypted secret after use, with the
            // same immutability caveat as the password above.
            decryptedSecret = null;
        }

        let response = null;
        try {
            response = await sendMessage(payload);
        } catch {
            response = null;
        }

        if (response && response.signature) {
            window.close();
            return;
        }
        // The button comes back only when the approval is still pending in
        // the background; otherwise it stays disabled and the message says
        // why, because a control that cannot succeed must not look like it
        // can.
        const outcome = describeSigningFailure(
            response,
            "The message could not be signed.",
        );
        showError("approve-sign-error", outcome.message);
        if (outcome.retryable) setSignButtonBusy(false);
    });

    $("btn-reject-sign").addEventListener("click", () => {
        notify({
            type: "AUTISTMASK_SIGN_RESPONSE",
            id: approvalId,
            approved: false,
        });
        window.close();
    });
}

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