const {
    $,
    addressTitle,
    escapeHtml,
    showView,
    showError,
    hideError,
    renderAddressHtml,
    attachCopyHandlers,
    onViewLeave,
} = require("./helpers");
const { state, saveState, currentNetwork } = require("../../shared/state");
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 { decryptWithPassword } = require("../../shared/vault");
const { getSignerForAddress } = require("../../shared/wallet");
const { walletDefect } = require("../../shared/walletDefects");
const { getProvider } = require("../../shared/balances");
const { describeSigningFailure } = require("../../shared/approvalVerify");
const txStatus = require("./txStatus");
const uniswap = require("../../shared/uniswap");
const runtime =
    typeof browser !== "undefined" ? browser.runtime : chrome.runtime;

const erc20Iface = new Interface(ERC20_ABI);

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

function formatTxValue(val) {
    const parts = val.split(".");
    if (parts.length === 1) return val + ".0000";
    const dec = (parts[1] + "0000").slice(0, 4);
    return parts[0] + "." + dec;
}

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;

    // 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;
            const tokenDecimals = token ? token.decimals : 18;
            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;
                const amountRaw = isUnlimited
                    ? "Unlimited"
                    : formatTxValue(formatUnits(rawAmount, tokenDecimals));
                const amountStr = isUnlimited
                    ? "Unlimited"
                    : amountRaw + (tokenSymbol ? " " + 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: amountStr,
                            rawValue: amountRaw,
                        },
                    ],
                };
            }

            if (parsed.name === "transfer") {
                const to = parsed.args[0];
                const rawAmount = parsed.args[1];
                const amountRaw = formatTxValue(
                    formatUnits(rawAmount, tokenDecimals),
                );
                const amountStr =
                    amountRaw + (tokenSymbol ? " " + 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: amountStr,
                            rawValue: amountRaw,
                        },
                    ],
                };
            }
        }
    } catch {
        // Not ERC-20 — fall through
    }

    // Try Uniswap Universal Router
    const routerResult = uniswap.decode(data, toAddress);
    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");
    }
}

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

    pendingTxParams = details.txParams;

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

    // Build txInfo for status screens
    pendingTxDetails = {
        from: state.activeAddress,
        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(details.txParams.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(state.activeAddress);

    // 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(details.txParams.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 + ")" : "");

    // 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 (details.txParams.data && details.txParams.data !== "0x") {
        $("approve-tx-data").textContent = details.txParams.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");
}

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;

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

    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");
}

function show(id) {
    approvalId = id;
    runtime.connect({ name: "approval:" + id });
    runtime.sendMessage({ type: "AUTISTMASK_GET_APPROVAL", id }, (details) => {
        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;
let pendingTxDetails = null;
// The exact parameters shown to the user, kept so the popup signs what it
// displayed rather than re-fetching anything at approval time. Both are
// repopulated by show() when the popup is closed and reopened.
let pendingTxParams = null;
let pendingSignParams = 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 active address 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) {
    const active = findActiveWallet();
    const defect = active ? walletDefect(active.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 the currently active
// address. Returns null when no wallet holds it.
function findActiveWallet() {
    for (const wallet of state.wallets) {
        for (let i = 0; i < wallet.addresses.length; i++) {
            if (wallet.addresses[i].address === state.activeAddress) {
                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");
}

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", () => {
        const remember = $("approve-remember").checked;
        runtime.sendMessage({
            type: "AUTISTMASK_APPROVAL_RESPONSE",
            id: approvalId,
            approved: true,
            remember,
        });
        window.close();
    });

    $("btn-reject").addEventListener("click", () => {
        const remember = $("approve-remember").checked;
        runtime.sendMessage({
            type: "AUTISTMASK_APPROVAL_RESPONSE",
            id: approvalId,
            approved: false,
            remember,
        });
        window.close();
    });

    $("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 = findActiveWallet();
        if (!active) {
            password = null;
            showError(
                "approve-tx-error",
                "No wallet was found for the active address.",
            );
            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,
            );
            const provider = getProvider(state.rpcUrl);
            const connected = signer.connect(provider);
            // This is the sequence ethers' own sendTransaction() runs
            // internally, so nonce, gas, fee and chain id population are
            // identical to when the background did the signing.
            const populated =
                await connected.populateTransaction(pendingTxParams);
            delete populated.from;
            payload.rawSignedTx = await connected.signTransaction(populated);
        } 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;
        }

        runtime.sendMessage(payload, (response) => {
            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", () => {
        runtime.sendMessage({
            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 = findActiveWallet();
        if (!active) {
            password = null;
            showError(
                "approve-sign-error",
                "No wallet was found for the active address.",
            );
            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;
        }

        runtime.sendMessage(payload, (response) => {
            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", () => {
        runtime.sendMessage({
            type: "AUTISTMASK_SIGN_RESPONSE",
            id: approvalId,
            approved: false,
        });
        window.close();
    });
}

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