from decimal import Decimal, getcontext
from datetime import date
from calendar import isleap

getcontext().prec = 28


class DayCount:

    @staticmethod
    def year_fraction(start_date, end_date, convention):
        if end_date <= start_date:
            return Decimal('0')

        if convention == 'ACT/360':
            return Decimal((end_date - start_date).days) / Decimal(360)

        elif convention in ('ACT/365', 'ACT/365F'):
            return Decimal((end_date - start_date).days) / Decimal(365)

        elif convention == 'ACT/ACT_ISDA':
            return DayCount._act_act_isda(start_date, end_date)

        elif convention == '30/360_US':
            return DayCount._30_360_us(start_date, end_date)

        elif convention == '30E/360':
            return DayCount._30e_360(start_date, end_date)

        elif convention == '30E/360_ISDA':
            return DayCount._30e_360_isda(start_date, end_date)

        else:
            raise ValueError(f"Unsupported convention {convention}")

    # ---------- IMPLEMENTATIONS ----------

    @staticmethod
    def _act_act_isda(start_date, end_date):
        total = Decimal('0')
        current = start_date

        while current < end_date:
            year_end = date(current.year, 12, 31)
            period_end = min(year_end.replace(day=31) + 
                             (date(current.year + 1, 1, 1) - year_end),
                             end_date)

            days_in_period = (period_end - current).days
            days_in_year = 366 if isleap(current.year) else 365

            total += Decimal(days_in_period) / Decimal(days_in_year)
            current = period_end

        return total

    @staticmethod
    def _30_360_us(d1, d2):
        d1_day = 30 if d1.day == 31 else d1.day
        if d1.day in (30, 31) and d2.day == 31:
            d2_day = 30
        else:
            d2_day = d2.day

        days = ((d2.year - d1.year) * 360 +
                (d2.month - d1.month) * 30 +
                (d2_day - d1_day))

        return Decimal(days) / Decimal(360)

    @staticmethod
    def _30e_360(d1, d2):
        d1_day = min(d1.day, 30)
        d2_day = min(d2.day, 30)

        days = ((d2.year - d1.year) * 360 +
                (d2.month - d1.month) * 30 +
                (d2_day - d1_day))

        return Decimal(days) / Decimal(360)

    @staticmethod
    def _30e_360_isda(d1, d2):
        d1_day = 30 if d1.day == 31 else d1.day
        d2_day = 30 if d2.day == 31 else d2.day

        days = ((d2.year - d1.year) * 360 +
                (d2.month - d1.month) * 30 +
                (d2_day - d1_day))

        return Decimal(days) / Decimal(360)


class InterestCalculator:

    @staticmethod
    def calculate(
        start_date,
        end_date,
        rate,
        rate_type='annual',  # 'annual' or 'monthly'
        convention='ACT/360',
        compounding='simple',  # simple, annual, monthly, continuous
    ):
        """
        Retourne le facteur d'intérêt (pas le montant).
        """

        if not start_date or not end_date:
            return Decimal('0')

        if end_date <= start_date:
            return Decimal('0')

        rate = Decimal(str(rate))

        # Conversion en taux annuel si besoin
        if rate_type == 'monthly':
            annual_rate = rate * Decimal(12)
        else:
            annual_rate = rate

        yf = DayCount.year_fraction(start_date, end_date, convention)

        if compounding == 'simple':
            return annual_rate * yf

        elif compounding == 'annual':
            return (Decimal(1) + annual_rate) ** yf - Decimal(1)

        elif compounding == 'monthly':
            monthly_rate = annual_rate / Decimal(12)
            months = yf * Decimal(12)
            return (Decimal(1) + monthly_rate) ** months - Decimal(1)

        elif compounding == 'continuous':
            from math import exp
            return Decimal(exp(float(annual_rate * yf))) - Decimal(1)

        else:
            raise ValueError("Unsupported compounding mode")
