import math
from dataclasses import dataclass, field
from typing import Dict, List, Optional

# ==============================================================================
# NETTOPOLIZZE.AT — RECHNERLOGIK (OPEN SOURCE)
# ==============================================================================
# Dieses Skript zeigt die mathematische Logik unseres Vergleichsrechners
# auf https://nettopolizze.at. Es vergleicht ein klassisches
# Wertpapierdepot (Broker Account) mit einer Nettopolizze (Insurance Wrapper).
#
# Österreichisches Steuerrecht:
# - KESt (27,5%) auf Kapitalerträge im Depot
# - Versicherungssteuer (4%) auf Prämien in der Nettopolizze
#
# Hochrechnungs-Konvention (LV-InfoV 2018):
# - Hochgerechnet wird nach Produktkosten (Versicherungssteuer + Kosten der
#   Versicherungshülle). Die Rendite-Annahme gilt dabei als Wertentwicklung
#   nach Fondskosten; die Fondskosten (TER) werden separat erfasst und am
#   Laufzeitende zusätzlich vom ausgewiesenen Endergebnis abgezogen.
# - Benchmark sind die gesetzlich verpflichtenden Modellrechnungen
#   (Hochrechnungen) der Versicherer.
#
# Kennzahlen: IRR (Internal Rate of Return) und RiY (Reduction in Yield).
# Die Gesamt-Zinsminderung (RiY) wird in FÜNF Kostenschichten zerlegt:
#   Alpha (Abschluss-/Transaktionskosten), Beta/Gamma (Verwaltungskosten),
#   Risikokosten, Versicherungssteuer und Fondskosten (TER).
# Mechanik: EIN Simulationslauf mit FV-Impact-Akkumulatoren je Schicht —
# jeder Kostenabzug wird mit dem Fondszins auf das Laufzeitende aufgezinst,
# anschließend wird die Gesamt-Zinsminderung proportional zu diesen
# FV-Impacts aufgeteilt. Bewusst KEINE fünf separaten Läufe mit jeweils
# abgeschalteter Kostenschicht: Das erzeugt Interaktionseffekte zwischen
# den Schichten (bis hin zu negativen Risikokosten-Anteilen).
#
# Die tatsächlichen Produktparameter stammen aus unserer Datenbank.
# Details zu jedem Anbieter: https://nettopolizze.at/vergleich/nettopolizze/anbieter/
# ==============================================================================

@dataclass
class ChartDataPoint:
    month: int
    value: float

@dataclass
class CostSummary:
    broker_fees: float
    etf_costs: float
    taxes: float
    total: float

@dataclass
class InsuranceCostSummary:
    product_costs: float
    alpha_costs: float
    beta_gamma_costs: float
    risk_costs: float
    etf_costs: float
    insurance_tax: float
    duration_penalty_tax: float
    total: float

@dataclass
class BrokerageRiyAttribution:
    """RiY-Zerlegung Depot. total = etf + depot + tax (annualisiert, dezimal)."""
    total: float = 0.0   # Gesamt-Zinsminderung = Bruttorendite - EAI
    etf: float = 0.0     # TER-Anteil (Re-Simulation ohne Fondskosten)
    depot: float = 0.0   # Ordergebühren-Anteil (Re-Simulation ohne Broker-Gebühr)
    tax: float = 0.0     # KESt-Anteil (Residual)

@dataclass
class RiyAttribution:
    """RiY-Zerlegung Nettopolizze in die fünf Kostenschichten.
    total = alpha + beta_gamma + risk + etf + tax (annualisiert, dezimal)."""
    total: float = 0.0       # Gesamt-Zinsminderung = (Bruttorendite + TER) - EAI
    alpha: float = 0.0       # Abschluss-/Transaktionskosten
    beta_gamma: float = 0.0  # Verwaltungskosten (fix, Depotwert-%, NBS-basiert, Jahresprämien-%)
    risk: float = 0.0        # Risikokosten Todesfallschutz
    etf: float = 0.0         # Fondskosten (TER)
    tax: float = 0.0         # Versicherungssteuer

@dataclass
class BenchmarkResult:
    final_value: float
    net_profit: float
    costs: CostSummary
    chart_data: List[ChartDataPoint]
    irr: float = 0.0
    effective_annual_interest: float = 0.0
    perf_gross_etf: float = 0.0
    riy: float = 0.0  # Depot + KESt (OHNE TER — TER separat in riy_attribution.etf)
    riy_attribution: Optional[BrokerageRiyAttribution] = None

@dataclass
class PerformanceResult:
    final_value: float
    net_profit: float
    costs: InsuranceCostSummary
    chart_data: List[ChartDataPoint]
    irr: float = 0.0
    effective_annual_interest: float = 0.0
    perf_gross_etf: float = 0.0
    riy: float = 0.0  # NUR Produktkosten: alpha + beta_gamma + risk + tax (OHNE TER)
    riy_attribution: Optional[RiyAttribution] = None

