# Copyright (C) 2018 The Electrum developers
# Distributed under the MIT software license, see the accompanying
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
#
# BTCQ migration (post-rewrite):
#
#   The extended-key derivation logic that used to live in this module
#   has moved to :mod:`electrum.btcq_extkey`. ML-DSA-65 keys cannot be
#   modeled with secp256k1 ``ECPubkey`` / ``ECPrivkey`` wrappers, which
#   was the silent-bug pattern this rewrite eliminated.
#
#   What remains here:
#     - BIP32 path-string utilities (``convert_bip32_*``,
#       ``is_bip32_derivation``, ``normalize_bip32_derivation``,
#       ``is_all_public_derivation``)
#     - ``KeyOriginInfo`` (PSBT-format key-origin record, no crypto)
#     - Constants (``BIP32_PRIME``, ``UINT32_MAX``, ``BIP32_HARDENED_CHAR``)
#
#   What moved out (use :mod:`electrum.btcq_extkey` instead):
#     - ``BIP32Node`` → ``BtcqExtKey``
#     - ``CKD_priv`` / ``_CKD_priv`` / ``CKD_pub`` / ``_CKD_pub`` (BTCQ has
#       no public-key derivation; private derivation lives on
#       ``BtcqExtKey.subkey_at_private_derivation``)
#     - ``xprv_header`` / ``xpub_header``
#     - ``is_xpub`` / ``is_xprv`` / ``xpub_from_xprv`` / ``xpub_type``
#     - ``root_fp_and_der_prefix_from_xkey``
#     - ``is_xkey_consistent_with_key_origin_info``
#     - ``InvalidMasterKeyVersionBytes``
#
#   Stubs for the moved symbols below raise :class:`RuntimeError` on call,
#   so any missed migration site fails loudly instead of silently
#   reaching for the wrong abstraction. Stubs are scheduled for removal
#   one release after the migration lands.

import binascii
import struct
from typing import List, Tuple, NamedTuple, Sequence, Optional

from .util import bfh


BIP32_PRIME = 0x80000000
UINT32_MAX = (1 << 32) - 1

BIP32_HARDENED_CHAR = "h"  # default "hardened" char we put in str paths


_MIGRATED_TO_BTCQ_EXTKEY = (
    "This symbol moved to electrum.btcq_extkey as part of the BTCQ HD "
    "derivation rewrite. Update the caller to import from btcq_extkey "
    "instead. See electrum/btcq_extkey.py for details."
)


def _migrated(*args, **kwargs):
    raise RuntimeError(_MIGRATED_TO_BTCQ_EXTKEY)


# ---- raising stubs for symbols that moved to btcq_extkey ---------------

class InvalidMasterKeyVersionBytes(Exception):
    """Stub. Use :class:`electrum.btcq_extkey.InvalidMasterKeyVersionBytes`."""


class BIP32Node:
    """Stub. Use :class:`electrum.btcq_extkey.BtcqExtKey`.

    Kept as a class symbol so disabled-feature modules (Lightning,
    hardware-wallet plugins) that still type-hint ``BIP32Node`` can
    import successfully. Any attempt to instantiate or call class
    methods raises :class:`RuntimeError`.
    """
    __slots__ = ()

    def __new__(cls, *args, **kwargs):
        raise RuntimeError(_MIGRATED_TO_BTCQ_EXTKEY)

    @classmethod
    def from_xkey(cls, *args, **kwargs):
        raise RuntimeError(_MIGRATED_TO_BTCQ_EXTKEY)

    @classmethod
    def from_rootseed(cls, *args, **kwargs):
        raise RuntimeError(_MIGRATED_TO_BTCQ_EXTKEY)

    @classmethod
    def from_bytes(cls, *args, **kwargs):
        raise RuntimeError(_MIGRATED_TO_BTCQ_EXTKEY)


# Function stubs for everything else that moved.
CKD_priv = _migrated
CKD_pub = _migrated
_CKD_priv = _migrated
_CKD_pub = _migrated
xprv_header = _migrated
xpub_header = _migrated
is_xpub = _migrated
is_xprv = _migrated
xpub_from_xprv = _migrated
xpub_type = _migrated
root_fp_and_der_prefix_from_xkey = _migrated
is_xkey_consistent_with_key_origin_info = _migrated


def protect_against_invalid_ecpoint(func):
    """Stub. The original decorator existed to catch secp256k1
    point-at-infinity returns from CKD_pub; BTCQ has no public-key
    derivation, so it has no purpose.
    """
    raise RuntimeError(_MIGRATED_TO_BTCQ_EXTKEY)


# ---- BIP32 path-string utilities (kept here, no crypto) ---------------

def convert_bip32_strpath_to_intpath(n: str) -> List[int]:
    """Convert bip32 path str to list of uint32 integers with prime flags
    m/0/-1/1' -> [0, 0x80000001, 0x80000001]

    based on code in trezorlib
    """
    if not n:
        return []
    if n.endswith("/"):
        n = n[:-1]
    n = n.split('/')
    # cut leading "m" if present, but do not require it
    if n[0] == "m":
        n = n[1:]
    path = []
    for x in n:
        if x == '':
            # gracefully allow repeating "/" chars in path.
            # makes concatenating paths easier
            continue
        prime = 0
        if x.endswith("'") or x.endswith("h"):  # note: some implementations also accept "H", "p", "P"
            x = x[:-1]
            prime = BIP32_PRIME
        if x.startswith('-'):
            if prime:
                raise ValueError(f"bip32 path child index is signalling hardened level in multiple ways")
            prime = BIP32_PRIME
        try:
            x_int = int(x)
        except ValueError as e:
            raise ValueError(f"failed to parse bip32 path: {(str(e))}") from None
        child_index = abs(x_int) | prime
        if child_index > UINT32_MAX:
            raise ValueError(f"bip32 path child index too large: {child_index} > {UINT32_MAX}")
        path.append(child_index)
    return path


