"""*Bankr* module for data manipulation and statistics."""

import i18n
import sys
import time

import pandas as pd
from schwifty import IBAN

import bankr.tuitable as tt
from bankr.money import Money


# Helper - Transfer cents to euros
def _cents2euro(x: int) -> float:
    return x / 100.0


# Helper - Month to Term
def _month2term(month: int) -> int:
    if month < 7:
        term = 1
    else:
        term = 2
    return term


# Bankr - Helpers


def tract_existing(book: pd.DataFrame, uuid: str) -> bool:
    """Is a Transaction ID (UUID) existing in the Book?"""
    return uuid in book.index.values


def iban_valid(iban: str) -> bool:
    """Is a given IBAN string a valid IBAN?"""
    return IBAN(iban, allow_invalid=True).is_valid


def list_years_with_tracts(book: pd.DataFrame) -> list[int]:
    """bla"""
    years: list[str] = []
    book["year"] = book["date"].dt.year
    for year in range(1900, 2100):
        if (book["year"] == year).sum() > 0:
            years.append(str(year))
    return years


# Operations on the Book


def add_page_to_book(page: pd.DataFrame, book: pd.DataFrame) -> pd.DataFrame:
    """Add a Page to the Book.

    This is an in memory and in place operation.
    """
    book = pd.concat([page, book], axis=0, ignore_index=False)
    book.sort_values(by=["date", "iban"], ascending=[False, True], inplace=True)
    return book


def extract_page_from_book(book: pd.DataFrame, **kwargs) -> pd.DataFrame:
    """_Extract a Page from the Book, based on different conditions_

    The extraction is done based on the category `cat` of the Transaction, its `iban` or `bounds` (date interval),
    or its offset account. All parameters are assumed to be sanitized.

    `offsets` in the given list are considered as valid, and are therefore removed from the Page. All other parameters
    are treated as inclusive, if given. The defaults below mean unfiltered, respectively.

    Args:
        book (pd.DataFrame): _A conditioned import of the Book_

    Returns:
        pd.DataFrame: _The extracted Page_
    """

    # Update default params
    params = {"cat": "", "iban": "", "bounds": [], "offsets": []}
    params |= kwargs

    # Extract Page from Book
    page = book
    bounds = params["bounds"]
    if len(bounds) > 0:
        page = page[page["date"].between(pd.to_datetime(bounds[0]), pd.to_datetime(bounds[1]), inclusive="both")]
    if params["cat"]:
        page = page[page["cat"] == params["cat"]]
    if params["iban"]:
        page = page[page["iban"] == params["iban"]]
    page = page.loc[~(page["offset"].isin(params["offsets"]))]
    return page


def autocat_tracts(page: pd.DataFrame, filtrs: list) -> None:
    """Auto-categorize a Page or the Book in place.

    Applies only to Transactions with cat == "none", since other, potential manually overruled
    categories must NOT be changed.
    *Technical remarks*
    (1) return not needed due to inplace operations.
    (2) Tilde operator ~ is an elementwise Not (needed, see "where" operator).
    (3) catcon["nones"] limits replacement to page["cat"] = "none". Index reset needed for "catcon" setup.
    (4) catcon["keys"] limits replacement, where the filter is fullfilled.
    """
    catcon = pd.DataFrame(columns=["nones", "keys"], index=range(page.shape[0]))
    page.reset_index(inplace=True)
    catcon.nones = page["cat"].str.contains("none")
    for filtr in filtrs:
        category = filtr["cat"]
        column = filtr["col"]
        keys = filtr["keys"]
        catcon["keys"] = page[column].str.contains(keys, case=False)
        page["cat"] = page["cat"].where(~catcon.all(axis=1), other=category)
    page.set_index("uuid", inplace=True)


def mancat_tracts(book: pd.DataFrame, cats: list) -> None:
    """Manually categorize the Book in place.

    Loop over all Transactions with `cat == "none"`, and offer a manual change of the category.
    The manual categorization can be stopped at any time. In this case, the function will return a `"quit"` to the
    caller.
    """
    all_tracts = book.shape[0]
    none_tracts = sum(book["cat"] == "none")
    none = 1

    for n in range(all_tracts):
        tract = book.iloc[n]
        if tract["cat"] != "none":
            continue
        print("+" * 120)
        print(f"{i18n.t('general.transaction')} {none}/{none_tracts}")
        tt.show_transaction(book, tract.name, True)
        selection = select_category(cats)
        if selection == "quit":
            return
        book.loc[tract.name, "cat"] = selection
        print(f"{i18n.t('general.new_cat')}: {i18n.t('cats.' + selection)}")
        none += 1
        time.sleep(0.5)


# Operations on Transations


def delete_transaction(book: pd.DataFrame, uuid: str) -> pd.DataFrame:
    """Delete a Transaction from the Book or from a Page."""
    try:
        return book.drop(index=uuid, inplace=False)
    except KeyError:
        sys.exit(f"Bankr Error - Transaction {uuid} not found.")


def select_transaction(book: pd.DataFrame, uuid: str) -> pd.Series:
    """Select a Transaction from the Book or a Page."""
    try:
        return book.loc[uuid]
    except KeyError:
        sys.exit(f"Bankr Error - Transaction {uuid} not found.")


