from binascii import hexlify, unhexlify
import copy
from io import BytesIO
import struct
from codecs import encode
import time
from enum import IntEnum
from typing import Optional, Tuple
from ..serialize import (
    deser_compact_size,
    deser_uint256,
    SER_DEFAULT,
    ser_compact_size,
    ser_uint256,
    uint256_from_bigendian,
    uint256_from_str,
    deser_string,
    ser_string,
    deser_vector,
    ser_vector,
    uint256_from_compact,
)
from ..util import hash256, sha256, uint256_to_rpc_hex
from ..constants import (
    COIN,
    BTCBCH_SIGHASH_ALL,
    BTCBCH_SIGHASH_FORKID,
    BTCBCH_SIGHASH_SINGLE,
    BTCBCH_SIGHASH_NONE,
    BTCBCH_SIGHASH_ANYONECANPAY,
)


class COutPoint:
    def __init__(self, txhash: str | bytes = 0, n=0):
        if isinstance(txhash, str):
            txhash = uint256_from_bigendian(hash)
        if isinstance(txhash, bytes):
            txhash = uint256_from_str(hash)
        self.hash = txhash
        self.n = n

    def deserialize(self, f):
        self.hash = deser_uint256(f)
        self.n = struct.unpack("<I", f.read(4))[0]

    def serialize(self, stype=SER_DEFAULT):
        r = b""
        r += ser_uint256(self.hash, stype)
        r += struct.pack("<I", self.n)
        return r

    def __repr__(self):
        return f"COutPoint(hash={self.hash:064x} n={int(self.n)})"


class CTxIn:
    def __init__(self, outpoint=None, script_sig=b"", n_sequence=0):
        if outpoint is None:
            self.prevout = COutPoint()
        else:
            self.prevout = outpoint
        self.script_sig = script_sig
        self.n_sequence = n_sequence

    def deserialize(self, f):
        self.prevout = COutPoint()
        self.prevout.deserialize(f)
        self.script_sig = deser_string(f)
        self.n_sequence = struct.unpack("<I", f.read(4))[0]

    def serialize(self, stype=SER_DEFAULT):
        r = b""
        r += self.prevout.serialize(stype)
        r += ser_string(self.script_sig, stype)
        r += struct.pack("<I", self.n_sequence)
        return r

    def __repr__(self):
        return f"CTxIn(prevout={repr(self.prevout)} scriptSig={hexlify(self.script_sig)} nSequence={int(self.n_sequence)})"


class TokenStructure(IntEnum):
    HAS_AMOUNT = 0x10
    HAS_NFT = 0x20
    HAS_COMMITMENT_LENGTH = 0x40


class TokenCapability(IntEnum):
    NO_CAPABILITY = 0x00
    MUTABLE = 0x01
    MINTING = 0x02


