import types
from enum import Enum
from typing import Dict, List, Optional, Tuple, Union

from bsv.constants import OpCode
from bsv.utils import encode_pushdata, read_script_chunks


def build_pushdrop_locking_script(items: list[Union[str, bytes]]) -> str:
    """
    Build a PushDrop locking script:
    <data1> OP_DROP <data2> OP_DROP ... OP_TRUE
    Items may be str (utf-8 encoded) or bytes.
    """
    parts: list[bytes] = []
    for it in items:
        data = it.encode("utf-8") if isinstance(it, str) else bytes(it)
        parts.append(encode_pushdata(data))
        parts.append(OpCode.OP_DROP)
    parts.append(OpCode.OP_TRUE)
    return b"".join(parts).hex()


def parse_pushdrop_locking_script(script: Union[bytes, str]) -> list[bytes]:
    """
    Parse a PushDrop locking script built as: <data> OP_DROP ... OP_TRUE
    Returns the sequence of pushed data items.
    """
    # Convert hex string to bytes if needed
    if isinstance(script, str):
        script = bytes.fromhex(script)

    items: list[bytes] = []
    i = 0
    n = len(script)

    while i < n:
        op = script[i]
        i += 1

        if op == 0x51:  # OP_TRUE / OP_1
            break

        result = _parse_push_opcode(op, script, i, n)
        if result is None:
            continue  # OP_DROP or other non-push opcode

        data, new_i = result
        if data is None:
            break  # Invalid data, stop parsing

        # Skip empty data pushes for PushDrop
        if data:
            items.append(data)
        i = new_i

    return items


def _parse_push_opcode(op: int, script: bytes, i: int, n: int) -> Optional[tuple]:
    """Parse a single push opcode and return (data, new_index) or None if not a push."""
    # Ensure op is an integer for comparison
    if isinstance(op, bytes):
        op = op[0] if len(op) > 0 else 0
    if op <= 75:
        return _parse_direct_push(op, script, i, n)
    elif op == 0x4C:  # OP_PUSHDATA1
        return _parse_pushdata1(script, i, n)
    elif op == 0x4D:  # OP_PUSHDATA2
        return _parse_pushdata2(script, i, n)
    elif op == 0x4E:  # OP_PUSHDATA4
        return _parse_pushdata4(script, i, n)
    else:
        return None  # Not a push opcode


def _parse_direct_push(ln: int, script: bytes, i: int, n: int) -> Optional[tuple]:
    """Parse a direct push (length encoded in opcode)."""
    if i + ln > n:
        return None, None
    return script[i : i + ln], i + ln


def _parse_pushdata1(script: bytes, i: int, n: int) -> Optional[tuple]:
    """Parse OP_PUSHDATA1 (1-byte length)."""
    if i >= n:
        return None, None
    ln = script[i]
    i += 1
    if i + ln > n:
        return None, None
    return script[i : i + ln], i + ln


def _parse_pushdata2(script: bytes, i: int, n: int) -> Optional[tuple]:
    """Parse OP_PUSHDATA2 (2-byte length)."""
    if i + 1 >= n:
        return None, None
    ln = int.from_bytes(script[i : i + 2], "little")
    i += 2
    if i + ln > n:
        return None, None
    return script[i : i + ln], i + ln


def _parse_pushdata4(script: bytes, i: int, n: int) -> Optional[tuple]:
    """Parse OP_PUSHDATA4 (4-byte length)."""
    if i + 3 >= n:
        return None, None
    ln = int.from_bytes(script[i : i + 4], "little")
    i += 4
    if i + ln > n:
        return None, None
    return script[i : i + ln], i + ln


def parse_identity_reveal(items: list[bytes]) -> list[tuple[str, str]]:
    """
    Given data items from parse_pushdrop_locking_script, interpret as identity.reveal payload:
    [b'identity.reveal', b'field1', b'value1', ...] -> [(field1, value1), ...]
    """
    out: list[tuple[str, str]] = []
    if not items:
        return out
    try:
        if items[0].decode("utf-8") != "identity.reveal":
            return out
    except Exception:
        return out
    i = 1
    while i + 1 < len(items):
        try:
            k = items[i].decode("utf-8")
            v = items[i + 1].decode("utf-8")
            out.append((k, v))
        except Exception:
            break
        i += 2
    return out


