// Post-broadcast transaction status views: wait, success, error.

const {
    $,
    showView,
    addressTitle,
    escapeHtml,
    renderAddressHtml,
    attachCopyHandlers,
    copyableHtml,
    etherscanLinkHtml,
    explorerUrl,
    displaySymbol,
    clearViewStack,
} = require("./helpers");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
const { state } = require("../../shared/state");
const { getProvider } = require("../../shared/balances");
const { log } = require("../../shared/log");

// Receipt poll cadence and the deadline after which the wait is reported as
// a timeout. Both are documented in the WaitTx section of README.md.
const POLL_INTERVAL_MS = 10000;
const TIMEOUT_MS = 60000;

// How many receipt lookups may fail in a row before the wait is ended and
// the failure reported. A lookup that throws says nothing about the
// transaction, so one must not end the wait — but an RPC that never answers
// (a mistyped URL in settings is the ordinary case) must not leave the wait
// running forever either, least of all a persisted one that every popup
// open would resume. Six is 60 seconds at the poll cadence: the same
// patience the confirmation deadline gets. Any lookup that answers, with a
// receipt or with null, resets the count.
const MAX_CONSECUTIVE_LOOKUP_FAILURES = 6;

let ctx;
let elapsedTimer = null;
let pollTimer = null;

// Identifies the wait currently on screen. Bumped by endWait(), so a timer
// callback or an in-flight receipt lookup that outlives its wait can tell
// that it is stale and leave the current view alone. Without it, a receipt
// resolving after the wait has ended renders over whatever view replaced it.
let waitId = 0;

// End the wait on screen: stop its timers and invalidate its pending async
// work. Called on receipt, on timeout, when a new wait starts, and when the
// user navigates away.
function endWait() {
    waitId++;
    if (elapsedTimer) {
        clearInterval(elapsedTimer);
        elapsedTimer = null;
    }
    if (pollTimer) {
        clearInterval(pollTimer);
        pollTimer = null;
    }
}

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

function txHashHtml(hash) {
    const link = explorerUrl("tx", hash);
    return copyableHtml(hash, "break-all") + etherscanLinkHtml(link);
}

function blockNumberHtml(blockNumber) {
    const num = String(blockNumber);
    const link = explorerUrl("block", num);
    return copyableHtml(num) + etherscanLinkHtml(link);
}

// Render the wait view and start polling for the receipt. broadcastTime is
// when the transaction was broadcast, which is what the elapsed counter and
// the timeout deadline are both measured from; pollNow runs one lookup
// immediately instead of waiting a full poll interval.
function startWait(txInfo, txHash, broadcastTime, pollNow) {
    endWait();
    const id = waitId;

    const symbol =
        txInfo.token === "ETH"
            ? "ETH"
            : displaySymbol(txInfo.tokenSymbol || "?");
    $("wait-tx-summary").textContent = txInfo.amount + " " + symbol;
    $("wait-tx-to").innerHTML = toAddressHtml(txInfo.to);
    $("wait-tx-hash").innerHTML = txHashHtml(txHash);
    attachCopyHandlers("view-wait-tx");

    // Persisted so closing and reopening the popup resumes this wait
    // instead of silently abandoning it.
    state.viewData = {
        pendingWait: {
            txInfo: txInfo,
            hash: txHash,
            broadcastTime: broadcastTime,
        },
    };

    function renderElapsed() {
        const elapsed = Math.floor((Date.now() - broadcastTime) / 1000);
        $("wait-tx-status").textContent =
            "Waiting for confirmation... " + elapsed + "s";
    }
    renderElapsed();

    elapsedTimer = setInterval(() => {
        if (id !== waitId) return;
        renderElapsed();
    }, 1000);

    const provider = getProvider(state.rpcUrl, state.networkId);
    let consecutiveFailures = 0;

    async function poll() {
        if (id !== waitId) return;
        let receipt = null;
        let answered = true;
        try {
            receipt = await provider.getTransactionReceipt(txHash);
        } catch (e) {
            // A thrown lookup means "no answer this tick", not "no
            // receipt": the RPC failed, the chain said nothing. Declaring
            // the timeout off it would report a confirmed transaction as
            // failed — which matters most on a resumed wait, where the
            // first poll is already past the deadline.
            answered = false;
            log.errorf("poll receipt failed:", e.message);
        }
        // The lookup is async: the wait may have ended while it was in
        // flight, in which case this result must not touch the view.
        if (id !== waitId) return;
        // Exactly one outcome per wait. A receipt wins even on the tick
        // that crosses the deadline, because the transaction did confirm.
        if (receipt) {
            showSuccess(txInfo, txHash, receipt.blockNumber);
            return;
        }
        if (!answered) {
            consecutiveFailures++;
            // The failure is the user's news, and it is a different fact
            // from "the transaction did not confirm" — the chain was never
            // asked. Ending the wait here is what keeps it bounded and
            // gives the user a Done button to leave by.
            if (consecutiveFailures >= MAX_CONSECUTIVE_LOOKUP_FAILURES) {
                showError(
                    txInfo,
                    txHash,
                    "The network could not be reached to check this transaction — " +
                        MAX_CONSECUTIVE_LOOKUP_FAILURES +
                        " lookups failed in a row. Check the RPC URL in Settings. The transaction may still have confirmed — check Etherscan.",
                );
            }
            // Otherwise keep polling: the next tick may answer.
            return;
        }
        consecutiveFailures = 0;
        if (Date.now() - broadcastTime >= TIMEOUT_MS) {
            showError(
                txInfo,
                txHash,
                "Transaction was not confirmed within 60 seconds. It may still confirm later \u2014 check Etherscan.",
            );
        }
    }

    pollTimer = setInterval(poll, POLL_INTERVAL_MS);

    showView("wait-tx");

    if (pollNow) poll();
}

