//! Finanzmathematische Funktionen des Vorbilds (`FV#`, `PV#`, `Pmt#`,
//! `IPmt#`, `PPmt#`, `NPer#`, `Rate#`, `NPV#`, `IRR#`, `MIRR#`, `SLN#`,
//! `SYD#`, `DDB#`).
//!
//! Vorzeichenkonvention wie beim Vorbild: ausgehende Zahlungen sind negativ,
//! eingehende positiv. Das Fälligkeitsargument (`faellig`) ist 0 für
//! nachschüssige und 1 für vorschüssige Zahlung. Gerechnet wird in DOUBLE.
//!
//! `Rate#` und `IRR#` nähern sich iterativ; die Abbruchbedingung steht in
//! [`ITERATIONEN`] und [`TOLERANZ`] und ist in docs/bibliothek.md
//! dokumentiert.

use crate::errors::RuntimeError;

/// Höchstzahl der Näherungsschritte von `Rate#` und `IRR#`.
pub const ITERATIONEN: usize = 100;
/// Abbruchschranke der Näherung (Betrag der Restabweichung).
pub const TOLERANZ: f64 = 1e-9;

type R = Result<f64, RuntimeError>;

fn ungueltig() -> RuntimeError {
    RuntimeError::ILLEGAL_FUNCTION_CALL
}

/// `(1 + zins)^perioden`.
fn faktor(zins: f64, perioden: f64) -> f64 {
    (1.0 + zins).powf(perioden)
}

/// Barwertfaktor der Rentenzahlungen.
fn rentenfaktor(zins: f64, perioden: f64, faellig: f64) -> f64 {
    if zins == 0.0 {
        perioden
    } else {
        (1.0 + zins * faellig) * (faktor(zins, perioden) - 1.0) / zins
    }
}

/// `FV#` — Endwert.
pub fn fv(zins: f64, perioden: f64, zahlung: f64, barwert: f64, faellig: f64) -> R {
    if zins <= -1.0 {
        return Err(ungueltig());
    }
    Ok(-(barwert * faktor(zins, perioden) + zahlung * rentenfaktor(zins, perioden, faellig)))
}

/// `PV#` — Barwert.
pub fn pv(zins: f64, perioden: f64, zahlung: f64, endwert: f64, faellig: f64) -> R {
    if zins <= -1.0 {
        return Err(ungueltig());
    }
    Ok(-(endwert + zahlung * rentenfaktor(zins, perioden, faellig)) / faktor(zins, perioden))
}

/// `Pmt#` — Zahlung je Periode.
pub fn pmt(zins: f64, perioden: f64, barwert: f64, endwert: f64, faellig: f64) -> R {
    if perioden == 0.0 || zins <= -1.0 {
        return Err(ungueltig());
    }
    let rf = rentenfaktor(zins, perioden, faellig);
    if rf == 0.0 {
        return Err(ungueltig());
    }
    Ok(-(endwert + barwert * faktor(zins, perioden)) / rf)
}

/// `NPer#` — Anzahl Perioden.
pub fn nper(zins: f64, zahlung: f64, barwert: f64, endwert: f64, faellig: f64) -> R {
    if zins <= -1.0 {
        return Err(ungueltig());
    }
    if zins == 0.0 {
        if zahlung == 0.0 {
            return Err(ungueltig());
        }
        return Ok(-(barwert + endwert) / zahlung);
    }
    let z = zahlung * (1.0 + zins * faellig);
    let zaehler = z - endwert * zins;
    let nenner = barwert * zins + z;
    if nenner == 0.0 || zaehler / nenner <= 0.0 {
        return Err(ungueltig());
    }
    Ok((zaehler / nenner).ln() / (1.0 + zins).ln())
}

/// `IPmt#` — Zinsanteil der Periode `periode` (1-basiert).
pub fn ipmt(zins: f64, periode: f64, perioden: f64, barwert: f64, endwert: f64, faellig: f64) -> R {
    if periode < 1.0 || periode > perioden {
        return Err(ungueltig());
    }
    let zahlung = pmt(zins, perioden, barwert, endwert, faellig)?;
    if faellig != 0.0 && periode == 1.0 {
        // Vorschüssig fällt in der ersten Periode kein Zins an.
        return Ok(0.0);
    }
    let vorher = if faellig != 0.0 {
        fv(zins, periode - 2.0, zahlung, barwert, faellig)?
    } else {
        fv(zins, periode - 1.0, zahlung, barwert, faellig)?
    };
    Ok(vorher * zins)
}

/// `PPmt#` — Tilgungsanteil der Periode.
pub fn ppmt(zins: f64, periode: f64, perioden: f64, barwert: f64, endwert: f64, faellig: f64) -> R {
    let zahlung = pmt(zins, perioden, barwert, endwert, faellig)?;
    let zinsanteil = ipmt(zins, periode, perioden, barwert, endwert, faellig)?;
    Ok(zahlung - zinsanteil)
}