# --- TS/Go-compatible lock-before PushDrop helpers ---


def create_minimally_encoded_script_chunk(data: bytes) -> str:
    """Return minimal encoding for data (OP_0/OP_1NEGATE/OP_1..OP_16 when applicable)."""
    if len(data) == 0:
        return b"\x00".hex()
    if len(data) == 1:
        b0 = data[0]
        if b0 == 0x00:
            return b"\x00".hex()  # OP_0
        if b0 == 0x81:
            return b"\x4f".hex()  # OP_1NEGATE
        if 0x01 <= b0 <= 0x10:
            return bytes([0x50 + b0]).hex()  # OP_1..OP_16
    result = encode_pushdata(data).hex()
    return result


def build_lock_before_pushdrop(
    fields: list[bytes],
    public_key: bytes,
    *,
    include_signature: bool = False,
    signature: Optional[bytes] = None,
    lock_position: str = "before",
) -> str:
    """
    Create a lock-before (or lock-after) PushDrop script:
    <pubkey> OP_CHECKSIG <fields...> OP_DROP/OP_2DROP...  (lock_position="before")
    <fields...> OP_DROP/OP_2DROP... <pubkey> OP_CHECKSIG  (lock_position="after")
    """
    lock_chunks = _create_lock_chunks(public_key)
    pushdrop_chunks = _create_pushdrop_chunks(fields, include_signature, signature)
    chunks = _arrange_chunks_by_position(lock_chunks, pushdrop_chunks, lock_position)
    byte_chunks = _convert_chunks_to_bytes(chunks)
    result = b"".join(byte_chunks)
    return result.hex()


def _create_lock_chunks(public_key: bytes) -> list[bytes]:
    """Create the locking chunks (pubkey + OP_CHECKSIG)."""
    return [bytes.fromhex(create_minimally_encoded_script_chunk(public_key)), OpCode.OP_CHECKSIG]


def _create_pushdrop_chunks(fields: list[bytes], include_signature: bool, signature: Optional[bytes]) -> list[bytes]:
    """Create PushDrop data chunks with appropriate DROP operations."""
    data_fields = list(fields)
    if include_signature and signature is not None:
        data_fields.append(signature)

    pushdrop_chunks = [bytes.fromhex(create_minimally_encoded_script_chunk(field)) for field in data_fields]

    not_yet_dropped = len(data_fields)

    while not_yet_dropped > 1:
        pushdrop_chunks.append(OpCode.OP_2DROP)
        not_yet_dropped -= 2

    if not_yet_dropped != 0:
        pushdrop_chunks.append(OpCode.OP_DROP)

    return pushdrop_chunks


def _arrange_chunks_by_position(
    lock_chunks: list[bytes], pushdrop_chunks: list[bytes], lock_position: str
) -> list[bytes]:
    """Arrange chunks based on lock position."""
    if lock_position == "before":
        return lock_chunks + pushdrop_chunks
    return pushdrop_chunks + lock_chunks


def _convert_chunks_to_bytes(chunks: list[bytes]) -> list[bytes]:  # NOSONAR - Complexity (16), requires refactoring
    """Convert all chunks to bytes, handling OpCodes."""
    byte_chunks = []
    for chunk in chunks:
        if isinstance(chunk, bytes):
            byte_chunks.append(chunk)
        elif hasattr(chunk, "value"):  # OpCode enum
            byte_chunks.append(chunk.value)
        else:
            try:
                if hasattr(chunk, "__bytes__"):
                    byte_chunks.append(bytes(chunk))
                else:
                    print(f"[ERROR] Cannot convert to bytes: {type(chunk)}, value: {chunk}")
                    byte_chunks.append(b"\x51")  # Fallback to OP_TRUE
            except Exception as e:
                print(f"[ERROR] Failed to convert {type(chunk)} to bytes: {e}")
                byte_chunks.append(b"\x51")  # Fallback to OP_TRUE

    return byte_chunks


