const {
    $,
    showView,
    showFlash,
    balanceLinesForAddress,
    addressDotHtml,
    addressTitle,
    escapeHtml,
    displaySymbol,
    truncateMiddle,
    renderAddressHtml,
    attachCopyHandlers,
    goBack,
    pushCurrentView,
} = require("./helpers");
const { state, saveState } = require("../../shared/state");
const { formatAddressTotal, getAddressValue } = require("../../shared/prices");
const {
    fetchRecentTransactions,
    filterTransactions,
} = require("../../shared/transactions");
const { resolveEnsNames } = require("../../shared/ens");
const {
    updateSendBalance,
    renderSendTokenSelect,
    resetSendValidation,
} = require("./send");
const { log } = require("../../shared/log");
const makeBlockie = require("ethereum-blockies-base64");
const exportPrivkey = require("./exportPrivkey");
const { walletDefect } = require("../../shared/walletDefects");

// The defect of the wallet the selected address belongs to, or null. Both the
// send and the private-key export path check it before asking for a password,
// so a wallet that cannot derive its keys says so instead of failing after the
// user has typed one in.
function selectedWalletDefect() {
    if (state.selectedWallet === null) return null;
    return walletDefect(state.wallets[state.selectedWallet]);
}

let ctx;

function show() {
    state.selectedToken = null;
    const wallet = state.wallets[state.selectedWallet];
    const addr = wallet.addresses[state.selectedAddress];
    const ai = state.selectedAddress;
    $("address-title").textContent =
        wallet.name + " \u2014 Address " + (ai + 1);
    const blockieEl = $("address-jazzicon");
    blockieEl.innerHTML = "";
    const img = document.createElement("img");
    img.src = makeBlockie(addr.address);
    img.width = 48;
    img.height = 48;
    img.style.imageRendering = "pixelated";
    img.style.borderRadius = "50%";
    blockieEl.appendChild(img);
    const addrTitle = addressTitle(addr.address, state.wallets);
    $("address-line").innerHTML = renderAddressHtml(addr.address, {
        title: addrTitle,
        ensName: addr.ensName,
    });
    $("address-line").dataset.full = addr.address;
    attachCopyHandlers($("address-line"));
    const usdTotal = formatAddressTotal(getAddressValue(addr));
    $("address-usd-total").innerHTML = usdTotal || "&nbsp;";
    const ensEl = $("address-ens");
    // ENS is now shown inside renderAddressHtml, hide the separate element
    ensEl.classList.add("hidden");
    $("address-balances").innerHTML = balanceLinesForAddress(
        addr,
        state.trackedTokens,
        state.showZeroBalanceTokens,
    );
    $("address-balances")
        .querySelectorAll(".balance-row")
        .forEach((row) => {
            row.addEventListener("click", () => {
                state.selectedToken = row.dataset.token;
                ctx.showAddressToken();
            });
        });
    renderSendTokenSelect(addr);
    $("tx-list").innerHTML =
        '<div class="text-muted text-xs py-1">Loading...</div>';
    showView("address");
    loadTransactions(addr.address);
}

function isoDate(timestamp) {
    const d = new Date(timestamp * 1000);
    const pad = (n) => String(n).padStart(2, "0");
    if (state.utcTimestamps) {
        return (
            d.getUTCFullYear() +
            "-" +
            pad(d.getUTCMonth() + 1) +
            "-" +
            pad(d.getUTCDate()) +
            "T" +
            pad(d.getUTCHours()) +
            ":" +
            pad(d.getUTCMinutes()) +
            ":" +
            pad(d.getUTCSeconds()) +
            "Z"
        );
    }
    const offsetMin = -d.getTimezoneOffset();
    const sign = offsetMin >= 0 ? "+" : "-";
    const absOff = Math.abs(offsetMin);
    const tzStr = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
    return (
        d.getFullYear() +
        "-" +
        pad(d.getMonth() + 1) +
        "-" +
        pad(d.getDate()) +
        "T" +
        pad(d.getHours()) +
        ":" +
        pad(d.getMinutes()) +
        ":" +
        pad(d.getSeconds()) +
        tzStr
    );
}

function timeAgo(timestamp) {
    const seconds = Math.floor(Date.now() / 1000 - timestamp);
    if (seconds < 60) return seconds + " seconds ago";
    const minutes = Math.floor(seconds / 60);
    if (minutes < 60)
        return minutes + " minute" + (minutes !== 1 ? "s" : "") + " ago";
    const hours = Math.floor(minutes / 60);
    if (hours < 24) return hours + " hour" + (hours !== 1 ? "s" : "") + " ago";
    const days = Math.floor(hours / 24);
    if (days < 30) return days + " day" + (days !== 1 ? "s" : "") + " ago";
    const months = Math.floor(days / 30);
    if (months < 12)
        return months + " month" + (months !== 1 ? "s" : "") + " ago";
    const years = Math.floor(days / 365);
    return years + " year" + (years !== 1 ? "s" : "") + " ago";
}

let loadedTxs = [];

let ensNameMap = new Map();

