"""
Builder for creating a Transaction. Calculate fees, add signers and sign.
"""

from neo3.api import noderpc
from neo3.api.helpers import signing
from neo3.network.payloads import transaction, verification
from neo3.wallet import account
from neo3.vm import ScriptBuilder, Syscalls
from typing import Optional


class TxBuilder:
    """
    Transaction builder.
    """

    def __init__(self, client: noderpc.NeoRpcClient, script: Optional[bytes] = None):
        self.client = client
        self.tx = transaction.Transaction(
            version=0,
            nonce=123,
            system_fee=0,
            network_fee=0,
            valid_until_block=0,
            attributes=[],
            signers=[],
            script=b"" if script is None else script,
        )
        self.signing_funcs: list[signing.SigningFunction] = []
        self.network = -1

    async def init(self) -> None:
        """
        Initialize the builder.
        """
        res = await self.client.get_version()
        self.network = res.protocol.network

    async def calculate_system_fee(self, _emit_log_marker=False) -> None:
        """
        Calculates and set the system fee. Requires at least one signer.
        """
        if len(self.tx.signers) == 0:
            raise ValueError(
                "Need at least one signer (a.k.a the sender who pays for the transaction) or the "
                "fee calculation will be incorrect"
            )
        if _emit_log_marker:
            await self.client.invoke_script(_log_marker_start())
        res = await self.client.invoke_script(self.tx.script, self.tx.signers)
        if _emit_log_marker:
            await self.client.invoke_script(_log_marker_stop())
        self.tx.system_fee = res.gas_consumed

    async def set_valid_until_block(self, blocks: int = 1500) -> None:
        """
        Set maximum time the transaction is valid in the mempool. Defaults to about 24h for a network with 15s blocktime.

        Args:
            blocks: until how many blocks from the current chain height is the transaction valid. Defaults to ~24 hours.
        """
        self.tx.valid_until_block = await self.client.get_block_count() + blocks

    async def calculate_network_fee(self) -> None:
        """
        Calculate and set the network fee. Requires at least one signer.
        """
        if len(self.tx.witnesses) == 0:
            if len(self.tx.signers) == 0:
                raise ValueError("Cannot calculate network fee without signers")
            # adding a witness(es) so we can calculate the network fee
            for _ in range(len(self.tx.signers)):
                self.tx.witnesses.append(TxBuilder._dummy_signing_witness())
            self.tx.network_fee = await self.client.calculate_network_fee(self.tx)
            # removing it here as it will be replaced by a proper one once we're signing
            self.tx.witnesses = []
        else:
            if len(self.tx.signers) == 0:
                raise ValueError("Cannot calculate network fee without signers")
            self.tx.network_fee = await self.client.calculate_network_fee(self.tx)

    @staticmethod
    def _dummy_signing_witness() -> verification.Witness:
        """single signature account witness"""
        acc = account.Account.create_new()
        if acc.contract is None:
            raise Exception(
                "Unreachable"
            )  # we know this can't happen, but mypy doesn't
        return verification.Witness(
            invocation_script=b"", verification_script=acc.contract.script
        )

    async def build_and_sign(self) -> transaction.Transaction:
        """
        Sign the transaction with all signers and return the finalized result.
        """
        len_signers = len(self.tx.signers)
        if len_signers == 0:
            raise ValueError("Cannot sign transaction without signers")

        if self.network == -1:
            raise ValueError(
                "Network value not valid (-1). Call init() to automatically sync it from the network or set the `network` attribute"
            )

        for f, s in zip(self.signing_funcs, self.tx.signers):
            await f(self.tx, signing.SigningDetails(self.network))
        return self.tx

    def build_unsigned(self) -> transaction.Transaction:
        """
        Return the unsigned transaction. For example for use in an offline signing scenario.
        """
        return self.tx

    def add_signer(
        self, func: signing.SigningFunction, signer: verification.Signer
    ) -> None:
        """
        Add a Signer with scopes to the transaction and its signing function.

        Args:
            func: one of neo3.api.helpers.signing.
            signer: a Signer determining the validity of the signature.

        Returns:

        """
        for s in self.tx.signers:
            if signer.account == s.account:
                raise ValueError(
                    f"Signer with same account ({signer.account} already exists."
                )
        self.tx.signers.append(signer)
        self.signing_funcs.append(func)


START_IGNORE_RUNTIMELOG = "START_IGNORE_RUNTIMELOG"
STOP_IGNORE_RUNTIMELOG = "STOP_IGNORE_RUNTIMELOG"


def _log_marker_start() -> bytes:
    sb = ScriptBuilder()
    sb.emit_push(START_IGNORE_RUNTIMELOG)
    sb.emit_syscall(Syscalls.SYSTEM_RUNTIME_LOG)
    return sb.to_array()


def _log_marker_stop() -> bytes:
    sb = ScriptBuilder()
    sb.emit_push(STOP_IGNORE_RUNTIMELOG)
    sb.emit_syscall(Syscalls.SYSTEM_RUNTIME_LOG)
    return sb.to_array()