# pylint: disable=redefined-builtin
class TokenOutputData:
    __slots__ = ("id", "bitfield", "amount", "commitment")

    def __init__(
        self,
        id: int = 0,
        amount: int = 1,
        commitment: bytes = b"",
        bitfield: int = TokenStructure.HAS_AMOUNT,
    ):
        self.id = id
        self.amount = amount
        self.commitment = commitment
        self.bitfield = bitfield

    @property
    def id_hex(self) -> str:
        return ser_uint256(self.id)[::-1].hex()

    @id_hex.setter
    def id_hex(self, hex: str):
        b = bytes.fromhex(hex)
        assert len(b) == 32
        self.id = uint256_from_str(b[::-1])

    def deserialize(self, f):
        self.id = deser_uint256(f)
        self.bitfield = struct.unpack("<B", f.read(1))[0]
        if self.has_commitment_length():
            self.commitment = deser_string(f)
        else:
            self.commitment = b""
        if self.has_amount():
            self.amount = deser_compact_size(f)
        else:
            self.amount = 0

    def serialize(self) -> bytes:
        r = bytearray()
        r += ser_uint256(self.id)
        r += struct.pack("B", self.bitfield)
        if self.has_commitment_length():
            r += ser_string(self.commitment)
        if self.has_amount():
            r += ser_compact_size(self.amount)
        return bytes(r)

    def get_capability(self) -> int:
        return self.bitfield & 0x0F

    def has_commitment_length(self) -> bool:
        return bool(self.bitfield & TokenStructure.HAS_COMMITMENT_LENGTH)

    def has_amount(self) -> bool:
        return bool(self.bitfield & TokenStructure.HAS_AMOUNT)

    def has_nft(self) -> bool:
        return bool(self.bitfield & TokenStructure.HAS_NFT)

    def is_minting_nft(self) -> bool:
        return self.has_nft() and self.get_capability() == TokenCapability.MINTING

    def is_mutable_nft(self) -> bool:
        return self.has_nft() and self.get_capability() == TokenCapability.MUTABLE

    def is_immutable_nft(self) -> bool:
        return self.has_nft() and self.get_capability() == TokenCapability.NO_CAPABILITY

    def is_valid_bitfield(self) -> bool:
        s = self.bitfield & 0xF0
        if s >= 0x80 or s == 0x00:
            return False
        if self.bitfield & 0x0F > 2:
            return False
        if not self.has_nft() and not self.has_amount():
            return False
        if not self.has_nft() and (self.bitfield & 0x0F) != 0:
            return False
        if not self.has_nft() and self.has_commitment_length():
            return False
        return True

    def __repr__(self) -> str:
        return (
            f"TokenOutputData(id={self.id_hex} bitfield={self.bitfield:02x} amount={self.amount} "
            f"commitment={self.commitment[:40].hex()})"
        )


class Token:
    """Emulate the C++ 'token' namespace"""

    PREFIX_BYTE = bytes([0xEF])
    Structure = TokenStructure
    Capability = TokenCapability
    OutputData = TokenOutputData

    @classmethod
    def wrap_spk(
        cls, token_data: Optional[TokenOutputData], script_pub_key: bytes
    ) -> bytes:
        if not token_data:
            return script_pub_key
        buf = bytearray()
        buf += cls.PREFIX_BYTE
        buf += token_data.serialize()
        buf += script_pub_key
        return bytes(buf)

    @classmethod
    def unwrap_spk(cls, wrapped_spk: bytes) -> Tuple[Optional[TokenOutputData], bytes]:
        if not wrapped_spk or wrapped_spk[0] != cls.PREFIX_BYTE[0]:
            return None, wrapped_spk
        token_data = TokenOutputData()
        f = BytesIO(wrapped_spk)
        pfx = f.read(1)  # consume prefix byte
        assert pfx == cls.PREFIX_BYTE
        token_data.deserialize(
            f
        )  # unserialize token_data from buffer after prefix_byte
        if (
            not token_data.is_valid_bitfield()
            or (token_data.has_amount() and not token_data.amount)
            or (token_data.has_commitment_length() and not token_data.commitment)
        ):
            # Bad bitfield or 0 serialized amount or empty serialized commitment is a deserialization error,
            # just return the entire buffer as spk
            return None, wrapped_spk
        spk = wrapped_spk[f.tell() :]  # leftover bytes go to real spk
        return token_data, spk  # Parsed ok


class CTxOut:
    __slots__ = ("n_value", "script_pubkey", "token_data")

    def __init__(self, n_value=0, script_pubkey=b"", token_data=None):
        self.n_value = n_value
        self.script_pubkey = script_pubkey
        self.token_data = token_data

    def deserialize(self, f):
        self.n_value = struct.unpack("<q", f.read(8))[0]
        self.token_data, self.script_pubkey = Token.unwrap_spk(deser_string(f))

    def serialize(self, stype=SER_DEFAULT):
        r = b""
        r += struct.pack("<q", int(self.n_value))
        r += ser_string(Token.wrap_spk(self.token_data, self.script_pubkey), stype)
        return r

    def __repr__(self):
        return f"CTxOut(nValue={int(self.n_value // COIN)}.{int(self.n_value % COIN):08} scriptPubKey={hexlify(self.script_pubkey)} token_data={repr(self.token_data)}"


