# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
import hashlib, hmac, bech32, os
from typing import Union
from io import BytesIO
try:
    from pysecp256k1 import (
        ec_seckey_verify, ec_pubkey_create, ec_pubkey_serialize, ec_pubkey_parse,
        ec_seckey_tweak_add, ec_pubkey_tweak_add, tagged_sha256
    )
    from pysecp256k1.extrakeys import xonly_pubkey_from_pubkey, xonly_pubkey_serialize, xonly_pubkey_tweak_add
except ImportError:
    import ecdsa
    SECP256k1 = ecdsa.curves.SECP256k1
    CURVE_GEN = ecdsa.ecdsa.generator_secp256k1
    CURVE_ORDER = CURVE_GEN.order()
    FIELD_ORDER = SECP256k1.curve.p()
    INFINITY = ecdsa.ellipticcurve.INFINITY

from helpers import hash160, str_to_path
from base58 import encode_base58_checksum, decode_base58_checksum
from constants import BIP_341_H

HARDENED = 2 ** 31

Prv_or_PubKeyNode = Union["PrvKeyNode", "PubKeyNode"]


class InvalidKeyError(Exception):
    """Raised when derived key is invalid"""


def big_endian_to_int(b: bytes) -> int:
    """
    Big endian representation to integer.

    :param b: big endian representation
    :return: integer
    """
    return int.from_bytes(b, "big")


def int_to_big_endian(n: int, length: int) -> bytes:
    """
    Represents integer in big endian byteorder.

    :param n: integer
    :param length: byte length
    :return: big endian
    """
    return n.to_bytes(length, "big")


def hmac_sha512(key: bytes, msg: bytes) -> bytes:
    """
    Hash-based message authentication code with sha512

    :param key: secret key
    :param msg: message
    :return: digest bytes
    """
    return hmac.new(key=key, msg=msg, digestmod=hashlib.sha512).digest()


class PrivateKey(object):

    __slots__ = (
        "k",
        "K"
    )

    def __init__(self, sec_exp: Union[bytes, int]):
        """
        Initializes private key.

        :param sec_exp: secret
        """
        if isinstance(sec_exp, int):
            sec_exp = int_to_big_endian(sec_exp, 32)
        try:
            ec_seckey_verify(sec_exp)
            self.k = sec_exp
            self.K = PublicKey(ec_pubkey_create(self.k))
        except NameError:
            k = ecdsa.SigningKey.from_string(sec_exp, curve=SECP256k1)
            self.K = PublicKey(pub_key=k.get_verifying_key())
            self.k = k.to_string()
        else:
            ec_seckey_verify(self.k)
            self.K = PublicKey(ec_pubkey_create(self.k))

    def __bytes__(self) -> bytes:
        """
        Encodes private key into corresponding byte sequence.

        :return: byte representation of PrivateKey object
        """
        return self.k

    def __eq__(self, other: "PrivateKey") -> bool:
        """
        Checks whether two private keys are equal.

        :param other: other private key
        """
        return self.k == other.k

    def wif(self, compressed: bool = True, testnet: bool = False) -> str:
        """
        Encodes private key into wallet import/export format.

        :param compressed: whether public key is compressed (default=True)
        :param testnet: whether to encode as a testnet key (default=False)
        :return: WIF encoded private key
        """
        prefix = b"\xef" if testnet else b"\x80"
        suffix = b"\x01" if compressed else b""
        return encode_base58_checksum(prefix + bytes(self) + suffix)

    def tweak_add(self, tweak32: bytes) -> "PrivateKey":
        tweaked = ec_seckey_tweak_add(self.k, tweak32)
        return PrivateKey(sec_exp=tweaked)

    def address(self, compressed: bool = True, chain: str = "BTC",
                addr_fmt: str = "p2wpkh") -> str:
        return self.K.address(compressed, chain, addr_fmt)

    @classmethod
    def from_wif(cls, wif_str: str) -> "PrivateKey":
        """
        Initializes private key from wallet import format encoding.

        :param wif_str: wallet import format private key
        :return: private key
        """
        decoded = decode_base58_checksum(s=wif_str)
        if wif_str[0] in ("K", "L", "c"):
            # compressed key --> so remove last byte that has to be 01
            assert decoded[-1] == 1
            decoded = decoded[:-1]
        return cls(sec_exp=decoded[1:])

    @classmethod
    def parse(cls, key_bytes: bytes) -> "PrivateKey":
        """
        Initializes private key from byte sequence.

        :param key_bytes: byte representation of private key
        :return: private key
        """
        return cls(sec_exp=key_bytes)

    @classmethod
    def from_int(cls, sec_exp: int) -> "Privatekey":
        return cls(sec_exp=int_to_big_endian(sec_exp, 32))


