// Send view: collect To, Amount, Token. Then go to confirmation.

const {
    $,
    showFlash,
    addressTitle,
    displaySymbol,
    renderAddressHtml,
    attachCopyHandlers,
    goBack,
} = require("./helpers");
const { state, currentAddress } = require("../../shared/state");
let ctx;
const { getProvider } = require("../../shared/balances");
const { resolveTokenDecimals } = require("../../shared/approvalAmount");
const { resolveSymbol } = require("../../shared/tokenList");
const { isLowHolderCount } = require("../../shared/holders");
const { isSpoofedSymbol } = require("../../shared/symbolSpoof");
const { getAddress } = require("ethers");

const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

/**
 * Validate a destination address string.
 * Returns { valid: true } or { valid: false, error: "..." }.
 */
function validateToAddress(value) {
    const v = value.trim();
    if (!v) return { valid: false, error: "" };

    // ENS names: contains a dot and doesn't start with 0x
    if (v.includes(".") && !v.startsWith("0x")) {
        // Basic ENS format check: at least one label before and after dot
        if (/^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$/.test(v)) {
            return { valid: true };
        }
        return {
            valid: false,
            error: "Please enter a valid ENS name.",
        };
    }

    // Must look like an Ethereum address
    if (!/^0x[0-9a-fA-F]{40}$/.test(v)) {
        return {
            valid: false,
            error: "Please enter a valid Ethereum address.",
        };
    }

    // Reject zero address
    if (v.toLowerCase() === ZERO_ADDRESS) {
        return {
            valid: false,
            error: "Sending to the zero address is not allowed.",
        };
    }

    // EIP-55 checksum validation: all-lowercase is ok, otherwise must match checksum
    if (v !== v.toLowerCase()) {
        try {
            const checksummed = getAddress(v);
            if (checksummed !== v) {
                return {
                    valid: false,
                    error: "Address checksum is invalid. Please double-check the address.",
                };
            }
        } catch {
            return {
                valid: false,
                error: "Address checksum is invalid. Please double-check the address.",
            };
        }
    }

    // Warn if sending to own address
    const addr = currentAddress();
    if (addr && v.toLowerCase() === addr.address.toLowerCase()) {
        // Allow but will warn — we return valid with a warning
        return {
            valid: true,
            warning: "This is your own address. Are you sure?",
        };
    }

    return { valid: true };
}

function updateToValidation() {
    const input = $("send-to");
    const errorEl = $("send-to-error");
    const btn = $("btn-send-review");
    const value = input.value.trim();

    if (!value) {
        errorEl.textContent = "";
        btn.disabled = true;
        btn.classList.add("opacity-50");
        return;
    }

    const result = validateToAddress(value);
    if (!result.valid) {
        errorEl.textContent = result.error;
        errorEl.style.color = "#cc0000";
        btn.disabled = true;
        btn.classList.add("opacity-50");
    } else if (result.warning) {
        errorEl.textContent = result.warning;
        errorEl.style.color = "#b8860b";
        btn.disabled = false;
        btn.classList.remove("opacity-50");
    } else {
        errorEl.textContent = "";
        btn.disabled = false;
        btn.classList.remove("opacity-50");
    }
}

function renderSendTokenSelect(addr) {
    const sel = $("send-token");
    sel.innerHTML = '<option value="ETH">ETH</option>';
    const fraudSet = new Set(
        (state.fraudContracts || []).map((a) => a.toLowerCase()),
    );
    for (const t of addr.tokenBalances || []) {
        if (isSpoofedSymbol(t.symbol, t.address)) continue;
        if (fraudSet.has(t.address.toLowerCase())) continue;
        // An unknown holder count does not withhold a token the user holds:
        // only a count the explorer actually reported as below the threshold
        // does.  Otherwise a missing field makes a real asset unspendable.
        if (state.hideLowHolderTokens && isLowHolderCount(t.holders)) continue;
        const opt = document.createElement("option");
        opt.value = t.address;
        opt.textContent = displaySymbol(t.symbol);
        sel.appendChild(opt);
    }
}

function updateSendBalance() {
    const addr = currentAddress();
    if (!addr) return;
    const title = addressTitle(addr.address, state.wallets);
    $("send-from").innerHTML = renderAddressHtml(addr.address, {
        title,
        ensName: addr.ensName,
    });
    attachCopyHandlers($("send-from"));
    const token = state.selectedToken || $("send-token").value;
    if (token === "ETH") {
        $("send-balance").textContent =
            "Current balance: " + (addr.balance || "0") + " ETH";
    } else {
        const tb = (addr.tokenBalances || []).find(
            (t) => t.address.toLowerCase() === token.toLowerCase(),
        );
        const symbol = resolveSymbol(
            token,
            addr.tokenBalances,
            state.trackedTokens,
        );
        // A null balance is a holding whose scale nothing knows. Saying "0"
        // for it would be a claim about the amount; the send itself is
        // refused later by transferAmountUnits() for the same missing scale.
        const bal = tb ? tb.balance : "0";
        $("send-balance").textContent =
            bal == null
                ? "Current balance: unknown (" + symbol + ")"
                : "Current balance: " + bal + " " + symbol;
    }
}

