from collections import defaultdict
from datetime import date, datetime, timedelta
from decimal import Decimal
from functools import reduce
from typing import Iterable

from django.db.models import QuerySet, Sum
from pandas._libs.tslibs.offsets import BDay
from psycopg.types.range import DateRange
from wbaccounting.generators.base import AbstractBookingEntryGenerator
from wbaccounting.models import BookingEntry
from wbaccounting.models.booking_entry import BookingEntryType
from wbcore.contrib.directory.models import Entry
from wbcrm.models import Account
from wbfdm.models import InstrumentPrice
from wbportfolio.models import Claim, Product

from wbcommission.models import CommissionType, Rebate


def get_booking_entry_map():
    return {
        "management": BookingEntryType.objects.get_or_create(
            key="management-fees", defaults={"title": "Management Fees"}
        )[0],
        "performance": BookingEntryType.objects.get_or_create(
            key="performance-fees", defaults={"title": "Performance Fees"}
        )[0],
    }


class RebateGenerator(AbstractBookingEntryGenerator):
    TITLE = "Rebate Generation"

    @staticmethod
    def generate_booking_entries(
        from_date: date, to_date: date, counterparty: Entry, booking_date: date
    ) -> Iterable[BookingEntry]:
        commission_types = CommissionType.objects.all()
        vat = counterparty.entry_accounting_information.vat.get_rate(booking_date)
        booking_entry_map = get_booking_entry_map()
        for commission_type in commission_types:
            booking_type = booking_entry_map[commission_type.key]
            base_rebates = Rebate.objects.filter(
                commission_type=commission_type, date__gte=from_date, date__lte=to_date, recipient=counterparty
            )
            latest_navs = dict(
                InstrumentPrice.objects.filter(
                    instrument__in=base_rebates.distinct("product").values("product"),
                    calculated=False,
                    date__gte=(to_date - BDay(3)).date(),
                    date__lte=to_date,
                )
                .order_by("instrument", "-date")
                .distinct("instrument")
                .values_list("instrument", "net_value")
            )
            for account in Account.all_objects.filter(id__in=base_rebates.distinct("account").values("account")):
                account_rebates = base_rebates.filter(account=account)
                account_total_shares = dict(
                    Claim.get_valid_and_approved_claims(to_date, account)
                    .values("product")
                    .annotate(sum_shares=Sum("shares"))
                    .values_list("product", "sum_shares")
                )

                for product in Product.objects.filter(id__in=account_rebates.distinct("product").values("product")):
                    product_rebates = account_rebates.filter(product=product)
                    if product_rebates.exists():
                        rebate_sum = product_rebates.aggregate(value_sum=Sum("value"))["value_sum"] or Decimal("0")
                        latest_nav = latest_navs.get(product.id, None)
                        latest_quantity = account_total_shares.get(product.id, None)
                        parameters = []  # we use a list to allow ordering of fields
                        if latest_quantity is not None:
                            parameters.append(("Shares", f"{latest_quantity:,}"))
                        if latest_nav is not None:
                            parameters.append(("NAV", f"${latest_nav}"))
                        if latest_quantity is not None and latest_nav is not None:
                            parameters.append(("AUM", f"${round(latest_nav * latest_quantity, 2):,}"))
                        if rebate_sum > 0:
                            yield BookingEntry(
                                product=product.accounting_product,
                                account=account,
                                type=booking_type,
                                booking_date=booking_date,
                                reference_date=to_date,
                                gross_value=Decimal(-1 * rebate_sum),
                                vat=vat,
                                currency=product.currency,
                                counterparty=counterparty,
                                period=DateRange(
                                    from_date, to_date + timedelta(days=1)
                                ),  # we have to add one day to the upper bound because we treat date range with default bound [) but to_date is inclusive
                                parameters={"annotation": parameters},
                                quantity=latest_quantity,
                                backlinks={
                                    "comm-rebates": {
                                        "title": "Rebates",
                                        "reverse": "wbcommission:rebatetable-list",
                                        "parameters": {
                                            "date": f"{from_date.strftime('%Y-%m-%d')},{to_date.strftime('%Y-%m-%d')}",
                                            "product": product.id,
                                            "account": account.id,
                                            "recipient": counterparty.id,
                                            "group_by": "ACCOUNT",
                                        },
                                    },
                                },
                            )

    @staticmethod
    def _compare(d1: str, d2: str) -> str:
        d1_lb, d1_ub = d1.split(",")
        d2_lb, d2_ub = d2.split(",")

        lb = min(datetime.strptime(d1_lb, "%Y-%m-%d"), datetime.strptime(d2_lb, "%Y-%m-%d"))
        ub = max(datetime.strptime(d1_ub, "%Y-%m-%d"), datetime.strptime(d2_ub, "%Y-%m-%d"))

        return f"{lb.strftime('%Y-%m-%d')},{ub.strftime('%Y-%m-%d')}"

    @staticmethod
    def _merge_key_into_dict(d: dict, key: str, value: dict) -> dict:
        d[key]["title"] = value["title"]
        d[key]["reverse"] = value["reverse"]
        if "recipient" in value["parameters"]:
            d[key]["parameters"]["recipient"] = value["parameters"]["recipient"]
            d[key]["parameters"]["date"] = RebateGenerator._compare(
                d[key]["parameters"].get("date", "9999-12-31,1111-01-01"), value["parameters"]["date"]
            )
        return d

    @staticmethod
    def _iter_backlinks(booking_entries: QuerySet) -> Iterable:
        for backlink in booking_entries:
            yield from backlink.items()

    @staticmethod
    def merge_backlinks(booking_entries: QuerySet[BookingEntry]) -> dict:
        return reduce(
            lambda d, v: RebateGenerator._merge_key_into_dict(d, *v),
            RebateGenerator._iter_backlinks(
                booking_entries.filter(backlinks__isnull=False).values_list("backlinks", flat=True)
            ),
            defaultdict(lambda: dict(parameters=dict())),
        )