/// `Rate#` — Zinssatz je Periode, iterativ genähert.
pub fn rate(
    perioden: f64,
    zahlung: f64,
    barwert: f64,
    endwert: f64,
    faellig: f64,
    schaetzung: f64,
) -> R {
    if perioden <= 0.0 {
        return Err(ungueltig());
    }
    // Sekantenverfahren auf f(r) = FV(r) − Sollendwert.
    let f = |r: f64| -> Option<f64> {
        if r <= -1.0 {
            return None;
        }
        Some(barwert * faktor(r, perioden) + zahlung * rentenfaktor(r, perioden, faellig) + endwert)
    };
    let mut r0 = schaetzung;
    let mut r1 = if schaetzung == 0.0 {
        0.1
    } else {
        schaetzung * 1.1
    };
    let (mut f0, mut f1) = match (f(r0), f(r1)) {
        (Some(a), Some(b)) => (a, b),
        _ => return Err(ungueltig()),
    };
    for _ in 0..ITERATIONEN {
        if f1.abs() < TOLERANZ {
            return Ok(r1);
        }
        if (f1 - f0).abs() < f64::MIN_POSITIVE {
            break;
        }
        let r2 = r1 - f1 * (r1 - r0) / (f1 - f0);
        let Some(f2) = f(r2) else { break };
        r0 = r1;
        f0 = f1;
        r1 = r2;
        f1 = f2;
    }
    if f1.abs() < TOLERANZ * 1e3 {
        Ok(r1)
    } else {
        Err(ungueltig())
    }
}

/// `NPV#` — Kapitalwert einer Zahlungsreihe (erste Zahlung am Ende der
/// ersten Periode).
pub fn npv(zins: f64, werte: &[f64]) -> R {
    if zins <= -1.0 || werte.is_empty() {
        return Err(ungueltig());
    }
    let mut summe = 0.0;
    let mut nenner = 1.0 + zins;
    for w in werte {
        summe += w / nenner;
        nenner *= 1.0 + zins;
    }
    Ok(summe)
}

/// `IRR#` — interner Zinsfuß, iterativ genähert.
pub fn irr(werte: &[f64], schaetzung: f64) -> R {
    // Ohne Vorzeichenwechsel gibt es keine Lösung.
    let hat_plus = werte.iter().any(|w| *w > 0.0);
    let hat_minus = werte.iter().any(|w| *w < 0.0);
    if !hat_plus || !hat_minus {
        return Err(ungueltig());
    }
    let f = |r: f64| -> Option<f64> {
        if r <= -1.0 {
            return None;
        }
        let mut summe = 0.0;
        let mut nenner = 1.0;
        for w in werte {
            summe += w / nenner;
            nenner *= 1.0 + r;
        }
        Some(summe)
    };
    let mut r0 = schaetzung;
    let mut r1 = if schaetzung == 0.0 {
        0.1
    } else {
        schaetzung * 1.1
    };
    let (mut f0, mut f1) = match (f(r0), f(r1)) {
        (Some(a), Some(b)) => (a, b),
        _ => return Err(ungueltig()),
    };
    for _ in 0..ITERATIONEN {
        if f1.abs() < TOLERANZ {
            return Ok(r1);
        }
        if (f1 - f0).abs() < f64::MIN_POSITIVE {
            break;
        }
        let r2 = r1 - f1 * (r1 - r0) / (f1 - f0);
        let Some(f2) = f(r2) else { break };
        r0 = r1;
        f0 = f1;
        r1 = r2;
        f1 = f2;
    }
    if f1.abs() < TOLERANZ * 1e3 {
        Ok(r1)
    } else {
        Err(ungueltig())
    }
}

/// `MIRR#` — modifizierter interner Zinsfuß.
pub fn mirr(werte: &[f64], finanzierungszins: f64, wiederanlagezins: f64) -> R {
    let n = werte.len();
    if n < 2 || finanzierungszins <= -1.0 || wiederanlagezins <= -1.0 {
        return Err(ungueltig());
    }
    let negative: Vec<f64> = werte.iter().map(|w| w.min(0.0)).collect();
    let positive: Vec<f64> = werte.iter().map(|w| w.max(0.0)).collect();
    let bw_neg = npv(finanzierungszins, &negative)?;
    let bw_pos = npv(wiederanlagezins, &positive)?;
    if bw_neg == 0.0 {
        return Err(ungueltig());
    }
    let n = n as f64;
    let verhaeltnis =
        -bw_pos * (1.0 + wiederanlagezins).powf(n) / (bw_neg * (1.0 + finanzierungszins));
    if verhaeltnis <= 0.0 {
        return Err(ungueltig());
    }
    Ok(verhaeltnis.powf(1.0 / (n - 1.0)) - 1.0)
}

/// `SLN#` — lineare Abschreibung.
pub fn sln(anschaffung: f64, restwert: f64, nutzungsdauer: f64) -> R {
    if nutzungsdauer == 0.0 {
        return Err(ungueltig());
    }
    Ok((anschaffung - restwert) / nutzungsdauer)
}