@dataclass
class Product:
    tax_rate: Optional[float] = None
    commission_period: Optional[int] = None
    closing_commission: Optional[float] = None
    max_basis_calculation: Optional[int] = None
    admin_cost_sum_of_payments_net: Optional[float] = None
    additional_payment_cost: Optional[float] = None
    transaction_cost_cap_25k: Optional[float] = None
    transaction_cost_cap_100k: Optional[float] = None
    transaction_cost_payment: Optional[float] = None
    fixed_annual_admin_cost: Optional[float] = None
    min_fixed_annual_admin_cost: Optional[float] = None
    max_fixed_annual_admin_cost: Optional[float] = None
    percentage_depot_value_admin_cost: Optional[float] = None
    threshold_value: Optional[float] = None
    threshold_value_admin_cost: Optional[float] = None
    threshold_year: Optional[int] = None
    threshold_year_admin_cost: Optional[float] = None
    admin_cost_of_yearly_payments: Optional[float] = None

    # ── Risikokosten (Todesfallschutz) ────────────────────────────────────
    # Produktiv werden amtliche Sterbetafeln je Tarif aus der Datenbank
    # geladen (z.B. ÖST 2010/12 Unisex, DAV 2008 T, MLV 2010/12), teils mit
    # tarifspezifischem Skalierungsfaktor. Die tarifspezifischen Sonderregeln
    # (Alters-Cutoff, reduzierte Todesfallleistung ab Alter X, garantiertes
    # Minimum) sind hier als neutrale Produktparameter abgebildet.
    death_benefit_type: Optional[str] = "SurplusOnFV"  # Deficit | FixedPctOfPremiumSum | SurplusOnFV
    death_benefit_pct_start: Optional[float] = None
    death_benefit_pct_end: Optional[float] = None
    mortality_table: Optional[Dict[int, float]] = None  # Alter -> qx (jährl. Sterbewahrscheinlichkeit)
    mortality_scale_factor: Optional[float] = None      # multipliziert qx; None/0 = 1.0
    risk_cost_ongoing_annual: Optional[float] = None    # Fallback-Flat-Rate (% p.a.), nur ohne Sterbetafel
    risk_cost_depot_value: Optional[float] = None       # Fallback-Flat-Rate Deficit (% p.a.)
    risk_coverage_max_age: Optional[int] = None         # Risikoschutz endet VOR diesem Alter (z.B. 75)
    surplus_reduced_pct: Optional[float] = None         # z.B. 1.01: reduzierte Todesfallleistung ...
    surplus_reduced_from_age: Optional[int] = None      # ... ab diesem Alter (z.B. 70)
    guaranteed_minimum_on_ongoing: bool = False         # max(FV%, eingez. Nettoprämien) auch bei lfd. Prämie

    provider_id: Optional[int] = None  # Nur für anbieter-spezifische Kostenformeln relevant

# Constants
CHART_DATA_SAMPLING_RATE = 4

# Todesfallleistung SurplusOnFV: Standard 105% des Fondswerts
SURPLUS_BENEFIT_PCT = 1.05

# Provider-IDs für anbieter-spezifische Kostenformeln.
# Die IDs entsprechen internen Datenbank-Schlüsseln und dienen
# ausschließlich der korrekten Zuordnung der Kostenlogik.
PROVIDER_NBS_CAP = 6         # Anbieter mit NBS-Deckelung (€300.000)
PROVIDER_DURATION_SCALING = 10  # Anbieter mit Laufzeit-Umrechnung (% pro 10 Jahre)

# ------------------------------------------------------------------------------
# Beispiel-Sterbetafel (ILLUSTRATIVER AUSZUG)
# Gerundete Anker-Näherung in der Größenordnung der amtlichen ÖST 2010/12
# Unisex (Statistik Austria), linear interpoliert. Produktiv laden wir die
# vollständigen amtlichen Tabellenwerte je Tarif aus der Datenbank — dieser
# Auszug dient nur der lauffähigen Demonstration der Rechenstruktur.
# ------------------------------------------------------------------------------
_QX_ANCHORS: Dict[int, float] = {
    18: 0.00040, 25: 0.00045, 30: 0.00050, 35: 0.00070, 40: 0.00100,
    45: 0.00160, 50: 0.00260, 55: 0.00420, 60: 0.00700, 65: 0.01100,
    70: 0.01800, 75: 0.03000, 80: 0.05200, 85: 0.09000, 90: 0.15000,
}

def _build_example_mortality_table() -> Dict[int, float]:
    ages = sorted(_QX_ANCHORS)
    table: Dict[int, float] = {}
    for a0, a1 in zip(ages, ages[1:]):
        q0, q1 = _QX_ANCHORS[a0], _QX_ANCHORS[a1]
        for age in range(a0, a1):
            table[age] = q0 + (q1 - q0) * (age - a0) / (a1 - a0)
    table[ages[-1]] = _QX_ANCHORS[ages[-1]]
    return table

EXAMPLE_MORTALITY_TABLE: Dict[int, float] = _build_example_mortality_table()

def get_qx(table: Dict[int, float], age: int) -> float:
    """qx für ein Alter; fehlendes Alter -> 0 (produktiv identisch)."""
    return table.get(age, 0.0)

def add_chart_data_point(chart_data: List[ChartDataPoint], month: int, value: float, total_months: int = 0):
    if month % CHART_DATA_SAMPLING_RATE == 0 or month == 1 or (total_months > 0 and month == total_months):
        chart_data.append(ChartDataPoint(month, value))

def calculate_irr(cash_flows: List[float], guess: float = 0.005) -> float:
    """
    Newton-Raphson IRR calculation.
    Returns the monthly internal rate of return.
    """
    max_iterations = 50
    precision = 1e-7
    rate = guess

    for _ in range(max_iterations):
        npv = 0.0
        derivative_npv = 0.0

        for t, cf in enumerate(cash_flows):
            div = (1 + rate) ** t
            npv += cf / div
            derivative_npv -= t * cf / (div * (1 + rate))

        if abs(npv) < precision:
            return rate

        if abs(derivative_npv) < precision:
            return 0.0

        new_rate = rate - (npv / derivative_npv)

        if abs(new_rate - rate) < precision:
            return new_rate

        rate = new_rate

    return rate

