###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import binascii
import os
import configparser
from collections.abc import MutableMapping
from typing import Optional, Union, Dict, Any, List, Iterator
from threading import Lock

import txaio
import nacl

from eth_account.account import Account
from eth_account.signers.local import LocalAccount

from py_eth_sig_utils.eip712 import encode_typed_data
from py_eth_sig_utils.utils import ecsign, ecrecover_to_pub, checksum_encode, sha3
from py_eth_sig_utils.signing import v_r_s_to_signature, signature_to_v_r_s

from autobahn.wamp.interfaces import ISecurityModule, IEthereumKey
from autobahn.xbr._mnemonic import mnemonic_to_private_key
from autobahn.util import parse_keyfile
from autobahn.wamp.cryptosign import CryptosignKey

__all__ = ('EthereumKey', 'SecurityModuleMemory', )


class EthereumKey(object):
    """
    Base class to implement :class:`autobahn.wamp.interfaces.IEthereumKey`.
    """

    def __init__(self, key_or_address: Union[LocalAccount, str, bytes], can_sign: bool,
                 security_module: Optional[ISecurityModule] = None,
                 key_no: Optional[int] = None) -> None:
        if can_sign:
            # https://eth-account.readthedocs.io/en/latest/eth_account.html#eth_account.account.Account
            assert type(key_or_address) == LocalAccount
            self._key = key_or_address
            self._address = key_or_address.address
        else:
            assert type(key_or_address) in (str, bytes)
            self._key = None
            self._address = key_or_address
        self._can_sign = can_sign
        self._security_module = security_module
        self._key_no = key_no

    @property
    def security_module(self) -> Optional['ISecurityModule']:
        """
        Implements :meth:`autobahn.wamp.interfaces.IKey.security_module`.
        """
        return self._security_module

    @property
    def key_no(self) -> Optional[int]:
        """
        Implements :meth:`autobahn.wamp.interfaces.IKey.key_no`.
        """
        return self._key_no

    @property
    def key_type(self) -> str:
        """
        Implements :meth:`autobahn.wamp.interfaces.IKey.key_type`.
        """
        return 'ethereum'

    def public_key(self, binary: bool = False) -> Union[str, bytes]:
        """
        Implements :meth:`autobahn.wamp.interfaces.IKey.public_key`.
        """
        raise NotImplementedError()

    @property
    def can_sign(self) -> bool:
        """
        Implements :meth:`autobahn.wamp.interfaces.IKey.can_sign`.
        """
        return self._can_sign

    def address(self, binary: bool = False) -> Union[str, bytes]:
        """
        Implements :meth:`autobahn.wamp.interfaces.IEthereumKey.address`.
        """
        if binary:
            return binascii.a2b_hex(self._address[2:])
        else:
            return self._address

    def sign(self, data: bytes) -> bytes:
        """
        Implements :meth:`autobahn.wamp.interfaces.IKey.sign`.
        """
        # FIXME: implement signing of raw data
        raise NotImplementedError()

    def recover(self, data: bytes, signature: bytes) -> bytes:
        """
        Implements :meth:`autobahn.wamp.interfaces.IKey.recover`.
        """
        # FIXME: implement signing address recovery from signature of raw data
        raise NotImplementedError()

    def sign_typed_data(self, data: Dict[str, Any], binary=True) -> bytes:
        """
        Implements :meth:`autobahn.wamp.interfaces.IEthereumKey.sign_typed_data`.
        """
        if self._security_module:
            assert self._security_module.is_open and not self._security_module.is_locked, 'security module must be open and unlocked'
        try:
            # encode typed data dict and return message hash
            msg_hash = encode_typed_data(data)

            # ECDSA signatures in Ethereum consist of three parameters: v, r and s.
            # The signature is always 65-bytes in length.
            #     r = first 32 bytes of signature
            #     s = second 32 bytes of signature
            #     v = final 1 byte of signature
            signature_vrs = ecsign(msg_hash, self._key.key)

            # concatenate signature components into byte string
            signature = v_r_s_to_signature(*signature_vrs)
        except Exception as e:
            return txaio.create_future_error(e)
        else:
            if binary:
                return txaio.create_future_success(signature)
            else:
                return txaio.create_future_success(binascii.b2a_hex(signature).decode())

    def verify_typed_data(self, data: Dict[str, Any], signature: bytes) -> bool:
        """
        Implements :meth:`autobahn.wamp.interfaces.IEthereumKey.verify_typed_data`.
        """
        if self._security_module:
            assert self._security_module.is_open and not self._security_module.is_locked, 'security module must be open and unlocked'
        try:
            msg_hash = encode_typed_data(data)
            signature_vrs = signature_to_v_r_s(signature)
            public_key = ecrecover_to_pub(msg_hash, *signature_vrs)
            address_bytes = sha3(public_key)[-20:]
            address = checksum_encode(address_bytes)
        except Exception as e:
            return txaio.create_future_error(e)
        else:
            return txaio.create_future_success(address == self._address)

    @classmethod
    def from_address(cls, address: Union[str, bytes]) -> 'EthereumKey':
        """
        Create a public key from an address, which can be used to verify signatures.

        :param address: The Ethereum address (20 octets).
        :return: New instance of :class:`EthereumKey`
        """
        return EthereumKey(key_or_address=address, can_sign=False)

    @classmethod
    def from_bytes(cls, key: bytes) -> 'EthereumKey':
        """
        Create a private key from seed bytes, which can be used to sign and create signatures.

        :param key: The Ethereum private key seed (32 octets).
        :return: New instance of :class:`EthereumKey`
        """
        if type(key) != bytes:
            raise ValueError("invalid seed type {} (expected binary)".format(type(key)))

        if len(key) != 32:
            raise ValueError("invalid seed length {} (expected 32)".format(len(key)))

        account: LocalAccount = Account.from_key(key)
        return EthereumKey(key_or_address=account, can_sign=True)

    @classmethod
    def from_seedphrase(cls, seedphrase: str, index: int = 0) -> 'EthereumKey':
        """
        Create a private key from the given BIP-39 mnemonic seed phrase and index,
        which can be used to sign and create signatures.

        :param seedphrase: The BIP-39 seedphrase ("Mnemonic") from which to derive the account.
        :param index: The account index in account hierarchy defined by the seedphrase.
        :return: New instance of :class:`EthereumKey`
        """
        # Base HD Path:  m/44'/60'/0'/0/{account_index}
        derivation_path = "m/44'/60'/0'/0/{}".format(index)

        key = mnemonic_to_private_key(seedphrase, str_derivation_path=derivation_path)
        assert type(key) == bytes
        assert len(key) == 32

        account: LocalAccount = Account.from_key(key)
        return EthereumKey(key_or_address=account, can_sign=True)

    @classmethod
    def from_keyfile(cls, keyfile: str) -> 'EthereumKey':
        """
        Create a public or private key from reading the given public or private key file.

        Here is an example key file that includes an Ethereum private key ``private-key-eth``, which
        is loaded in this function, and other fields, which are ignored by this function:

        .. code-block::

            This is a comment (all lines until the first empty line are comments indeed).

            creator: oberstet@intel-nuci7
            created-at: 2022-07-05T12:29:48.832Z
            user-id: oberstet@intel-nuci7
            public-key-ed25519: 7326d9dc0307681cc6940fde0e60eb31a6e4d642a81e55c434462ce31f95deed
            public-adr-eth: 0x10848feBdf7f200Ba989CDf7E3eEB3EC03ae7768
            private-key-ed25519: f750f42b0430e28a2e272c3cedcae4dcc4a1cf33bc345c35099d3322626ab666
            private-key-eth: 4d787714dcb0ae52e1c5d2144648c255d660b9a55eac9deeb80d9f506f501025

        :param keyfile: Path (relative or absolute) to a public or private keys file.
        :return: New instance of :class:`EthereumKey`
        """
        if not os.path.exists(keyfile) or not os.path.isfile(keyfile):
            raise RuntimeError('keyfile "{}" is not a file'.format(keyfile))

        # now load the private or public key file - this returns a dict which should
        # include (for a private key):
        #
        #   private-key-eth: 6b08b6e186bd2a3b9b2f36e6ece3f8031fe788ab3dc4a1cfd3a489ea387c496b
        #
        # or (for a public key only):
        #
        #   public-adr-eth: 0x10848feBdf7f200Ba989CDf7E3eEB3EC03ae7768
        #
        data = parse_keyfile(keyfile)

        privkey_eth_hex = data.get('private-key-eth', None)
        if privkey_eth_hex is None:
            pub_adr_eth = data.get('public-adr-eth', None)
            if pub_adr_eth is None:
                raise RuntimeError('neither "private-key-eth" nor "public-adr-eth" found in keyfile {}'.format(keyfile))
            else:
                return EthereumKey.from_address(pub_adr_eth)
        else:
            return EthereumKey.from_bytes(binascii.a2b_hex(privkey_eth_hex))