def decode_lock_before_pushdrop(
    script: Union[bytes, str], *, lock_position: str = "before"
) -> Optional[dict[str, object]]:
    """
    Decode a lock-before (or lock-after) PushDrop script.
    Returns dict with pubkey and fields (list of bytes).
    """
    chunks = read_script_chunks(script)
    print("[decode] chunks:", [(c.op, c.data.hex() if c.data else None) for c in chunks])

    if len(chunks) < 2:
        print("[decode] not enough chunks")
        return None

    if lock_position == "before":
        return _decode_lock_before(chunks)
    else:
        return _decode_lock_after(chunks)


def _opcode_to_int(op) -> int:
    """Convert opcode to integer."""
    if isinstance(op, bytes):
        return int.from_bytes(op, "little")
    return op


def _decode_lock_before(chunks) -> Optional[dict[str, object]]:
    """Decode lock-before pattern: <pubkey> OP_CHECKSIG <fields...> DROP..."""
    first, second = chunks[0], chunks[1]
    print(f"[decode] first.op={first.op}, first.data={first.data.hex() if first.data else None}, second.op={second.op}")

    # Validate header
    sop = _opcode_to_int(second.op)
    opcs = _opcode_to_int(OpCode.OP_CHECKSIG)

    if sop != opcs or first.data is None or len(first.data) not in (33, 65):
        print("[decode] header mismatch")
        return None

    pubkey = first.data
    fields = _extract_fields_from_chunks(chunks, 2, len(chunks))
    return {"pubkey": pubkey, "fields": fields}


def _decode_lock_after(chunks) -> Optional[dict[str, object]]:
    """Decode lock-after pattern: <fields...> DROP... <pubkey> OP_CHECKSIG."""
    # Validate footer
    last_op = _opcode_to_int(chunks[-1].op)
    opcs = _opcode_to_int(OpCode.OP_CHECKSIG)

    if last_op != opcs:
        print("[decode] lock-after: no OP_CHECKSIG at end")
        return None

    pubkey_chunk = chunks[-2]
    print(
        f"[decode] lock-after: pubkey_chunk.op={pubkey_chunk.op}, pubkey_chunk.data={pubkey_chunk.data.hex() if pubkey_chunk.data else None}"
    )

    if pubkey_chunk.data is None or len(pubkey_chunk.data) not in (33, 65):
        print("[decode] lock-after: pubkey length mismatch")
        return None

    pubkey = pubkey_chunk.data
    fields = _extract_fields_from_chunks(chunks, 0, len(chunks) - 2)
    return {"pubkey": pubkey, "fields": fields}


def _extract_fields_from_chunks(chunks, start_idx: int, end_idx: int) -> list[bytes]:
    """Extract data fields from chunks, stopping at DROP opcodes."""
    fields: list[bytes] = []
    drop = _opcode_to_int(OpCode.OP_DROP)
    twodrop = _opcode_to_int(OpCode.OP_2DROP)

    for i in range(start_idx, end_idx):
        c = chunks[i]
        cop = _opcode_to_int(c.op)

        # Stop at DROP opcodes
        if _is_drop_opcode(cop, drop, twodrop):
            break

        # Process chunk and extract field data
        field_data = _process_chunk_for_field(c, cop)
        if field_data is not None:
            fields.append(field_data)

    return fields


def _is_drop_opcode(opcode: int, drop: int, twodrop: int) -> bool:
    """Check if opcode is a DROP or 2DROP."""
    return opcode in (drop, twodrop)


def _process_chunk_for_field(chunk, opcode: int) -> Optional[bytes]:
    """Process a chunk and return the field data."""
    # Handle empty data with special opcodes
    if _is_empty_data(chunk.data):
        return _get_special_opcode_value(opcode)

    return chunk.data or b""


def _is_empty_data(data) -> bool:
    """Check if data is None or empty."""
    return data is None or (isinstance(data, (bytes, bytearray)) and len(data) == 0)


def _get_special_opcode_value(opcode: int) -> Optional[bytes]:
    """Get special value for empty data opcodes."""
    if opcode == 0x00:
        return b"\x00"
    if opcode == 0x4F:
        return b"\x81"
    if 0x51 <= opcode <= 0x60:
        return bytes([opcode - 0x50])
    return None


# ---------------------------------------------------------------------------
# PushDrop class (TS/Go-like) – lock/unlock/decode
# ---------------------------------------------------------------------------