function init(_ctx) {
    ctx = _ctx;
    $("send-token").addEventListener("change", updateSendBalance);

    // Initial state: disable review button until address is entered
    $("btn-send-review").disabled = true;
    $("btn-send-review").classList.add("opacity-50");

    // Validate address on input
    $("send-to").addEventListener("input", updateToValidation);

    $("btn-send-review").addEventListener("click", async () => {
        const to = $("send-to").value.trim();
        const amount = $("send-amount").value.trim();
        if (!to) {
            showFlash("Please enter a recipient address.");
            return;
        }

        // Re-validate before proceeding
        const validation = validateToAddress(to);
        if (!validation.valid) {
            showFlash(
                validation.error || "Please enter a valid Ethereum address.",
            );
            return;
        }
        if (!amount || isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) {
            showFlash("Please enter a valid amount.");
            return;
        }

        // Resolve ENS if needed
        let resolvedTo = to;
        let ensName = null;
        if (to.includes(".") && !to.startsWith("0x")) {
            try {
                const provider = getProvider(state.rpcUrl, state.networkId);
                const resolved = await provider.resolveName(to);
                if (!resolved) {
                    showFlash("Could not resolve " + to);
                    return;
                }
                resolvedTo = resolved;
                ensName = to;
            } catch {
                showFlash("Failed to resolve ENS name.");
                return;
            }
        }

        const token = state.selectedToken || $("send-token").value;
        const addr = currentAddress();

        let tokenSymbol = null;
        let tokenBalance = null;
        // The scale the amount and the balance below are rendered at, carried
        // forward so the transfer is encoded with the number the user read
        // rather than with whatever the contract answers at signing time. See
        // src/shared/transferAmount.js.
        let tokenDecimals = null;
        if (token !== "ETH") {
            const tb = (addr.tokenBalances || []).find(
                (t) => t.address.toLowerCase() === token.toLowerCase(),
            );
            tokenSymbol = resolveSymbol(
                token,
                addr.tokenBalances,
                state.trackedTokens,
            );
            // null carried through rather than flattened to "0": the confirm
            // screen states an unknown balance as unknown, and
            // validateTransfer() treats it as no balance to spend from, which
            // is the fail-closed side of an amount nobody can check.
            tokenBalance = tb ? (tb.balance ?? null) : "0";
            // Resolved the same way balances.js resolved the scale it
            // DISPLAYED this token's balance at: bundled list, then the user's
            // tracked tokens, then the explorer. The stored
            // tokenBalances[].decimals is the explorer's own answer alone, so
            // reading it raw carries a null forward for a token the wallet
            // does know the scale of — and displayedDecimals() then throws
            // inside estimateGas(), which the confirmation screen reports as
            // an unestimable fee. Unsendable, over a scale that was never in
            // doubt (https://git.eeqj.de/sneak/AutistMask/issues/349).
            // Still null when nothing knows: no fallback.
            //
            // Resolved WITH `wallets`, which balances.js does not pass: that
            // adds explorerDecimals()'s cross-address check, so a contract two
            // addresses report different scales for answers null rather than
            // picking one. That check has to apply here, because this value
            // encodes a transfer; balances.js is formatting one explorer row
            // at fetch time and cannot consult a state it is in the middle of
            // replacing.
            tokenDecimals = resolveTokenDecimals(token, {
                trackedTokens: state.trackedTokens,
                wallets: state.wallets,
            });
            // The two resolutions can therefore differ, and where they do, the
            // stored `balance` is a quantity computed at a scale this screen
            // has just declined to stand behind. Stating it would leave
            // validateTransfer() checking the amount against a number the
            // wallet does not vouch for, and — since the unknown-balance path
            // is gated on the balance, not on the scale — would leave the
            // fee-estimate failure as the only thing on the confirmation
            // screen, which says nothing about decimals. Unknown scale means
            // unknown balance. Only a stored quantity is withdrawn: the "0"
            // for a token that has no row at all is an absence of holdings,
            // which is true at every scale.
            if (tb && tokenDecimals === null) tokenBalance = null;
        }

        ctx.showConfirmTx({
            from: addr.address,
            to: resolvedTo,
            ensName: ensName,
            amount: amount,
            token: token,
            balance: addr.balance,
            tokenSymbol: tokenSymbol,
            tokenBalance: tokenBalance,
            tokenDecimals: tokenDecimals,
        });
    });

    $("btn-send-back").addEventListener("click", () => {
        $("send-token").classList.remove("hidden");
        $("send-token-static").classList.add("hidden");
        goBack();
    });
}

function resetSendValidation() {
    const errorEl = $("send-to-error");
    const btn = $("btn-send-review");
    if (errorEl) errorEl.textContent = "";
    if (btn) {
        btn.disabled = true;
        btn.classList.add("opacity-50");
    }
}

module.exports = {
    init,
    updateSendBalance,
    renderSendTokenSelect,
    resetSendValidation,
};