async function loadTransactions(address) {
    try {
        const rawTxs = await fetchRecentTransactions(
            address,
            state.blockscoutUrl,
        );
        const result = filterTransactions(rawTxs, {
            hideSpoofedSymbols: state.hideSpoofedSymbols,
            hideLowHolderTokens: state.hideLowHolderTokens,
            hideFraudContracts: state.hideFraudContracts,
            hideDustTransactions: state.hideDustTransactions,
            dustThresholdGwei: state.dustThresholdGwei,
            fraudContracts: state.fraudContracts,
        });
        const txs = result.transactions;

        // Persist any newly discovered fraud contracts
        if (result.newFraudContracts.length > 0) {
            for (const addr of result.newFraudContracts) {
                if (!state.fraudContracts.includes(addr)) {
                    state.fraudContracts.push(addr);
                }
            }
            await saveState();
        }

        loadedTxs = txs;

        // Collect ALL unique addresses (from + to) for ENS resolution so
        // that reverse lookups work for every displayed address, not just
        // the ones that were originally entered as ENS names.
        const counterparties = [
            ...new Set(txs.flatMap((tx) => [tx.from, tx.to].filter(Boolean))),
        ];
        if (counterparties.length > 0) {
            try {
                ensNameMap = await resolveEnsNames(
                    counterparties,
                    state.rpcUrl,
                    state.networkId,
                );
            } catch {
                ensNameMap = new Map();
            }
        }

        renderTransactions(txs);
    } catch (e) {
        log.errorf("loadTransactions failed:", e.message);
        $("tx-list").innerHTML =
            '<div class="text-muted text-xs py-1">Failed to load transactions.</div>';
    }
}

function renderTransactions(txs) {
    const list = $("tx-list");
    if (txs.length === 0) {
        list.innerHTML =
            '<div class="text-muted text-xs py-1">No transactions found.</div>';
        return;
    }
    let html = "";
    let i = 0;
    for (const tx of txs) {
        // For swap transactions, show the user's own labelled wallet
        // address instead of the contract address (see issue #55).
        const counterparty =
            tx.direction === "contract" && tx.directionLabel === "Swap"
                ? tx.from
                : tx.direction === "sent" || tx.direction === "contract"
                  ? tx.to
                  : tx.from;
        const ensName = ensNameMap.get(counterparty) || null;
        const title = addressTitle(counterparty, state.wallets);
        // The explorer's method name for a contract call, title-cased.
        const dirLabel = escapeHtml(tx.directionLabel);
        const sym = displaySymbol(tx.symbol);
        const amountStr = tx.value
            ? escapeHtml(tx.value + " " + sym)
            : escapeHtml(sym);
        const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10));
        const displayAddr =
            title || ensName || truncateMiddle(counterparty, maxAddr);
        const addrStr = escapeHtml(displayAddr);
        const dot = addressDotHtml(counterparty);
        const err = tx.isError ? " (failed)" : "";
        const opacity = tx.isError ? " opacity:0.5;" : "";
        const ago = escapeHtml(timeAgo(tx.timestamp));
        const iso = escapeHtml(isoDate(tx.timestamp));
        html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
        html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
        html += `<div class="flex justify-between"><span class="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`;
        html += `</div>`;
        i++;
    }
    list.innerHTML = html;
    list.querySelectorAll(".tx-row").forEach((row) => {
        row.addEventListener("click", () => {
            const idx = parseInt(row.dataset.tx, 10);
            const tx = loadedTxs[idx];
            tx.fromEns = ensNameMap.get(tx.from) || null;
            tx.toEns = ensNameMap.get(tx.to) || null;
            ctx.showTransactionDetail(tx);
        });
    });
}

function init(_ctx) {
    ctx = _ctx;

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

    $("btn-send").addEventListener("click", () => {
        const defect = selectedWalletDefect();
        if (defect) {
            showFlash(defect.shortMessage);
            return;
        }
        const addr =
            state.wallets[state.selectedWallet].addresses[
                state.selectedAddress
            ];
        if (!addr.balance || parseFloat(addr.balance) === 0) {
            showFlash("Cannot send \u2014 zero balance.");
            return;
        }
        $("send-to").value = "";
        $("send-amount").value = "";
        $("send-token").classList.remove("hidden");
        $("send-token-static").classList.add("hidden");
        updateSendBalance();
        resetSendValidation();
        pushCurrentView();
        showView("send");
    });

    $("btn-receive").addEventListener("click", () => {
        ctx.showReceive();
    });

    $("btn-add-token").addEventListener("click", ctx.showAddTokenView);

    // More menu dropdown
    const moreBtn = $("btn-more-menu");
    const moreDropdown = $("more-menu-dropdown");
    moreBtn.addEventListener("click", (e) => {
        e.stopPropagation();
        const isOpen = !moreDropdown.classList.toggle("hidden");
        moreBtn.classList.toggle("bg-fg", isOpen);
        moreBtn.classList.toggle("text-bg", isOpen);
    });
    document.addEventListener("click", () => {
        moreDropdown.classList.add("hidden");
        moreBtn.classList.remove("bg-fg", "text-bg");
    });
    moreDropdown.addEventListener("click", (e) => {
        e.stopPropagation();
    });

    $("btn-export-privkey").addEventListener("click", () => {
        moreDropdown.classList.add("hidden");
        moreBtn.classList.remove("bg-fg", "text-bg");
        // There is no private key to export for an address this wallet
        // cannot derive. Without this the export screen would take a
        // password and then report it as wrong.
        const defect = selectedWalletDefect();
        if (defect) {
            showFlash(defect.shortMessage);
            return;
        }
        // No pushCurrentView() here: exportPrivkey.show() can return
        // without navigating, so it does its own push.
        exportPrivkey.show(state.selectedWallet, state.selectedAddress);
    });

    exportPrivkey.init();
}

module.exports = { init, show };