class PublicKey(object):

    __slots__ = (
        "K"
    )

    def __init__(self, pub_key):
        """
        Initializes PublicKey object.

        :param key: secp256k1 pubkey or ecdsa.VerifyingKey
        """
        self.K = pub_key

    def __eq__(self, other: "PublicKey") -> bool:
        """
        Checks whether two public keys are equal.

        :param other: other public key
        """
        return self.sec() == other.sec()

    @property
    def point(self): # -> ecdsa.ellipticcurve.Point:
        """
        Point on curve (x and y coordinates).

        :return: point on curve
        """
        return self.K.pubkey.point

    def sec(self, compressed: bool = True) -> bytes:
        """
        Encodes public key to SEC format.

        :param compressed: whether to use compressed format (default=True)
        :return: SEC encoded public key
        """
        try:
            return ec_pubkey_serialize(self.K, compressed=compressed)
        except NameError:
            return self.K.to_string(encoding="compressed" if compressed else "uncompressed")

    def tweak_add(self, tweak32: bytes) -> "PublicKey":
        assert len(tweak32) == 32
        return PublicKey(pub_key=ec_pubkey_tweak_add(self.K, tweak32))

    def taptweak(self, tweak32: bytes = None) -> "bytes":
        xonly_key, _ = xonly_pubkey_from_pubkey(self.K)
        tweak = tweak32 or xonly_pubkey_serialize(xonly_key)
        tweak = tagged_sha256(b"TapTweak", tweak)
        tweaked_pubkey = xonly_pubkey_tweak_add(xonly_key, tweak)
        tweaked_xonly_pubkey, parity = xonly_pubkey_from_pubkey(tweaked_pubkey)
        return xonly_pubkey_serialize(tweaked_xonly_pubkey)

    @classmethod
    def parse(cls, key_bytes: bytes) -> "PublicKey":
        """
        Initializes public key from byte sequence.

        :param key_bytes: byte representation of public key
        :return: public key
        """
        try:
            return cls(pub_key=ec_pubkey_parse(key_bytes))
        except NameError:
            return cls(ecdsa.VerifyingKey.from_string(key_bytes, curve=SECP256k1))

    @classmethod
    def from_point(cls, point) -> "PublicKey":
        """
        Initializes public key from point on elliptic curve.

        :param point: point on elliptic curve
        :return: public key
        """
        return cls(ecdsa.VerifyingKey.from_public_point(point, curve=SECP256k1))

    def h160(self, compressed: bool = True) -> bytes:
        """
        SHA256 followed by RIPEMD160 of public key.

        :param compressed: whether to use compressed format (default=True)
        :return: SHA256(RIPEMD160(public key))
        """
        return hash160(self.sec(compressed=compressed))

    def address(self, compressed: bool = True, chain: str = "BTC",
                addr_fmt: str = "p2wpkh") -> str:
        """
        Generates bitcoin address from public key.

        :param compressed: whether to use compressed format (default=True)
        :param testnet: whether to encode as a testnet address (default=False)
        :param addr_type: which address type to generate:
                            1. p2pkh
                            2. p2sh-p2wpkh
                            3. p2wpkh (default)
        :return: bitcoin address
        """
        if chain == "BTC":
            hrp = "bc"
            pkh_prefix = b"\x00"
            sh_prefix = b"\x05"
        else:
            pkh_prefix = b"\x6f"
            sh_prefix = b"\xc4"
            if chain == "XRT":
                hrp = "bcrt"
            elif chain == "XTN":
                hrp = "tb"
            else:
                assert False

        if addr_fmt == "p2tr":
            tweaked_xonly = self.taptweak()
            return bech32.encode(hrp=hrp, witver=1, witprog=tweaked_xonly)

        h160 = self.h160(compressed=compressed)
        if addr_fmt == "p2pkh":
            return encode_base58_checksum(pkh_prefix + h160)
        elif addr_fmt == "p2wpkh":
            return bech32.encode(hrp=hrp, witver=0, witprog=h160)
        elif addr_fmt == "p2sh-p2wpkh":
            scr = b"\x00\x14" + h160  # witversion 0 + pubkey hash
            h160 = hash160(scr)
            return encode_base58_checksum(sh_prefix + h160)

        raise ValueError("Unsupported address type.")