class CTransaction:
    def __init__(self, tx=None):
        if tx is None:
            self.n_version = 1
            self.vin = []
            self.vout = []
            self.n_lock_time = 0
            self.sha256 = None
            self.hash = None
        else:
            self.n_version = tx.n_version
            self.vin = copy.deepcopy(tx.vin)
            self.vout = copy.deepcopy(tx.vout)
            self.n_lock_time = tx.n_lock_time
            self.sha256 = None
            self.hash = None

    def deserialize(self, f):
        if isinstance(f, str):
            # str - assumed to be hex string
            f = BytesIO(unhexlify(f))
        elif isinstance(f, bytes):
            f = BytesIO(f)
        self.n_version = struct.unpack("<i", f.read(4))[0]
        self.vin = deser_vector(f, CTxIn)
        self.vout = deser_vector(f, CTxOut)
        self.n_lock_time = struct.unpack("<I", f.read(4))[0]
        self.sha256 = None
        self.hash = None
        return self

    def serialize(self, stype=SER_DEFAULT):
        r = b""
        r += struct.pack("<i", self.n_version)
        r += ser_vector(self.vin, stype)
        r += ser_vector(self.vout, stype)
        r += struct.pack("<I", self.n_lock_time)
        return r

    def to_hex(self):
        """Return the hex string serialization of this object"""
        return hexlify(self.serialize()).decode("utf-8")

    def rehash(self):
        self.sha256 = None
        self.calc_sha256()
        return self.hash

    def calc_sha256(self):
        if self.sha256 is None:
            self.sha256 = uint256_from_str(hash256(self.serialize()))
        self.hash = encode(hash256(self.serialize())[::-1], "hex_codec").decode("ascii")
        return self.hash

    def get_hash(self):
        if self.sha256 is None:
            self.rehash()
        return self.sha256

    def get_id(self):
        return self.get_hash()

    def is_valid(self):
        self.calc_sha256()
        for tout in self.vout:
            if tout.n_value < 0 or tout.n_value > 21000000 * COIN:
                return False
        return True

    def get_rpc_hex_id(self):
        """Returns the Id in the same format it would be returned via a RPC call"""
        return uint256_to_rpc_hex(self.get_hash())

    def __repr__(self):
        return f"CTransaction(n_version={int(self.n_version)} vin={repr(self.vin)} vout={repr(self.vout)} nLockTime={int(self.n_lock_time)})"

    # pylint: disable=too-many-arguments,too-many-positional-arguments
    def signature_hash(
        self,
        in_number,
        script_code,
        n_value,
        hashcode=BTCBCH_SIGHASH_ALL | BTCBCH_SIGHASH_FORKID,
        single=False,
        debug=False,
    ):
        """Calculate hash digest for given input, using SIGHASH_FORKID
        (Bitcoin Cash signing). Returns it in binary, little-endian.

        txin is the corresponding input CTransaction. Supplying it is
        necessary to include the scriptPubKey in the hashed output.

        If single is True, just a single invocation of SHA256 is done,
        instead of the usual, expected double hashing. This is to aid
        applications such as CHECKDATASIG(VERIFY).
        """
        hashdata = struct.pack("<I", self.n_version)

        h_prevouts = self.hash_prevouts(hashcode)
        if debug:
            print("Hash prevouts:", hexlify(h_prevouts[::-1]))
        hashdata += h_prevouts

        h_sequence = self.hash_sequence(hashcode)
        if debug:
            print("Hash sequence:", hexlify(h_sequence[::-1]))

        hashdata += h_sequence
        hashdata += self.vin[in_number].prevout.serialize()

        # FIXME: long scriptPubKeys not supported yet
        assert 75 >= len(script_code) > 0
        hashdata += struct.pack("<B", len(script_code))
        hashdata += script_code
        hashdata += struct.pack("<Q", n_value)
        hashdata += struct.pack("<I", self.vin[in_number].nSequence)

        h_outputs = self.hash_outputs(hashcode, in_number)
        if debug:
            print("Hash outputs:", hexlify(h_outputs[::-1]))
        hashdata += h_outputs
        hashdata += struct.pack("<I", self.n_lock_time)
        hashdata += struct.pack("<I", hashcode)

        if debug:
            print("Hash all data:", hexlify(hashdata))
        if single:
            return sha256(hashdata)

        return hash256(hashdata)

    def hash_prevouts(self, hashcode):
        if hashcode & BTCBCH_SIGHASH_ANYONECANPAY:
            return 32 * b"\x00"

        op_ser = b""
        for inp in self.vin:
            op_ser += inp.prevout.serialize()
        return hash256(op_ser)

    def hash_sequence(self, hashcode):
        if (
            hashcode & BTCBCH_SIGHASH_ANYONECANPAY
            or hashcode & 0x1F == BTCBCH_SIGHASH_SINGLE
            or hashcode & 0x1F == BTCBCH_SIGHASH_NONE
        ):
            return 32 * b"\x00"

        seq_ser = b""
        for inp in self.vin:
            seq_ser += struct.pack("<I", inp.n_sequence)
        return hash256(seq_ser)

    def hash_outputs(self, hashcode, in_number):
        if hashcode & 0x1F == BTCBCH_SIGHASH_SINGLE and in_number < len(self.vout):
            return hash256(self.vout[in_number].serialize())
        if (not hashcode & 0x1F == BTCBCH_SIGHASH_SINGLE) and (
            not hashcode & 0x1F == BTCBCH_SIGHASH_NONE
        ):
            out_ser = b""
            for out in self.vout:
                out_ser += out.serialize()
            return hash256(out_ser)

        return 32 * b"\x00"