class PushDrop:
    def __init__(self, wallet, originator: Optional[str] = None):
        self.wallet = wallet
        self.originator = originator

    @staticmethod
    def decode(script: bytes) -> dict[str, object]:
        res = decode_lock_before_pushdrop(script) or decode_lock_before_pushdrop(script, lock_position="after") or {}
        # TS parity: key name lockingPublicKey
        if res:
            return {"lockingPublicKey": res.get("pubkey"), "fields": res.get("fields", [])}
        return {"lockingPublicKey": None, "fields": []}

    def lock(
        self,
        fields: list[bytes],
        protocol_id,
        key_id: str,
        counterparty,
        *,
        for_self: bool = False,
        include_signature: bool = True,
        lock_position: str = "before",
    ) -> str:  # 返り値をhex stringに
        pubhex = self._get_public_key_hex(protocol_id, key_id, counterparty, for_self)
        sig_bytes = self._create_signature_if_needed(fields, protocol_id, key_id, counterparty, include_signature)
        return self._build_locking_script(fields, pubhex, sig_bytes, include_signature, lock_position)

    def _get_public_key_hex(self, protocol_id, key_id, counterparty, for_self):
        """Get the public key hex from wallet."""
        args = {
            "protocolID": protocol_id,
            "keyID": key_id,
            "counterparty": counterparty,
            "forSelf": for_self,
        }
        pub = self.wallet.get_public_key(args, self.originator) or {}
        pubhex = pub.get("publicKey") or ""
        return pubhex

    def _create_signature_if_needed(self, fields, protocol_id, key_id, counterparty, include_signature):
        """Create signature if requested."""
        if not include_signature:
            return None

        data_to_sign = b"".join(fields)
        sargs = {
            "encryption_args": {
                "protocol_id": (
                    protocol_id if isinstance(protocol_id, dict) else {"securityLevel": 0, "protocol": str(protocol_id)}
                ),
                "key_id": key_id,
                "counterparty": counterparty,
            },
            "data": data_to_sign,
        }

        try:
            cres = self.wallet.create_signature(sargs, self.originator) or {}
            sig = cres.get("signature")
            if isinstance(sig, (bytes, bytearray)):
                return bytes(sig)
            return b"\x00"  # ensure an extra field exists when requested
        except Exception:
            return b"\x00"

    def _build_locking_script(self, fields, pubhex, sig_bytes, include_signature, lock_position):
        """Build the locking script from components."""
        if not isinstance(pubhex, str) or len(pubhex) < 66:
            return b"\x51".hex()

        try:
            result = build_lock_before_pushdrop(
                fields,
                bytes.fromhex(pubhex),
                include_signature=include_signature,
                signature=sig_bytes,
                lock_position=lock_position,
            )
            return result
        except Exception:
            return b"\x51".hex()

    def unlock(
        self,
        protocol_id,
        key_id: str,
        counterparty,
        *,
        sign_outputs: str = "all",
        anyone_can_pay: bool = False,
        prev_txid: Optional[str] = None,
        prev_vout: Optional[int] = None,
        prev_satoshis: Optional[int] = None,
        prev_locking_script: Optional[bytes] = None,
        outs: Optional[list] = None,
    ):
        # Map sign_outputs string to mode
        mode = SignOutputsMode.ALL
        so = (sign_outputs or "all").lower()
        if so == "none":
            mode = SignOutputsMode.NONE
        elif so == "single":
            mode = SignOutputsMode.SINGLE
        unlocker = PushDropUnlocker(
            self.wallet,
            protocol_id,
            key_id,
            counterparty,
            sign_outputs_mode=mode,
            anyone_can_pay=anyone_can_pay,
            prev_txid=prev_txid,
            prev_vout=prev_vout,
            prev_satoshis=prev_satoshis,
            prev_locking_script=prev_locking_script,
            outs=outs,
        )

        # Return an object exposing sign() that returns only the signature push (no pubkey push),
        # matching TS/Go tests that expect a single push and inspect the last SIGHASH byte.
        def _sign_only_sig(tx, input_index):
            full = unlocker.sign(tx, input_index)
            # full may be "<sig> <pubkey>". Return only first push.
            from bsv.utils import read_script_chunks

            try:
                ch = read_script_chunks(full)
                if ch and ch[0].data is not None:
                    from bsv.utils import encode_pushdata

                    return encode_pushdata(ch[0].data)
            except Exception:
                pass
            return full

        return types.SimpleNamespace(
            sign=_sign_only_sig,
            estimateLength=lambda: unlocker.estimate_length(),
        )