def update_transaction(book: pd.DataFrame, tract: pd.Series) -> pd.DataFrame:
    """Update a Transaction of the Book or of a Page.

    If the Transaction ID is not existing, the Transaction will be appended.
    """
    uuid = tract.name
    try:
        book.loc[uuid] = tract
        return book
    except KeyError:
        sys.exit(f"Bankr Error - Transaction {uuid} could not be replaced or appended.")


def select_category(cats: list) -> str:
    """Provide a list of categories for selection.

    The categories are enumerated as they show up in `cats.yaml`.
    It returns the internal name of the category selected.
    """
    n = 0
    for cat in cats:
        print(f"[{n:>2}] - {i18n.t('cats.' + cat['cat']):<15}{cat['desc'][:58]}")
        n = n + 1
    print("-" * 80)
    while True:
        try:
            selection = input("Bankr Action - Select a category by [number] or (q)uit. ")
            if selection == "q":
                return "quit"  # Quit
            selection = int(selection)
            if selection < 0 or selection >= len(cats):
                raise ValueError
            break
        except ValueError:
            continue
    print("-" * 80)
    return cats[selection]["cat"]


# Statistics: Incomes


def calc_book_stats(book: pd.DataFrame, accounts: pd.DataFrame) -> list[dict]:
    """Calculate the account statistics of the Book.

    tbc
    """
    extended_accounts: list[dict] = []
    for account in accounts:
        account["tracts"] = sum(book["iban"] == account["iban"])
        account["balance"] = Money.m(book[book["iban"] == account["iban"]].cents.sum() / 100.0)
        try:
            account["first_tract"] = book[book["iban"] == account["iban"]].date.min().strftime("%d.%m.%Y")
        except:  # noqa: E722
            account["first_tract"] = ""
        try:
            account["last_tract"] = book[book["iban"] == account["iban"]].date.max().strftime("%d.%m.%Y")
        except:  # noqa: E722
            account["last_tract"] = ""
        extended_accounts.append(account)
    #    book_stats = pd.DataFrame(accounts)
    return extended_accounts


def sum_per_cat_period(book: pd.DataFrame, year: int, span: int, **kwargs) -> pd.DataFrame:
    """Calculate the sum of cents per category and per period. Returns a stacked or pivoted
    ("amounts-type") version of the sums. The sums are given in the main currency unit.

    The period is given as string YYYY-mm (months), YYYY-Qn (quarters), YYYY-Tn (terms), and YYYY.
    The stacked data consists of 3 columns time, cat, and main. The pivoted data is time in rows,
    and cat in columns.

    Parameters:
    - year: starting year of the total time considered
    - span: years to consider
    - period: 1-char string to mark (m)onth [default], (q)uarter, (t)erm, or (f)ull year, kwarg
    - pivot: True if pivoted, otherwise stacked, kwarg
    """

    # Update params (defaults) with kwargs
    params = {"period": "m", "pivot": False}
    params |= kwargs

    # Limit Page to [year, year + span) if year and span > 0, otherwise take the Book
    # Exit, if there are no Transactions in Page
    book["year"] = book["date"].dt.year
    page = book
    if year > 0 and span > 0:
        page = book[(book["year"] >= year) & (book["year"] < (year + span))]
    if not page.shape[0]:
        sys.exit(f"Bankr Error - No values in {year}!")

    # Create the Series time and concat it with the Page
    time = page["date"].dt.strftime("%Y-%m").rename("time")
    if params["period"] != "m":
        time = page["year"].astype(str).rename("time")
    if params["period"] == "q":
        quarter = page["date"].dt.quarter.astype(str)
        time = time.str.cat(others=quarter, sep="-Q")
    if params["period"] == "t":
        term = page["date"].dt.month.apply(_month2term).astype(str)
        time = time.str.cat(others=term, sep="-T")
    page = pd.concat([page, time], axis=1, copy=False)

    # Calculate sums and return it stacked or pivoted
    stacked = page.groupby(["time", "cat"], as_index=False, observed=False)["cents"].sum()
    stacked["main"] = stacked["cents"].apply(_cents2euro)
    stacked.drop("cents", axis=1, inplace=True)
    if params["pivot"]:
        return stacked.pivot(index="time", columns="cat", values="main")
    else:
        return stacked


def append_totals(amounts: pd.DataFrame) -> pd.DataFrame:
    """Calculate incomes/expenses/balances per row, and column totals.

    It expects an "amounts-type" dataframe. Firstly, it appends it with three columns,
    the total incomes per time interval (row), the total expenses, and the balance (sum).
    Secondly, it adds a row with column totals.
    """
    amounts.loc["total"] = amounts.sum(axis=0)
    income = amounts.mask(amounts < 0, 0)
    income["internal"] = 0
    expense = amounts.mask(amounts > 0, 0)
    expense["internal"] = 0
    amounts["income"] = income.sum(axis=1)
    amounts["expense"] = expense.sum(axis=1)
    amounts["balance"] = amounts["income"] + amounts["expense"]

    return amounts  # TODO Remove categories, return totals only