# pylint: disable=too-many-instance-attributes
class CBlockHeader:
    def __init__(self, header=None):
        if header is None:
            self.set_null()
        else:
            self.n_version = header.n_version
            self.hash_prev_block = header.hash_prev_block
            self.hash_merkle_root = header.hash_merkle_root
            self.n_time = header.n_time
            self.n_bits = header.n_bits
            self.n_nonce = header.n_nonce
            self.sha256 = header.sha256
            self.hash = header.hash
            self.calc_sha256()

    def set_null(self):
        self.n_version = 1
        self.hash_prev_block = 0
        self.hash_merkle_root = 0
        self.n_time = 0
        self.n_bits = 0
        self.n_nonce = 0
        self.sha256 = None
        self.hash = None

    def deserialize(self, f):
        self.n_version = struct.unpack("<i", f.read(4))[0]
        self.hash_prev_block = deser_uint256(f)
        self.hash_merkle_root = deser_uint256(f)
        self.n_time = struct.unpack("<I", f.read(4))[0]
        self.n_bits = struct.unpack("<I", f.read(4))[0]
        self.n_nonce = struct.unpack("<I", f.read(4))[0]
        self.sha256 = None
        self.hash = None

    def serialize(self, stype=SER_DEFAULT):
        r = b""
        r += struct.pack("<i", self.n_version)
        r += ser_uint256(self.hash_prev_block, stype)
        r += ser_uint256(self.hash_merkle_root, stype)
        r += struct.pack("<I", self.n_time)
        r += struct.pack("<I", self.n_bits)
        r += struct.pack("<I", self.n_nonce)
        return r

    def calc_sha256(self):
        if self.sha256 is None:
            r = b""
            r += struct.pack("<i", self.n_version)
            r += ser_uint256(self.hash_prev_block)
            r += ser_uint256(self.hash_merkle_root)
            r += struct.pack("<I", self.n_time)
            r += struct.pack("<I", self.n_bits)
            r += struct.pack("<I", self.n_nonce)
            self.sha256 = uint256_from_str(hash256(r))
            self.hash = encode(hash256(r)[::-1], "hex_codec").decode("ascii")
        return self.hash

    def gethashprevblock(self, encoding="int"):
        assert encoding in ("hex", "int")
        if encoding == "int":
            return self.hash_prev_block
        return hex(self.hash_prev_block)

    def gethash(self, encoding="int"):
        assert encoding in ("hex", "int")
        self.calc_sha256()
        if encoding == "int":
            return self.sha256
        # return as a 32 bytes hex (64 / 2)
        return f"{self.sha256:064x}"

    def rehash(self):
        self.sha256 = None
        self.calc_sha256()
        return self.sha256

    def summary(self):
        s = []
        s.append(
            f"Block:  {self.gethash():064x}  Time:{time.ctime(self.n_time)}  Version:0x{self.n_version:x} Bits:0x{self.n_bits:08x}\n"
        )
        s.append(
            f"Parent: {self.hash_prev_block:064x}  Merkle: {self.hash_merkle_root:064x}"
        )
        return "".join(s)

    def __str__(self):
        return f"CBlockHeader(hash={self.gethash():064x} n_version={int(self.n_version)} hash_prev_block={self.hash_prev_block:064x} hash_merkle_root={self.hash_merkle_root:064x} n_time={time.ctime(self.n_time)} n_bits={self.n_bits:08x} n_nonce={self.n_nonce:08x})"

    def __repr__(self):
        return f"CBlockHeader(n_version={int(self.n_version)} hash_prev_block={self.hash_prev_block:064x} hash_merkle_root={self.hash_merkle_root:064x} n_time={time.ctime(self.n_time)} n_bits={self.n_bits:08x} n_nonce={self.n_nonce:08x})"


