from calendar import monthrange
from datetime import date, timedelta
from decimal import Decimal, ROUND_HALF_UP
from django.db import transaction
from django.db.models import Sum
from django.utils import timezone

D = Decimal
CENT = D("0.01")


def money(value):
    return D(value).quantize(CENT, rounding=ROUND_HALF_UP)


def months_after(start_date, months):
    y = start_date.year + (start_date.month - 1 + months) // 12
    m = (start_date.month - 1 + months) % 12 + 1
    day = min(start_date.day, monthrange(y, m)[1])
    return date(y, m, day)


def simple_interest(principal, annual_rate, months):
    return D(principal) * D(annual_rate) / D("100") * D(months) / D("12")


def compound_maturity(principal, annual_rate, months, frequency="MONTHLY"):
    freq_map = {"MONTHLY": 12, "QUARTERLY": 4, "HALF_YEARLY": 2, "YEARLY": 1}
    n_per_year = freq_map.get((frequency or "MONTHLY").upper(), 12)
    periods = D(months) / D("12") * D(n_per_year)
    periods_int = int(periods)
    if D(periods_int) != periods:
        raise ValueError("Tenure is not compatible with the selected compounding frequency.")
    return D(principal) * (D("1") + D(annual_rate) / D("100") / D(n_per_year)) ** periods_int


def platinum_rd_maturity(monthly_payment, months, annual_rate):
    p = D(monthly_payment)
    n = D(months)
    r = D(annual_rate)
    principal = p * n
    interest = p * n * (n + D("1")) / (D("2") * D("12")) * r / D("100")
    return money(principal + interest)


def platinum_actual_rd_maturity(investment):
    """
    Uses the actual monthly contribution records.
    Each contribution is treated as one RD installment under the supplied
    standard RD formula. Missing months contribute zero.
    """
    contributions = investment.recurring_contributions.all().order_by("contribution_date")
    total_principal = D("0")
    total_interest = D("0")
    maturity_date = investment.maturity_date or months_after(
        investment.investment_date, investment.scheme.tenure_months
    )
    for c in contributions:
        total_principal += D(c.amount)
        remaining_months = max(
            D("0"),
            D((maturity_date.year - c.contribution_date.year) * 12
              + (maturity_date.month - c.contribution_date.month))
        )
        # Standard monthly RD approximation for the remaining months.
        total_interest += D(c.amount) * remaining_months * (remaining_months + D("1")) / (D("2") * D("12"))             * D(investment.investor_annual_rate) / D("100")
    return money(total_principal + total_interest)


def maturity_value(investment):
    scheme = investment.scheme
    if scheme.category == scheme.Categories.SILVER:
        if investment.amount is None:
            raise ValueError("Silver investment requires a one-time amount.")
        return money(D(investment.amount) + simple_interest(
            investment.amount, investment.investor_annual_rate, scheme.tenure_months
        ))

    if scheme.category == scheme.Categories.GOLD:
        if investment.amount is None:
            raise ValueError("Gold investment requires a one-time amount.")
        return money(compound_maturity(
            investment.amount, investment.investor_annual_rate,
            scheme.tenure_months, scheme.compounding_frequency or "MONTHLY"
        ))

    if scheme.category == scheme.Categories.PLATINUM:
        return platinum_actual_rd_maturity(investment)

    if scheme.category == scheme.Categories.DIAMOND:
        if investment.amount is None:
            raise ValueError("Diamond investment requires a one-time amount.")
        # Diamond behaviour is not defined in the supplied requirements.
        raise ValueError("Diamond calculation behaviour has not been defined yet.")

    raise ValueError("Unsupported scheme category.")


def premature_value(investment, as_of=None):
    as_of = as_of or timezone.localdate()
    maturity = maturity_value(investment)
    penalty = maturity * D(investment.scheme.penalty_rate) / D("100")
    return money(max(D("0"), maturity - penalty))


def daily_business_credit(amount, annual_rate):
    return money(D(amount) * D(annual_rate) / D("100") / D("365"))


def daily_investor_soft_debit(investment, entry_date):
    """
    Daily payable accrual based on the investment principal/contributions.
    The scheme's annual rate is copied onto the investment at creation.
    """
    if investment.scheme.category == investment.scheme.Categories.PLATINUM:
        principal = investment.recurring_contributions.filter(
            contribution_date__lte=entry_date
        ).aggregate(v=Sum("amount"))["v"] or D("0")
    else:
        principal = investment.amount or D("0")
    return money(D(principal) * D(investment.investor_annual_rate) / D("100") / D("365"))


