# (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# decoders.py - Convert QR (or text) values into useful bitcoin-related objects.
#
#  included in Q builds only, not Mk4 --> manifest_q1.py
#
import ngu, bip39, ure, stash, json
from ubinascii import unhexlify as a2b_hex
from exceptions import QRDecodeExplained
from bbqr import TYPE_LABELS
from utils import decode_bip21_text


def decode_seed_qr(data):
    # SeedQR: 4 digit groups of index into word list
    parts = [data[pos:pos + 4] for pos in range(0, len(data), 4)]
    words = [bip39.wordlist_en[int(n)] for n in parts]
    return words

def txn_decoding_taster(txt):
    # look at first 4 bytes, and assume it's txn version number (LE32 0x1 or 0x2), then decode 
    # - working in normal RAM, won't handle full sized txn
    # - will not be binary
    # - not a very conclusive test, maybe should decode it more here?
    from ubinascii import a2b_base64

    if txt[0:8] in { '01000000', '02000000'}:
        # transaction in hex format
        return a2b_hex(txt)
    elif txt[0:4] in { 'AQAA', 'AgAA' }:
        # Base64 encoded
        return a2b_base64(txt)
    else:
        raise ValueError("not txn")
    

def decode_secret(got):
    # Decode a few different ways to store a master secret (in a QR), or raise
    # - xprv / tprv
    # - words (either full or prefixes, case insensitive)
    # - SeedQR (github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md)

    if len(got) > 300:
        raise ValueError("Too big.")

    # remove bitcoin: if present (unlikely)
    if ':' in got:
        _, got = got.split(':', 1)

    if got[1:4] == 'prv':
        # xprv or tprv: private key import for sure
        # - verify checksum is right
        try:
            raw = ngu.codecs.b58_decode(got)  
        except:
            raise ValueError('corrupt xprv?')

        return 'xprv', got

    if len(got) in (51, 52):
        try:
            from wif import decode_wif
            kp, testnet, compressed = decode_wif(got)
            return 'wif', (got, kp, compressed, testnet)
        except: pass
    
    taste = got.strip().lower()

    if taste.isdigit():
        try:
            words = decode_seed_qr(taste)
        except:
            raise ValueError('corrupt SeedQR?')
        assert len(words) in stash.SEED_LEN_OPTS, "seed len"
        return 'words', words

    words = taste.strip().split(' ')
    if len(words) in stash.SEED_LEN_OPTS:
        # looks like bip-39 words, decode and re-expand
        idx = [bip39.get_word_index(w) for w in words]
        return 'words', [bip39.wordlist_en[n] for n in idx]

    raise ValueError('no idea')

def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=False):
    # Could be BBQr or text
    # - if expect_text, just give us unparsed text back; after BBQr decode
    # - if expect_bbqr, always return tuple: (file_type, len, data)
    # - otherwise, returns a tuple: (type, (*parsed_data))

    if hasattr(got, 'storage'):
        # BBQr object
        try:
            ty, final_size, got = got.storage.finalize()
        except BaseException as exc:
            #import sys; sys.print_exception(exc)
            raise QRDecodeExplained("BBQr decode failed: " + str(exc))

        if expect_bbqr:
            return (ty, final_size, got)

        if expect_secret and ty in 'PT':
            raise QRDecodeExplained('Expected secrets not PSBT/TXN')

        if expect_text:
            if ty != 'U':
                raise QRDecodeExplained('Expected text, got ' + TYPE_LABELS.get(ty, ty))
            return got.decode()

        if ty == 'P':
            # may already be in PSRAM, avoid a copy here
            from glob import PSRAM
            if PSRAM.is_at(got, 0):
                got = 'PSRAM'       # see qr_psbt_sign()

            return 'psbt', (None, final_size, got)

        elif ty == 'T':
            return 'txn', (got,)

        elif ty == 'U':
            # continue thru code below for TEXT
            pass

        elif ty == 'J':
            what = "json"
            if "msg" in got:
                what = "smsg"

            return what, (got,)

        elif ty in 'RSE':
            # key-teleport related

            from pincodes import pa
            if pa.hobbled_mode and ty != 'E':
                raise QRDecodeExplained("KT Blocked")

            if ty == 'R' and len(got) != 33:
                raise QRDecodeExplained("Truncated KT RX")

            return 'teleport', (ty, got)
        else:
            msg = TYPE_LABELS.get(ty, 'Unknown FileType')
            raise QRDecodeExplained("Sorry, %s not useful." % msg)

    elif expect_bbqr:
        # convert as if it was BBQr of text
        return ('U', len(got), got)

    elif expect_text:
        # caller just wants text anyway, so we are done
        return got

    # First can we decode a master secret of some type?

    try:
        mode, value = decode_secret(got)
        return mode, (value,)
    except QRDecodeExplained:
        raise
    except BaseException as exc:
        #import sys; sys.print_exception(exc)
        if expect_secret:
            raise QRDecodeExplained("Unable to decode as secret")

    if expect_secret:
        raise QRDecodeExplained("Not a secret?")

    try:
        dct = json.loads(got)
        if "msg" in dct:
            return "smsg", (got,)
    except: pass

    # Sparrow compat
    if "signmessage" in got:
        return "smsg", (got,)

    # try to recognize various bitcoin-related text strings...
    return decode_short_text(got)

def decode_short_text(got):
    # Study short text received over QR or NFC, for useful things.
    # - case may be "wrong" but some values are case-sensitive (base58)
    # - not binary, but might be some other encoding than BBQr
    # - if bad checksum on bitcoin addr, we treat as text... since might be
    # return: what-it-is, (tuple)

    if not isinstance(got, str):
        # decode utf-8
        try:
            got = got.decode()
        except UnicodeError:
            raise QRDecodeExplained('UTF-8 decode failed')

    # might be a PSBT?
    if len(got) > 100:
        if got.lstrip().startswith("-----BEGIN BITCOIN SIGNED MESSAGE-----"):
            return "vmsg", (got,)

        from auth import psbt_encoding_taster
        try: 
            decoder, _, psbt_len = psbt_encoding_taster(got[0:10].encode(), len(got))
            return 'psbt', (decoder, psbt_len, got)
        except QRDecodeExplained:
            raise
        except:
            pass

        # might be txn, as hex or base64
        try:
            return 'txn', (txn_decoding_taster(got), )
        except:
            # was something else.
            pass

    from descriptor import Descriptor
    if Descriptor.is_descriptor(got):
        return 'minisc', (got,)

    # Things with newlines in them are not URL's
    # - working URLs are not >4k
    # - might be a story in text, etc.
    if (len(got) > 4096) or ('\n' in got):
        return 'text', (got,)

    # Might be an address or pubkey?
    try:
        return decode_bip21_text(got)
    except:
        # keep looking
        pass

    # catch-all as text. Can still show on-screen perhaps useful for other applications
    return 'text', (got,)


# EOF