class PubKeyNode(object):

    mark: str = "M"
    testnet_version: int = 0x043587CF
    mainnet_version: int = 0x0488B21E

    __slots__ = (
        "parent",
        "key",
        "chain_code",
        "depth",
        "index",
        "parsed_parent_fingerprint",
        "parsed_version",
        "testnet",
        "children"
    )

    def __init__(self, key: bytes, chain_code: bytes, index: int = 0,
                 depth: int = 0, testnet: bool = False,
                 parent: Union["PubKeyNode", "PrvKeyNode"] = None,
                 parent_fingerprint: bytes = None):
        """
        Initializes Pub/PrvKeyNode.

        :param key: public or private key
        :param chain_code: chain code
        :param index: current node derivation index (default=0)
        :param depth: current node depth (default=0)
        :param testnet: whether this node is testnet node (default=False)
        :param parent: parent node of the current node (default=None)
        :param parent_fingerprint: fingerprint of parent node (default=None)
        """
        self.parent = parent
        self.key = key
        self.chain_code = chain_code
        self.depth = depth
        self.index = index
        self.parsed_parent_fingerprint = parent_fingerprint
        self.parsed_version = None
        self.testnet = testnet

    def __eq__(self, other) -> bool:
        """
        Checks whether two private/public key nodes are equal.

        :param other: other private/public key node
        """
        if type(self) != type(other):
            return False
        self_key = big_endian_to_int(self.key)
        other_key = big_endian_to_int(other.key)
        return self_key == other_key and \
            self.chain_code == other.chain_code and \
            self.depth == other.depth and \
            self.index == other.index and \
            self.testnet == other.testnet and \
            self.parent_fingerprint == other.parent_fingerprint

    @property
    def public_key(self) -> PublicKey:
        """
        Public key node's public key.

        :return: public key of public key node
        """
        return PublicKey.parse(key_bytes=self.key)

    @property
    def parent_fingerprint(self) -> bytes:
        """
        Gets parent fingerprint.

        If node is parsed from extended key, only parsed parent fingerprint
        is available. If node is derived, parent fingerprint is calculated
        from parent node.

        :return: parent fingerprint
        """
        if self.parent:
            fingerprint = self.parent.fingerprint()
        else:
            fingerprint = self.parsed_parent_fingerprint
        # in case there is still None here - it is master
        return fingerprint or b"\x00\x00\x00\x00"

    @property
    def pub_version(self) -> int:
        """
        Decides which extended public key version integer to use
        based on testnet parameter.

        :return: extended public key version
        """
        if self.testnet:
            return PubKeyNode.testnet_version
        return PubKeyNode.mainnet_version

    def __repr__(self) -> str:
        if self.is_master() or self.is_root():
            return self.mark
        if self.is_hardened():
            index = str(self.index - 2**31) + "'"
        else:
            index = str(self.index)
        parent = str(self.parent) if self.parent else self.mark
        return parent + "/" + index

    def is_hardened(self) -> bool:
        """Check whether current key node is hardened."""
        return self.index >= 2**31

    def is_master(self) -> bool:
        """Check whether current key node is master node."""
        return self.depth == 0 and self.index == 0 and self.parent is None

    def is_root(self) -> bool:
        """Check whether current key node is root (has no parent)."""
        return self.parent is None

    def fingerprint(self) -> bytes:
        """
        Gets current node fingerprint.

        :return: first four bytes of SHA256(RIPEMD160(public key))
        """
        return hash160(self.public_key.sec())[:4]

    @classmethod
    def parse(cls, s: Union[str, bytes, BytesIO],
              testnet: bool = False) -> Prv_or_PubKeyNode:
        """
        Initializes private/public key node from serialized node or
        extended key.

        :param s: serialized node or extended key
        :param testnet: whether this node is testnet node
        :return: public/private key node
        """
        if isinstance(s, str):
            s = BytesIO(decode_base58_checksum(s=s))
        elif isinstance(s, bytes):
            s = BytesIO(s)
        elif isinstance(s, BytesIO):
            pass
        else:
            raise ValueError("has to be bytes, str or BytesIO")
        return cls._parse(s, testnet=testnet)

    @classmethod
    def _parse(cls, s: BytesIO, testnet: bool = False) -> Prv_or_PubKeyNode:
        """
        Initializes private/public key node from serialized node buffer.

        :param s: serialized node buffer
        :param testnet: whether this node is testnet node (default=False)
        :return: public/private key node
        """
        version = big_endian_to_int(s.read(4))
        depth = big_endian_to_int(s.read(1))
        parent_fingerprint = s.read(4)
        index = big_endian_to_int(s.read(4))
        chain_code = s.read(32)
        key_bytes = s.read(33)
        key = cls(
            key=key_bytes,
            chain_code=chain_code,
            index=index,
            depth=depth,
            testnet=testnet,
            parent_fingerprint=parent_fingerprint,
        )
        key.parsed_version = version
        return key

    def _serialize(self, key: bytes, version: int = None) -> bytes:
        """
        Serializes public/private key node to extended key format.

        :param version: extended public/private key version (default=None)
        :return: serialized extended public/private key node
        """
        # 4 byte: version bytes
        result = int_to_big_endian(version, 4)
        # 1 byte: depth: 0x00 for master nodes, 0x01 for level-1 derived keys
        result += int_to_big_endian(self.depth, 1)
        # 4 bytes: the fingerprint of the parent key (0x00000000 if master key)
        if self.is_master():
            result += int_to_big_endian(0x00000000, 4)
        else:
            result += self.parent_fingerprint
        # 4 bytes: child number. This is ser32(i) for i in xi = xpar/i,
        # with xi the key being serialized. (0x00000000 if master key)
        result += int_to_big_endian(self.index, 4)
        # 32 bytes: the chain code
        result += self.chain_code
        # 33 bytes: the public key or private key data
        # (serP(K) for public keys, 0x00 || ser256(k) for private keys)
        result += key
        return result

    def serialize_public(self, version: int = None) -> bytes:
        """
        Serializes public key node to extended key format.

        :param version: extended public key version (default=None)
        :return: serialized extended public key node
        """
        return self._serialize(
            version=self.pub_version if version is None else version,
            key=self.public_key.sec()
        )

    def extended_public_key(self, version: int = None) -> str:
        """
        Base58 encodes serialized public key node. If version is not
        provided (default) it is determined by result of self.pub_version.

        :param version: extended public key version (default=None)
        :return: extended public key
        """
        return encode_base58_checksum(self.serialize_public(version=version))

    def ckd(self, index: int) -> "PubKeyNode":
        """
        The function CKDpub((Kpar, cpar), i) → (Ki, ci) computes a child
        extended public key from the parent extended public key.
        It is only defined for non-hardened child keys.

        * Check whether i ≥ 231 (whether the child is a hardened key).
        * If so (hardened child):
            return failure
        * If not (normal child):
            let I = HMAC-SHA512(Key=cpar, Data=serP(Kpar) || ser32(i)).
        * Split I into two 32-byte sequences, IL and IR.
        * The returned child key Ki is point(parse256(IL)) + Kpar.
        * The returned chain code ci is IR.
        * In case parse256(IL) ≥ n or Ki is the point at infinity,
            the resulting key is invalid, and one should proceed with the next
             value for i.

        :param index: derivation index
        :return: derived child
        """
        if index >= HARDENED:
            raise RuntimeError("failure: hardened child for public ckd")
        I = hmac_sha512(
            key=self.chain_code,
            msg=self.key + int_to_big_endian(index, 4)
        )
        IL, IR = I[:32], I[32:]
        # TODO this does not check whether IL is not zero (secp256k1 also does not check)
        try:
            Ki = self.public_key.tweak_add(IL)
        except NameError:
            if big_endian_to_int(IL) >= CURVE_ORDER:
                InvalidKeyError(
                    "public key {} is greater/equal to curve order".format(
                        big_endian_to_int(IL)
                    )
                )
            point = PrivateKey.parse(IL).K.point + self.public_key.point
            if point == INFINITY:
                raise InvalidKeyError("public key is a point at infinity")
            Ki = PublicKey.from_point(point=point)

        child = self.__class__(
            key=Ki.sec(),
            chain_code=IR,
            index=index,
            depth=self.depth + 1,
            testnet=self.testnet,
            parent=self
        )
        return child


