// Wallet operations: mnemonic generation, HD derivation, signing.
// All crypto delegated to ethers.js.

const { Mnemonic, HDNodeWallet, Wallet } = require("ethers");
const { DEBUG, DEBUG_MNEMONIC, BIP44_ETH_PATH } = require("./constants");

function generateMnemonic() {
    // This must stay the compile-time DEBUG constant. Do NOT switch it to
    // isDebug() from log.js: that also ORs in the runtime debugMode flag the
    // settings toggle drives, which would let a user of a release build turn
    // the hardcoded, publicly known test phrase back on for real wallets.
    if (DEBUG) return DEBUG_MNEMONIC;
    const m = Mnemonic.fromEntropy(
        globalThis.crypto.getRandomValues(new Uint8Array(16)),
    );
    return m.phrase;
}

// Every extended key (xprv or xpub) entering the app goes through this.
//
// ethers' HDNodeWallet.fromExtendedKey does NOT verify the base58 checksum
// when the decoded payload is the usual 82 bytes, which is exactly the case
// the checksum exists to catch: a key with a one-character typo parses into a
// *different* wallet instead of being rejected. Re-encoding the parsed node
// reproduces a well-formed key byte for byte, checksum included, so comparing
// the round trip against the input rejects any altered character. Measured by
// the sweep in tests/wallet.test.js over every single-character substitution
// of the BIP-32 vector 1 master key: 199 parse without the round-trip
// comparison, 0 with it.
//
// Returns the parsed node, or null if the key is not a well-formed extended
// key. Callers turn null into a user-facing error; none of them may fall back
// to fromExtendedKey directly.
function parseExtendedKey(key) {
    if (typeof key !== "string") return null;
    try {
        const node = HDNodeWallet.fromExtendedKey(key);
        return node.extendedKey === key ? node : null;
    } catch {
        return null;
    }
}

// A master key is at depth 0. Only from there is BIP44_ETH_PATH the absolute
// path it names; deriving it under an account-level or child key yields
// addresses that correspond to nothing the user holds.
const MASTER_DEPTH = 0;

// Parse an extended private key that the BIP-44 Ethereum account path can be
// derived from, or throw. Both callers derive BIP44_ETH_PATH from the result.
function masterXprvOrThrow(key) {
    const node = parseExtendedKey(key);
    if (!node) {
        throw new Error("Not a valid extended private key (xprv).");
    }
    if (!node.privateKey) {
        throw new Error("Not an extended private key (xprv).");
    }
    if (node.depth !== MASTER_DEPTH) {
        throw new Error(
            "Not a master extended private key (xprv): an account-level or " +
                "child key cannot be imported.",
        );
    }
    return node;
}

function deriveAddressFromXpub(xpub, index) {
    const node = parseExtendedKey(xpub);
    if (!node) {
        throw new Error("Not a valid extended key.");
    }
    return node.deriveChild(index).address;
}

function hdWalletFromMnemonic(mnemonic) {
    const node = HDNodeWallet.fromPhrase(mnemonic, "", BIP44_ETH_PATH);
    const xpub = node.neuter().extendedKey;
    const firstAddress = node.deriveChild(0).address;
    return { xpub, firstAddress };
}

function hdWalletFromXprv(xprv) {
    // BIP44_ETH_PATH is absolute ("m/..."), which ethers will only derive from
    // a depth-0 node. The relative form this used to derive would have been
    // applied *beneath* an account-level key instead of being refused.
    const node = masterXprvOrThrow(xprv).derivePath(BIP44_ETH_PATH);
    const xpub = node.neuter().extendedKey;
    const firstAddress = node.deriveChild(0).address;
    return { xpub, firstAddress };
}

// Well-formed extended private key. Says nothing about depth: the import view
// reports a non-master key separately, since "check it for a typo" is the
// wrong advice for a key the user copied correctly.
function isValidXprv(key) {
    const node = parseExtendedKey(key);
    return !!(node && node.privateKey);
}

// Whether an extended key is a master key, i.e. the one BIP44_ETH_PATH can be
// derived from. False for anything parseExtendedKey rejects.
function isMasterExtendedKey(key) {
    const node = parseExtendedKey(key);
    return !!node && node.depth === MASTER_DEPTH;
}

function addressFromPrivateKey(key) {
    const w = new Wallet(key);
    return w.address;
}

function getSignerForAddress(walletData, addrIndex, decryptedSecret) {
    if (walletData.type === "hd") {
        const node = HDNodeWallet.fromPhrase(
            decryptedSecret,
            "",
            BIP44_ETH_PATH,
        );
        return node.deriveChild(addrIndex);
    }
    if (walletData.type === "xprv") {
        // Checked here rather than through masterXprvOrThrow so the message
        // fits the situation: nobody is importing anything at signing time,
        // and this wallet is already in storage. src/shared/walletDefects.js
        // catches it at list-render time; this is the backstop behind that.
        const node = parseExtendedKey(decryptedSecret);
        if (!node || !node.privateKey) {
            throw new Error(
                "This wallet's stored key is not a valid extended private " +
                    "key, so it cannot sign.",
            );
        }
        if (node.depth !== MASTER_DEPTH) {
            throw new Error(
                "This wallet was imported from an extended private key that " +
                    "is not a master key, so it cannot sign.",
            );
        }
        return node.derivePath(BIP44_ETH_PATH).deriveChild(addrIndex);
    }
    return new Wallet(decryptedSecret);
}

function isValidMnemonic(mnemonic) {
    return Mnemonic.isValidMnemonic(mnemonic);
}

// Only an HD wallet has a recovery phrase. A "key" wallet holds a bare
// private key and an "xprv" wallet an extended private key; neither can be
// turned back into words, so neither may ever be offered the phrase display.
// Written as an allowlist on purpose: a wallet type added later is excluded
// until someone decides otherwise.
function walletHasRecoveryPhrase(walletData) {
    return !!walletData && walletData.type === "hd";
}

module.exports = {
    generateMnemonic,
    parseExtendedKey,
    deriveAddressFromXpub,
    hdWalletFromMnemonic,
    hdWalletFromXprv,
    isValidXprv,
    isMasterExtendedKey,
    addressFromPrivateKey,
    getSignerForAddress,
    isValidMnemonic,
    walletHasRecoveryPhrase,
};
