# 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/>.

from __future__ import annotations
from typing import Any
import logging
from gi.repository import Adw, Gio, Gtk, GLib
from saldo.widgets.clients import Clients
from saldo.widgets.account_row import AccountRow
from saldo.widgets.transaction_row import TransactionRow
from saldo.widgets.transaction_details import TransactionDetails  # noqa
from saldo.backend.backend import BankingBackend
from saldo.backend.helper import money
from saldo.backend.helper import readable_date


def _transaction_listbox_placeholder():
    status_page = Adw.StatusPage()
    status_page.set_icon_name("edit-find-symbolic")
    status_page.set_title("No Transactions Found")
    return status_page


def _statistics_listbox_placeholder():
    status_page = Adw.StatusPage()
    status_page.set_icon_name("edit-find-symbolic")
    status_page.set_title("No Statistic Available")
    return status_page


@Gtk.Template(resource_path="/org/tabos/saldo/ui/unlocked_page.ui")
class UnlockedPage(Adw.Bin):
    __gtype_name__ = "UnlockedPage"

    _content_box = Gtk.Template.Child()
    _detail_window = Gtk.Template.Child()
    _detail_title = Gtk.Template.Child()
    _sidebar = Gtk.Template.Child()
    _account_listbox = Gtk.Template.Child()
    _transaction_listbox = Gtk.Template.Child()
    subpage_leaflet = Gtk.Template.Child()
    _details_bin = Gtk.Template.Child()
    _search_bar = Gtk.Template.Child()
    _search_entry = Gtk.Template.Child()
    _refresh_button = Gtk.Template.Child()
    _refresh_spinner_stack = Gtk.Template.Child()
    refresh_spinner = Gtk.Template.Child()
    _statistic_listbox = Gtk.Template.Child()
    stack = Gtk.Template.Child()
    dbus_subscription_id: int | None = None

    def __init__(self, window: Adw.ApplicationWindow, backend: BankingBackend):
        super().__init__()

        self.window = window
        self._backend = backend

        self._total_balance = ""
        self._last_transaction_added = None

        self._account_listbox.set_header_func(self.update_account_header)
        self._account_listbox.set_sort_func(self._account_sort)

        self._transaction_listbox.set_header_func(self.update_transaction_header)
        self._transaction_listbox.set_placeholder(_transaction_listbox_placeholder())
        self._transaction_listbox.set_sort_func(self._transaction_sort)
        self._transaction_listbox.set_filter_func(self._transaction_filter)
        self._transaction_listbox.add_css_class("background")

        self._statistic_listbox.set_header_func(self._statistic_header)
        self._statistic_listbox.set_placeholder(_statistics_listbox_placeholder())
        self._statistic_listbox.set_sort_func(self._statistic_sort)
        self._statistic_listbox.set_filter_func(self._statistic_filter)
        self._statistic_listbox.add_css_class("background")

        self.stack.connect(
            "notify::visible-child-name", self._on_stack_notify_visible_child_name
        )

        self._search_bar.connect_entry(self._search_entry)
        self._search_bar.set_key_capture_widget(self.window)

        controller = Gtk.EventControllerKey()
        controller.connect("key-pressed", self._on_key_pressed)
        self.add_controller(controller)

        self._backend.connect("accounts_refreshed", self._on_accounts_refreshed)
        self._backend.connect("notify::loading", self._on_loading_changed)

        if not backend.clients:
            clients = Clients(self._backend)
            clients.present(self.window)
        else:
            self._populate_data()

        self.register_dbus_signal()

    def _on_stack_notify_visible_child_name(self, stack, param):
        self.window.start_lock_timer()

        if stack.get_visible_child_name() == "statistic":
            self.__statistic_switch = False
            self.__search_text = None
            self._search_entry.set_text("")
            self._search_bar.set_search_mode(False)
            self._statistic_listbox.invalidate_filter()
        else:
            self._transaction_listbox.invalidate_filter()

    def _on_loading_changed(self, backend, param):
        if backend.props.loading:
            self.refresh_spinner.start()
            self._refresh_spinner_stack.set_visible_child_name("spinner")
        else:
            self._refresh_spinner_stack.set_visible_child_name("image")
            self.refresh_spinner.stop()

    def _on_key_pressed(self, event, keyval, keycode, state):  # pylint: disable=unused-argument
        # Reset lock timer on each key press
        self.window.start_lock_timer()
        return False

    def do_dispose(self):  # pylint: disable=arguments-differ
        self._search_entry.set_key_capture_widget(None)

    def do_realize(self):  # pylint: disable=arguments-differ
        Gtk.Widget.do_realize(self)
        self._account_listbox.grab_focus()

    def update_account_header(
        self, row: Gtk.ListBoxRow, before: Gtk.ListBoxRow
    ) -> None:
        if before is None:
            vbox = Gtk.Box()
            vbox.set_margin_bottom(6)
            vbox.add_css_class("background")
            vbox.set_orientation(Gtk.Orientation.VERTICAL)

            hbox = Gtk.Box()
            vbox.append(hbox)
            hbox.set_orientation(Gtk.Orientation.HORIZONTAL)
            hbox.set_hexpand_set(True)
            hbox.set_margin_start(6)
            hbox.set_margin_end(6)

            update = Gtk.Label()
            update.set_hexpand(True)
            update.set_text(row.account.last_updated)
            update.set_halign(Gtk.Align.START)
            update.add_css_class("dim-label")
            hbox.append(update)

            total = Gtk.Label()
            total.set_text(self._total_balance)
            total.set_hexpand(True)
            total.set_halign(Gtk.Align.END)
            total.add_css_class("dim-label")
            hbox.append(total)

            separator = Gtk.Separator()
            vbox.append(separator)

            row.set_header(vbox)
        else:
            row.set_header(None)

    def update_transaction_header(
        self, row: Gtk.ListBoxRow, before: Gtk.ListBoxRow
    ) -> None:
        if self._last_transaction_added != row.transaction.date or before is None:
            vbox = Gtk.Box()
            vbox.add_css_class("background")
            vbox.set_orientation(Gtk.Orientation.VERTICAL)

            hbox = Gtk.Box()
            vbox.append(hbox)
            hbox.set_orientation(Gtk.Orientation.HORIZONTAL)
            hbox.set_hexpand_set(True)
            hbox.set_margin_start(6)
            hbox.set_margin_end(6)
            hbox.set_margin_top(6)
            hbox.set_margin_bottom(6)

            date_str = readable_date(row.transaction.date)

            update = Gtk.Label()
            update.set_text(date_str)
            update.add_css_class("dim-label")
            hbox.append(update)

            self._last_transaction_added = row.transaction.date

            vbox = Gtk.Label()
            vbox.set_text(date_str)
            vbox.set_halign(Gtk.Align.START)
            vbox.set_margin_top(12)
            vbox.add_css_class("h4")
            vbox.add_css_class("heading")
            row.set_header(vbox)
        else:
            row.set_header(None)

    def _statistic_header(self, row: Gtk.ListBoxRow, before: Gtk.ListBoxRow) -> None:
        if before is None:
            vbox = Gtk.Box()
            vbox.add_css_class("background")
            vbox.set_orientation(Gtk.Orientation.VERTICAL)

            hbox = Gtk.Box()
            vbox.append(hbox)
            hbox.set_orientation(Gtk.Orientation.HORIZONTAL)
            hbox.set_hexpand_set(True)
            hbox.set_margin_start(6)
            hbox.set_margin_end(6)
            hbox.set_margin_top(6)
            hbox.set_margin_bottom(6)

            row.set_header(vbox)
        else:
            row.set_header(None)

    def _clear_listbox(self, listbox: Gtk.ListBox) -> None:
        child = listbox.get_row_at_index(0)
        while child:
            next_child = listbox.get_row_at_index(1)
            listbox.remove(child)
            child = next_child

    def _populate_data(self):
        total_balance = 0
        currency = ""

        # Update or add new accounts
        for account in self._backend.accounts:
            total_balance += account.balance
            currency = account.currency

            if not self._update_account_row(account):
                row = AccountRow(account, self._backend)
                self._account_listbox.append(row)

        # TODO: Remove existing accounts not refreshed?

        balance = money(total_balance)
        self._total_balance = f"{balance} {currency}"

        # In case an account is selected retrigger it for a refresh
        row = self._account_listbox.get_selected_row()
        if not row:
            row = self._account_listbox.get_row_at_index(0)
            self.__active_account_row = row

        if row:
            self._update_account_transactions()
            self._update_statistic()

        # Ensure headers are updated
        self._account_listbox.invalidate_headers()

    def _on_accounts_refreshed(self, backend: BankingBackend) -> None:
        GLib.idle_add(self._refreshed_idle)

    def _refreshed_idle(self):
        if not self._backend.accounts:
            # Backend has no accounts, ensure everything is deleted
            self._clear_listbox(self._account_listbox)
            self._clear_listbox(self._transaction_listbox)
            self._total_balance = ""
        else:
            self._populate_data()

    def _update_account_row(self, account):
        idx = 0
        while True:
            row = self._account_listbox.get_row_at_index(idx)
            if row is None:
                break

            idx += 1

            if row.account.iban == account.iban:
                row.set_info(account)
                return True

        return False

    def _update_account_transactions(self) -> None:
        # Clear existing entries
        self._clear_listbox(self._transaction_listbox)

        # Update detail headerbar
        self._detail_title.set_title(self.__active_account_row.account.product_name)
        self._detail_title.set_subtitle(self.__active_account_row.account.iban)

        self._transaction_listbox.set_header_func(None)

        # Add new transactions
        if self.__active_account_row.account.transactions:
            for transaction in self.__active_account_row.transactions:
                self._transaction_listbox.append(transaction)

            self._transaction_listbox.set_header_func(self.update_transaction_header)
            self._transaction_listbox.invalidate_headers()

    def _add_statistic_row(self, listbox, dict, debit_total, credit_total):
        row = Adw.ActionRow()
        row.set_activatable(True)
        row.add_css_class("card")
        row.set_margin_top(6)
        row.set_margin_bottom(6)
        row.connect("activated", self._on_statistic_listbox_row_activated)
        row.set_title(dict["name"])

        if dict["amount"] < 0:
            percent = (
                -round(dict["amount"] / credit_total * 100, 1)
                if credit_total != 0
                else 0
            )
        else:
            percent = (
                round(dict["amount"] / debit_total * 100, 1) if debit_total != 0 else 0
            )

        row.set_subtitle(f"{percent}%")

        avatar = Adw.Avatar()
        avatar.set_size(32)
        self._backend.category_set_avatar_by_category_name(avatar, dict["name"])
        row.add_prefix(avatar)

        amount = money(dict["amount"])
        amount_label = Gtk.Label()
        amount_label.set_text(amount + " " + "EUR")  # self._transaction.currency)

        row.add_suffix(amount_label)
        if dict["amount"] >= 0.0:
            amount_label.add_css_class("success")
        else:
            amount_label.add_css_class("error")

        listbox.append(row)

    def _update_statistic(self) -> None:
        self._clear_listbox(self._statistic_listbox)

        dct: dict[str, Any] = {}
        for transaction in self.__active_account_row.transactions:
            category = self._backend.category_get_name(transaction.transaction.name)

            if category not in dct:
                dct[category] = {}
                dct[category]["name"] = category
                dct[category]["internal_name"] = (
                    self._backend.category_get_internal_name(
                        transaction.transaction.name
                    )
                )
                dct[category]["amount"] = 0.0

            dct[category]["amount"] += transaction.transaction.amount

        debit_total = 0.0
        credit_total = 0.0

        for entry in dct:
            if dct[entry]["amount"] < 0:
                credit_total += dct[entry]["amount"]
            else:
                debit_total += dct[entry]["amount"]

        for entry in dct:
            self._add_statistic_row(
                self._statistic_listbox, dct[entry], debit_total, credit_total
            )

    @Gtk.Template.Callback()
    def _on_account_listbox_row_selected(self, _: Gtk.ListBox, row: AccountRow) -> None:
        self.window.start_lock_timer()

        self.__active_account_row = row

        self._update_account_transactions()
        self._update_statistic()

        self._content_box.set_visible_child(self._detail_window)

    @Gtk.Template.Callback()
    def _on_back_button_clicked(self, _: Gtk.Widget) -> None:
        self.window.start_lock_timer()
        self._content_box.set_visible_child(self._sidebar)

    @Gtk.Template.Callback()
    def _on_refresh_button_clicked(self, _: Gtk.Widget) -> None:
        self.window.start_lock_timer()

        self.window.backend.refresh_accounts()

    @Gtk.Template.Callback()
    def _on_transaction_listbox_row_selected(
        self, _: Gtk.Widget, row: TransactionRow
    ) -> None:
        self.window.start_lock_timer()

        if not self._details_bin.props.child:
            details = TransactionDetails(self)
            self._details_bin.props.child = details

        details = self._details_bin.props.child
        details.set_transaction(row.transaction)
        self.subpage_leaflet.navigate(Adw.NavigationDirection.FORWARD)

    def _on_statistic_listbox_row_activated(self, row: Adw.ActionRow) -> None:
        self.window.start_lock_timer()

        self._search_entry.set_text(row.get_title())
        self._search_bar.set_search_mode(True)
        self.__statistic_switch = True
        self.stack.set_visible_child_name("transactions")

    @Gtk.Template.Callback()
    def _on_search_changed(self, search_entry: Gtk.Widget) -> None:  # pylint: disable=unused-argument
        self.window.start_lock_timer()

        if self.stack.get_visible_child_name() == "transactions":
            self._transaction_listbox.invalidate_filter()
        else:
            self._statistic_listbox.invalidate_filter()

    def _transaction_sort(self, row1: TransactionRow, row2: TransactionRow) -> int:
        """
        Sort transactions based on date information
        :param row1: first row
        :param row2: second row
        :return: 0 if there is a match, 1 if less -1 if greater than row2
        """
        date1 = row1.transaction.date
        date2 = row2.transaction.date

        if date1 < date2:
            return 1
        if date1 > date2:
            return -1
        return 0

    def _transaction_filter(self, row: TransactionRow) -> bool:
        """
        Filter transactions (search for sub strings) based on
         - name
         - posting text
         - category

         :param row: current row to filter
         :return: %True if there is a match otherwise %False
        """
        search_text = self._search_entry.get_text().lower()
        ret = search_text in row.transaction.name.lower()

        if not ret:
            ret = search_text in row.transaction.posting_text.lower()

        if not ret:
            ret = (
                search_text
                in self._backend.category_get_name(row.transaction.name).lower()
            )

        return ret

    def _account_sort(self, row1: AccountRow, row2: AccountRow) -> int:
        key1 = row1.account.iban
        key2 = row2.account.iban

        if key1 < key2:
            return -1
        if key1 > key2:
            return 1
        return 0

    def _statistic_sort(self, row1: Adw.AdwActionRow, row2: Adw.ActionRow) -> int:
        title1 = float(row1.get_subtitle()[:-1])
        title2 = float(row2.get_subtitle()[:-1])

        if title1 < 0 and title2 > 0:
            return 1

        if title1 > 0 and title2 < 0:
            return -1

        title1 = abs(title1)
        title2 = abs(title2)

        if title1 < title2:
            return 1
        if title1 > title2:
            return -1
        return 0

    def _statistic_filter(self, row: Adw.AdwActionRow) -> bool:
        search_text = self._search_entry.get_text().lower()
        ret = search_text in row.get_title().lower()

        return ret

    def register_dbus_signal(self) -> None:
        """Register a listener so we get notified about screensave kicking in

        In this case we will call self.on_session_lock()"""
        logging.debug("Subscribed to org.gnome.ScreenSaver")
        connection = Gio.Application.get_default().get_dbus_connection()
        self.dbus_subscription_id = connection.signal_subscribe(
            None,
            "org.gnome.ScreenSaver",
            "ActiveChanged",
            "/org/gnome/ScreenSaver",
            None,
            Gio.DBusSignalFlags.NONE,
            self._on_session_lock,
        )

    def _on_session_lock(
        self, _connection, _unique_name, _object_path, _interface, _signal, state
    ):
        if state[0]:
            self.window.view = self.window.View.LOCKED

    def update_transactions(self):
        idx = 0
        while True:
            row = self._transaction_listbox.get_row_at_index(idx)
            if row is None:
                break

            idx += 1

            row.update_category()

        self._update_statistic()
