# Copyright (c) 2020-2022 Jan-Michael Brummer <jan.brummer@tabos.org>
#
# This program 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 program 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/>.

# pylint: disable=C0413

import datetime
from gettext import gettext as _
import json
import logging
import os
from pathlib import Path
from schwifty import IBAN
import threading
import time
import mt940
from fints.client import FinTSOperations, FinTS3PinTanClient, NeedTANResponse
from fints.exceptions import FinTSClientPINError
from fints.formals import CreditDebit2
from fints.models import SEPAAccount
from gi.repository import Adw, Gtk, Gio, GObject, GLib
from saldo.backend import sqlitewrapper

from saldo.backend.account_data import AccountData
from saldo.backend.demo_bank import init_demo_bank
import saldo.backend.fints_extra
import saldo.config_manager as config
from saldo.widgets.mechanism_dialog import MechanismDialog
from saldo.widgets.tan_dialog import TanDialog
from saldo.backend.transaction_data import TransactionData


BASE_KEY = "org.tabos.saldo"

TABLE_CLIENTS = "clients"
TABLE_ACCOUNTS = "accounts"
TABLE_TRANSACTIONS = "transactions"
TABLE_CATEGORIES = "categories"

CLIENT_COLUMNS = [
    ["user", "TEXT"],
    ["password", "TEXT"],
    ["server", "TEXT"],
    ["blz", "TEXT"],
    ["tan-mechanism", "TEXT"],
]

ACCOUNT_COLUMNS = [
    ["client_id", "INT"],
    ["logo", "TEXT"],
    ["bank_name", "TEXT"],
    ["product_name", "TEXT"],
    ["currency", "TEXT"],
    ["iban", "TEXT"],
    ["subaccount_number", "TEXT"],
    ["owner_name", "TEXT"],
    ["last_updated", "REAL"],
    ["balance", "REAL"],
]

TRANSACTION_COLUMNS = [
    ["account_id", "INT"],
    ["status", "TEXT"],
    ["funds_code", "TEXT"],
    ["amount", "REAL"],
    ["id", "TEXT"],
    ["customer_reference", "TEXT"],
    ["bank_reference", "TEXT"],
    ["extra_details", "TEXT"],
    ["currency", "TEXT"],
    ["date", "TEXT"],
    ["entry_date", "TEXT"],
    ["guessed_entry_date", "TEXT"],
    ["transaction_code", "TEXT"],
    ["posting_text", "TEXT"],
    ["prima_nota", "TEXT"],
    ["purpose", "TEXT"],
    ["applicant_bin", "TEXT"],
    ["applicant_iban", "TEXT"],
    ["applicant_name", "TEXT"],
    ["return_debit_notes", "TEXT"],
    ["recipient_name", "TEXT"],
    ["additional_purpose", "TEXT"],
    ["additional_position_reference", "TEXT"],
    ["end_to_end_reference", "TEXT"],
]

CATEGORIES_COLUMNS = [["account_id", "INT"], ["name", "TEXT"], ["category", "TEXT"]]