class PrvKeyNode(PubKeyNode):

    mark: str = "m"
    testnet_version: int = 0x04358394
    mainnet_version: int = 0x0488ADE4

    @property
    def private_key(self) -> PrivateKey:
        """
        Private key node's private key.

        :return: public key of private key node
        """
        if len(self.key) == 33 and self.key[0] == 0:
            return PrivateKey(self.key[1:])
        return PrivateKey(self.key)

    @property
    def public_key(self) -> PublicKey:
        """
        Private key node's public key.

        :return: public key of public key node
        """
        return self.private_key.K

    @property
    def prv_version(self) -> int:
        """
        Decides which extended private key version integer to use
        based on testnet parameter.

        :return: extended private key version
        """
        if self.testnet:
            return PrvKeyNode.testnet_version
        return PrvKeyNode.mainnet_version

    @classmethod
    def master_key(cls, bip39_seed: bytes, testnet=False) -> "PrvKeyNode":
        """
        Generates master private key node from bip39 seed.

        * Generate a seed byte sequence S (bip39_seed arg) of a chosen length
          (between 128 and 512 bits; 256 bits is advised) from a (P)RNG.
        * Calculate I = HMAC-SHA512(Key = "Bitcoin seed", Data = S)
        * Split I into two 32-byte sequences, IL and IR.
        * Use parse256(IL) as master secret key, and IR as master chain code.

        :param bip39_seed: bip39_seed
        :param testnet: whether this node is testnet node (default=False)
        :return: master private key node
        """
        I = hmac_sha512(key=b"Bitcoin seed", msg=bip39_seed)
        # private key
        IL = I[:32]
        # In case IL is 0 or ≥ n, the master key is invalid
        int_left_key = big_endian_to_int(IL)
        if int_left_key == 0:
            raise InvalidKeyError("master key is zero")
        try:
            ec_seckey_verify(IL)
        except NameError:
            if int_left_key >= CURVE_ORDER:
                raise InvalidKeyError(
                    "master key {} is greater/equal to curve order".format(
                        int_left_key
                    )
                )
        # chain code
        IR = I[32:]
        return cls(
            key=IL,
            chain_code=IR,
            testnet=testnet
        )

    def serialize_private(self, version: int = None) -> bytes:
        """
        Serializes private key node to extended key format.

        :param version: extended private key version (default=None)
        :return: serialized extended private key node
        """
        return self._serialize(
            version=self.prv_version if version is None else version,
            key=b"\x00" + bytes(self.private_key)
        )

    def extended_private_key(self, version: int = None) -> str:
        """
        Base58 encodes serialized private key node. If version is not
        provided (default) it is determined by result of self.prv_version.

        :param version: extended private key version (default=None)
        :return: extended private key
        """
        return encode_base58_checksum(self.serialize_private(version=version))

    def ckd(self, index: int) -> "PrvKeyNode":
        """
        The function CKDpriv((kpar, cpar), i) → (ki, ci) computes
        a child extended private key from the parent extended private key:

        * Check whether i ≥ 2**31 (whether the child is a hardened key).
        * If so (hardened child):
            let I = HMAC-SHA512(Key=cpar, Data=0x00 || ser256(kpar) || ser32(i))
            (Note: The 0x00 pads the private key to make it 33 bytes long.)
        * If not (normal child):
            let I = HMAC-SHA512(Key=cpar, Data=serP(point(kpar)) || ser32(i))
        * Split I into two 32-byte sequences, IL and IR.
        * The returned child key ki is parse256(IL) + kpar (mod n).
        * The returned chain code ci is IR.
        * In case parse256(IL) ≥ n or ki = 0, the resulting key is invalid,
            and one should proceed with the next value for i.
            (Note: this has probability lower than 1 in 2**127.)

        :param index: derivation index
        :return: derived child
        """
        if index >= HARDENED:
            # hardened
            data = b"\x00"+bytes(self.private_key) + int_to_big_endian(index, 4)
        else:
            data = self.public_key.sec() + int_to_big_endian(index, 4)
        I = hmac_sha512(key=self.chain_code, msg=data)
        IL, IR = I[:32], I[32:]
        try:
            ki = self.private_key.tweak_add(IL)
            # if ki == PrivateKey.from_int(0):
            #    InvalidKeyError("private key is zero")
        except NameError:
            if big_endian_to_int(IL) >= CURVE_ORDER:
                InvalidKeyError(
                    "private key {} is greater/equal to curve order".format(
                        big_endian_to_int(IL)
                    )
                )
            ki = (int.from_bytes(IL, "big") +
                  big_endian_to_int(bytes(self.private_key))) % CURVE_ORDER
            if ki == 0:
                InvalidKeyError("private key is zero")
            ki = int_to_big_endian(ki, 32)

        child = self.__class__(
            key=bytes(ki),
            chain_code=IR,
            index=index,
            depth=self.depth + 1,
            testnet=self.testnet,
            parent=self
        )
        return child


