from typing import TYPE_CHECKING

from trezor.crypto import base58
from trezor.crypto.hashlib import sha256
from trezor.utils import HashWriter
from trezor.wire import ProcessError

from apps.common import address_type

from .common import ecdsa_hash_pubkey, encode_bech32_address
from .scripts import output_script_native_segwit, write_output_script_multisig

if TYPE_CHECKING:
    from trezor.messages import MultisigRedeemScriptType
    from trezor.crypto import bip32
    from apps.common.coininfo import CoinInfo
    from trezor.enums import InputScriptType


def get_address(
    script_type: InputScriptType,
    coin: CoinInfo,
    node: bip32.HDNode,
    multisig: MultisigRedeemScriptType | None = None,
) -> str:
    from trezor.enums import InputScriptType
    from .multisig import multisig_get_pubkeys, multisig_pubkey_index

    node_public_key = node.public_key()  # result_cache

    if multisig:
        # Ensure that our public key is included in the multisig.
        multisig_pubkey_index(multisig, node_public_key)

    if script_type in (InputScriptType.SPENDADDRESS, InputScriptType.SPENDMULTISIG):
        if multisig:  # p2sh multisig
            if coin.address_type_p2sh is None:
                raise ProcessError("Multisig not enabled on this coin")

            pubkeys = multisig_get_pubkeys(multisig)
            address = _address_multisig_p2sh(pubkeys, multisig.m, coin)
            if coin.cashaddr_prefix is not None:
                address = address_to_cashaddr(address, coin)
            return address
        if script_type == InputScriptType.SPENDMULTISIG:
            raise ProcessError("Multisig details required")

        # p2pkh
        address = node.address(coin.address_type)
        if coin.cashaddr_prefix is not None:
            address = address_to_cashaddr(address, coin)
        return address

    elif script_type == InputScriptType.SPENDWITNESS:  # native p2wpkh or native p2wsh
        if not coin.segwit or not coin.bech32_prefix:
            raise ProcessError("Segwit not enabled on this coin")
        # native p2wsh multisig
        if multisig is not None:
            pubkeys = multisig_get_pubkeys(multisig)
            return _address_multisig_p2wsh(pubkeys, multisig.m, coin.bech32_prefix)

        # native p2wpkh
        return address_p2wpkh(node_public_key, coin)

    elif script_type == InputScriptType.SPENDTAPROOT:  # taproot
        if not coin.taproot or not coin.bech32_prefix:
            raise ProcessError("Taproot not enabled on this coin")

        if multisig is not None:
            raise ProcessError("Multisig not supported for taproot")

        return _address_p2tr(node_public_key, coin)

    elif (
        script_type == InputScriptType.SPENDP2SHWITNESS
    ):  # p2wpkh or p2wsh nested in p2sh
        if not coin.segwit or coin.address_type_p2sh is None:
            raise ProcessError("Segwit not enabled on this coin")
        # p2wsh multisig nested in p2sh
        if multisig is not None:
            pubkeys = multisig_get_pubkeys(multisig)
            return _address_multisig_p2wsh_in_p2sh(pubkeys, multisig.m, coin)

        # p2wpkh nested in p2sh
        return address_p2wpkh_in_p2sh(node_public_key, coin)

    else:
        raise ProcessError("Invalid script type")


def _address_multisig_p2sh(pubkeys: list[bytes], m: int, coin: CoinInfo) -> str:
    if coin.address_type_p2sh is None:
        raise ProcessError("Multisig not enabled on this coin")
    redeem_script = HashWriter(coin.script_hash())
    write_output_script_multisig(redeem_script, pubkeys, m)
    return address_p2sh(redeem_script.get_digest(), coin)


def _address_multisig_p2wsh_in_p2sh(
    pubkeys: list[bytes], m: int, coin: CoinInfo
) -> str:
    if coin.address_type_p2sh is None:
        raise ProcessError("Multisig not enabled on this coin")
    witness_script_h = HashWriter(sha256())
    write_output_script_multisig(witness_script_h, pubkeys, m)
    return _address_p2wsh_in_p2sh(witness_script_h.get_digest(), coin)


def _address_multisig_p2wsh(pubkeys: list[bytes], m: int, hrp: str) -> str:
    if not hrp:
        raise ProcessError("Multisig not enabled on this coin")
    witness_script_h = HashWriter(sha256())
    write_output_script_multisig(witness_script_h, pubkeys, m)
    return _address_p2wsh(witness_script_h.get_digest(), hrp)


def address_pkh(pubkey: bytes, coin: CoinInfo) -> str:
    s = address_type.tobytes(coin.address_type) + coin.script_hash(pubkey).digest()
    return base58.encode_check(bytes(s), coin.b58_hash)


def address_p2sh(redeem_script_hash: bytes, coin: CoinInfo) -> str:
    s = address_type.tobytes(coin.address_type_p2sh) + redeem_script_hash
    return base58.encode_check(bytes(s), coin.b58_hash)


def address_p2wpkh_in_p2sh(pubkey: bytes, coin: CoinInfo) -> str:
    pubkey_hash = ecdsa_hash_pubkey(pubkey, coin)
    redeem_script = output_script_native_segwit(0, pubkey_hash)
    redeem_script_hash = coin.script_hash(redeem_script).digest()
    return address_p2sh(redeem_script_hash, coin)


def _address_p2wsh_in_p2sh(witness_script_hash: bytes, coin: CoinInfo) -> str:
    redeem_script = output_script_native_segwit(0, witness_script_hash)
    redeem_script_hash = coin.script_hash(redeem_script).digest()
    return address_p2sh(redeem_script_hash, coin)


def address_p2wpkh(pubkey: bytes, coin: CoinInfo) -> str:
    assert coin.bech32_prefix is not None
    pubkeyhash = ecdsa_hash_pubkey(pubkey, coin)
    return encode_bech32_address(coin.bech32_prefix, 0, pubkeyhash)


def _address_p2wsh(witness_script_hash: bytes, hrp: str) -> str:
    return encode_bech32_address(hrp, 0, witness_script_hash)


def _address_p2tr(pubkey: bytes, coin: CoinInfo) -> str:
    from trezor.crypto.curve import bip340

    assert coin.bech32_prefix is not None
    output_pubkey = bip340.tweak_public_key(pubkey[1:])
    return encode_bech32_address(coin.bech32_prefix, 1, output_pubkey)


def address_to_cashaddr(address: str, coin: CoinInfo) -> str:
    from trezor.crypto import cashaddr

    assert coin.cashaddr_prefix is not None
    raw = base58.decode_check(address, coin.b58_hash)
    version, data = raw[0], raw[1:]
    if version == coin.address_type:
        version = cashaddr.ADDRESS_TYPE_P2KH
    elif version == coin.address_type_p2sh:
        version = cashaddr.ADDRESS_TYPE_P2SH
    else:
        raise ValueError("Unknown cashaddr address type")
    return cashaddr.encode(coin.cashaddr_prefix, version, data)


def address_short(coin: CoinInfo, address: str) -> str:
    if coin.cashaddr_prefix is not None and address.startswith(
        coin.cashaddr_prefix + ":"
    ):
        return address[len(coin.cashaddr_prefix) + 1 :]
    else:
        return address