class BankingBackend(GObject.Object):
    CATEGORY_DB = {
        "salary": {"name": _("Salary"), "icon": "bank-symbolic"},
        "hotel": {"name": _("Hotel"), "icon": "bed-symbolic"},
        "streaming": {"name": _("Streaming"), "icon": "camera-symbolic"},
        "atm": {"name": _("ATM"), "icon": "credit-card-symbolic"},
        "driving": {"name": _("Car"), "icon": "driving-symbolic"},
        "fast-food": {"name": _("Fast Food"), "icon": "fast-food-symbolic"},
        "fuel": {"name": _("Fuel"), "icon": "fuel-symbolic"},
        "games": {"name": _("Games"), "icon": "gamepad-symbolic"},
        "stock": {"name": _("Stock"), "icon": "money-symbolic"},
        "music": {"name": _("Music"), "icon": "music-note-symbolic"},
        "animal": {"name": _("Animal"), "icon": "penguin-symbolic"},
        "technology": {"name": _("Technology"), "icon": "phonelink2-symbolic"},
        "pharmarcy": {"name": _("Pharmarcy"), "icon": "plus-symbolic"},
        "restaurant": {"name": _("Restaurant"), "icon": "restaurant-symbolic"},
        "school": {"name": _("School"), "icon": "school-symbolic"},
        "shopping": {"name": _("Shopping"), "icon": "shopping-cart-symbolic"},
        "smartphone": {"name": _("Phone"), "icon": "smartphone-symbolic"},
        "leisure": {"name": _("Leisure"), "icon": "sun-symbolic"},
        "television": {"name": _("Television"), "icon": "tv-symbolic"},
        "fitness": {"name": _("Fitness"), "icon": "weight2-symbolic"},
        "home": {"name": _("Home"), "icon": "go-home-symbolic"},
        "barber": {"name": _("Barber"), "icon": "edit-cut-symbolic"},
        "lottery": {"name": _("Lottery"), "icon": "money-symbolic"},
    }

    @GObject.Signal(flags=GObject.SignalFlags.RUN_FIRST)
    def accounts_refreshed(self):
        self._load_accounts()
        self.set_property("loading", False)

    @GObject.Signal(
        flags=GObject.SignalFlags.RUN_LAST,
        return_type=bool,
        arg_types=(
            object,
            bool,
        ),
        accumulator=GObject.signal_accumulator_true_handled,
    )
    def new_transaction(
        self, transaction: TransactionData, known_applicant: bool
    ) -> None:
        pass

    def __init__(self):
        super().__init__()

        self._saldo_path = os.path.join(GLib.get_user_data_dir(), "saldo")
        if not os.path.exists(self._saldo_path):
            os.makedirs(self._saldo_path)

        logging.basicConfig(
            filename=os.path.join(self._saldo_path, "saldo.log"), level=logging.DEBUG
        )
        logging.info("Started")

        self._settings = Gio.Settings.new(BASE_KEY)
        self._mechanisms = None
        self._tan_ret = None
        self._mechanism_ret = None
        self._tan_msg = None
        self._refresh_thread = None
        self._client_connections = {}

        self.database_path = self._saldo_path + "/saldo.sqlite"
        self.database = None
        # self.db_secure = False  # Testing only!
        self.db_secure = True
        self.check_same_thread = False
        self._database_loaded = False
        self.__loading = False

        gbytes = Gio.resources_lookup_data(
            "/org/tabos/saldo/resources/database.json", Gio.ResourceLookupFlags.NONE
        )
        data = gbytes.get_data().decode("utf-8")
        self._client_db = json.loads(data)

        self._categories = {}

        self._accounts = []
        self._clients = []
        self._safe_password = None

    def _load_categories(self):
        self._categories = {}
        _, values = self.database.get_data_from_table(TABLE_CATEGORIES)

        for row in values:
            self._categories[row[2]] = (row[0], row[3])

    def category_set_avatar(self, avatar: Adw.Avatar, name: str) -> None:
        avatar.set_text(name)
        avatar.set_show_initials(False)

        try:
            cat = self.CATEGORY_DB[self._categories[name][1]]
            avatar.set_icon_name(cat["icon"])
        except (IndexError, KeyError):
            avatar.set_icon_name("dialog-question-symbolic")

    def category_set_avatar_by_category_name(
        self, avatar: Adw.Avatar, name: str
    ) -> None:
        avatar.set_text(name)
        avatar.set_show_initials(False)

        try:
            cat = self.CATEGORY_DB[name]

            avatar.set_icon_name(cat["icon"])
        except (IndexError, KeyError):
            avatar.set_icon_name("dialog-question-symbolic")

    def category_get_name(self, name: str) -> str:
        try:
            cat = self.CATEGORY_DB[self._categories[name][1]]
            return cat["name"]
        except (IndexError, KeyError):
            return _("Other Expenses")

    def category_get_internal_name(self, name: str) -> str:
        try:
            return self._categories[name][1]
        except (IndexError, KeyError):
            return ""

    def category_add_mapping(self, name: str, cat: str) -> None:
        if name in self._categories:
            category = self._categories[name][0]
            self.database.update_in_table(
                TABLE_CATEGORIES,
                category,
                "category",
                cat,
                commit=True,
                raise_error=True,
            )
        else:
            self.database.insert_into_table(
                TABLE_CATEGORIES, [0, name, cat], commit=True
            )

        self._load_categories()

    def category_remove_mapping(self, name: str) -> None:
        if name in self._categories:
            category = self._categories[name][0]
            self.database.delete_data_in_table(
                TABLE_CATEGORIES, category, commit=True, raise_error=True
            )
            self._load_categories()

    @property
    def database_exists(self):
        return Path(self.database_path).is_file()

    @property
    def safe_password(self):
        return self._safe_password

    @safe_password.setter
    def safe_password(self, safe_password):
        # Create database
        self.database = sqlitewrapper.SqliteCipher(
            self.database_path, self.check_same_thread, safe_password
        )

        # Add tables
        self.database.create_table(
            TABLE_CLIENTS, CLIENT_COLUMNS, make_secure=self.db_secure, commit=True
        )
        self.database.create_table(
            TABLE_ACCOUNTS, ACCOUNT_COLUMNS, make_secure=self.db_secure, commit=True
        )
        self.database.create_table(
            TABLE_TRANSACTIONS,
            TRANSACTION_COLUMNS,
            make_secure=self.db_secure,
            commit=True,
        )
        self.database.create_table(
            TABLE_CATEGORIES,
            CATEGORIES_COLUMNS,
            make_secure=self.db_secure,
            commit=True,
        )

    def is_safe_password_valid(self, safe_password):
        verifier = sqlitewrapper.SqliteCipher.get_verifier(
            self.database_path, self.check_same_thread
        )
        password_verifier = sqlitewrapper.SqliteCipher.sha512_converter(safe_password)

        if verifier == password_verifier:
            if not self.database:
                self.database = sqlitewrapper.SqliteCipher(
                    self.database_path, self.check_same_thread, safe_password
                )
                print("Set password")
                self._safe_password = safe_password

            return True

        return False

    def load_database(self):
        if not self._database_loaded:
            self._load_categories()
            self._load_clients()
            self._load_accounts()

            self._database_loaded = True

    @property
    def accounts(self):
        return self._accounts

    @property
    def clients(self):
        return self._clients

    def find_institute(self, blz_or_bic):
        """
        Find institute based on user provided blz or bic information
        :param blz_or_bic: user provided blz or bic
        :return: database entry if found otherwise None
        """
        for db in self._client_db["databases"]:
            blz = str(db["blz"])
            bic = str(db["bic"])
            if blz.lower() == blz_or_bic.lower() or bic.lower() == blz_or_bic.lower():
                return db

        return None

    def get_logo(self, client):
        for row in self._client_db["databases"]:
            blz = str(row["blz"])
            if blz == client["blz"]:
                return row["logo"]

        return None

    def _load_clients(self):
        self._clients = []

        clients_header, clients_val = self.database.get_data_from_table(TABLE_CLIENTS)

        for row in clients_val:
            client = dict(zip(clients_header, row))
            self._clients.append(client)

    def _load_accounts(self):
        self._accounts = []

        accounts_header, accounts_val = self.database.get_data_from_table(
            TABLE_ACCOUNTS
        )

        # Migration
        # Needs a better strategy with version number...
        lst = [item[0] for item in ACCOUNT_COLUMNS]
        if accounts_header[1:11] != lst:
            print("Database Migration")
            self.database.rename_column(TABLE_ACCOUNTS, "association", "logo")
            accounts_header, accounts_val = self.database.get_data_from_table(
                TABLE_ACCOUNTS
            )

        transaction_header, transaction_val = self.database.get_data_from_table(
            TABLE_TRANSACTIONS
        )

        for row in accounts_val:
            account = dict(zip(accounts_header, row))

            account_transactions = [
                dict(zip(transaction_header, x))
                for x in transaction_val
                if x[1] == account["ID"]
            ]
            account["transactions"] = account_transactions
            account_data = AccountData(account)

            self._accounts.append(account_data)

    def _on_tan_response(self, dialog, response, fints_client, event):
        if response == "submit":
            self.tan_transaction = fints_client.send_tan(
                self.tan_transaction, dialog.tan
            )
        else:
            self.tan_transaction = None

        event.set()

    def _ask_for_tan_idle(self, fints_client, event):
        """
        Ask user for a new TAN
        :param response: client response message
        """
        tan_dialog = TanDialog(self.tan_transaction)

        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        tan_dialog.connect("response", self._on_tan_response, fints_client, event)
        tan_dialog.present(win)

    def _ask_for_tan(self, fints_client: FinTS3PinTanClient, response: str) -> str:
        """
        Ask user for a new TAN
        :param fints_client: fints client
        :param response: client response message
        """
        event = threading.Event()
        self.tan_transaction = response
        GLib.idle_add(self._ask_for_tan_idle, fints_client, event)
        event.wait()

        return self.tan_transaction

    def _on_mechanism_response(
        self, dialog, response, fints_client, client, supported_mechanisms, event
    ):
        if response == "select":
            mechanism = supported_mechanisms[dialog.mechanism][0]
            self.set_tan_mechanism(client, mechanism)
            fints_client.set_tan_mechanism(mechanism)

        event.set()

    def get_mechanism(self, fints_client, client, event):
        supported_mechanisms = list(fints_client.get_tan_mechanisms().items())

        mechanism_dialog = MechanismDialog(supported_mechanisms)
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)

        mechanism_dialog.connect(
            "response",
            self._on_mechanism_response,
            fints_client,
            client,
            supported_mechanisms,
            event,
        )
        mechanism_dialog.present(win)

    def connect_client(self, client):
        if client["blz"] == "00000000":
            return True

        logging.debug("Connecting to server...")
        fints_client = FinTS3PinTanClient(
            client["blz"],
            client["user"],
            client["password"],
            client["server"],
            product_id="AA3B821AAECEA62FB87C27EF3",
        )

        if fints_client.get_current_tan_mechanism() is None:
            fints_client.fetch_tan_mechanisms()

            if client["tan-mechanism"] != "":
                fints_client.set_tan_mechanism(client["tan-mechanism"])
            elif fints_client.get_tan_mechanisms():
                event = threading.Event()
                GLib.idle_add(self.get_mechanism, fints_client, client, event)
                event.wait()

        if fints_client.init_tan_response:
            self._ask_for_tan(fints_client, fints_client.init_tan_response)

        return fints_client

    def _get_credit_card_balance(self, fints_client, sepa_access, credit_card_number):
        credit_card_transactions = fints_client.get_credit_card_transactions(
            sepa_access,
            credit_card_number,
            datetime.date.today() - datetime.timedelta(days=1),
            datetime.date.today(),
        )

        if not (
            credit_card_transactions
            and isinstance(
                credit_card_transactions[0], saldo.backend.fints_extra.DIKKU2
            )
        ):
            return None

        # Return fints.formals.Balance1 format
        balance1 = credit_card_transactions[0].balance
        return balance1

    def _get_credit_card_transactions(
        self, fints_client, sepa_access, credit_card_number
    ):
        credit_card_transactions = fints_client.get_credit_card_transactions(
            sepa_access,
            credit_card_number,
            datetime.date.today() - datetime.timedelta(days=config.get_safe_days()),
            datetime.date.today(),
        )

        if not (
            credit_card_transactions
            and isinstance(
                credit_card_transactions[0], saldo.backend.fints_extra.DIKKU2
            )
            and hasattr(credit_card_transactions[0], "transactions")
        ):
            # Actually these are at least 3 different cases
            # If only the last condition is false, and there is
            # no 'transactions' field it only means that there were simply
            # no transactions in the give timeframe so retuning the empty list
            # is completely valid, but if we didn't get back
            # a saldo.fints_extra.DIKKU2 type
            # then something very unexpected must have happened like a warning:
            # 'FinTSParserWarning: Ignoring parser error and
            #  returning generic object'
            # Or some completely unexpected type was returned and parsed in.
            return []

        # List of CreditCardTransaction1 objects
        transactions = credit_card_transactions[0].transactions

        # CreditCardTransaction1 type objects mapped to mt940 encodable dicts
        mt940_encodable_transactions = [
            {
                "iban": "",
                "date": str(t.receipt_date),
                "entry_date": str(t.booking_date),
                "guessed_entry_date": str(t.value_date),
                "currency": t.currency,
                "posting_text": "",
                "amount": {
                    "amount": (
                        ("-" if t.credit_debit == CreditDebit2.DEBIT else "")
                        + str(t.booked_amount)
                    ),
                    "currency": t.booked_currency,
                },
                "applicant_name": "",
                "applicant_iban": "",
                "applicant_bin": "",
                "purpose": " ".join([x for x in t.memo if x]),
                "applicant_creditor_id": "",
                "additional_position_reference": "",
                "end_to_end_reference": t.booking_reference,
            }
            for t in transactions
        ]

        return mt940_encodable_transactions

    def _get_sepa_account(self, iban):
        return next((a for a in self.sepa_accounts if a.iban == iban), None)

    def _fetch_error_dialog(self, text):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        dialog = Adw.MessageDialog()

        dialog.set_transient_for(win)
        dialog.set_heading(_("Error fetching account"))
        dialog.set_body(text)

        dialog.add_response("close", _("Close"))
        dialog.set_default_response("close")
        dialog.set_close_response("close")
        dialog.connect("response", self._on_error_dialog)
        dialog.present()

        self.emit("accounts_refreshed")

    def _request_client_account_data(self, client):
        if client["blz"] == "00000000":
            init_demo_bank(self, client)
            self.emit("accounts_refreshed")
            return

        try:
            fints_client = self._get_client_connection(client)
            with fints_client:
                info = fints_client.get_information()
                if not info:
                    logging.warning("Could not access client information, aborting.")
                    return

                if fints_client.init_tan_response:
                    self._ask_for_tan(fints_client, fints_client.init_tan_response)

                logging.debug("Got generic bank information")
                if "accounts" not in info:
                    logging.warning(
                        "No accounts found within client information, aborting."
                    )
                    return

                info = fints_client.get_information()
                logging.debug("Num accounts: %d", len(info["accounts"]))

                if info["bank"]["supported_operations"][
                    FinTSOperations.GET_SEPA_ACCOUNTS
                ]:
                    self.sepa_accounts = fints_client.get_sepa_accounts()
                else:
                    self.sepa_accounts = []

                for account in info["accounts"]:
                    # Account info
                    account_info = {}
                    account_info["logo"] = self.get_logo(client)
                    account_info["bank_name"] = info["bank"]["name"]
                    account_info["product_name"] = account.get("product_name", "")
                    account_info["currency"] = account["currency"]
                    account_info["last_updated"] = time.time()
                    account_info["iban"] = account.get("iban", "")
                    account_info["subaccount_number"] = account.get(
                        "subaccount_number", ""
                    )
                    account_info["owner_name"] = ", ".join(account["owner_name"])

                    credit_card_number = None

                    logging.debug("Get balance for: %s", account_info["product_name"])

                    # Get balance
                    sepa_account = self._get_sepa_account(account_info["iban"])
                    if sepa_account:
                        logging.debug("SEPA Account found")
                    else:
                        logging.debug(
                            "SEPA Account not found, checking for credit card information..."
                        )

                        gcct_supported = account["supported_operations"][
                            FinTSOperations.GET_CREDIT_CARD_TRANSACTIONS
                        ]

                        if not (account["subaccount_number"] and gcct_supported):
                            logging.debug("No SEPA access found, returning")
                            continue

                        # get_credit_card_transactions will need an
                        # 'accountnumber' field,
                        # but certain subaccounts only have an
                        # 'account_number' field, so let's wrap this
                        # into a SEPAAccount.
                        sepa_account = SEPAAccount(
                            iban="",
                            # The bic is a must have, so let's assume that
                            # there is at least 1 sepa_account
                            # Otherwise we should use db['bic']
                            bic=self.sepa_accounts[0].bic,
                            accountnumber=account["account_number"],
                            subaccount=account["subaccount_number"],
                            blz=client["blz"],
                        )

                        credit_card_number = account["account_number"]
                        logging.debug("Assuming credit card")

                    if account["supported_operations"][FinTSOperations.GET_BALANCE]:
                        balance = fints_client.get_balance(sepa_account)
                        while isinstance(balance, NeedTANResponse):
                            balance = self._ask_for_tan(fints_client, balance)
                        account_info["balance"] = float(balance.amount.amount)
                    elif account["supported_operations"][
                        FinTSOperations.GET_CREDIT_CARD_TRANSACTIONS
                    ]:
                        balance = self._get_credit_card_balance(
                            fints_client, sepa_account, credit_card_number
                        )
                        account_info["balance"] = float(balance.amount)
                    else:
                        logging.warning(
                            "Could not read balance, using 0.00 for balance."
                        )
                        account_info["balance"] = float(0.00)

                    # Get transactions
                    logging.debug("Get transactions")
                    if (
                        credit_card_number
                        and account["supported_operations"][
                            FinTSOperations.GET_CREDIT_CARD_TRANSACTIONS
                        ]
                    ):
                        transactions = self._get_credit_card_transactions(
                            fints_client, sepa_account, credit_card_number
                        )
                    elif account["supported_operations"][
                        FinTSOperations.GET_TRANSACTIONS
                    ]:
                        transactions = fints_client.get_transactions(
                            sepa_account,
                            datetime.date.today()
                            - datetime.timedelta(days=config.get_safe_days()),
                            datetime.date.today(),
                        )
                    else:
                        logging.warning(
                            "Could not read transaction, using empty list for transactions."
                        )
                        transactions = []

                    while isinstance(transactions, NeedTANResponse):
                        transactions = self._ask_for_tan(fints_client, transactions)

                    dump = json.dumps(
                        transactions, indent=4, cls=mt940.JSONEncoder
                    ).replace("null", '""')
                    transaction_json = json.loads(dump)
                    for item in transaction_json:
                        item["amount"] = float(item["amount"]["amount"])

                    account_id = self.add_account(client, account_info)
                    if account_id >= 0:
                        self.add_transactions(account_id, transaction_json)

                    account["transactions"] = transaction_json
                    account_data = AccountData(account)
                    self._accounts.append(account_data)

        except FinTSClientPINError as err:
            GLib.idle_add(self._fetch_error_dialog, str(err))

    def _on_error_dialog(self, window, _):
        Gtk.Window.destroy(window)

    def _get_client_connection(self, client):
        id = client["ID"]

        if id not in self._client_connections:
            ret = self.connect_client(client)
            self._client_connections[id] = ret

        return self._client_connections[id]

    def _refresh_accounts_thread(self):
        self._accounts = []

        for client in self._clients:
            self._request_client_account_data(client)

        self.emit("accounts_refreshed")

    def refresh_accounts(self):
        if self._refresh_thread and self._refresh_thread.is_alive():
            return

        self.set_property("loading", True)
        self._refresh_thread = threading.Thread(target=self._refresh_accounts_thread)
        self._refresh_thread.daemon = True
        self._refresh_thread.start()

    def add_client(self, user: str, password: str, server: str, blz: str) -> None:
        _, values = self.database.get_data_from_table(TABLE_CLIENTS, omit_id=True)

        client_data = [user, password, server, blz, ""]

        if client_data in values:
            logging.info("add_client: Found existing client, exit")
            return

        logging.info("add_client: Adding new client")
        self.database.insert_into_table(TABLE_CLIENTS, client_data, commit=True)
        self._load_clients()

    def set_tan_mechanism(self, client, value):
        self.database.update_in_table(
            TABLE_CLIENTS,
            client["ID"],
            "tan-mechanism",
            value,
            commit=True,
            raise_error=True,
        )

    def add_account(self, client_data, account_info):
        account_data = [
            client_data["ID"],
            account_info["logo"],
            account_info["bank_name"],
            account_info["product_name"],
            account_info["currency"],
            account_info["iban"],
            account_info["subaccount_number"] or "",
            account_info["owner_name"],
            account_info["last_updated"],
            account_info["balance"],
        ]

        _, values = self.database.get_data_from_table(TABLE_ACCOUNTS)

        for row in values:
            if account_data[0:8] == row[1:9]:
                logging.info("add_account: Account already exists, updating")
                account_id = row[0]
                self.database.update_in_table(
                    TABLE_ACCOUNTS,
                    account_id,
                    "last_updated",
                    account_info["last_updated"],
                    commit=True,
                    raise_error=True,
                )
                self.database.update_in_table(
                    TABLE_ACCOUNTS,
                    account_id,
                    "balance",
                    account_info["balance"],
                    commit=True,
                    raise_error=True,
                )
                return account_id

        logging.info("add_account: Adding new account")
        self.database.insert_into_table(TABLE_ACCOUNTS, account_data, commit=True)

        # FIXME: Stop guessing
        return len(values)

    def add_transactions(self, account_id, transactions):
        transaction_header, values = self.database.get_data_from_table(
            TABLE_TRANSACTIONS, omit_id=True
        )

        existing_list = account_id in (list[0] for list in values)

        for transaction in transactions:
            transaction_data = [
                account_id,
                transaction.get("status") or "",
                transaction.get("funds_code") or "",
                transaction["amount"],
                transaction.get("id") or "",
                transaction.get("customer_reference") or "",
                transaction.get("bank_reference") or "",
                transaction.get("extra_details") or "",
                transaction["currency"],
                transaction["date"],
                transaction.get("entry_date") or "",
                transaction.get("guessed_entry_date") or "",
                transaction.get("transaction_code") or "",
                transaction["posting_text"],
                transaction.get("prima_nota") or "",
                transaction["purpose"],
                transaction["applicant_bin"],
                transaction["applicant_iban"],
                transaction["applicant_name"],
                transaction.get("return_debit_notes") or "",
                transaction.get("recipient_name") or "",
                transaction.get("additional_purpose") or "",
                transaction.get("additional_position_reference") or "",
                transaction.get("end_to_end_reference") or "",
            ]

            if transaction_data in values:
                continue

            self.database.insert_into_table(
                TABLE_TRANSACTIONS, transaction_data, commit=True
            )

            # Only show notification for new entries to an existing database,
            # otherwise we would spam the user during initial setup
            if existing_list:
                data = TransactionData(transaction)

                known_applicant = False
                for row in values:
                    existing_transaction = dict(zip(transaction_header, row))

                    if (
                        existing_transaction["applicant_iban"]
                        == transaction["applicant_iban"]
                    ):
                        known_applicant = True
                        break

                self.emit("new-transaction", data, known_applicant)

    @GObject.Property(type=bool, default=False)
    def loading(self):
        return self.__loading

    @loading.setter  # type: ignore
    def loading(self, value):
        self.__loading = value

    def sepa_transfer(
        self,
        account: AccountData,
        account_name: str,
        recipient_name: str,
        iban: IBAN,
        amount: float,
        reason: str,
    ) -> None:
        client_found = None
        for client in self._clients:
            if client["ID"] == account.client_id:
                client_found = client
                break

        if not client_found:
            return

        fints_client = self._get_client_connection(client_found)
        if not fints_client:
            print("Invalid client, abort")
            return

        if isinstance(fints_client, bool):
            # Demo Bank
            print("Transfer successful")
            return

        res = fints_client.simple_sepa_transfer(
            account=account,
            iban=str(iban),
            bic=iban.bank_code,
            amount=amount,
            recipient_name=recipient_name,
            account_name=account_name,
            reason=reason,
            endtoend_id="NOTPROVIDED",
        )
        if isinstance(res, NeedTANResponse):
            self._ask_for_tan(fints_client, res)

    def change_safe_password(self, current_password: str, new_password: str) -> bool:
        if not self.is_safe_password_valid(current_password):
            print("Wrong password")
            return False

        for i in self.database.change_password(new_password):
            print(i)

        print("All tables updated")
        return True