def compute_monthly_risk_cost(
    product: Product,
    current_age: int,
    account_value: float,
    cum_premiums_gross: float,
    monthly_payment_gate: float,
    planned_premium_sum: float,
    t: int,
    duration_months: int,
) -> float:
    """
    Risikokosten eines Monats, modular nach DeathBenefitType — identische
    Struktur wie in der Produktion. Sterbetafel-Werte (qx) werden mit dem
    tarifspezifischen Skalierungsfaktor multipliziert; ohne Sterbetafel
    greift die Flat-Rate als Fallback.
    """
    db_type = product.death_benefit_type or "SurplusOnFV"
    mortality_table = product.mortality_table
    mortality_scale = product.mortality_scale_factor or 0.0
    if mortality_scale <= 0:
        mortality_scale = 1.0

    tax_rate = (product.tax_rate or 0.0) / 100.0
    risk = 0.0

    if db_type == "Deficit":
        # Todesfallleistung = max(Fondswert, eingezahlte Nettoprämien).
        # Risikokapital nur, solange der Fondswert unter den Prämien liegt.
        cum_prem_net = cum_premiums_gross / (1 + tax_rate)
        risk_cap = max(0.0, cum_prem_net - account_value)

        if risk_cap > 0:
            # Manche Tarife decken das Risiko nur bis zu einem Höchstalter
            # (z.B. "vor Vollendung des 75. Lebensjahres").
            within_coverage = (
                current_age < product.risk_coverage_max_age
                if product.risk_coverage_max_age is not None
                else True
            )
            if within_coverage:
                if mortality_table is not None:
                    qx = get_qx(mortality_table, current_age) * mortality_scale
                    risk += risk_cap * (qx / 12.0)
                else:
                    # Fallback: Flat-Rate
                    rv_rate = (product.risk_cost_ongoing_annual or 0.0) / 100.0
                    if rv_rate <= 0:
                        rv_rate = (product.risk_cost_depot_value or 0.0) / 100.0
                    if rv_rate > 0:
                        risk += risk_cap * (rv_rate / 12.0)

    elif db_type == "FixedPctOfPremiumSum":
        # Todesfallleistung = fester %-Satz der GEPLANTEN Brutto-Prämiensumme
        # (z.B. 10% -> 5%), linear über die Laufzeit sinkend.
        pct_start = (product.death_benefit_pct_start or 0.0) / 100.0
        pct_end = (product.death_benefit_pct_end or 0.0) / 100.0
        progress = min(1.0, t / duration_months)
        current_pct = pct_start + (pct_end - pct_start) * progress
        death_benefit = current_pct * planned_premium_sum
        # Wird ZUSÄTZLICH zum Fondswert ausbezahlt -> volle Summe ist Risikokapital
        risk_cap = death_benefit

        if risk_cap > 0:
            if mortality_table is not None:
                qx = get_qx(mortality_table, current_age) * mortality_scale
                risk += risk_cap * (qx / 12.0)
            else:
                rv_rate = (product.risk_cost_ongoing_annual or 0.0) / 100.0
                if rv_rate > 0:
                    risk += risk_cap * (rv_rate / 12.0)

    else:  # SurplusOnFV: Todesfallleistung = X% des Fondswerts
        # Standard 105%; manche Tarife reduzieren ab einem Alter (z.B. 101% ab 70)
        benefit_pct = SURPLUS_BENEFIT_PCT
        if (product.surplus_reduced_pct is not None
                and product.surplus_reduced_from_age is not None
                and current_age >= product.surplus_reduced_from_age):
            benefit_pct = product.surplus_reduced_pct

        death_benefit = benefit_pct * account_value
        # Garantiertes Minimum: max(X% * FV, eingezahlte Nettoprämien).
        # Gilt für Einmalerläge generell, bei manchen Tarifen auch für
        # laufende Prämien (guaranteed_minimum_on_ongoing).
        cum_prem_net = cum_premiums_gross / (1 + tax_rate)
        if ((monthly_payment_gate <= 0 or product.guaranteed_minimum_on_ongoing)
                and cum_prem_net > death_benefit):
            death_benefit = cum_prem_net

        risk_cap = max(0.0, death_benefit - account_value)

        if risk_cap > 0:
            if mortality_table is not None:
                qx = get_qx(mortality_table, current_age) * mortality_scale
                risk += risk_cap * (qx / 12.0)
            else:
                risk_rate = (product.risk_cost_ongoing_annual or 0.0) / 100.0
                if risk_rate > 0:
                    risk += risk_cap * (risk_rate / 12.0)

    return risk

