import math
import os
from logging import getLogger
from typing import Any, Dict, List, Optional, Tuple

import rlp
from eth.constants import SECPK1_N
from eth.vm.forks.frontier.transactions import FrontierTransaction
from eth_keys.exceptions import BadSignature
from eth_utils import to_canonical_address, to_checksum_address
from hexbytes import HexBytes
from web3 import Web3
from web3.contract import ContractConstructor

from gnosis.eth.constants import GAS_CALL_DATA_BYTE, NULL_ADDRESS
from gnosis.eth.contracts import (
    get_erc20_contract,
    get_paying_proxy_contract,
    get_safe_V0_0_1_contract,
)
from gnosis.eth.utils import mk_contract_address

logger = getLogger(__name__)


class InvalidERC20Token(Exception):
    pass


class SafeCreationTx:
    def __init__(
        self,
        w3: Web3,
        owners: List[str],
        threshold: int,
        signature_s: int,
        master_copy: str,
        gas_price: int,
        funder: Optional[str],
        payment_token: Optional[str] = None,
        payment_token_eth_value: float = 1.0,
        fixed_creation_cost: Optional[int] = None,
    ):
        """
        Prepare Safe creation
        :param w3: Web3 instance
        :param owners: Owners of the Safe
        :param threshold: Minimum number of users required to operate the Safe
        :param signature_s: Random s value for ecdsa signature
        :param master_copy: Safe master copy address
        :param gas_price: Gas Price
        :param funder: Address to refund when the Safe is created. Address(0) if no need to refund
        :param payment_token: Payment token instead of paying the funder with ether. If None Ether will be used
        :param payment_token_eth_value: Value of payment token per 1 Ether
        :param fixed_creation_cost: Fixed creation cost of Safe (Wei)
        """

        assert 0 < threshold <= len(owners)
        funder = funder or NULL_ADDRESS
        payment_token = payment_token or NULL_ADDRESS
        assert Web3.isChecksumAddress(master_copy)
        assert Web3.isChecksumAddress(funder)
        assert Web3.isChecksumAddress(payment_token)

        self.w3 = w3
        self.owners = owners
        self.threshold = threshold
        self.s = signature_s
        self.master_copy = master_copy
        self.gas_price = gas_price
        self.funder = funder
        self.payment_token = payment_token
        self.payment_token_eth_value = payment_token_eth_value
        self.fixed_creation_cost = fixed_creation_cost

        # Get bytes for `setup(address[] calldata _owners, uint256 _threshold, address to, bytes calldata data)`
        # This initializer will be passed to the proxy and will be called right after proxy is deployed
        safe_setup_data: bytes = self._get_initial_setup_safe_data(owners, threshold)

        # Calculate gas based on experience of previous deployments of the safe
        calculated_gas: int = self._calculate_gas(
            owners, safe_setup_data, payment_token
        )
        # Estimate gas using web3
        estimated_gas: int = self._estimate_gas(
            master_copy, safe_setup_data, funder, payment_token
        )
        self.gas = max(calculated_gas, estimated_gas)

        # Payment will be safe deploy cost + transfer fees for sending ether to the deployer
        self.payment = self._calculate_refund_payment(
            self.gas, gas_price, fixed_creation_cost, payment_token_eth_value
        )

        self.tx_dict: Dict[str, Any] = self._build_proxy_contract_creation_tx(
            master_copy=master_copy,
            initializer=safe_setup_data,
            funder=funder,
            payment_token=payment_token,
            payment=self.payment,
            gas=self.gas,
            gas_price=gas_price,
        )

        self.tx_pyethereum: FrontierTransaction = (
            self._build_contract_creation_tx_with_valid_signature(self.tx_dict, self.s)
        )
        self.tx_raw = rlp.encode(self.tx_pyethereum)
        self.tx_hash = self.tx_pyethereum.hash
        self.deployer_address = to_checksum_address(self.tx_pyethereum.sender)
        self.safe_address = mk_contract_address(self.tx_pyethereum.sender, 0)

        self.v = self.tx_pyethereum.v
        self.r = self.tx_pyethereum.r
        self.safe_setup_data = safe_setup_data

        assert mk_contract_address(self.deployer_address, nonce=0) == self.safe_address

    @property
    def payment_ether(self):
        return self.gas * self.gas_price

    @staticmethod
    def find_valid_random_signature(s: int) -> Tuple[int, int]:
        """
        Find v and r valid values for a given s
        :param s: random value
        :return: v, r
        """
        for _ in range(10000):
            r = int(os.urandom(31).hex(), 16)
            v = (r % 2) + 27
            if r < SECPK1_N:
                tx = FrontierTransaction(0, 1, 21000, b"", 0, b"", v=v, r=r, s=s)
                try:
                    tx.sender
                    return v, r
                except (BadSignature, ValueError):
                    logger.debug("Cannot find signature with v=%d r=%d s=%d", v, r, s)

        raise ValueError("Valid signature not found with s=%d", s)

    @staticmethod
    def _calculate_gas(
        owners: List[str], safe_setup_data: bytes, payment_token: str
    ) -> int:
        """
        Calculate gas manually, based on tests of previosly deployed safes
        :param owners: Safe owners
        :param safe_setup_data: Data for proxy setup
        :param payment_token: If payment token, we will need more gas to transfer and maybe storage if first time
        :return: total gas needed for deployment
        """
        # TODO Do gas calculation estimating the call instead this magic

        base_gas = 60580  # Transaction standard gas

        # If we already have the token, we don't have to pay for storage, so it will be just 5K instead of 20K.
        # The other 1K is for overhead of making the call
        if payment_token != NULL_ADDRESS:
            payment_token_gas = 55000
        else:
            payment_token_gas = 0

        data_gas = GAS_CALL_DATA_BYTE * len(safe_setup_data)  # Data gas
        gas_per_owner = 18020  # Magic number calculated by testing and averaging owners
        return (
            base_gas
            + data_gas
            + payment_token_gas
            + 270000
            + len(owners) * gas_per_owner
        )

    @staticmethod
    def _calculate_refund_payment(
        gas: int,
        gas_price: int,
        fixed_creation_cost: Optional[int],
        payment_token_eth_value: float,
    ) -> int:
        if fixed_creation_cost is None:
            # Payment will be safe deploy cost + transfer fees for sending ether to the deployer
            base_payment: int = (gas + 23000) * gas_price
            # Calculate payment for tokens using the conversion (if used)
            return math.ceil(base_payment / payment_token_eth_value)
        else:
            return fixed_creation_cost

    def _build_proxy_contract_creation_constructor(
        self,
        master_copy: str,
        initializer: bytes,
        funder: str,
        payment_token: str,
        payment: int,
    ) -> ContractConstructor:
        """
        :param master_copy: Master Copy of Gnosis Safe already deployed
        :param initializer: Data initializer to send to GnosisSafe setup method
        :param funder: Address that should get the payment (if payment set)
        :param payment_token: Address if a token is used. If not set, 0x0 will be ether
        :param payment: Payment
        :return: Transaction dictionary
        """
        if not funder or funder == NULL_ADDRESS:
            funder = NULL_ADDRESS
            payment = 0

        return get_paying_proxy_contract(self.w3).constructor(
            master_copy, initializer, funder, payment_token, payment
        )

    def _build_proxy_contract_creation_tx(
        self,
        master_copy: str,
        initializer: bytes,
        funder: str,
        payment_token: str,
        payment: int,
        gas: int,
        gas_price: int,
        nonce: int = 0,
    ):
        """
        :param master_copy: Master Copy of Gnosis Safe already deployed
        :param initializer: Data initializer to send to GnosisSafe setup method
        :param funder: Address that should get the payment (if payment set)
        :param payment_token: Address if a token is used. If not set, 0x0 will be ether
        :param payment: Payment
        :return: Transaction dictionary
        """
        return self._build_proxy_contract_creation_constructor(
            master_copy, initializer, funder, payment_token, payment
        ).buildTransaction(
            {
                "gas": gas,
                "gasPrice": gas_price,
                "nonce": nonce,
            }
        )

    def _build_contract_creation_tx_with_valid_signature(
        self, tx_dict: Dict[str, Any], s: int
    ) -> FrontierTransaction:
        """
        Use pyethereum `Transaction` to generate valid tx using a random signature
        :param tx_dict: Web3 tx dictionary
        :param s: Signature s value
        :return: PyEthereum creation tx for the proxy contract
        """
        zero_address = HexBytes("0x" + "0" * 40)
        f_address = HexBytes("0x" + "f" * 40)
        nonce = tx_dict["nonce"]
        gas_price = tx_dict["gasPrice"]
        gas = tx_dict["gas"]
        to = tx_dict.get("to", b"")  # Contract creation should always have `to` empty
        value = tx_dict["value"]
        data = tx_dict["data"]
        for _ in range(100):
            try:
                v, r = self.find_valid_random_signature(s)
                contract_creation_tx = FrontierTransaction(
                    nonce, gas_price, gas, to, value, HexBytes(data), v=v, r=r, s=s
                )
                sender_address = contract_creation_tx.sender
                contract_address: bytes = to_canonical_address(
                    mk_contract_address(sender_address, nonce)
                )
                if sender_address in (zero_address, f_address) or contract_address in (
                    zero_address,
                    f_address,
                ):
                    raise ValueError("Invalid transaction")
                return contract_creation_tx
            except BadSignature:
                pass
        raise ValueError("Valid signature not found with s=%d", s)

    def _estimate_gas(
        self, master_copy: str, initializer: bytes, funder: str, payment_token: str
    ) -> int:
        """
        Gas estimation done using web3 and calling the node
        Payment cannot be estimated, as no ether is in the address. So we add some gas later.
        :param master_copy: Master Copy of Gnosis Safe already deployed
        :param initializer: Data initializer to send to GnosisSafe setup method
        :param funder: Address that should get the payment (if payment set)
        :param payment_token: Address if a token is used. If not set, 0x0 will be ether
        :return: Total gas estimation
        """

        # Estimate the contract deployment. We cannot estimate the refunding, as the safe address has not any fund
        gas: int = self._build_proxy_contract_creation_constructor(
            master_copy, initializer, funder, payment_token, 0
        ).estimateGas()

        # We estimate the refund as a new tx
        if payment_token == NULL_ADDRESS:
            # Same cost to send 1 ether than 1000
            gas += self.w3.eth.estimate_gas({"to": funder, "value": 1})
        else:
            # Top should be around 52000 when storage is needed (funder no previous owner of token),
            # we use value 1 as we are simulating an internal call, and in that calls you don't pay for the data.
            # If it was a new tx sending 5000 tokens would be more expensive than sending 1 because of data costs
            try:
                gas += (
                    get_erc20_contract(self.w3, payment_token)
                    .functions.transfer(funder, 1)
                    .estimateGas({"from": payment_token})
                )
            except ValueError as exc:
                if "transfer amount exceeds balance" in str(exc):
                    return 70000
                raise InvalidERC20Token from exc

        return gas

    def _get_initial_setup_safe_data(self, owners: List[str], threshold: int) -> bytes:
        return (
            get_safe_V0_0_1_contract(self.w3, self.master_copy)
            .functions.setup(
                owners,
                threshold,
                NULL_ADDRESS,  # Contract address for optional delegate call
                b"",  # Data payload for optional delegate call
            )
            .buildTransaction(
                {
                    "gas": 1,
                    "gasPrice": 1,
                }
            )["data"]
        )
