# Copyright 2021 Vincent Texier <vit@free.fr>
#
# This software is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This software is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
from typing import Any, Dict, List, Optional
from uuid import UUID

from tikka.domains.certifications import Certifications
from tikka.domains.currencies import Currencies
from tikka.domains.entities.account import (
    Account,
    AccountBalance,
    AccountCryptoType,
    SearchAccount,
)
from tikka.domains.entities.constants import DERIVATION_SCAN_MAX_NUMBER
from tikka.domains.entities.events import AccountEvent, LastTransferEvent
from tikka.domains.events import EventDispatcher
from tikka.domains.passwords import Passwords
from tikka.domains.transfers import Transfers
from tikka.domains.universal_dividends import UniversalDividends
from tikka.domains.wallets import Wallets
from tikka.interfaces.adapters.network.indexer.accounts import IndexerAccountsInterface
from tikka.interfaces.adapters.network.node.accounts import NodeAccountsInterface
from tikka.interfaces.adapters.repository.accounts import AccountsRepositoryInterface
from tikka.interfaces.adapters.repository.file_wallets import (
    V1FileWalletsRepositoryInterface,
)
from tikka.libs.derivation import RE_TRANSPARENT_DERIVATION_PATH_PATTERN
from tikka.libs.keypair import Keypair
from tikka.libs.signing_key_v1 import SigningKey