def calculate_brokerage_account_strategy(
    start_capital: float,
    monthly_payment: float,
    duration: int,
    gross_annual_return: float,
    ter: float
) -> BenchmarkResult:
    chart_data = []

    CAPITAL_GAINS_TAX_RATE = 0.275
    DEEMED_DISTRIBUTABLE_INCOME_SHARE = 0.20
    DDI_TAXED_ANNUALLY_SHARE = 0.60
    BROKER_FEE_PER_TRANSACTION = 1.50

    net_annual_return = gross_annual_return - (ter / 100)
    monthly_return_rate = (1 + net_annual_return) ** (1.0 / 12.0) - 1

    current_value = start_capital
    total_broker_fees = 0.0
    total_annual_ddi_tax = 0.0
    accumulated_final_tax_base = 0.0
    total_ter_costs_absolute = 0.0
    value_at_year_start = start_capital
    deposits_this_year = 0.0

    ter_decimal = ter / 100.0
    cashflows_irr: List[float] = []

    for month in range(1, duration * 12 + 1):
        total_ter_costs_absolute += current_value * ((ter / 100) / 12)

        monthly_deposit = 0.0
        monthly_gross_outflow = 0.0
        if monthly_payment > 0:
            monthly_deposit = monthly_payment - BROKER_FEE_PER_TRANSACTION
            total_broker_fees += BROKER_FEE_PER_TRANSACTION
            deposits_this_year += monthly_payment
            monthly_gross_outflow = monthly_payment

        if month == 1 and start_capital > 0:
            monthly_gross_outflow += start_capital

        cashflows_irr.append(-monthly_gross_outflow)

        monthly_gains = (current_value + monthly_deposit / 2) * monthly_return_rate
        current_value += monthly_deposit + monthly_gains

        net_deposits_this_year = deposits_this_year
        if monthly_payment > 0:
            months_this_year = ((month - 1) % 12) + 1
            net_deposits_this_year -= months_this_year * BROKER_FEE_PER_TRANSACTION

        current_year_gains = current_value - value_at_year_start - net_deposits_this_year
        potential_taxable_base_for_chart = accumulated_final_tax_base + max(0, current_year_gains)
        potential_final_tax_for_chart = potential_taxable_base_for_chart * CAPITAL_GAINS_TAX_RATE
        value_for_chart = current_value - potential_final_tax_for_chart

        add_chart_data_point(chart_data, month, value_for_chart, duration * 12)

        if month % 12 == 0:
            yearly_deposits = monthly_payment * 12
            net_yearly_deposits = yearly_deposits - (12 * BROKER_FEE_PER_TRANSACTION) if yearly_deposits > 0 else 0
            annual_gross_gains = current_value - value_at_year_start - net_yearly_deposits

            ddi_return_rate = net_annual_return * DEEMED_DISTRIBUTABLE_INCOME_SHARE
            ddi_amount = value_at_year_start * ddi_return_rate
            annual_tax_on_ddi = max(0, ddi_amount * DDI_TAXED_ANNUALLY_SHARE) * CAPITAL_GAINS_TAX_RATE

            total_annual_ddi_tax += annual_tax_on_ddi
            current_value -= annual_tax_on_ddi

            ddi_base_for_final_tax = max(0, ddi_amount * (1 - DDI_TAXED_ANNUALLY_SHARE))
            capital_gains_base = max(0, annual_gross_gains - ddi_amount)
            accumulated_final_tax_base += ddi_base_for_final_tax + capital_gains_base

            value_at_year_start = current_value
            deposits_this_year = 0.0

    final_capital_gains_tax = accumulated_final_tax_base * CAPITAL_GAINS_TAX_RATE
    final_net_value = current_value - final_capital_gains_tax
    total_taxes = total_annual_ddi_tax + final_capital_gains_tax
    total_deposits = start_capital + (monthly_payment * duration * 12)

    net_profit = round(final_net_value - total_deposits, 2)

    # IRR computation
    # Terminal value as NEW entry so N discounting periods match N compounding periods
    cashflows_irr.append(final_net_value)

    irr = 0.0
    effective_annual_interest = 0.0
    try:
        irr = calculate_irr(cashflows_irr)
        effective_annual_interest = ((1 + irr) ** 12) - 1
    except Exception:
        effective_annual_interest = 0.0

    # Baseline = gross market return (before TER).
    # TER is already embedded in simulation (reduces fund value monthly),
    # so EAI already reflects TER drag. Using grossReturn gives:
    # RIY = allCosts(TER + broker + KeSt) = gross - EAI.
    perf_gross_etf = gross_annual_return
    riy_full = perf_gross_etf - effective_annual_interest

    # ── Produkt-RiY: Re-Simulation OHNE Fondskosten (TER) ─────────────────
    # Isoliert den TER-Anteil an der Zinsminderung. Der ausgewiesene
    # Depot-RiY = Ordergebühren + KESt (TER separat in der Attribution).
    irr_no_etf = irr
    try:
        cv = start_capital
        acc_final = 0.0
        vy_start = start_capital
        net_ann_ret_no_etf = gross_annual_return  # kein TER-Abzug
        m_rate = (1 + net_ann_ret_no_etf) ** (1.0 / 12.0) - 1
        cf_no_etf: List[float] = []

        for m in range(1, duration * 12 + 1):
            m_dep = 0.0
            m_out = 0.0
            if monthly_payment > 0:
                m_dep = monthly_payment - BROKER_FEE_PER_TRANSACTION
                m_out = monthly_payment
            if m == 1 and start_capital > 0:
                m_out += start_capital
            cf_no_etf.append(-m_out)
            cv += m_dep + (cv + m_dep / 2) * m_rate
            if m % 12 == 0:
                net_y_dep = monthly_payment * 12 - 12 * BROKER_FEE_PER_TRANSACTION if monthly_payment > 0 else 0.0
                ann_g = cv - vy_start - net_y_dep
                ddi_a = vy_start * (net_ann_ret_no_etf * DEEMED_DISTRIBUTABLE_INCOME_SHARE)
                ann_tax = max(0.0, ddi_a * DDI_TAXED_ANNUALLY_SHARE) * CAPITAL_GAINS_TAX_RATE
                cv -= ann_tax
                acc_final += max(0.0, ddi_a * (1 - DDI_TAXED_ANNUALLY_SHARE)) + max(0.0, ann_g - ddi_a)
                vy_start = cv
        cf_no_etf.append(cv - acc_final * CAPITAL_GAINS_TAX_RATE)
        irr_no_etf = calculate_irr(cf_no_etf)
    except Exception:
        irr_no_etf = irr

    eai_no_etf = (1 + irr_no_etf) ** 12 - 1
    riy_etf_depot = eai_no_etf - effective_annual_interest  # TER-Anteil
    riy_product = riy_full - riy_etf_depot                  # Depot + KESt (ohne TER)

    # ── RiY-Attribution: Ordergebühr isolieren ────────────────────────────
    # Re-Simulation ohne die 1,50-€-Ordergebühr, aber MIT TER (wie Hauptlauf).
    # riy_depot = EAI(ohne Gebühr) - EAI > 0; riy_tax = Residual (KESt).
    riy_depot_attrib = 0.0
    if monthly_payment > 0:
        try:
            cv = start_capital
            acc_final = 0.0
            vy_start = start_capital
            m_rate = monthly_return_rate  # inkl. TER, wie Hauptlauf
            cf_no_fee: List[float] = []

            for m in range(1, duration * 12 + 1):
                m_dep = monthly_payment  # volle Rate, keine Gebühr
                m_out = monthly_payment
                if m == 1 and start_capital > 0:
                    m_out += start_capital
                cf_no_fee.append(-m_out)
                cv += m_dep + (cv + m_dep / 2) * m_rate
                if m % 12 == 0:
                    net_y_dep = monthly_payment * 12  # keine Gebühr abgezogen
                    ann_g = cv - vy_start - net_y_dep
                    ddi_a = vy_start * (net_annual_return * DEEMED_DISTRIBUTABLE_INCOME_SHARE)
                    ann_tax = max(0.0, ddi_a * DDI_TAXED_ANNUALLY_SHARE) * CAPITAL_GAINS_TAX_RATE
                    cv -= ann_tax
                    acc_final += max(0.0, ddi_a * (1 - DDI_TAXED_ANNUALLY_SHARE)) + max(0.0, ann_g - ddi_a)
                    vy_start = cv
            cf_no_fee.append(cv - acc_final * CAPITAL_GAINS_TAX_RATE)
            irr_no_fee = calculate_irr(cf_no_fee)
            riy_depot_attrib = ((1 + irr_no_fee) ** 12 - 1) - effective_annual_interest
        except Exception:
            riy_depot_attrib = 0.0

    riy_tax_attrib = riy_product - riy_depot_attrib  # KESt-Residual

    return BenchmarkResult(
        final_value=round(max(0, final_net_value), 2),
        net_profit=net_profit,
        costs=CostSummary(
            broker_fees=round(total_broker_fees, 2),
            etf_costs=round(total_ter_costs_absolute, 2),
            taxes=round(total_taxes, 2),
            total=round(total_broker_fees + total_ter_costs_absolute + total_taxes, 2)
        ),
        chart_data=chart_data,
        irr=round(irr, 6),
        effective_annual_interest=round(effective_annual_interest, 6),
        perf_gross_etf=round(perf_gross_etf, 6),
        riy=round(riy_product, 6),  # Depot + KESt (ohne TER)
        riy_attribution=BrokerageRiyAttribution(
            total=round(riy_full, 6),
            etf=round(riy_etf_depot, 6),
            depot=round(riy_depot_attrib, 6),
            tax=round(riy_tax_attrib, 6),
        ),
    )

