import datetime
import decimal
import json
from typing import Dict, List

from funds.models import Fund, Operation, Investor
from funds.models import ValuesToWithdraw, InvestorsSnapshot, Investors
from funds.services import prices
from funds.services.exceptions import IllegalStateException
from funds.services.funds import funds

PERFORMANCE_FEE = decimal.Decimal(0.2)


def get_investors_list(fund: Fund) -> List[Investor]:
    return Investor.objects.filter(fund=fund).order_by('market_value')


def get_investor_by_id(investor_id: int) -> Investor:
    return Investor.objects.get(pk=investor_id)


def get_investor_by(fund: Fund, name: str) -> Investor:
    return Investor.objects.get(fund=fund, name=name)


def check_status_is_correct(investor: Investor) -> None:
    assert (round(investor.added_usd, 2) >= 0), f'{round(investor.added_usd, 2)} >= 0'
    assert (round(investor.market_value, 2) >= 0), f'{round(investor.market_value, 2)} >= 0'

    calculated_market_value = investor.added_usd * (1 + investor.get_total_return())
    assert round(investor.market_value, 2) == round(calculated_market_value, 2)


def apply_operation_to_fund_and_investor(fund: Fund, operation: Operation, target: str) -> Fund:
    owner = operation.owner
    if not owner:
        return fund

    usd_price = prices.get_usd_close_price_on_date(operation.date, target)
    usd_value = operation.units * usd_price

    operation_day = operation.date.date()
    new_investor = Investor(name=owner, joined_at=operation_day, fund=fund)
    investor = fund.get_investors().get(owner, new_investor)

    return funds.deposit(fund, investor, usd_value)


def save_historical_investors(fund_name: str, historical_investors: Dict[datetime.date, Investors]) -> None:
    """
    Given a fund name and a dictionary of historical investors, save the
    investors in the database.

    :param fund_name: name of the fund who owns the given historical investors
    :param historical_investors: Historical snapshots of investors of the same fund
    :return: None
    """
    fund = Fund.objects.get(name=fund_name)
    snapshots = InvestorsSnapshot.objects.filter(fund=fund)
    date_to_snapshot = {snapshot.date: snapshot for snapshot in snapshots}
    for day, investors_dict in historical_investors.items():
        json_investors = __to_json_investors(investors_dict)
        new_snapshot = InvestorsSnapshot(
            date=day,
            investors=json_investors,
            fund=fund
        )
        snapshot = date_to_snapshot.get(day, new_snapshot)
        snapshot.save()


# private

def __deposit_to_investor(investor: Investor, usd_value: decimal.Decimal) -> Investor:
    investor.added_usd += usd_value
    investor.market_value += usd_value
    return investor.save()


def __withdraw_money_from_investor(investor: Investor, value_before_fees: decimal.Decimal) -> ValuesToWithdraw:
    """Method to represent a withdraw operation from an investor.

    :param investor: The investor who wants to withdraw money.
    :param value_before_fees: Value to withdraw before fees
    :return: (cash_value, value_before_fees, fees, value_after_fees)

    Where each element from the tuple represents the following:
    * cash_value: value to withdraw from the added_usd
    * value_before_fees: value to withdraw before fees
    * fees: performance fees to pay by the investor
    * value_after_fees: value to withdraw after fees
    """
    original_investor_return = investor.get_total_return()
    values_to_withdraw = \
        __calculate_withdraw_values_before_fees(investor, value_before_fees)

    if values_to_withdraw.value_after_fees > investor.market_value:
        error = f'the investor {investor.name} ' \
                f'is trying to withdraw ({values_to_withdraw.value_after_fees}) ' \
                f'more than he/she has ({investor.market_value})'
        raise IllegalStateException(error)

    # withdraw value from investor
    investor.added_usd -= values_to_withdraw.cash_value
    investor.market_value -= values_to_withdraw.value_after_fees

    print(f'__withdraw_money_from_investor: investor: {investor.__dict__}')

    # assertions
    assert round(values_to_withdraw.value_after_fees, 2) == round(value_before_fees + values_to_withdraw.fees, 2)
    if values_to_withdraw.fees == 0:
        current_investor_return = investor.get_total_return()
        assert round(original_investor_return, 2) == round(current_investor_return, 2)

    investor.save()

    return values_to_withdraw


