
#
# Python-slip39 -- Ethereum SLIP-39 Account Generation and Recovery
#
# Copyright (c) 2022, Dominion Research & Development Corp.
#
# Python-slip39 is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.  It is also available under alternative (eg. Commercial) licenses, at
# your option.  See the LICENSE file at the top of the source tree.
#
# Python-slip39 is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#

from __future__		import annotations

import argparse
import codecs
import logging

from ..util		import log_cfg, log_level, input_secure, ordinal
from .			import recover, recover_bip39, produce_bip39

__author__                      = "Perry Kundert"
__email__                       = "perry@dominionrnd.com"
__copyright__                   = "Copyright (c) 2022 Dominion Research & Development Corp."
__license__                     = "Dual License: GPLv3 (or later) and Commercial (see LICENSE)"

log				= logging.getLogger( __package__ )


def main( argv=None ):
    ap				= argparse.ArgumentParser(
        description = "Recover and output secret Seed from SLIP-39 or BIP-39 Mnemonics",
        formatter_class = argparse.RawDescriptionHelpFormatter,
        epilog = """\
If you obtain a threshold number of SLIP-39 mnemonics, you can recover the original
secret Seed Entropy, and then re-generate one or more wallets from it.

Enter the mnemonics when prompted and/or via the command line with -m |--mnemonic "...".

The secret Seed Entropy can then be used to generate a new SLIP-39 encoded wallet:

    python3 -m slip39 --secret = "ab04...7f"

SLIP-39 Mnemonics may be encrypted with a passphrase; this is *not* Ledger-compatible, so it rarely
recommended!  Typically, on a Trezor "Model T", you recover using your SLIP-39 Mnemonics, and then
use the "Hidden wallet" feature (passwords entered on the device) to produce alternative sets of
accounts.

BIP-39 Mnemonics can be backed up as SLIP-39 Mnemonics, in two ways:

1) The actual BIP-39 standard 512-bit Seed can be generated by supplying --passphrase, but only at
the cost of 59-word SLIP-39 mnemonics.  This is because the *output* 512-bit BIP-39 Seed must be
stored in SLIP-39 -- not the *input* 128-, 160-, 192-, 224-, or 256-bit entropy used to create the
original BIP-39 mnemonic phrase.

2) The original BIP-39 12- or 24-word, 128- to 256-bit Seed Entropy can be recovered by supplying
--entropy.  This modifies the BIP-39 recovery to return the original BIP-39 Mnemonic Entropy, before
decryption and seed generation.  It has no effect for SLIP-39 recovery.

""" )

    ap.add_argument( '-v', '--verbose', action="count",
                     default=0,
                     help="Display logging information." )
    ap.add_argument( '-q', '--quiet', action="count",
                     default=0,
                     help="Reduce logging output." )
    ap.add_argument( '-m', '--mnemonic', action='append',
                     help="Supply another SLIP-39 (or a BIP-39) mnemonic phrase" )
    ap.add_argument( '-e', '--entropy', action='store_true',
                     default=None,
                     help="Return the BIP-39 Mnemonic Seed Entropy instead of the generated Seed (default: True if --using-bip39 w/o passphrase)" )
    ap.add_argument( '--no-entropy', dest='entropy', action='store_false',
                     help="Return the BIP-39 Mnemonic generated Seed" )
    ap.add_argument( '-b', '--bip39', action='store_true',
                     default=None,
                     help="Recover Entropy and generate 512-bit secret Seed from BIP-39 Mnemonic + passphrase" )
    ap.add_argument( '-u', '--using-bip39', action='store_true',
                     default=None,
                     help="Recover Entropy from SLIP-39, generate 512-bit secret Seed using BIP-39 Mnemonic + passphrase" )
    ap.add_argument( '--binary', action='store_true',
                     default=False,
                     help="Output seed in binary instead of hex" )
    ap.add_argument( '--language',
                     default=None,
                     help="BIP-39 Mnemonic language (default: english)" )
    ap.add_argument( '-p', '--passphrase',
                     default=None,
                     help="Decrypt the SLIP-39 or BIP-39 master secret w/ this passphrase, '-' reads it from stdin (default: None/'')" )
    args			= ap.parse_args( argv )

    log_cfg['level']		= log_level( args.verbose - args.quiet )

    # Set up logging; also, handle the degenerate case where logging has *already* been set up (and
    # basicConfig is a NO-OP), by (also) setting the logging level
    logging.basicConfig( **log_cfg )
    if args.verbose:
        logging.getLogger().setLevel( log_cfg['level'] )
    if args.entropy is None:
        # For SLIP-39, if using_bip39 (unless passphrase supplied), default to recover Seed Entropy
        # (not BIP-39 hashed derivation seed).  This defaults to recovering the BIP-39 encoded seed
        # entropy as Mnemonic.  Use --no-entropy to get it as a derivation Seed instead.
        args.entropy		= not args.bip39 and args.using_bip39 and not args.passphrase
    if args.entropy:
        assert not args.passphrase, "--entropy and --passphrase cannot be used together"
    # Optional passphrase (utf-8 encoded bytes)
    passphrase			= args.passphrase or ""
    if passphrase == '-':
        passphrase		= input_secure( 'Master seed passphrase: ', secret=True )
    elif passphrase:
        log.warning( "It is recommended to not use '-p|--passphrase <password>'; specify '-' to read from input" )
    passphrase			= passphrase.encode( 'UTF-8' )

    # If BIP-39 recovery designated, only a single mnemonic is allowed:
    secret			= None
    algo			= "BIP-39" if args.bip39 else "SLIP-39"
    mnemonics			= args.mnemonic or []
    if args.bip39:
        # Recover from a BIP-39 Mnemonic.  By default, outputs the generated 512-bit wallet Seed,
        # required to derive crypto accounts.  Optionally, with the --entropy option, returns the
        # un-decrypted original 128- to 256-bit Seed Entropy encoded in the BIP-39 Mnemonic phrase
        # (undecrypted; no --passphrase allowed).
        assert 0 <= len( mnemonics ) <= 1, "BIP-39 requires exactly one Mnemonic phrase"
        if not mnemonics:
            try:
                phrase		= input_secure( f"Enter {ordinal(len(mnemonics)+1)} {algo} mnemonic: ", secret=False )
            except KeyboardInterrupt:
                return 0
            mnemonics.append( phrase )
        try:
            secret		= recover_bip39(
                *mnemonics,
                passphrase	= passphrase,
                as_entropy	= args.entropy,
                language	= args.language,
            )
        except KeyboardInterrupt:
            return 0
        except Exception as exc:
            log.error( f"Could not recover {algo} seed with supplied mnemonic: {exc}" )
    else:
        # Collect more mnemonics 'til we can successfully recover the master secret seed
        while secret is None:
            try:
                secret		= recover(
                    mnemonics,
                    passphrase	= passphrase,
                    using_bip39	= args.using_bip39,
                    as_entropy	= args.entropy,
                    language	= args.language,
                )
            except KeyboardInterrupt:
                return 0
            except Exception as exc:
                if mnemonics:
                    log.info( f"Could not recover {algo} seed with {len(mnemonics)} supplied mnemonics: {exc}" )
                try:
                    phrase	= input_secure( f"Enter {ordinal(len(mnemonics)+1)} {algo} mnemonic: ", secret=False )
                except KeyboardInterrupt:
                    return 0
                if ':' in phrase:  # Discard any "<name>: <mnemonic>" name prefix.
                    _,phrase	= phrase.split( ':', 1 )
                mnemonics.append( phrase )
    if secret:
        if args.using_bip39 and args.entropy in (None, True):
            secret		= produce_bip39(
                entropy		= secret,
                language	= args.language,
            )
        else:
            secret		= codecs.encode( secret, 'hex_codec' ).decode( 'ascii' )
            log.info( f"Recovered {algo} secret; To re-generate SLIP-39 wallet, send it to: python3 -m slip39 --secret -" )
            if args.binary:
                secret		= ''.join( f"{int(h,16):0>4b}" for h in secret )
        print( secret )
    return 0 if secret else 1