function showWait(txInfo, txHash) {
    startWait(txInfo, txHash, Date.now(), false);
}

// Resume a wait persisted by a previous popup session. The deadline still
// runs from the original broadcast, so a wait that has already outlived it
// resolves on the immediate first poll rather than restarting the clock.
// Returns false when there is nothing resumable to resume. Every field
// startWait() goes on to use is validated, not just the presence of the
// containers: txInfo.to reaches addressTitle(), which calls
// address.toLowerCase(), and txInfo.amount is rendered into the summary, so
// an object merely missing one of them throws a TypeError out of
// restoreView() — which init() does not guard, skipping the rest of popup
// init and leaving wait-tx on screen with no back control. A non-numeric
// broadcastTime leaves an unexitable wait counting "NaNs". txInfo.token and
// txInfo.tokenSymbol are deliberately unchecked: they are compared and
// coalesced rather than dereferenced, and tokenSymbol is null for ETH.
function restoreWait() {
    const d = state.viewData;
    if (!d || !d.pendingWait) return false;
    const w = d.pendingWait;
    if (!w.hash) return false;
    // typeof [] is "object", so an array passes an object check.
    const info = w.txInfo;
    if (!info || typeof info !== "object" || Array.isArray(info)) return false;
    // A string is the whole requirement: the empty string is what a
    // contract-deployment approval persists (approval.js writes `to: toAddr
    // || ""`), and both fields render harmlessly when empty, so refusing it
    // would abandon a wait the live path itself created.
    if (typeof info.to !== "string") return false;
    if (typeof info.amount !== "string") return false;
    if (typeof w.broadcastTime !== "number" || !isFinite(w.broadcastTime)) {
        return false;
    }
    startWait(w.txInfo, w.hash, w.broadcastTime, true);
    return true;
}

function showSuccess(txInfo, txHash, blockNumber) {
    endWait();

    const symbol =
        txInfo.token === "ETH"
            ? "ETH"
            : displaySymbol(txInfo.tokenSymbol || "?");
    state.viewData = {
        amount: txInfo.amount,
        symbol: symbol,
        to: txInfo.to,
        hash: txHash,
        blockNumber: blockNumber,
        decoded: txInfo.decoded || null,
    };
    renderSuccess();
    ctx.doRefreshAndRender();
}

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