def convert_bip32_intpath_to_strpath(path: Sequence[int], *, hardened_char=BIP32_HARDENED_CHAR) -> str:
    assert isinstance(hardened_char, str), hardened_char
    assert len(hardened_char) == 1, hardened_char
    s = "m/"
    for child_index in path:
        if not isinstance(child_index, int):
            raise TypeError(f"bip32 path child index must be int: {child_index}")
        if not (0 <= child_index <= UINT32_MAX):
            raise ValueError(f"bip32 path child index out of range: {child_index}")
        prime = ""
        if child_index & BIP32_PRIME:
            prime = hardened_char
            child_index = child_index ^ BIP32_PRIME
        s += str(child_index) + prime + '/'
    # cut trailing "/"
    s = s[:-1]
    return s


def is_bip32_derivation(s: str) -> bool:
    try:
        if not (s == 'm' or s.startswith('m/')):
            return False
        convert_bip32_strpath_to_intpath(s)
    except Exception:
        return False
    else:
        return True


def normalize_bip32_derivation(s: Optional[str], *, hardened_char=BIP32_HARDENED_CHAR) -> Optional[str]:
    if s is None:
        return None
    if not is_bip32_derivation(s):
        raise ValueError(f"invalid bip32 derivation: {s}")
    ints = convert_bip32_strpath_to_intpath(s)
    return convert_bip32_intpath_to_strpath(ints, hardened_char=hardened_char)


def is_all_public_derivation(path) -> bool:
    """Returns whether all levels in path use non-hardened derivation."""
    if isinstance(path, str):
        path = convert_bip32_strpath_to_intpath(path)
    for child_index in path:
        if child_index < 0:
            raise ValueError('the bip32 index needs to be non-negative')
        if child_index & BIP32_PRIME:
            return False
    return True


# ---- KeyOriginInfo (PSBT-format key-origin record) --------------------

class KeyOriginInfo:
    """
    Object representing the origin of a key.

    from https://github.com/bitcoin-core/HWI/blob/5f300d3dee7b317a6194680ad293eaa0962a3cc7/hwilib/key.py
    # Copyright (c) 2020 The HWI developers
    # Distributed under the MIT software license.
    """
    def __init__(self, fingerprint: bytes, path: Sequence[int]) -> None:
        """
        :param fingerprint: The 4 byte BIP 32 fingerprint of a parent key from which this key is derived from
        :param path: The derivation path to reach this key from the key at ``fingerprint``
        """
        self.fingerprint: bytes = fingerprint
        self.path: Sequence[int] = path

    @classmethod
    def deserialize(cls, s: bytes) -> 'KeyOriginInfo':
        """
        Deserialize a serialized KeyOriginInfo.
        They will be serialized in the same way that PSBTs serialize derivation paths
        """
        fingerprint = s[0:4]
        s = s[4:]
        path = list(struct.unpack("<" + "I" * (len(s) // 4), s))
        return cls(fingerprint, path)

    def serialize(self) -> bytes:
        """
        Serializes the KeyOriginInfo in the same way that derivation paths are stored in PSBTs
        """
        r = self.fingerprint
        r += struct.pack("<" + "I" * len(self.path), *self.path)
        return r

    def _path_string(self) -> str:
        strpath = self.get_derivation_path()
        if len(strpath) >= 2:
            assert strpath.startswith("m/")
        return strpath[1:]  # cut leading "m"

    def to_string(self) -> str:
        """
        Return the KeyOriginInfo as a string in the form <fingerprint>/<index>/<index>/...
        This is the same way that KeyOriginInfo is shown in descriptors
        """
        s = binascii.hexlify(self.fingerprint).decode()
        s += self._path_string()
        return s

    @classmethod
    def from_string(cls, s: str) -> 'KeyOriginInfo':
        """
        Create a KeyOriginInfo from the string
        :param s: The string to parse
        """
        s = s.lower()
        entries = s.split("/")
        fingerprint = binascii.unhexlify(s[0:8])
        path: Sequence[int] = []
        if len(entries) > 1:
            path = convert_bip32_strpath_to_intpath(s[9:])
        return cls(fingerprint, path)

    def get_derivation_path(self) -> str:
        """
        Return the string for just the path
        """
        return convert_bip32_intpath_to_strpath(self.path)

    def get_full_int_list(self) -> List[int]:
        """
        Return a list of ints representing this KeyOriginInfo.
        The first int is the fingerprint, followed by the path
        """
        xfp = [struct.unpack("<I", self.fingerprint)[0]]
        xfp.extend(self.path)
        return xfp

    def __eq__(self, other) -> bool:
        if not isinstance(other, KeyOriginInfo):
            return False
        return self.serialize() == other.serialize()

    def __repr__(self) -> str:
        return f"<KeyOriginInfo {self.to_string()}>"