def calculate_insurance_wrapper_strategy(
    start_capital: float,
    monthly_payment: float,
    duration_years: int,
    age: int,
    gross_annual_return: float,
    ter: float,
    product: Product
) -> PerformanceResult:
    duration_months = duration_years * 12
    tax_rate = product.tax_rate / 100.0 if product.tax_rate else 0.0

    ongoing_rate_net = monthly_payment / (1 + tax_rate)
    start_capital_net = start_capital / (1 + tax_rate)

    annual_return_net = (1 + gross_annual_return) ** (1.0 / 12.0) - 1
    ter_decimal = ter / 100.0
    fund_cost_monthly_rate = (1 + ter_decimal) ** (1.0 / 12.0) - 1

    total_alpha_costs = 0.0
    total_beta_gamma_costs = 0.0
    total_risk_costs = 0.0
    total_etf_costs_abs = 0.0
    total_insurance_tax = 0.0
    account_value = 0.0
    cum_premiums_gross = 0.0

    # FV-Impact-Akkumulatoren: Jeder Kostenabzug C im Monat t reduziert den
    # Endwert um C × (1+r)^(Restmonate). Diese aufgezinsten Wirkungen werden
    # je Kostenschicht gesammelt und dienen der proportionalen RiY-Zerlegung.
    fv_impact_alpha = 0.0
    fv_impact_beta = 0.0
    fv_impact_risk = 0.0
    fv_impact_tax = 0.0
    fv_impact_etf = 0.0

    # Geplante Brutto-Prämiensumme (Basis für FixedPctOfPremiumSum)
    planned_premium_sum = (monthly_payment * duration_months) + start_capital

    cashflows_irr: List[float] = []
    chart_data: List[ChartDataPoint] = []

    # --- Alpha (closing commission spread over alpha_period) ---
    alpha_monthly = 0.0
    alpha_period = product.commission_period or 0
    closing_comm_rate = product.closing_commission or 0.0

    if closing_comm_rate > 0 and alpha_period > 0:
        if monthly_payment > 0:
            cap_months = product.max_basis_calculation or 0
            calc_months = duration_months
            if cap_months > 0:
                calc_months = min(duration_months, cap_months)
            nbs = ongoing_rate_net * calc_months
        else:
            # OneOff: NBS = net lump sum
            nbs = start_capital_net
        alpha_monthly = (nbs * (closing_comm_rate / 100.0)) / alpha_period

    # --- AdminCostSumOfPaymentsNet (provider-specific) ---
    special_rate = product.admin_cost_sum_of_payments_net or 0.0
    special_sum_cost_monthly = 0.0
    combined_admin_formula = False
    prov_id = product.provider_id or 0

    if (product.min_fixed_annual_admin_cost is not None
            and product.max_fixed_annual_admin_cost is not None):
        # Kombinierte Admin-Kostenformel (einige Anbieter):
        # Formel: X% × Jahresnettoprämie + Y% × Nettoprämiensumme
        # mit MIN/MAX-Korridor laut AVB
        nbs_total = ongoing_rate_net * duration_months
        if start_capital > 0:
            nbs_total += start_capital_net
        annual_net_premium = ongoing_rate_net * 12.0
        trans_rate = product.transaction_cost_payment or 0.0
        combined_annual = (annual_net_premium * trans_rate / 100.0) \
                        + (nbs_total * special_rate / 100.0)
        combined_annual = max(combined_annual, product.min_fixed_annual_admin_cost or 0.0)
        combined_annual = min(combined_annual, product.max_fixed_annual_admin_cost or 0.0)
        special_sum_cost_monthly = combined_annual / 12.0
        combined_admin_formula = True
    elif special_rate > 0:
        nbs_total = ongoing_rate_net * duration_months
        if start_capital > 0:
            nbs_total += start_capital_net
        # Einige Anbieter deckeln die NBS (z.B. €300.000 laut AVB) —
        # gilt nur bei laufender Prämie
        if prov_id == PROVIDER_NBS_CAP and monthly_payment > 0:
            nbs_total = min(nbs_total, 300_000.0)
        # Einige Anbieter rechnen den Satz pro 10 Jahre Vertragslaufzeit um
        effective_rate = special_rate
        if prov_id == PROVIDER_DURATION_SCALING and duration_years > 0 and monthly_payment > 0:
            effective_rate = special_rate * 10.0 / duration_years
        special_sum_cost_monthly = (nbs_total * (effective_rate / 100.0)) / 12.0

    # --- Fixed annual admin cost corridor ---
    fix_cost_annual_base = product.fixed_annual_admin_cost or 0.0
    if not combined_admin_formula:
        min_fix = product.min_fixed_annual_admin_cost or 0.0
        max_fix = product.max_fixed_annual_admin_cost or 0.0
        if min_fix > 0:
            fix_cost_annual_base = max(fix_cost_annual_base, min_fix)
        if max_fix > 0:
            fix_cost_annual_base = min(fix_cost_annual_base, max_fix)

    for t in range(1, duration_months + 1):
        current_month_gross = monthly_payment
        current_month_net_invest = ongoing_rate_net

        tax_this_month = monthly_payment - ongoing_rate_net
        if t == 1 and start_capital > 0:
            current_month_gross += start_capital
            current_month_net_invest += start_capital_net
            tax_this_month += (start_capital - start_capital_net)

        total_insurance_tax += tax_this_month
        # FV-Impact der Steuer: Steuerbetrag, aufgezinst über die Restmonate
        growth_factor = (1 + annual_return_net) ** (duration_months - t + 1)
        fv_impact_tax += tax_this_month * growth_factor

        cum_premiums_gross += current_month_gross
        cashflows_irr.append(-current_month_gross)
        account_value += current_month_net_invest

        costs_alpha = 0.0
        costs_beta_gamma = 0.0
        costs_risk = 0.0

        # ── Alpha: one-off transaction/closing costs on lump sum ──────────
        if t == 1 and start_capital > 0:
            one_off_alpha = 0.0
            if monthly_payment > 0:
                # OnGoing + Zuzahlung: AdditionalPaymentCost is separate fee on top-up
                add_cost_rate = product.additional_payment_cost or 0.0
                one_off_alpha = start_capital_net * (add_cost_rate / 100.0)
            else:
                CAP_1 = 25_000.0
                CAP_2 = 100_000.0
                total_capped = 0.0

                if start_capital_net >= CAP_1 and product.transaction_cost_cap_25k is not None:
                    capped_value = min(start_capital_net, CAP_2)
                    total_capped += capped_value * (product.transaction_cost_cap_25k / 100.0)

                if start_capital_net >= CAP_2 and product.transaction_cost_cap_100k is not None:
                    left_over = start_capital_net - CAP_2
                    total_capped += left_over * (product.transaction_cost_cap_100k / 100.0)

                if product.transaction_cost_cap_25k is None or start_capital_net < CAP_1:
                    tc_payment = product.transaction_cost_payment or 0.0
                    one_off_alpha = start_capital_net * (tc_payment / 100.0)

                one_off_alpha += total_capped
            costs_alpha += one_off_alpha

        # ── Alpha: recurring transaction cost on monthly payment ──────────
        # Bei kombinierten Admin-Formeln ist TransactionCostPayment bereits enthalten
        if monthly_payment > 0 and not combined_admin_formula:
            beta_rate = product.transaction_cost_payment or 0.0
            if beta_rate > 0:
                costs_alpha += ongoing_rate_net * (beta_rate / 100.0)

        # ── Alpha: spread over alpha period ───────────────────────────────
        if t <= alpha_period:
            costs_alpha += alpha_monthly

        # ── Gamma: fixed annual admin cost (with corridor already applied) ─
        if fix_cost_annual_base > 0:
            costs_beta_gamma += fix_cost_annual_base / 12.0

        # ── Gamma: % of depot value (threshold logic) ─────────────────────
        gamma_rate = product.percentage_depot_value_admin_cost or 0.0
        threshold_val = product.threshold_value or 0.0
        threshold_year = product.threshold_year or 0
        current_year = (t - 1) // 12 + 1

        # Threshold on account value
        if threshold_val > 0 and account_value >= threshold_val:
            better_rate = product.threshold_value_admin_cost or 0.0
            if better_rate > 0:
                gamma_rate = better_rate
        # Threshold on contract year
        if threshold_year > 0 and current_year >= threshold_year:
            better_year_rate = product.threshold_year_admin_cost or 0.0
            if better_year_rate > 0:
                gamma_rate = better_year_rate

        # NOTE: Gamma depot-% cost is deferred to AFTER return (see below)

        # ── Gamma: AdminCostSumOfPaymentsNet ──────────────────────────────
        if special_rate > 0:
            costs_beta_gamma += special_sum_cost_monthly

        # ── Gamma: AdminCostOfYearlyPayments ──────────────────────────────
        if monthly_payment > 0:
            yearly_pay_rate = product.admin_cost_of_yearly_payments or 0.0
            if yearly_pay_rate > 0:
                costs_beta_gamma += (ongoing_rate_net * 12) * ((yearly_pay_rate / 100.0) / 12.0)

        # ── Risk costs (death benefit, mortality-table based) ─────────────
        current_age = age + (t - 1) // 12  # Alter zu Beginn dieses Monats
        costs_risk += compute_monthly_risk_cost(
            product,
            current_age,
            account_value,
            cum_premiums_gross,
            monthly_payment,
            planned_premium_sum,
            t,
            duration_months,
        )

        # ── Cost deduction order ──────────────────────────────────────────
        # 1. Deduct alpha + beta/gamma (pre-return) + risk costs
        total_alpha_costs += costs_alpha
        total_risk_costs += costs_risk

        costs_before_return = costs_alpha + costs_beta_gamma + costs_risk
        account_value -= costs_before_return

        # FV-Impact der Vor-Rendite-Kosten (aufgezinst über Restmonate)
        growth_remaining = (1 + annual_return_net) ** (duration_months - t)
        fv_impact_alpha += costs_alpha * growth_remaining
        fv_impact_beta += costs_beta_gamma * growth_remaining
        fv_impact_risk += costs_risk * growth_remaining

        # ETF cost: tracked for cost display but NOT deducted from fund.
        # Per LV-InfoV 2018, the gross return input is treated as net-of-TER.
        etf_cost_this_month = account_value * fund_cost_monthly_rate
        total_etf_costs_abs += etf_cost_this_month
        fv_impact_etf += etf_cost_this_month * growth_remaining
        # NOTE: No account_value -= etf_cost_this_month (already in return)

        # 2. Apply monthly return
        account_value *= (1 + annual_return_net)

        # 3. Deduct depot admin cost AFTER return (computed on post-return fund value)
        # Contract: "fondsabhängige Verwaltungskosten aus dem veranlagten Vermögen"
        depot_admin_cost = 0.0
        if gamma_rate > 0:
            depot_admin_cost = account_value * ((gamma_rate / 100.0) / 12.0)
        account_value -= depot_admin_cost
        total_beta_gamma_costs += costs_beta_gamma + depot_admin_cost
        # FV-Impact des Nach-Rendite-Abzugs (aufgezinst über die vollen Restmonate)
        fv_impact_beta += depot_admin_cost * (1 + annual_return_net) ** (duration_months - t)

        add_chart_data_point(chart_data, t, account_value, duration_months)

    # ── Duration penalty tax (§ 27 EStG) ──────────────────────────────────
    min_duration_tax_free = 10 if age >= 50 else 15
    duration_penalty_tax = 0.0
    total_deposits = start_capital + (monthly_payment * duration_years * 12)

    if duration_years < min_duration_tax_free:
        duration_penalty_tax = total_deposits * 0.07

    final_value_after_tax = account_value - duration_penalty_tax
    total_product_costs = total_alpha_costs + total_beta_gamma_costs + total_risk_costs
    total_costs_all = total_product_costs + total_etf_costs_abs + total_insurance_tax + duration_penalty_tax

    # ── IRR computation ───────────────────────────────────────────────────
    # Terminal value as NEW entry so N discounting periods match N compounding periods
    cashflows_irr.append(final_value_after_tax)

    irr = 0.0
    effective_annual_interest = 0.0
    try:
        irr = calculate_irr(cashflows_irr)
        effective_annual_interest = ((1 + irr) ** 12) - 1
    except Exception:
        effective_annual_interest = 0.0

    # Baseline: gross market return + TER.
    # TER is NOT applied in the fund simulation (the 6%/8% input is treated as
    # net-of-TER per LV-InfoV convention). By adding TER to the baseline,
    # the displayed RIY correctly includes TER as a cost component.
    # This matches official Hochrechnungen: RIY = (gross + TER) - EAI.
    perf_gross_etf = gross_annual_return + ter_decimal
    riy_total = perf_gross_etf - effective_annual_interest

    # ── RiY-Attribution über direkte FV-Impact-Gewichtung ─────────────────
    # Statt 5 Re-Simulationen mit jeweils abgeschalteter Kostenschicht
    # (Interaktionseffekte, negative Risiko-Anteile) nutzen wir die
    # FV-Impact-Akkumulatoren aus dem Hauptlauf und teilen die
    # Gesamt-Zinsminderung proportional auf die Schichten auf.
    fv_impact_product = fv_impact_alpha + fv_impact_beta + fv_impact_risk
    fv_impact_all = fv_impact_product + fv_impact_tax + fv_impact_etf

    # ETF-RiY = TER direkt (LV-InfoV-Konvention: Rendite-Input ist netto-nach-TER)
    etf_riy = ter_decimal

    riy_alpha = 0.0
    riy_beta = 0.0
    riy_risk = 0.0
    riy_tax = 0.0
    if fv_impact_all > 0:
        # Produkt-RiY (ohne ETF) = Gesamt-RiY minus TER
        riy_product_and_tax = riy_total - etf_riy
        fv_impact_no_etf = fv_impact_product + fv_impact_tax
        if fv_impact_no_etf > 0:
            riy_alpha = riy_product_and_tax * (fv_impact_alpha / fv_impact_no_etf)
            riy_beta = riy_product_and_tax * (fv_impact_beta / fv_impact_no_etf)
            riy_risk = riy_product_and_tax * (fv_impact_risk / fv_impact_no_etf)
            riy_tax = riy_product_and_tax * (fv_impact_tax / fv_impact_no_etf)

    # ETF-Kosten (TER) am Laufzeitende zusätzlich vom Endergebnis abziehen
    # (LV-InfoV-Konvention: nicht im Zinseszins, aber im ausgewiesenen Endwert).
    final_value_after_tax -= total_etf_costs_abs

    riy_attribution = RiyAttribution(
        total=round(riy_total, 6),
        alpha=round(riy_alpha, 6),
        beta_gamma=round(riy_beta, 6),
        risk=round(riy_risk, 6),
        etf=round(etf_riy, 6),
        tax=round(riy_tax, 6),
    )

    return PerformanceResult(
        chart_data=chart_data,
        final_value=round(max(0, final_value_after_tax), 2),
        net_profit=round(final_value_after_tax - total_deposits, 2),
        costs=InsuranceCostSummary(
            product_costs=round(total_product_costs, 2),
            alpha_costs=round(total_alpha_costs, 2),
            beta_gamma_costs=round(total_beta_gamma_costs, 2),
            risk_costs=round(total_risk_costs, 2),
            etf_costs=round(total_etf_costs_abs, 2),
            insurance_tax=round(total_insurance_tax, 2),
            duration_penalty_tax=round(duration_penalty_tax, 2),
            total=round(total_costs_all, 2)
        ),
        irr=round(irr, 6),
        effective_annual_interest=round(effective_annual_interest, 6),
        perf_gross_etf=round(perf_gross_etf, 6),
        # riy = NUR Produktkosten (TER separat in riy_attribution.etf ausgewiesen)
        riy=round(riy_attribution.alpha + riy_attribution.beta_gamma
                  + riy_attribution.risk + riy_attribution.tax, 6),
        riy_attribution=riy_attribution,
    )