class Accounts:
    """
    Accounts domain class
    """

    def __init__(
        self,
        repository: AccountsRepositoryInterface,
        network_node: NodeAccountsInterface,
        network_indexer: IndexerAccountsInterface,
        passwords: Passwords,
        wallets: Wallets,
        transfers: Transfers,
        universal_dividends: UniversalDividends,
        certifications: Certifications,
        file_wallets_repository: V1FileWalletsRepositoryInterface,
        currencies: Currencies,
        event_dispatcher: EventDispatcher,
    ):
        """
        Init Accounts domain

        :param repository: AccountsRepositoryInterface instance
        :param network_node: NodeAccountsInterface instance
        :param network_indexer: IndexerAccountsInterface instance
        :param passwords: Passwords domain instance
        :param wallets: Wallets domain instance
        :param transfers: Transfers domain instance
        :param universal_dividends: UniversalDividends domain instance,
        :param certifications: Certifications domain instance
        :param file_wallets_repository: FileWalletsRepository adapter instance
        :param currencies: Currencies domain instance
        :param event_dispatcher: EventDispatcher instance
        """
        self.repository = repository
        self.network_node = network_node
        self.network_indexer = network_indexer
        self.passwords = passwords
        self.wallets = wallets
        self.transfers = transfers
        self.universal_dividends = universal_dividends
        self.certifications = certifications
        self.file_wallets_repository = file_wallets_repository
        self.currencies = currencies
        self.event_dispatcher = event_dispatcher

        # subscribe to events
        self.event_dispatcher.add_event_listener(
            LastTransferEvent.EVENT_TYPE_LAST_TRANSFER_CHANGED,
            self.on_last_transfer_changed,
        )

    @staticmethod
    def get_instance(address: str, name: Optional[str] = None) -> Account:
        """
        Return an Account instance from arguments

        :param address: Account address
        :param name: Optional account name
        :return:
        """
        return Account(address=address, name=name)

    def get_list(
        self,
        filters: Optional[Dict[str, Any]] = None,
        sort_column: Optional[str] = None,
        sort_order: str = AccountsRepositoryInterface.SORT_ORDER_ASCENDING,
    ) -> List[Account]:
        """
        Return accounts from repository with optional filters and sort_column

        :param filters: Dict with {column: value} filters or None
        :param sort_column: Sort column constant like COLUMN_ADDRESS or None
        :param sort_order: Sort order constant SORT_ORDER_ASCENDING or SORT_ORDER_DESCENDING
        :return:
        """
        # get accounts from database
        return self.repository.list(filters, sort_column, sort_order)

    def add(self, account: Account):
        """
        Add account

        :param account: Account instance
        :return:
        """
        # add account
        self.repository.add(account)

        # dispatch event
        event = AccountEvent(
            AccountEvent.EVENT_TYPE_ADD,
            account,
        )
        self.event_dispatcher.dispatch_event(event)

    def update(self, account: Account):
        """
        Update account

        :param account: Account instance
        :return:
        """
        self.repository.update(account)

        # dispatch event
        event = AccountEvent(
            AccountEvent.EVENT_TYPE_UPDATE,
            account,
        )
        self.event_dispatcher.dispatch_event(event)

    def get_by_index(self, index: int) -> Account:
        """
        Return account instance from index

        :param index: Index in account list
        :return:
        """
        return self.get_list()[index]

    def get_by_address(self, address: str) -> Optional[Account]:
        """
        Return account instance from address

        :param address: Account address
        :return:
        """
        for account in self.get_list():
            if account.address == address:
                return account

        return None

    def delete(self, account: Account) -> None:
        """
        Delete account in list and repository

        :param account: Account instance to delete
        :return:
        """
        if account.root is None:
            # list derived accounts
            derived_account_addresses = [
                account_.address
                for account_ in self.get_list(
                    filters={AccountsRepositoryInterface.COLUMN_ROOT: account.address}
                )
            ]
            exclude_addresses = [
                account_.address
                for account_ in self.get_list()
                if account_.address != account.address
                and account_.address not in derived_account_addresses
            ]
            # remove transfers and certifications for each derived account...
            for derived_account_address in derived_account_addresses:
                self.transfers.delete_by_address(
                    derived_account_address, exclude_addresses
                )
                self.universal_dividends.delete_by_address(derived_account_address)
                self.certifications.delete_by_address(
                    derived_account_address, exclude_addresses
                )
        else:
            exclude_addresses = [
                account_.address
                for account_ in self.get_list()
                if account_.address != account.address
            ]
        self.transfers.delete_by_address(account.address, exclude_addresses)
        self.universal_dividends.delete_by_address(account.address)
        self.certifications.delete_by_address(account.address, exclude_addresses)

        self.repository.delete(account.address)

        # dispatch event
        event = AccountEvent(
            AccountEvent.EVENT_TYPE_DELETE,
            account,
        )
        self.event_dispatcher.dispatch_event(event)

    def delete_all(self) -> None:
        """
        Delete all accounts in repository

        :return:
        """
        self.repository.delete_all()

    def count(self) -> int:
        """
        Return total number of accounts

        :return:
        """
        return self.repository.count()

    def get_total_balance(self) -> int:
        """
        Return total balance of all accounts

        :return:
        """
        return self.repository.total_balance()

    def unlock(self, account: Account, wallet_password: str) -> bool:
        """
        Unlock wallet of account if wallet_password is OK

        :param account: Account instance
        :param wallet_password: Passphrase
        :return:
        """
        result = self.wallets.unlock(account.address, wallet_password)
        if not result:
            return False

        # dispatch event
        event = AccountEvent(
            AccountEvent.EVENT_TYPE_UPDATE,
            account,
        )
        self.event_dispatcher.dispatch_event(event)
        return True

    def lock(self, account: Account):
        """
        Lock account

        :param account: Account instance
        :return:
        """
        result = self.wallets.lock(account.address)
        if not result:
            return False

        # dispatch event
        event = AccountEvent(
            AccountEvent.EVENT_TYPE_UPDATE,
            account,
        )
        self.event_dispatcher.dispatch_event(event)
        return True

    def forget_wallet(self, account: Account) -> None:
        """
        Delete stored wallet for this account

        :return:
        """
        self.wallets.delete(account.address)
        # update account display
        self.event_dispatcher.dispatch_event(
            AccountEvent(AccountEvent.EVENT_TYPE_UPDATE, account)
        )

    def network_update_balance(self, account: Account) -> Account:
        """
        Update account balance from network

        :param account: Account instance
        :return:
        """
        account_balance = self.network_node.get_balance(account.address)
        if account_balance is not None:
            account.balance = account_balance.total
            account.balance_available = account_balance.available
            account.balance_reserved = account_balance.reserved
        else:
            account.balance = None
            account.balance_available = None
            account.balance_reserved = None
        self.repository.update(account)

        return account

    def network_update_account(self, account: Account) -> Account:
        """
        Update
        - balance
        - total_transfers_count
        from network

        :param account: Account instance
        :return:
        """
        account_balance = self.network_node.get_balance(account.address)
        if account_balance is not None:
            account.balance = account_balance.total
            account.balance_available = account_balance.available
            account.balance_reserved = account_balance.reserved
        else:
            account.balance = None
            account.balance_available = None
            account.balance_reserved = None
        self.repository.update(account)

        return account

    def network_update_balances(self, accounts: List[Account]):
        """
        Update balances of account list from network

        :param accounts: Account list
        :return:
        """
        balances = self.network_node.get_balances(
            [account.address for account in accounts]
        )
        for account in accounts:
            if balances[account.address] is not None:
                account.balance = balances[account.address].total  # type: ignore
                account.balance_available = balances[account.address].available  # type: ignore
                account.balance_reserved = balances[account.address].reserved  # type: ignore
            else:
                account.balance = None
                account.balance_available = None
                account.balance_reserved = None
            self.repository.update(account)

    def network_get_balance(self, address: str) -> Optional[AccountBalance]:
        """
        Return AccountBalance instance from network

        :param address: Account address
        :return:
        """
        return self.network_node.get_balance(address)

    def network_get_balances(
        self, addresses: List[str]
    ) -> Dict[str, Optional[AccountBalance]]:
        """
        Return balances of address list from network

        :param addresses: Account address list
        :return:
        """
        return self.network_node.get_balances(addresses)

    def network_update_total_transfers_count(self, account: Account) -> Account:
        """
        Update account total_transfers_count from network

        :param account: Account to update
        :return:
        """
        account.total_transfers_count = (
            self.transfers.network_fetch_total_count_for_address(account.address)
        )
        self.repository.update(account)
        return account

    def list_by_category_id(self, category_id: Optional[UUID]) -> List[Account]:
        """
        Return all accounts in category_id

        :param category_id: Category ID
        :return:
        """
        result = []
        for account in self.get_list():
            # do not list derived accounts
            if account.category_id is None and account.root is not None:
                continue
            if account.category_id == category_id:
                result.append(account)

        return result

    def get_derivation_accounts(self, address: str) -> List[Account]:
        """
        Return list of derivation accounts from this root address

        :param address: Root account address
        :return:
        """
        filters = {AccountsRepositoryInterface.COLUMN_ROOT: address}
        # sort ascending by derivation path numbers
        return self.repository.list(
            filters=filters,
            sort_column=AccountsRepositoryInterface.COLUMN_PATH,
            sort_order=AccountsRepositoryInterface.SORT_ORDER_ASCENDING,
        )

    def create_new_account(
        self,
        mnemonic: str,
        language_code: str,
        derivation: str,
        crypto_type: AccountCryptoType,
        name: str,
        password: str,
        add_event: bool = True,
    ) -> Account:
        """
        Create a root account (read-only if derivation not empty) with mnemonic, if it does not already exist,
        and create a derived account if derivation is not "" as per RFC 0019.
        Return root account if derivation == "", derived account otherwise.

        :param mnemonic: Mnemonic phrase
        :param language_code: Mnemonic language code
        :param derivation: Derivation path
        :param crypto_type: Key type as AccountCryptoType instance
        :param name: Account name
        :param password: Wallet password
        :param add_event: Trigger event if True
        :return:
        """
        # get root keypair
        root_keypair = Keypair.create_from_mnemonic(
            mnemonic=mnemonic,
            language_code=language_code,
            crypto_type=crypto_type,
            ss58_format=self.currencies.get_current().ss58_format,
        )
        existing_root_account = self.get_by_address(root_keypair.ss58_address)
        if existing_root_account is None:
            # store new password for new root account
            if derivation == "":
                self.create_new_root_account(
                    mnemonic, language_code, crypto_type, name, password
                )
            else:
                # read-only root account
                self.create_new_root_account(
                    mnemonic, language_code, crypto_type, None, None, False
                )
            self.passwords.new(root_keypair, password)
            root_account = self.get_by_address(root_keypair.ss58_address)
        else:
            # get password of root account
            clear_password = self.passwords.get_clear_password(root_keypair)
            if clear_password is not None:
                password = clear_password
            root_account = existing_root_account

        if derivation == "":
            return root_account  # type: ignore

        # create keypair from mnemonic and path
        keypair = Keypair.create_from_uri(
            suri=mnemonic + derivation,
            language_code=language_code,
            crypto_type=crypto_type,
            ss58_format=self.currencies.get_current().ss58_format,
        )

        # create and store Account instance
        account = Account(
            keypair.ss58_address,
            crypto_type=crypto_type,
            legacy_v1=False,
            name=None if name == "" else name,
            root=root_account.address,  # type: ignore
            path=derivation,
        )

        if add_event:
            self.add(account)
        else:
            self.repository.add(account)

        # create and store Wallet instance
        wallet = self.wallets.create(keypair, password)
        if not self.wallets.exists(keypair.ss58_address):
            self.wallets.add(wallet)
            self.wallets.unlock(keypair.ss58_address, password)

        return account

    def create_new_root_account(
        self,
        mnemonic: str,
        language_code: str,
        crypto_type: AccountCryptoType,
        name: Optional[str],
        password: Optional[str],
        add_event: bool = True,
    ) -> Account:
        """
        Create and return a root account from mnemonic

        :param mnemonic: Mnemonic phrase
        :param language_code: Mnemonic language code
        :param crypto_type: Key type as AccountCryptoType instance
        :param name: Optional account name
        :param password: Optional Wallet password, if None no wallet will be created
        :param add_event: Optional, Default True to trigger add event
        :return:
        """

        # get root keypair
        root_keypair = Keypair.create_from_mnemonic(
            mnemonic=mnemonic,
            language_code=language_code,
            crypto_type=crypto_type,
            ss58_format=self.currencies.get_current().ss58_format,
        )

        root_account = Account(
            root_keypair.ss58_address, crypto_type=crypto_type, name=name
        )

        if add_event:
            self.add(root_account)
        else:
            # add root account in repository without event
            self.repository.add(root_account)

        if password is not None:
            # create and store Wallet instance
            wallet = self.wallets.new(root_keypair, password)
            self.wallets.unlock(wallet.address, password)
            # store new password for new V1 root account
            self.passwords.new(root_keypair, password)

        return root_account

    def create_new_root_account_v1_from_credentials(
        self, secret_id: str, password_id: str, name: str, password: str
    ) -> Optional[Account]:
        """
        Create an unlocked V1 root account with credentials secret_id and password_id

        :param secret_id: Secret ID
        :param password_id: Password ID
        :param name: Name of account
        :param password: Password of wallet
        :return:
        """
        signing_key = SigningKey.from_credentials(secret_id, password_id)
        return self.create_new_root_account_v1_from_seed(
            signing_key.seed.hex(), name, password
        )

    def create_new_root_account_v1_from_seed(
        self, seed_hex: str, name: str, password: str
    ) -> Optional[Account]:
        """
        Create an unlocked V1 root account from hexadecimal seed string

        :param seed_hex: Hexadecimal seed string
        :param name: Name of account
        :param password: Password of wallet
        :return:
        """
        keypair = Keypair.create_from_seed(
            seed_hex=seed_hex,
            ss58_format=self.currencies.get_current().ss58_format,
            crypto_type=AccountCryptoType.ED25519,
        )
        address = keypair.ss58_address
        account = self.get_by_address(address)
        if account is not None:
            return account

        # create and store Account instance
        account = Account(
            address,
            crypto_type=AccountCryptoType.ED25519,
            legacy_v1=True,
            name=None if name == "" else name,
        )
        self.add(account)

        # create and store Wallet instance
        wallet = self.wallets.create(keypair, password)
        self.wallets.add(wallet)
        self.wallets.unlock(wallet.address, password)

        # store new password for new V1 root account
        self.passwords.new(keypair, password)

        return account

    def get_available_derivation_list(self, root_account: Account) -> List[str]:
        """
        Return a list of derivation accounts from root account given, not already in account list

        :param root_account: Root account instance
        :return:
        """
        derived_accounts = self.get_derivation_accounts(root_account.address)
        available_derivation_list = []

        # get max existing transparent derivation number
        derivation_numbers: List[int] = []
        max_derivation_number = 0
        for account in derived_accounts:
            if account.path is None:
                continue
            match = RE_TRANSPARENT_DERIVATION_PATH_PATTERN.fullmatch(account.path)
            if match:
                account_derivation_number = int(match.group(1))
                derivation_numbers.append(account_derivation_number)
                max_derivation_number = max(
                    max_derivation_number, account_derivation_number
                )

        for derivation_number in range(
            0, max(max_derivation_number, DERIVATION_SCAN_MAX_NUMBER) + 1, 1
        ):
            if derivation_number not in derivation_numbers:
                # add next available transparent derivation path
                available_derivation_list.append(f"//{derivation_number}")

        return available_derivation_list

    def update_category_id(
        self, category_id: UUID, new_category_id: Optional[UUID]
    ) -> None:
        """
        Move all accounts with category_id to new_category_id

        :param category_id: Category ID
        :param new_category_id: New category ID
        :return:
        """
        for account in self.list_by_category_id(category_id):
            account.category_id = new_category_id
            self.update(account)

    def reset_password(
        self, root_keypair: Keypair, mnemonic: str, language_code: str, password: str
    ):
        """
        Update wallet encryption with password for all wallets derived from mnemonic

        :param root_keypair: Keypair of root account
        :param mnemonic: Mnemonic phrase
        :param language_code: Language code
        :param password: Password to set
        :return:
        """
        # if root wallet exists...
        if self.wallets.exists(root_keypair.ss58_address):
            # update root wallet with new password
            self.wallets.update(self.wallets.create(root_keypair, password))

        for account in self.get_derivation_accounts(root_keypair.ss58_address):
            # if account wallet exists...
            if self.wallets.exists(account.address) and account.path is not None:
                # update account wallet with new password
                suri = mnemonic + account.path
                keypair = Keypair.create_from_uri(
                    suri=suri,
                    language_code=language_code,
                    crypto_type=root_keypair.crypto_type,
                    ss58_format=root_keypair.ss58_format,
                )
                self.wallets.update(self.wallets.create(keypair, password))

    def on_last_transfer_changed(self, event: LastTransferEvent):
        """
        Update account with last transfer timestamp got from network

        :param event: LastTransferEvent instance
        :return:
        """
        account = self.get_by_address(event.address)
        if account is not None:
            account.last_transfer_timestamp = event.transfer.timestamp
            self.repository.update(account)

    def network_is_legacy_v1(self, address: str) -> bool:
        """
        Return True if account is a legacy v1 account (present in genesis)

        :param address: ss58 account address
        :return:
        """
        return self.network_indexer.is_legacy_v1(address)

    def network_search(self, pattern: str, limit: int) -> List[SearchAccount]:
        """
        Return list of SearchAccount instances

        :param pattern: Pattern to search in address or identity name or identity index
        :param limit: Pattern to search in address or identity name or identity index
        :return:
        """
        return self.network_indexer.search(pattern, limit)