function decodedDetailsHtml(decoded) {
    if (!decoded || !decoded.details) return "";
    let html = `<div class="border border-border border-dashed p-2 mb-3">`;
    if (decoded.name) {
        html += `<div class="mb-2"><div class="text-xs text-muted mb-1">Action</div>`;
        html += `<div class="font-bold">${escapeHtml(decoded.name)}</div></div>`;
    }
    if (decoded.description) {
        html += `<div class="mb-2"><div class="text-xs text-muted mb-1">Description</div>`;
        html += `<div>${escapeHtml(decoded.description)}</div></div>`;
    }
    for (const d of decoded.details) {
        html += `<div class="mb-2">`;
        html += `<div class="text-xs text-muted mb-1">${escapeHtml(d.label)}</div>`;
        if (d.address) {
            if (d.isToken) {
                const sym = tokenLabel(d.address) || "Unknown token";
                html += `<div class="font-bold">${escapeHtml(sym)}</div>`;
                html += toAddressHtml(d.address);
            } else {
                html += toAddressHtml(d.address);
            }
        } else {
            html += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
        }
        html += `</div>`;
    }
    html += `</div>`;
    return html;
}

function renderSuccess() {
    const d = state.viewData;
    if (!d || !d.hash) return;

    const hasDecoded = d.decoded && d.decoded.details;

    // When decoded details are present, the Amount and To are already
    // shown inside the decoded well — hide the top-level duplicates.
    const summarySection = $("success-tx-summary").parentElement;
    const toSection = $("success-tx-to").parentElement;
    if (hasDecoded) {
        summarySection.classList.add("hidden");
        toSection.classList.add("hidden");
    } else {
        summarySection.classList.remove("hidden");
        toSection.classList.remove("hidden");
        $("success-tx-summary").textContent = d.amount + " " + d.symbol;
        $("success-tx-to").innerHTML = toAddressHtml(d.to);
    }

    $("success-tx-block").innerHTML = blockNumberHtml(d.blockNumber);
    $("success-tx-hash").innerHTML = txHashHtml(d.hash);

    // Show decoded calldata details if present
    const decodedEl = $("success-tx-decoded");
    if (decodedEl && hasDecoded) {
        decodedEl.innerHTML = decodedDetailsHtml(d.decoded);
        decodedEl.classList.remove("hidden");
    } else if (decodedEl) {
        decodedEl.classList.add("hidden");
    }

    attachCopyHandlers("view-success-tx");
    showView("success-tx");
}

function showError(txInfo, txHash, message) {
    endWait();

    const symbol =
        txInfo.token === "ETH"
            ? "ETH"
            : displaySymbol(txInfo.tokenSymbol || "?");
    state.viewData = {
        amount: txInfo.amount,
        symbol: symbol,
        to: txInfo.to,
        hash: txHash || null,
        message: message,
    };
    renderError();
}

function renderError() {
    const d = state.viewData;
    if (!d || !d.message) return;
    $("error-tx-summary").textContent = d.amount + " " + d.symbol;
    $("error-tx-to").innerHTML = toAddressHtml(d.to);
    $("error-tx-message").textContent = d.message;

    if (d.hash) {
        $("error-tx-hash").innerHTML = txHashHtml(d.hash);
        $("error-tx-hash-section").classList.remove("hidden");
        attachCopyHandlers("view-error-tx");
    } else {
        $("error-tx-hash-section").classList.add("hidden");
    }

    showView("error-tx");
}

function isApprovalPopup() {
    return new URLSearchParams(window.location.search).has("approval");
}

function navigateBack() {
    // Nothing should still be polling by now, but leaving a view is the
    // point at which its timers must be gone.
    endWait();
    if (isApprovalPopup()) {
        window.close();
        return;
    }
    // After a completed transaction, reset the navigation stack
    // and go directly to the address view (token or detail).
    // Use require() lazily to call show() without the ctx push wrapper.
    clearViewStack();
    state.viewStack.push("main");
    if (state.selectedToken) {
        state.viewStack.push("address");
        require("./addressToken").show();
    } else {
        require("./addressDetail").show();
    }
}

function init(_ctx) {
    ctx = _ctx;

    $("btn-success-tx-done").addEventListener("click", navigateBack);
    $("btn-error-tx-done").addEventListener("click", navigateBack);
}

module.exports = {
    init,
    showWait,
    restoreWait,
    endWait,
    showError,
    renderSuccess,
    renderError,
};
