from trezor.crypto import nem
from trezor.messages import NEMModificationType, NEMSupplyChangeType
from trezor.messages.NEMSignTx import (
    NEMAggregateModification,
    NEMImportanceTransfer,
    NEMMosaicCreation,
    NEMMosaicSupplyChange,
    NEMProvisionNamespace,
    NEMSignTx,
    NEMTransactionCommon,
    NEMTransfer,
)
from trezor.wire import ProcessError

from .helpers import (
    NEM_MAX_DIVISIBILITY,
    NEM_MAX_ENCRYPTED_PAYLOAD_SIZE,
    NEM_MAX_PLAIN_PAYLOAD_SIZE,
    NEM_MAX_SUPPLY,
    NEM_NETWORK_MAINNET,
    NEM_NETWORK_MIJIN,
    NEM_NETWORK_TESTNET,
    NEM_PUBLIC_KEY_SIZE,
)


def validate(msg: NEMSignTx):
    if msg.transaction is None:
        raise ProcessError("No common provided")

    _validate_single_tx(msg)
    _validate_common(msg.transaction)

    if msg.multisig:
        _validate_common(msg.multisig, True)
        _validate_multisig(msg.multisig, msg.transaction.network)
    if not msg.multisig and msg.cosigning:
        raise ProcessError("No multisig transaction to cosign")

    if msg.transfer:
        _validate_transfer(msg.transfer, msg.transaction.network)
    if msg.provision_namespace:
        _validate_provision_namespace(msg.provision_namespace, msg.transaction.network)
    if msg.mosaic_creation:
        _validate_mosaic_creation(msg.mosaic_creation, msg.transaction.network)
    if msg.supply_change:
        _validate_supply_change(msg.supply_change)
    if msg.aggregate_modification:
        _validate_aggregate_modification(
            msg.aggregate_modification, msg.multisig is None
        )
    if msg.importance_transfer:
        _validate_importance_transfer(msg.importance_transfer)


def validate_network(network: int) -> int:
    if network is None:
        return NEM_NETWORK_MAINNET
    if network not in (NEM_NETWORK_MAINNET, NEM_NETWORK_TESTNET, NEM_NETWORK_MIJIN):
        raise ProcessError("Invalid NEM network")
    return network


def _validate_single_tx(msg: NEMSignTx):
    # ensure exactly one transaction is provided
    tx_count = (
        bool(msg.transfer)
        + bool(msg.provision_namespace)
        + bool(msg.mosaic_creation)
        + bool(msg.supply_change)
        + bool(msg.aggregate_modification)
        + bool(msg.importance_transfer)
    )
    if tx_count == 0:
        raise ProcessError("No transaction provided")
    if tx_count > 1:
        raise ProcessError("More than one transaction provided")


def _validate_common(common: NEMTransactionCommon, inner: bool = False):
    common.network = validate_network(common.network)

    err = None
    if common.timestamp is None:
        err = "timestamp"
    if common.fee is None:
        err = "fee"
    if common.deadline is None:
        err = "deadline"

    if not inner and common.signer:
        raise ProcessError("Signer not allowed in outer transaction")

    if inner and common.signer is None:
        err = "signer"

    if err:
        if inner:
            raise ProcessError("No %s provided in inner transaction" % err)
        else:
            raise ProcessError("No %s provided" % err)

    if common.signer is not None:
        _validate_public_key(
            common.signer, "Invalid signer public key in inner transaction"
        )


def _validate_public_key(public_key: bytes, err_msg: str):
    if not public_key:
        raise ProcessError("%s (none provided)" % err_msg)
    if len(public_key) != NEM_PUBLIC_KEY_SIZE:
        raise ProcessError("%s (invalid length)" % err_msg)


def _validate_importance_transfer(importance_transfer: NEMImportanceTransfer):
    if importance_transfer.mode is None:
        raise ProcessError("No mode provided")
    _validate_public_key(
        importance_transfer.public_key, "Invalid remote account public key provided"
    )


def _validate_multisig(multisig: NEMTransactionCommon, network: int):
    if multisig.network != network:
        raise ProcessError("Inner transaction network is different")
    _validate_public_key(multisig.signer, "Invalid multisig signer public key provided")


def _validate_aggregate_modification(
    aggregate_modification: NEMAggregateModification, creation: bool = False
):

    if creation and not aggregate_modification.modifications:
        raise ProcessError("No modifications provided")

    for m in aggregate_modification.modifications:
        if not m.type:
            raise ProcessError("No modification type provided")
        if m.type not in (
            NEMModificationType.CosignatoryModification_Add,
            NEMModificationType.CosignatoryModification_Delete,
        ):
            raise ProcessError("Unknown aggregate modification")
        if creation and m.type == NEMModificationType.CosignatoryModification_Delete:
            raise ProcessError("Cannot remove cosignatory when converting account")
        _validate_public_key(m.public_key, "Invalid cosignatory public key provided")