/// `SYD#` — arithmetisch-degressive Abschreibung.
pub fn syd(anschaffung: f64, restwert: f64, nutzungsdauer: f64, periode: f64) -> R {
    if nutzungsdauer <= 0.0 || periode < 1.0 || periode > nutzungsdauer {
        return Err(ungueltig());
    }
    Ok(
        (anschaffung - restwert) * (nutzungsdauer - periode + 1.0) * 2.0
            / (nutzungsdauer * (nutzungsdauer + 1.0)),
    )
}

/// `DDB#` — geometrisch-degressive Abschreibung (doppelter linearer Satz).
pub fn ddb(anschaffung: f64, restwert: f64, nutzungsdauer: f64, periode: f64) -> R {
    if nutzungsdauer <= 0.0 || periode < 1.0 || periode > nutzungsdauer {
        return Err(ungueltig());
    }
    let mut buchwert = anschaffung;
    let mut betrag = 0.0;
    let satz = 2.0 / nutzungsdauer;
    for _ in 0..(periode as u64) {
        betrag = (buchwert * satz).min(buchwert - restwert).max(0.0);
        buchwert -= betrag;
    }
    Ok(betrag)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn nah(a: f64, b: f64) {
        assert!((a - b).abs() < 1e-6, "{a} ≠ {b}");
    }

    #[test]
    fn endwert_einer_rentenzahlung() {
        // 10 Perioden, 5 % Zins, Zahlung 100 nachschüssig, Barwert 0.
        nah(fv(0.05, 10.0, -100.0, 0.0, 0.0).unwrap(), 1257.789253);
    }

    #[test]
    fn barwert_und_endwert_sind_gegenlaeufig() {
        let b = pv(0.05, 10.0, -100.0, 0.0, 0.0).unwrap();
        nah(fv(0.05, 10.0, -100.0, b, 0.0).unwrap(), 0.0);
    }

    #[test]
    fn zins_und_tilgung_ergeben_die_zahlung() {
        let (r, n, bw) = (0.05, 10.0, 1000.0);
        let z = pmt(r, n, bw, 0.0, 0.0).unwrap();
        for periode in 1..=10 {
            let i = ipmt(r, periode as f64, n, bw, 0.0, 0.0).unwrap();
            let t = ppmt(r, periode as f64, n, bw, 0.0, 0.0).unwrap();
            nah(i + t, z);
        }
    }

    #[test]
    fn zinssatz_wird_zurueckgerechnet() {
        let z = pmt(0.05, 10.0, 1000.0, 0.0, 0.0).unwrap();
        nah(rate(10.0, z, 1000.0, 0.0, 0.0, 0.1).unwrap(), 0.05);
    }

    #[test]
    fn nper_zaehlt_die_perioden() {
        let z = pmt(0.05, 10.0, 1000.0, 0.0, 0.0).unwrap();
        nah(nper(0.05, z, 1000.0, 0.0, 0.0).unwrap(), 10.0);
    }

    #[test]
    fn kapitalwert_und_interner_zinsfuss() {
        let reihe = [-1000.0, 400.0, 400.0, 400.0];
        let r = irr(&reihe, 0.1).unwrap();
        // Bei r ist der Kapitalwert (inkl. Anfangsauszahlung) null.
        let mut summe = 0.0;
        let mut nenner = 1.0;
        for w in reihe {
            summe += w / nenner;
            nenner *= 1.0 + r;
        }
        nah(summe, 0.0);
        nah(npv(0.05, &[100.0, 100.0]).unwrap(), 185.941043);
    }

    #[test]
    fn irr_ohne_vorzeichenwechsel_ist_fehler() {
        assert!(irr(&[100.0, 200.0], 0.1).is_err());
        assert!(irr(&[-100.0, -200.0], 0.1).is_err());
    }

    #[test]
    fn abschreibungen() {
        nah(sln(1000.0, 100.0, 10.0).unwrap(), 90.0);
        // SYD: Summe über alle Perioden = Abschreibungsvolumen.
        let summe: f64 = (1..=10)
            .map(|p| syd(1000.0, 100.0, 10.0, p as f64).unwrap())
            .sum();
        nah(summe, 900.0);
        nah(syd(1000.0, 100.0, 10.0, 1.0).unwrap(), 163.636363);
        nah(ddb(1000.0, 100.0, 10.0, 1.0).unwrap(), 200.0);
        nah(ddb(1000.0, 100.0, 10.0, 2.0).unwrap(), 160.0);
    }

    #[test]
    fn ungueltige_argumente() {
        assert!(sln(1.0, 0.0, 0.0).is_err());
        assert!(syd(1.0, 0.0, 10.0, 11.0).is_err());
        assert!(ipmt(0.05, 11.0, 10.0, 1000.0, 0.0, 0.0).is_err());
        assert!(pmt(0.05, 0.0, 1000.0, 0.0, 0.0).is_err());
        assert!(nper(0.0, 0.0, 1000.0, 0.0, 0.0).is_err());
    }

    #[test]
    fn mirr_liegt_zwischen_den_zinsen() {
        let r = mirr(&[-1000.0, 400.0, 400.0, 400.0], 0.05, 0.06).unwrap();
        assert!(r > 0.0 && r < 0.3, "{r}");
    }
}