if __name__ == "__main__":
    # ══════════════════════════════════════════════════════════════════════
    # BEISPIELBERECHNUNG
    # ══════════════════════════════════════════════════════════════════════
    # Dieses Skript zeigt die mathematische Logik des Vergleichsrechners.
    # Die tatsächlichen Produktparameter stammen aus unserer Datenbank und
    # sind auf den jeweiligen Produktseiten unter https://nettopolizze.at
    # einsehbar.
    #
    # RISIKOKOSTEN-HINWEIS:
    # Die Risikokosten hängen vom Alter der versicherten Person ab: Die
    # monatliche Belastung ergibt sich aus dem Risikokapital (abhängig vom
    # Todesfallleistungs-Typ) multipliziert mit der altersabhängigen
    # Sterbewahrscheinlichkeit qx aus der amtlichen Sterbetafel (z.B.
    # ÖST 2010/12 Unisex der Statistik Austria), ggf. mit tarifspezifischem
    # Skalierungsfaktor. Junge Personen zahlen dadurch deutlich weniger als
    # ältere. Die Rechenstruktur hier entspricht der Produktion; die
    # EXAMPLE_MORTALITY_TABLE ist ein gerundeter, interpolierter
    # Beispielauszug — produktiv kommen die vollständigen amtlichen
    # Tabellenwerte je Tarif aus der Datenbank.
    #
    # Genaue Informationen zu den Risikokosten jedes Produkts finden
    # Sie auf den jeweiligen Produktseiten auf https://nettopolizze.at
    # ══════════════════════════════════════════════════════════════════════

    # ── Eingabeparameter (hier anpassen) ──────────────────────────────────
    START_CAPITAL = 0.0            # Einmalerlag in EUR (0 = nur laufend)
    MONTHLY_PAYMENT = 300.0        # Monatliche Sparrate in EUR
    DURATION_YEARS = 25            # Laufzeit in Jahren
    AGE = 30                       # Alter bei Vertragsbeginn
    GROSS_ANNUAL_RETURN = 0.06     # Erwartete Bruttorendite p.a. (6%)
    TER = 0.22                     # Gewichtete ETF-Gesamtkostenquote (%)

    # ── Beispiel-Produktparameter (generisch) ─────────────────────────────
    # Diese Werte sind illustrativ. Die echten Werte variieren je Anbieter
    # und sind auf https://nettopolizze.at/tarife einsehbar.
    example_product = Product(
        tax_rate=4.0,                              # Versicherungssteuer (%)
        closing_commission=2.0,                    # Abschlusskosten Produkt (%)
        commission_period=60,                      # Verteilung über X Monate
        transaction_cost_payment=1.0,              # Transaktionskosten je Prämie (%)
        percentage_depot_value_admin_cost=0.30,    # Verwaltungskosten auf Depotwert (% p.a.)
        fixed_annual_admin_cost=24.0,              # Fixe jährliche Verwaltungskosten (EUR)

        # Risikokosten: altersabhängig über die Sterbetafel (siehe oben).
        # risk_cost_ongoing_annual bleibt als Fallback-Flat-Rate und wirkt
        # nur, wenn KEINE Sterbetafel gesetzt ist.
        death_benefit_type="SurplusOnFV",          # Todesfallleistung: 105% des Fondswerts
        mortality_table=EXAMPLE_MORTALITY_TABLE,
        risk_cost_ongoing_annual=0.05,
    )

    # ── Berechnung ────────────────────────────────────────────────────────
    print("=" * 70)
    print("  NETTOPOLIZZE VERGLEICHSRECHNER — Pseudocode-Demonstration")
    print("  Methodik: https://nettopolizze.at/methodik")
    print("=" * 70)

    res_broker = calculate_brokerage_account_strategy(
        start_capital=START_CAPITAL,
        monthly_payment=MONTHLY_PAYMENT,
        duration=DURATION_YEARS,
        gross_annual_return=GROSS_ANNUAL_RETURN,
        ter=TER
    )

    res_ins = calculate_insurance_wrapper_strategy(
        start_capital=START_CAPITAL,
        monthly_payment=MONTHLY_PAYMENT,
        duration_years=DURATION_YEARS,
        age=AGE,
        gross_annual_return=GROSS_ANNUAL_RETURN,
        ter=TER,
        product=example_product
    )

    total_deposits = START_CAPITAL + (MONTHLY_PAYMENT * DURATION_YEARS * 12)

    print(f"\n  Eingabe:")
    print(f"    Startkapital:     {START_CAPITAL:>10,.0f} EUR")
    print(f"    Monatlich:        {MONTHLY_PAYMENT:>10,.0f} EUR")
    print(f"    Laufzeit:         {DURATION_YEARS:>10} Jahre")
    print(f"    Alter:            {AGE:>10} Jahre")
    print(f"    Bruttorendite:    {GROSS_ANNUAL_RETURN*100:>9.1f}%")
    print(f"    ETF TER:          {TER:>9.2f}%")
    print(f"    Einzahlungen:     {total_deposits:>10,.0f} EUR")

    print(f"\n  {'─' * 66}")
    print(f"  {'':30} {'Depot':>16}   {'Nettopolizze':>16}")
    print(f"  {'─' * 66}")
    print(f"  {'Endwert':30} {res_broker.final_value:>14,.2f}€   {res_ins.final_value:>14,.2f}€")
    print(f"  {'Nettogewinn':30} {res_broker.net_profit:>14,.2f}€   {res_ins.net_profit:>14,.2f}€")
    print(f"  {'Effektiver Jahreszins (EAI)':30} {res_broker.effective_annual_interest*100:>13.3f}%   {res_ins.effective_annual_interest*100:>13.3f}%")
    print(f"  {'RiY gesamt':30} {res_broker.riy_attribution.total*100:>13.3f}%   {res_ins.riy_attribution.total*100:>13.3f}%")
    print(f"  {'RiY Produkt (ohne TER)':30} {res_broker.riy*100:>13.3f}%   {res_ins.riy*100:>13.3f}%")

    diff = res_ins.final_value - res_broker.final_value
    print(f"\n  {'─' * 66}")
    print(f"  Vorteil Nettopolizze: {diff:>+,.2f} EUR")
    print(f"  {'─' * 66}")

    print(f"\n  RiY-Zerlegung Depot (fünf Schichten-Logik, hier drei Anteile):")
    ba = res_broker.riy_attribution
    print(f"    TER (ETF):        {ba.etf*100:>9.3f}%")
    print(f"    Ordergebühren:    {ba.depot*100:>9.3f}%")
    print(f"    KESt:             {ba.tax*100:>9.3f}%")
    print(f"    Gesamt:           {ba.total*100:>9.3f}%")

    print(f"\n  RiY-Zerlegung Nettopolizze (fünf Kostenschichten):")
    ia = res_ins.riy_attribution
    print(f"    Abschluss/Transaktion (Alpha): {ia.alpha*100:>9.3f}%")
    print(f"    Verwaltung (Beta/Gamma):       {ia.beta_gamma*100:>9.3f}%")
    print(f"    Risikokosten:                  {ia.risk*100:>9.3f}%")
    print(f"    Versicherungssteuer:           {ia.tax*100:>9.3f}%")
    print(f"    Fondskosten (TER):             {ia.etf*100:>9.3f}%")
    print(f"    Gesamt:                        {ia.total*100:>9.3f}%")

    print(f"\n  Kostenübersicht Depot:")
    print(f"    Broker-Gebühren:  {res_broker.costs.broker_fees:>10,.2f} EUR")
    print(f"    ETF-Kosten:       {res_broker.costs.etf_costs:>10,.2f} EUR")
    print(f"    Steuern (KESt):   {res_broker.costs.taxes:>10,.2f} EUR")
    print(f"    Gesamt:           {res_broker.costs.total:>10,.2f} EUR")

    print(f"\n  Kostenübersicht Nettopolizze:")
    print(f"    Produktkosten:    {res_ins.costs.product_costs:>10,.2f} EUR")
    print(f"      davon Abschluss/Transaktion: {res_ins.costs.alpha_costs:>10,.2f} EUR")
    print(f"      davon Verwaltung:            {res_ins.costs.beta_gamma_costs:>10,.2f} EUR")
    print(f"      davon Risikokosten:          {res_ins.costs.risk_costs:>10,.2f} EUR")
    print(f"    ETF-Kosten:       {res_ins.costs.etf_costs:>10,.2f} EUR")
    print(f"    Versich.-Steuer:  {res_ins.costs.insurance_tax:>10,.2f} EUR")
    print(f"    Strafsteuer:      {res_ins.costs.duration_penalty_tax:>10,.2f} EUR")
    print(f"    Gesamt:           {res_ins.costs.total:>10,.2f} EUR")

    print(f"\n{'=' * 70}")
    print(f"  Alle Produktdetails & Tarife: https://nettopolizze.at/tarife")
    print(f"{'=' * 70}")