def _validate_supply_change(supply_change: NEMMosaicSupplyChange):
    if supply_change.namespace is None:
        raise ProcessError("No namespace provided")
    if supply_change.mosaic is None:
        raise ProcessError("No mosaic provided")
    if supply_change.type is None:
        raise ProcessError("No type provided")
    elif supply_change.type not in [
        NEMSupplyChangeType.SupplyChange_Decrease,
        NEMSupplyChangeType.SupplyChange_Increase,
    ]:
        raise ProcessError("Invalid supply change type")
    if supply_change.delta is None:
        raise ProcessError("No delta provided")


def _validate_mosaic_creation(mosaic_creation: NEMMosaicCreation, network: int):
    if mosaic_creation.definition is None:
        raise ProcessError("No mosaic definition provided")
    if mosaic_creation.sink is None:
        raise ProcessError("No creation sink provided")
    if mosaic_creation.fee is None:
        raise ProcessError("No creation sink fee provided")

    if not nem.validate_address(mosaic_creation.sink, network):
        raise ProcessError("Invalid creation sink address")

    if mosaic_creation.definition.name is not None:
        raise ProcessError("Name not allowed in mosaic creation transactions")
    if mosaic_creation.definition.ticker is not None:
        raise ProcessError("Ticker not allowed in mosaic creation transactions")
    if mosaic_creation.definition.networks:
        raise ProcessError("Networks not allowed in mosaic creation transactions")

    if mosaic_creation.definition.namespace is None:
        raise ProcessError("No mosaic namespace provided")
    if mosaic_creation.definition.mosaic is None:
        raise ProcessError("No mosaic name provided")

    if (
        mosaic_creation.definition.supply is not None
        and mosaic_creation.definition.divisibility is None
    ):
        raise ProcessError(
            "Definition divisibility needs to be provided when supply is"
        )
    if (
        mosaic_creation.definition.supply is None
        and mosaic_creation.definition.divisibility is not None
    ):
        raise ProcessError(
            "Definition supply needs to be provided when divisibility is"
        )

    if mosaic_creation.definition.levy is not None:
        if mosaic_creation.definition.fee is None:
            raise ProcessError("No levy fee provided")
        if mosaic_creation.definition.levy_address is None:
            raise ProcessError("No levy address provided")
        if mosaic_creation.definition.levy_namespace is None:
            raise ProcessError("No levy namespace provided")
        if mosaic_creation.definition.levy_mosaic is None:
            raise ProcessError("No levy mosaic name provided")

        if mosaic_creation.definition.divisibility is None:
            raise ProcessError("No divisibility provided")
        if mosaic_creation.definition.supply is None:
            raise ProcessError("No supply provided")
        if mosaic_creation.definition.mutable_supply is None:
            raise ProcessError("No supply mutability provided")
        if mosaic_creation.definition.transferable is None:
            raise ProcessError("No mosaic transferability provided")
        if mosaic_creation.definition.description is None:
            raise ProcessError("No description provided")

        if mosaic_creation.definition.divisibility > NEM_MAX_DIVISIBILITY:
            raise ProcessError("Invalid divisibility provided")
        if mosaic_creation.definition.supply > NEM_MAX_SUPPLY:
            raise ProcessError("Invalid supply provided")

        if not nem.validate_address(mosaic_creation.definition.levy_address, network):
            raise ProcessError("Invalid levy address")


def _validate_provision_namespace(
    provision_namespace: NEMProvisionNamespace, network: int
):
    if provision_namespace.namespace is None:
        raise ProcessError("No namespace provided")
    if provision_namespace.sink is None:
        raise ProcessError("No rental sink provided")
    if provision_namespace.fee is None:
        raise ProcessError("No rental sink fee provided")

    if not nem.validate_address(provision_namespace.sink, network):
        raise ProcessError("Invalid rental sink address")


def _validate_transfer(transfer: NEMTransfer, network: int):
    if transfer.recipient is None:
        raise ProcessError("No recipient provided")
    if transfer.amount is None:
        raise ProcessError("No amount provided")

    if transfer.public_key is not None:
        _validate_public_key(transfer.public_key, "Invalid recipient public key")
        if transfer.payload is None:
            raise ProcessError("Public key provided but no payload to encrypt")

    if transfer.payload:
        if len(transfer.payload) > NEM_MAX_PLAIN_PAYLOAD_SIZE:
            raise ProcessError("Payload too large")
        if (
            transfer.public_key
            and len(transfer.payload) > NEM_MAX_ENCRYPTED_PAYLOAD_SIZE
        ):
            raise ProcessError("Payload too large")

    if not nem.validate_address(transfer.recipient, network):
        raise ProcessError("Invalid recipient address")

    for m in transfer.mosaics:
        if m.namespace is None:
            raise ProcessError("No mosaic namespace provided")
        if m.mosaic is None:
            raise ProcessError("No mosaic name provided")
        if m.quantity is None:
            raise ProcessError("No mosaic quantity provided")