# ---------------------------------------------------------------------------
# Unlocker helper (stub) – will sign PushDrop outputs for spending
# ---------------------------------------------------------------------------


class SignOutputsMode(Enum):
    ALL = 1
    NONE = 2
    SINGLE = 3


class PushDropUnlocker:
    """Generate unlocking script for a PushDrop output (lock-before pattern).

    The locking script is:
        <pubkey> OP_CHECKSIG  <data...> ...
    Unlocking script therefore pushes a valid ECDSA signature for that pubkey.
    """

    def __init__(
        self,
        wallet,
        protocol_id,
        key_id,
        counterparty,
        sign_outputs_mode=SignOutputsMode.ALL,
        anyone_can_pay: bool = False,
        prev_txid: Optional[str] = None,
        prev_vout: Optional[int] = None,
        prev_satoshis: Optional[int] = None,
        prev_locking_script: Optional[bytes] = None,
        outs: Optional[list] = None,
    ):
        self.wallet = wallet
        self.protocol_id = protocol_id
        self.key_id = key_id
        self.counterparty = counterparty
        self.sign_outputs_mode = sign_outputs_mode
        self.anyone_can_pay = anyone_can_pay
        # Optional precise BIP143 context (TS/Go equivalent unlock params)
        self.prev_txid = prev_txid
        self.prev_vout = prev_vout
        self.prev_satoshis = prev_satoshis
        self.prev_locking_script = prev_locking_script
        # Outputs information for looking up corresponding public keys
        self.outs = outs

    def estimate_length(self) -> int:
        """Approximate unlocking script length for a single DER signature.

        Estimates: 1-byte length prefix + 最大73バイトのDER署名＋1バイトのSIGHASHフラグ。
        """
        return 1 + 73 + 1

    def estimate_length_bounds(self) -> tuple[int, int]:
        """Return (min_estimate, max_estimate) for unlocking script length.

        DER署名の長さは低S値などにより68〜73バイトの範囲で変動する。PUSHDATA長1＋DER長＋SIGHASH 1の範囲。
        """
        min_len = 1 + 68 + 1
        max_len = 1 + 73 + 1
        return (min_len, max_len)

    def sign(self, tx, input_index: int) -> bytes:
        """Create a signature for the given input using SIGHASH flags and return as pushdata.

        Flags: base (ALL/NONE/SINGLE) derived from sign_outputs_mode, always includes FORKID,
        and optionally ANYONECANPAY when anyone_can_pay is True.
        """
        # Try to get locking script from transaction input if not already set
        if not self.prev_locking_script:
            self._extract_locking_script_from_tx(tx, input_index)

        sighash_flag = self._compute_sighash_flag()
        hash_to_sign, used_preimage = self._compute_hash_to_sign(tx, input_index, sighash_flag)

        # Try script-specific signature methods first
        if self.prev_locking_script:
            sig = self._try_script_specific_signatures(hash_to_sign, sighash_flag, used_preimage)
            if sig:
                return sig

        # Fallback to derived key signature
        return self._create_fallback_signature(hash_to_sign, sighash_flag, used_preimage)

    def _extract_locking_script_from_tx(self, tx, input_index: int) -> None:
        """Extract locking script from transaction input if available."""
        if not hasattr(tx, "inputs") or input_index >= len(tx.inputs):
            return

        tin = tx.inputs[input_index]
        if not hasattr(tin, "source_transaction") or not tin.source_transaction:
            return

        src_tx = tin.source_transaction
        src_idx = getattr(tin, "source_output_index", 0)
        if not hasattr(src_tx, "outputs") or src_idx >= len(src_tx.outputs):
            return

        out = src_tx.outputs[src_idx]
        if not hasattr(out, "locking_script"):
            return

        ls = out.locking_script
        if hasattr(ls, "to_bytes"):
            self.prev_locking_script = ls.to_bytes()
        elif isinstance(ls, bytes):
            self.prev_locking_script = ls
        elif isinstance(ls, str):
            try:
                self.prev_locking_script = bytes.fromhex(ls)
            except Exception:
                pass

    def _try_script_specific_signatures(
        self, hash_to_sign: bytes, sighash_flag: int, used_preimage: bool
    ) -> Optional[bytes]:
        """Try P2PKH and PushDrop signature methods. Returns signature if successful, None otherwise."""
        sig = self._try_p2pkh_signature(hash_to_sign, sighash_flag)
        if sig:
            return sig

        return self._try_pushdrop_signature(hash_to_sign, sighash_flag, used_preimage)

    def _compute_sighash_flag(self) -> int:
        """Compute SIGHASH flag from sign_outputs_mode and anyone_can_pay settings."""
        base = 0x01  # ALL
        mode = self.sign_outputs_mode

        if isinstance(mode, SignOutputsMode):
            if mode is SignOutputsMode.ALL:
                base = 0x01
            elif mode is SignOutputsMode.NONE:
                base = 0x02
            elif mode is SignOutputsMode.SINGLE:
                base = 0x03
        # Back-compat for int/str usage
        elif mode in (2, "none", "NONE"):
            base = 0x02
        elif mode in (3, "single", "SINGLE"):
            base = 0x03

        sighash_flag = base | 0x40  # include FORKID
        if self.anyone_can_pay:
            sighash_flag |= 0x80
        return sighash_flag

    def _compute_hash_to_sign(self, tx, input_index: int, sighash_flag: int) -> tuple[bytes, bool]:
        """Compute the hash/preimage to sign. Returns (hash, used_preimage_flag)."""
        try:
            from bsv.transaction import Transaction as _Tx

            if isinstance(tx, _Tx):
                return self._compute_bip143_preimage(tx, input_index, sighash_flag)
            raise TypeError
        except Exception:
            return self._compute_fallback_hash(tx, input_index)

    def _compute_bip143_preimage(self, tx, input_index: int, sighash_flag: int) -> tuple[bytes, bool]:
        """Compute BIP143 preimage for Transaction objects."""
        from bsv.transaction_preimage import tx_preimage as _tx_preimage

        # If caller provided precise prevout context, use it
        if (
            self.prev_txid is not None
            and self.prev_vout is not None
            and self.prev_satoshis is not None
            and self.prev_locking_script is not None
        ):
            return self._compute_synthetic_preimage(tx, sighash_flag, _tx_preimage), True

        # Otherwise use tx.inputs if available
        return self._compute_inputs_preimage(tx, input_index, sighash_flag, _tx_preimage), True

    def _compute_synthetic_preimage(self, tx, sighash_flag: int, tx_preimage_fn) -> bytes:
        """Compute BIP143 preimage using explicit prevout context."""
        from bsv.script.script import Script
        from bsv.transaction_input import TransactionInput

        synthetic = TransactionInput(
            source_txid=self.prev_txid,
            source_output_index=int(self.prev_vout),
        )
        synthetic.satoshis = int(self.prev_satoshis)
        synthetic.locking_script = Script(self.prev_locking_script)
        synthetic.sighash = sighash_flag
        return tx_preimage_fn(0, [synthetic], tx.outputs, tx.version, tx.locktime)

    def _compute_inputs_preimage(self, tx, input_index: int, sighash_flag: int, tx_preimage_fn) -> bytes:
        """Compute BIP143 preimage using tx.inputs context."""
        for i, _in in enumerate(getattr(tx, "inputs", []) or []):
            if not hasattr(_in, "sighash"):
                _in.sighash = 65
            if i == int(input_index):
                _in.sighash = sighash_flag
        return tx_preimage_fn(input_index, tx.inputs, tx.outputs, tx.version, tx.locktime)

    def _compute_fallback_hash(self, tx, input_index: int) -> tuple[bytes, bool]:
        """Compute hash for non-Transaction objects using fallback methods."""
        if hasattr(tx, "preimage") and callable(tx.preimage):
            try:
                return tx.preimage(input_index), True
            except Exception:
                pass

        # Final fallback: use raw bytes
        if isinstance(tx, (bytes, bytearray)):
            return tx, False
        if hasattr(tx, "serialize"):
            return tx.serialize(), False
        return getattr(tx, "bytes", b""), False

    def _try_p2pkh_signature(self, hash_to_sign: bytes, sighash_flag: int) -> Optional[bytes]:
        """Try to create signature for P2PKH script. Returns None if not P2PKH."""
        # P2PKH: OP_DUP OP_HASH160 <hash160> OP_EQUALVERIFY OP_CHECKSIG
        if not (
            len(self.prev_locking_script) == 25
            and self.prev_locking_script[0:3] == b"v\xa9\x14"
            and self.prev_locking_script[-2:] == b"\x88\xac"
        ):
            return None

        hash160_bytes = self.prev_locking_script[3:23]

        create_args = {
            "protocolID": self.protocol_id,
            "keyID": self.key_id,
            "counterparty": self.counterparty,
            "hash160": hash160_bytes.hex(),
            "data": hash_to_sign,
        }
        res = self.wallet.create_signature(create_args, "") if hasattr(self.wallet, "create_signature") else {}
        sig = res.get("signature", b"")
        sig = bytes(sig) + bytes([sighash_flag])
        return encode_pushdata(sig)

    def _try_pushdrop_signature(self, hash_to_sign: bytes, sighash_flag: int, used_preimage: bool) -> Optional[bytes]:
        """Try to create signature for PushDrop script. Returns None if not PushDrop or fails."""
        try:
            decoded = PushDrop.decode(self.prev_locking_script)
            locking_pubkey = decoded.get("lockingPublicKey")
            if not locking_pubkey:
                print("[WARN] PushDropUnlocker.sign: Could not extract public key from PushDrop script")
                return None
            # Use protocol_id/key_id/counterparty to derive the key (same as fallback)
            # The derived key should match the locking public key if the protocol/key_id match
            create_args = {
                "protocol_id": self.protocol_id,
                "key_id": self.key_id,
                "counterparty": self.counterparty,
            }
            if used_preimage:
                create_args["hash_to_directly_sign"] = hash_to_sign
            else:
                create_args["data"] = hash_to_sign
            res = self.wallet.create_signature(create_args, "") if hasattr(self.wallet, "create_signature") else {}
            sig = res.get("signature", b"")
            sig = bytes(sig) + bytes([sighash_flag])
            return encode_pushdata(sig)
        except Exception as e:
            print(f"[WARN] PushDropUnlocker.sign: Error decoding PushDrop script: {e}")
            return None

    def _create_fallback_signature(self, hash_to_sign: bytes, sighash_flag: int, used_preimage: bool) -> bytes:
        """Create signature using derived key (fallback method)."""
        create_args = {
            "protocolID": self.protocol_id,
            "keyID": self.key_id,
            "counterparty": self.counterparty,
        }
        if used_preimage:
            create_args["hash_to_directly_sign"] = hash_to_sign
        else:
            create_args["data"] = hash_to_sign
        res = self.wallet.create_signature(create_args, "") if hasattr(self.wallet, "create_signature") else {}
        sig = res.get("signature", b"")
        sig = bytes(sig) + bytes([sighash_flag])
        return encode_pushdata(sig)


def make_pushdrop_unlocker(
    wallet,
    protocol_id,
    key_id,
    counterparty,
    sign_outputs_mode: SignOutputsMode = SignOutputsMode.ALL,
    anyone_can_pay: bool = False,
    prev_txid: Optional[str] = None,
    prev_vout: Optional[int] = None,
    prev_satoshis: Optional[int] = None,
    prev_locking_script: Optional[bytes] = None,
    outs: Optional[list] = None,
) -> PushDropUnlocker:
    """Convenience factory mirroring Go/TS helper to construct an unlocker.

    Returns a `PushDropUnlocker` ready to `sign(tx_bytes, input_index)`.
    """
    return PushDropUnlocker(
        wallet,
        protocol_id,
        key_id,
        counterparty,
        sign_outputs_mode,
        anyone_can_pay,
        prev_txid,
        prev_vout,
        prev_satoshis,
        prev_locking_script,
        outs,
    )