class CBlock(CBlockHeader):
    def __init__(self, header=None):
        super().__init__(header)
        self.vtx = []

    def deserialize(self, f):
        super().deserialize(f)
        self.vtx = deser_vector(f, CTransaction)

    def serialize(self, stype=SER_DEFAULT):
        r = b""
        r += super().serialize(stype)
        r += ser_vector(self.vtx, stype)
        return r

    def calc_merkle_root(self):
        hashes = []
        for tx in self.vtx:
            tx.rehash()
            hashes.append(ser_uint256(tx.sha256))
        while len(hashes) > 1:
            newhashes = []
            for i in range(0, len(hashes), 2):
                i2 = min(i + 1, len(hashes) - 1)
                newhashes.append(hash256(hashes[i] + hashes[i2]))
            hashes = newhashes
        if hashes:
            return uint256_from_str(hashes[0])
        return 0

    def is_valid(self):
        self.calc_sha256()
        target = uint256_from_compact(self.n_bits)
        if self.sha256 > target:
            return False
        for tx in self.vtx:
            if not tx.is_valid():
                return False
        if self.calc_merkle_root() != self.hash_merkle_root:
            return False
        return True

    def solve(self):
        self.rehash()
        target = uint256_from_compact(self.n_bits)
        while self.sha256 > target:
            self.n_nonce += 1
            self.rehash()

    def __str__(self):
        return f"CBlock(n_version={int(self.n_version)} hash_prev_block={self.hash_prev_block:064x} hash_merkle_root={self.hash_merkle_root:064x} n_time={time.ctime(self.n_time)} n_bits={self.n_bits:08x} n_nonce={self.n_nonce:08x} vtx_len={len(self.vtx)})"

    def __repr__(self):
        return f"CBlock(n_version={int(self.n_version)} hash_prev_block={self.hash_prev_block:064x} hash_merkle_root={self.hash_merkle_root:064x} n_time={time.ctime(self.n_time)} n_bits={self.n_bits:08x} n_nonce={self.n_nonce:08x} vtx={repr(self.vtx)})"


class MsgTx:
    command = b"tx"

    def __init__(self, tx=CTransaction()):
        self.tx = tx

    def deserialize(self, f):
        self.tx.deserialize(f)

    def serialize(self):
        return self.tx.serialize()

    def __repr__(self):
        return f"msg_tx(tx={repr(self.tx)})"


class MsgBlock:
    command = b"block"

    def __init__(self, block=None):
        if block is None:
            self.block = CBlock()
        else:
            self.block = block

    def deserialize(self, f):
        self.block.deserialize(f)

    def serialize(self):
        return self.block.serialize()

    def __str__(self):
        return f"msg_block(block={str(self.block)})"

    def __repr__(self):
        return f"msg_block(block={repr(self.block)})"


class MsgHeaders:
    """
    headers message has
    <count> <vector of block headers>
    """

    command = b"headers"

    def __init__(self, headers=None):
        self.headers = [] if headers is None else headers

    def deserialize(self, f):
        # comment in bitcoind indicates these should be deserialized as blocks
        blocks = deser_vector(f, CBlock)
        for x in blocks:
            self.headers.append(CBlockHeader(x))

    def serialize(self, stype=SER_DEFAULT):
        blocks = [CBlock(x) for x in self.headers]
        return ser_vector(blocks, stype)

    def __repr__(self):
        return f"msg_headers(headers={repr(self.headers)})"