IEthereumKey.register(EthereumKey)


class SecurityModuleMemory(MutableMapping):
    """
    A transient, memory-based implementation of :class:`ISecurityModule`.
    """

    def __init__(self, keys: Optional[List[Union[CryptosignKey, EthereumKey]]] = None):
        self._mutex = Lock()
        self._is_open = False
        self._is_locked = True
        self._keys: Dict[int, Union[CryptosignKey, EthereumKey]] = {}
        self._counters: Dict[int, int] = {}
        if keys:
            for i, key in enumerate(keys):
                self._keys[i] = key

    def __len__(self) -> int:
        """
        Implements :meth:`ISecurityModule.__len__`
        """
        assert self._is_open, 'security module not open'

        return len(self._keys)

    def __contains__(self, key_no: int) -> bool:
        assert self._is_open, 'security module not open'

        return key_no in self._keys

    def __iter__(self) -> Iterator[int]:
        """
        Implements :meth:`ISecurityModule.__iter__`
        """
        assert self._is_open, 'security module not open'

        yield from self._keys

    def __getitem__(self, key_no: int) -> Union[CryptosignKey, EthereumKey]:
        """
        Implements :meth:`ISecurityModule.__getitem__`
        """
        assert self._is_open, 'security module not open'

        if key_no in self._keys:
            return self._keys[key_no]
        else:
            raise IndexError('key_no {} not found'.format(key_no))

    def __setitem__(self, key_no: int, key: Union[CryptosignKey, EthereumKey]) -> None:
        assert self._is_open, 'security module not open'

        assert key_no >= 0
        if key_no in self._keys:
            # FIXME
            pass
        self._keys[key_no] = key

    def __delitem__(self, key_no: int) -> None:
        assert self._is_open, 'security module not open'

        if key_no in self._keys:
            del self._keys[key_no]
        else:
            raise IndexError()

    def open(self):
        """
        Implements :meth:`ISecurityModule.open`
        """
        assert not self._is_open, 'security module already open'

        self._is_open = True
        return txaio.create_future_success(None)

    def close(self):
        """
        Implements :meth:`ISecurityModule.close`
        """
        assert self._is_open, 'security module not open'

        self._is_open = False
        self._is_locked = True
        return txaio.create_future_success(None)

    @property
    def is_open(self) -> bool:
        """
        Implements :meth:`ISecurityModule.is_open`
        """
        return self._is_open

    @property
    def can_lock(self) -> bool:
        """
        Implements :meth:`ISecurityModule.can_lock`
        """
        return True

    @property
    def is_locked(self) -> bool:
        """
        Implements :meth:`ISecurityModule.is_locked`
        """
        return self._is_locked

    def lock(self):
        """
        Implements :meth:`ISecurityModule.lock`
        """
        assert self._is_open, 'security module not open'
        assert not self._is_locked

        self._is_locked = True
        return txaio.create_future_success(None)

    def unlock(self):
        """
        Implements :meth:`ISecurityModule.unlock`
        """
        assert self._is_open, 'security module not open'
        assert self._is_locked

        self._is_locked = False
        return txaio.create_future_success(None)

    def create_key(self, key_type: str) -> int:
        assert self._is_open, 'security module not open'

        key_no = len(self._keys)
        if key_type == 'cryptosign':
            key = CryptosignKey(key=nacl.signing.SigningKey(os.urandom(32)),
                                can_sign=True,
                                security_module=self,
                                key_no=key_no)
        elif key_type == 'ethereum':
            key = EthereumKey(key_or_address=Account.from_key(os.urandom(32)),
                              can_sign=True,
                              security_module=self,
                              key_no=key_no)
        else:
            raise ValueError('invalid key_type "{}"'.format(key_type))
        self._keys[key_no] = key
        return txaio.create_future_success(key_no)

    def delete_key(self, key_no: int):
        assert self._is_open, 'security module not open'

        if key_no in self._keys:
            del self._keys[key_no]
            return txaio.create_future_success(key_no)
        else:
            return txaio.create_future_success(None)

    def get_random(self, octets: int) -> bytes:
        """
        Implements :meth:`ISecurityModule.get_random`
        """
        assert self._is_open, 'security module not open'

        data = os.urandom(octets)
        return txaio.create_future_success(data)

    def get_counter(self, counter_no: int) -> int:
        """
        Implements :meth:`ISecurityModule.get_counter`
        """
        assert self._is_open, 'security module not open'

        self._mutex.acquire()
        res = self._counters.get(counter_no, 0)
        self._mutex.release()
        return txaio.create_future_success(res)

    def increment_counter(self, counter_no: int) -> int:
        """
        Implements :meth:`ISecurityModule.increment_counter`
        """
        assert self._is_open, 'security module not open'

        self._mutex.acquire()
        if counter_no not in self._counters:
            self._counters[counter_no] = 0
        self._counters[counter_no] += 1
        res = self._counters[counter_no]
        self._mutex.release()
        return txaio.create_future_success(res)

    @classmethod
    def from_seedphrase(cls, seedphrase: str, num_eth_keys: int = 1,
                        num_cs_keys: int = 1) -> 'SecurityModuleMemory':
        """
        Create a new memory-backed security module with

        1. ``num_eth_keys`` keys of type :class:`EthereumKey`, followed by
        2. ``num_cs_keys`` keys of type :class:`CryptosignKey`

        computed from a (common) BIP44 seedphrase.

        :param seedphrase: BIP44 seedphrase to use.
        :param num_eth_keys: Number of Ethereum keys to derive.
        :param num_cs_keys: Number of Cryptosign keys to derive.
        :return: New memory-backed security module instance.
        """
        keys: List[Union[EthereumKey, CryptosignKey]] = []

        # first, add num_eth_keys EthereumKey(s), numbering starting at 0
        for i in range(num_eth_keys):
            key = EthereumKey.from_seedphrase(seedphrase, i)
            keys.append(key)

        # second, add num_cs_keys CryptosignKey(s), numbering starting at num_eth_keys (!)
        for i in range(num_cs_keys):
            key = CryptosignKey.from_seedphrase(seedphrase, i + num_eth_keys)
            keys.append(key)

        # initialize security module from collected keys
        sm = SecurityModuleMemory(keys=keys)
        return sm

    @classmethod
    def from_config(cls, config: str, profile: str = 'default') -> 'SecurityModuleMemory':
        """
        Create a new memory-backed security module with keys referred from a profile in
        the given configuration file.

        :param config: Path (relative or absolute) to an INI configuration file.
        :param profile: Name of the profile within the given INI configuration file.
        :return: New memory-backed security module instance.
        """
        keys: List[Union[EthereumKey, CryptosignKey]] = []

        cfg = configparser.ConfigParser()
        cfg.read(config)

        if not cfg.has_section(profile):
            raise RuntimeError('profile "{}" not found in configuration file "{}"'.format(profile, config))

        if not cfg.has_option(profile, 'privkey'):
            raise RuntimeError('missing option "privkey" in profile "{}" of configuration file "{}"'.format(profile, config))

        privkey = os.path.join(os.path.dirname(config), cfg.get(profile, 'privkey'))
        if not os.path.exists(privkey) or not os.path.isfile(privkey):
            raise RuntimeError('privkey "{}" is not a file in profile "{}" of configuration file "{}"'.format(privkey, profile, config))

        # now load the private key file - this returns a dict which should include:
        # private-key-eth: 6b08b6e186bd2a3b9b2f36e6ece3f8031fe788ab3dc4a1cfd3a489ea387c496b
        # private-key-ed25519: 20e8c05d0ede9506462bb049c4843032b18e8e75b314583d0c8d8a4942f9be40
        data = parse_keyfile(privkey)

        # first, add Ethereum key
        privkey_eth_hex = data.get('private-key-eth', None)
        keys.append(EthereumKey.from_bytes(binascii.a2b_hex(privkey_eth_hex)))

        # second, add Cryptosign key
        privkey_ed25519_hex = data.get('private-key-ed25519', None)
        keys.append(CryptosignKey.from_bytes(binascii.a2b_hex(privkey_ed25519_hex)))

        # initialize security module from collected keys
        sm = SecurityModuleMemory(keys=keys)
        return sm

    @classmethod
    def from_keyfile(cls, keyfile: str) -> 'SecurityModuleMemory':
        """
        Create a new memory-backed security module with keys referred from a profile in
        the given configuration file.

        :param keyfile: Path (relative or absolute) to a private keys file.
        :return: New memory-backed security module instance.
        """
        keys: List[Union[EthereumKey, CryptosignKey]] = []

        if not os.path.exists(keyfile) or not os.path.isfile(keyfile):
            raise RuntimeError('keyfile "{}" is not a file'.format(keyfile))

        # now load the private key file - this returns a dict which should include:
        # private-key-eth: 6b08b6e186bd2a3b9b2f36e6ece3f8031fe788ab3dc4a1cfd3a489ea387c496b
        # private-key-ed25519: 20e8c05d0ede9506462bb049c4843032b18e8e75b314583d0c8d8a4942f9be40
        data = parse_keyfile(keyfile)

        # first, add Ethereum key
        privkey_eth_hex = data.get('private-key-eth', None)
        if privkey_eth_hex is None:
            raise RuntimeError('"private-key-eth" not found in keyfile {}'.format(keyfile))
        keys.append(EthereumKey.from_bytes(binascii.a2b_hex(privkey_eth_hex)))

        # second, add Cryptosign key
        privkey_ed25519_hex = data.get('private-key-ed25519', None)
        if privkey_ed25519_hex is None:
            raise RuntimeError('"private-key-ed25519" not found in keyfile {}'.format(keyfile))
        keys.append(CryptosignKey.from_bytes(binascii.a2b_hex(privkey_ed25519_hex)))

        # initialize security module from collected keys
        sm = SecurityModuleMemory(keys=keys)
        return sm


ISecurityModule.register(SecurityModuleMemory)