def __calculate_withdraw_values_before_fees(investor: Investor, value_before_fees: decimal.Decimal) -> ValuesToWithdraw:
    """
    Returns the values required to withdraw money given the value to
    withdraw BEFORE fees.

    This method returns an object of type ValuesToWithdraw: (cash_value, fees, value_after_fees)
    Where cash value is the value to withdraw (excluding fees) adjusted
    to the cash the investor added to the fund.

    :param investor: The investor how wants to withdraw money.
    :param value_before_fees: Value to withdraw before fees.
    :return: ValuesToWithdraw
    """
    cash_value = __calculate_cash_value(investor, value_before_fees)
    if investor.is_manager:
        fees = 0
        value_after_fees = value_before_fees
        return ValuesToWithdraw(cash_value, value_before_fees, fees, value_after_fees)
        # return cash_value, fees, value_before_fees, value_after_fees

    investor_profit = max(0, value_before_fees - cash_value)
    fees = investor_profit * decimal.Decimal(PERFORMANCE_FEE)
    value_after_fees = value_before_fees + fees
    return ValuesToWithdraw(cash_value, value_before_fees, fees, value_after_fees)


def __calculate_cash_value(investor: Investor, value: decimal.Decimal) -> decimal.Decimal:
    """
    Return the investor cash value, proportional to the given value
    and the current market value.

    :param investor: The investor we want to calculate the cash value for.
    :param value: Value we want to adjust to the investor cash value.
    :return: The value given as argument, adjusted to represent cash.
    """
    if investor.market_value == 0:
        return 0
    return decimal.Decimal(investor.added_usd * (value / investor.market_value))


def __withdraw_all_money_from_investor(investor: Investor) -> ValuesToWithdraw:
    """
    Method to withdraw all the money from the investor

    :param investor: The investor who wants to withdraw all of its money
    :return: values_to_withdraw
    """
    values_to_withdraw = \
        __calculate_withdraw_values_after_fees(investor, investor.market_value)
    investor.added_usd -= values_to_withdraw.cash_value
    investor.market_value -= values_to_withdraw.value_after_fees

    # assertions
    assert round(investor.added_usd, 2) == 0
    assert round(investor.market_value, 2) == 0

    investor.save()
    return values_to_withdraw


def __calculate_withdraw_values_after_fees(investor: Investor, value_after_fees: decimal.Decimal) -> ValuesToWithdraw:
    """
    Returns the values required to withdraw money given the value to
    withdraw AFTER fees.

    This method returns the tuple: (cash_value, fees, value_after_fees)
    Where cash value is the value to withdraw (excluding fees) adjusted
    to the cash the investor added to the fund.

    :param investor: The investor how wants to withdraw money.
    :param value_after_fees: Value to withdraw after fees.
    :return: (cash_value, fees, value_before_fees, value_after_fees)
    """
    fees = decimal.Decimal(0)
    cash_value = __calculate_cash_value(investor, value_after_fees)
    if investor.is_manager:
        value_before_fees = value_after_fees - fees
        return ValuesToWithdraw(cash_value, value_before_fees, fees, value_after_fees)

    investor_profit = max(0, value_after_fees - cash_value)
    fees = decimal.Decimal(investor_profit * PERFORMANCE_FEE)
    value_before_fees = value_after_fees - fees
    return ValuesToWithdraw(cash_value, value_before_fees, fees, value_after_fees)


def __take_snapshot(investor: Investor) -> None:
    print(f'{investor.name}, {investor.added_usd}, '
          f'{investor.market_value}, {investor.ownership_percentage}, '
          f'{investor.get_total_return()}')


def __to_json_investors(investors_dict):
    investors = []
    for _, investor in investors_dict.items():
        investors.append(investor.to_dict())

    return json.dumps(investors, default=str)