def build_terms_snapshot(scheme, investment):
    snapshot = {
        "category": scheme.category,
        "scheme_name": scheme.name,
        "tenure_months": scheme.tenure_months,
        "annual_interest_rate": str(scheme.annual_interest_rate),
        "penalty_rate": str(scheme.penalty_rate),
        "withdrawal_type": scheme.withdrawal_type,
        "compounding_frequency": scheme.compounding_frequency,
        "rules": scheme.rules,
    }
    if scheme.category == scheme.Categories.PLATINUM:
        snapshot["monthly_installment"] = str(investment.monthly_installment)
    else:
        snapshot["investment_amount_type"] = "ONE_TIME"
    return snapshot


def audit(actor, operation, module, obj=None, description="", metadata=None):
    from .models import AuditLog
    return AuditLog.objects.create(
        actor=actor,
        operation=operation,
        module=module,
        object_type=obj.__class__.__name__ if obj else "",
        object_id=str(obj.pk) if obj and getattr(obj, "pk", None) else "",
        description=description,
        metadata=metadata or {},
    )


@transaction.atomic
def create_investment_with_snapshot(
    *, investor, business, scheme, amount=None, monthly_installment=None,
    created_by, investment_date=None
):
    from .models import Investment
    investment_date = investment_date or timezone.localdate()

    if scheme.category == scheme.Categories.PLATINUM:
        if monthly_installment is None or D(monthly_installment) <= 0:
            raise ValueError("Platinum investment requires a positive monthly_installment.")
        amount = None
    else:
        if amount is None or D(amount) <= 0:
            raise ValueError("Silver, Gold and Diamond investments require a positive one-time amount.")
        monthly_installment = None

    maturity_date = months_after(investment_date, scheme.tenure_months)
    # The scheme percentage is authoritative; admin does not type a separate rate.
    investor_rate = scheme.annual_interest_rate

    obj = Investment.objects.create(
        investor=investor,
        business=business,
        scheme=scheme,
        amount=amount,
        monthly_installment=monthly_installment,
        investor_annual_rate=investor_rate,
        investment_date=investment_date,
        maturity_date=maturity_date,
        created_by=created_by,
    )
    obj.terms_snapshot = build_terms_snapshot(scheme, obj)
    obj.save(update_fields=["terms_snapshot"])
    return obj


@transaction.atomic
def generate_daily_ledger(entry_date=None):
    """
    Idempotent daily ledger generation. Intended to be called automatically
    at 12:00 AM server time and may also be called manually through the API.
    """
    from .models import KuberBusinessInvestment, Investment, LedgerEntry, Scheme

    entry_date = entry_date or timezone.localdate()
    created_ids = []

    kuber_qs = KuberBusinessInvestment.objects.filter(
        deleted_at__isnull=True, investment_date__lte=entry_date
    ).select_related("business")

    for ki in kuber_qs:
        if LedgerEntry.objects.filter(
            kuber_investment=ki, entry_date=entry_date,
            entry_type=LedgerEntry.EntryTypes.CREDIT_PROFIT
        ).exists():
            continue
        amount = daily_business_credit(ki.amount, ki.annual_rate)
        if amount:
            e = LedgerEntry.objects.create(
                business=ki.business, kuber_investment=ki,
                entry_date=entry_date,
                entry_type=LedgerEntry.EntryTypes.CREDIT_PROFIT,
                amount=amount, annual_rate=ki.annual_rate,
                description="Automatic daily Kuber-to-business credit profit.",
                reference=f"KBI-{ki.pk}-{entry_date}",
            )
            created_ids.append(e.pk)

    investments = Investment.objects.filter(
        deleted_at__isnull=True,
        investment_date__lte=entry_date,
        status=Investment.Status.ACTIVE,
    ).select_related("business", "investor", "scheme").prefetch_related("recurring_contributions")

    for inv in investments:
        amount = daily_investor_soft_debit(inv, entry_date)
        if not amount:
            continue
        if LedgerEntry.objects.filter(
            investment=inv, entry_date=entry_date,
            entry_type=LedgerEntry.EntryTypes.SOFT_DEBIT
        ).exists():
            continue
        e = LedgerEntry.objects.create(
            business=inv.business, investment=inv, investor=inv.investor,
            entry_date=entry_date,
            entry_type=LedgerEntry.EntryTypes.SOFT_DEBIT,
            amount=amount, annual_rate=inv.investor_annual_rate,
            description=f"Automatic daily {inv.scheme.get_category_display()} investor soft debit.",
            reference=f"INV-{inv.pk}-{entry_date}",
        )
        created_ids.append(e.pk)

    return {"date": entry_date, "created_entry_ids": created_ids}


def daily_ledger_job():
    return generate_daily_ledger(timezone.localdate())