class BIP32Node:
    def __init__(self, node, netcode="XTN"):
        self.node = node
        self._netcode = netcode

    @classmethod
    def from_master_secret(cls, bip39_seed: bytes, netcode="XTN"):
        return cls(PrvKeyNode.master_key(bip39_seed, False if netcode == "BTC" else True),
                   netcode=netcode)

    @classmethod
    def from_hwif(cls, extended_key):
        assert extended_key[0] in "xt"
        testnet = extended_key[0] == "t"
        if extended_key[1:4] == "prv":
            ek = PrvKeyNode.parse(extended_key, testnet)
        else:
            ek = PubKeyNode.parse(extended_key, testnet)
        return cls(ek, netcode="XTN" if testnet else "BTC")

    @classmethod
    def from_chaincode_pubkey(cls, chain_code, pubkey, netcode="XTN"):
        node = PubKeyNode(pubkey, chain_code, 0, 0,
                          False if netcode == "BTC" else True)
        return cls(node, netcode=netcode)

    def subkey_for_path(self, path):
        path_list = str_to_path(path)
        node = self.node
        for idx in path_list:
            node = node.ckd(idx)
        return BIP32Node(node)

    def hwif(self, as_private=False):
        is_pub = type(self.node) is PubKeyNode
        if is_pub and as_private:
            raise ValueError("no private key")
        if as_private:
            return self.node.extended_private_key()
        return self.node.extended_public_key()

    @classmethod
    def from_wallet_key(cls, extended_key):
        return cls.from_hwif(extended_key)

    def hash160(self, compressed=True):
        return self.node.public_key.h160(compressed)

    def address(self, compressed=True, chain="XTN", addr_fmt="p2pkh"):
        return self.node.public_key.address(compressed, addr_fmt=addr_fmt,
                                            chain=chain)

    def sec(self, compressed=True):
        return self.node.public_key.sec(compressed)

    def fingerprint(self):
        return self.node.fingerprint()

    def netcode(self):
        return self._netcode

    def chain_code(self):
        return self.node.chain_code

    def privkey(self):
        assert isinstance(self.node, PrvKeyNode)
        return bytes(self.node.private_key)

    def parent_fingerprint(self):
        return self.node.parent_fingerprint


def ranged_unspendable_internal_key(chain_code=32 * b"\x01", subderiv="/<0;1>/*"):
    # provide ranged provably unspendable key in serialized extended key format for core to understand it
    # core does NOT understand 'unspend('
    pk = b"\x02" + bytes.fromhex(BIP_341_H)
    node = BIP32Node.from_chaincode_pubkey(chain_code, pk)
    return node.hwif() + subderiv


def random_keys(num_keys, path="86h/1h/0h"):
    keys = []
    for _ in range(num_keys):
        k = BIP32Node.from_master_secret(os.urandom(32))
        key = f"[{k.fingerprint().hex()}/{path}]{k.subkey_for_path(path).hwif()}"
        keys.append(key)

    return keys