"""Parsing and computing budgets."""

from collections import defaultdict, namedtuple, Counter

from beancount.core.data import Custom
from beancount.core.number import Decimal

from fava.util.date import (Interval, days_in_daterange,
                            number_of_days_in_period)
from fava.core.helpers import FavaModule

Budget = namedtuple('Budget', 'account date_start period number currency')
BudgetError = namedtuple('BudgetError', 'source message entry')


class BudgetModule(FavaModule):
    """Parses budget entries."""

    def __init__(self, ledger):
        super().__init__(ledger)
        self.budget_entries = None

    def load_file(self):
        self.budget_entries, errors = parse_budgets(
            self.ledger.all_entries_by_type[Custom])
        self.ledger.errors.extend(errors)

    def calculate(self, account_name, begin_date, end_date):
        """Calculate the budget for an account in an interval."""
        return calculate_budget(self.budget_entries, account_name, begin_date,
                                end_date)

    def calculate_children(self, account_name, begin_date, end_date):
        """Calculate the budget for an account including its children."""
        return calculate_budget_children(self.budget_entries, account_name,
                                         begin_date, end_date)

    def __bool__(self):
        return bool(self.budget_entries)


def parse_budgets(custom_entries):
    """Parse budget directives from custom entries.

    Args:
        custom_entries: the Custom entries to parse budgets from.

    Returns:
        A dict of accounts to lists of budgets.

    Example:
        2015-04-09 custom "budget" Expenses:Books "monthly" 20.00 EUR
    """

    budgets = defaultdict(list)
    errors = []

    interval_map = {
        'daily': Interval.DAY,
        'weekly': Interval.WEEK,
        'monthly': Interval.MONTH,
        'quarterly': Interval.QUARTER,
        'yearly': Interval.YEAR,
    }

    for entry in custom_entries:
        if entry.type == 'budget':
            try:
                interval = interval_map.get(str(entry.values[1].value))
                if not interval:
                    errors.append(
                        BudgetError(entry.meta,
                                    'Invalid interval for budget entry',
                                    entry))
                    continue
                budget = Budget(entry.values[0].value, entry.date, interval,
                                entry.values[2].value.number,
                                entry.values[2].value.currency)
                budgets[budget.account].append(budget)
            except (IndexError, TypeError):
                errors.append(
                    BudgetError(entry.meta, 'Failed to parse budget entry',
                                entry))

    return budgets, errors


def _matching_budgets(budgets, account_name, date_active):
    """Find matching budgets.

    Returns:
        The budget that is active on the specified date for the
        specified account.
    """
    last_seen_budgets = {}
    for budget in budgets[account_name]:
        if budget.date_start <= date_active:
            last_seen_budgets[budget.currency] = budget
        else:
            break
    return last_seen_budgets


def calculate_budget(budgets, account_name, date_from, date_to):
    """Calculate budget for an account.

    Args:
        budgets: A list of :class:`Budget` entries.
        account_name: An account name.
        date_from: Starting date.
        date_to: End date (exclusive).

    Returns:
        A dictionary of currency to Decimal with the budget for the
        specified account and period.
    """
    if account_name not in budgets:
        return {}

    currency_dict = defaultdict(Decimal)

    for single_day in days_in_daterange(date_from, date_to):
        matches = _matching_budgets(budgets, account_name, single_day)
        for budget in matches.values():
            currency_dict[budget.currency] += \
                budget.number / number_of_days_in_period(budget.period,
                                                         single_day)
    return currency_dict


def calculate_budget_children(budgets, account_name, date_from, date_to):
    """Calculate budget for an account including budgets of its children.

    Args:
        budgets: A list of :class:`Budget` entries.
        account_name: An account name.
        date_from: Starting date.
        date_to: End date (exclusive).

    Returns:
        A dictionary of currency to Decimal with the budget for the
        specified account and period.
    """
    currency_dict = Counter()

    for account in budgets.keys():
        if account.startswith(account_name):
            currency_dict.update(
                calculate_budget(budgets, account, date_from, date_to))
    return currency_dict
