//! Copula Dependence Modeling for joint distribution analysis.
//!
//! Implements five copula families for modeling dependence structures
//! between prediction market outcomes:
//! - Gaussian (normal) copula
//! - Student-t copula (nu=4 fixed degrees of freedom)
//! - Clayton copula (lower tail dependence)
//! - Gumbel copula (upper tail dependence)
//! - Frank copula (symmetric, no tail dependence)
//!
//! Estimation via maximum likelihood with golden section search.
//! All math from scratch in Rust -- no external crate dependencies beyond PyO3.
//!
//! Pro tier required.

use pyo3::prelude::*;

// ===========================================================================
// Constants
// ===========================================================================

const PI: f64 = std::f64::consts::PI;
const SQRT_2: f64 = std::f64::consts::SQRT_2;
const LN_2PI: f64 = 1.8378770664093453; // ln(2*pi)
const GOLDEN_RATIO: f64 = 0.6180339887498949; // (sqrt(5)-1)/2
const STUDENT_T_NU: f64 = 4.0; // fixed degrees of freedom for Student-t

// ===========================================================================
// Helpers
// ===========================================================================

#[inline]
fn finite_or_zero(v: f64) -> f64 {
    if v.is_finite() { v } else { 0.0 }
}

/// Clamp to open interval (eps, 1-eps) to avoid log(0) etc.
#[inline]
fn clamp_unit(x: f64) -> f64 {
    x.clamp(1e-10, 1.0 - 1e-10)
}

// ===========================================================================
// Normal CDF and inverse (public for vine_copula.rs)
// ===========================================================================

/// Standard normal CDF using Abramowitz & Stegun approximation 26.2.17.
/// Max error ~7.5e-8.
pub fn normal_cdf(x: f64) -> f64 {
    if !x.is_finite() {
        if x > 0.0 { return 1.0; }
        return 0.0;
    }
    let a1 = 0.254829592;
    let a2 = -0.284496736;
    let a3 = 1.421413741;
    let a4 = -1.453152027;
    let a5 = 1.061405429;
    let p = 0.3275911;

    let sign = if x < 0.0 { -1.0 } else { 1.0 };
    let z = x.abs() / SQRT_2;
    let t = 1.0 / (1.0 + p * z);
    let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-z * z).exp();
    finite_or_zero(0.5 * (1.0 + sign * y))
}

/// Inverse standard normal CDF (Peter Acklam's algorithm).
/// Rational approximation, max relative error < 1.15e-9.
pub fn normal_cdf_inv(p: f64) -> f64 {
    if p <= 0.0 { return -8.0; }
    if p >= 1.0 { return 8.0; }

    // Coefficients for rational approximation
    const A: [f64; 6] = [
        -3.969683028665376e+01,
         2.209460984245205e+02,
        -2.759285104469687e+02,
         1.383577518672690e+02,
        -3.066479806614716e+01,
         2.506628277459239e+00,
    ];
    const B: [f64; 5] = [
        -5.447609879822406e+01,
         1.615858368580409e+02,
        -1.556989798598866e+02,
         6.680131188771972e+01,
        -1.328068155288572e+01,
    ];
    const C: [f64; 6] = [
        -7.784894002430293e-03,
        -3.223964580411365e-01,
        -2.400758277161838e+00,
        -2.549732539343734e+00,
         4.374664141464968e+00,
         2.938163982698783e+00,
    ];
    const D: [f64; 4] = [
         7.784695709041462e-03,
         3.224671290700398e-01,
         2.445134137142996e+00,
         3.754408661907416e+00,
    ];

    let p_low = 0.02425;
    let p_high = 1.0 - p_low;

    let result = if p < p_low {
        // Lower region
        let q = (-2.0 * p.ln()).sqrt();
        (((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
            / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
    } else if p <= p_high {
        // Central region
        let q = p - 0.5;
        let r = q * q;
        (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
            / (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
    } else {
        // Upper region
        let q = (-2.0 * (1.0 - p).ln()).sqrt();
        -(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
            / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
    };

    finite_or_zero(result)
}

/// Standard normal PDF.
#[inline]
#[allow(dead_code)]
fn normal_pdf(x: f64) -> f64 {
    finite_or_zero((-0.5 * x * x).exp() / (2.0 * PI).sqrt())
}

// ===========================================================================
// Student-t CDF (nu=4 via incomplete beta regularized)
// ===========================================================================

/// Incomplete beta function via continued fraction (Lentz's algorithm).
fn betacf(a: f64, b: f64, x: f64) -> f64 {
    let max_iter = 200;
    let eps = 3.0e-12;
    let tiny = 1.0e-30;

    let qab = a + b;
    let qap = a + 1.0;
    let qam = a - 1.0;
    let mut c = 1.0;
    let val0 = 1.0 - qab * x / qap;
    let mut d = if val0.abs() < tiny { tiny } else { val0 }.recip();
    let mut h = d;

    for m in 1..=max_iter {
        let m_f64 = m as f64;
        // Even step
        let aa = m_f64 * (b - m_f64) * x / ((qam + 2.0 * m_f64) * (a + 2.0 * m_f64));
        let vd = 1.0 + aa * d;
        d = if vd.abs() < tiny { tiny } else { vd }.recip();
        let vc = 1.0 + aa / c;
        c = if vc.abs() < tiny { tiny } else { vc };
        h *= d * c;
        // Odd step
        let aa = -(a + m_f64) * (qab + m_f64) * x / ((a + 2.0 * m_f64) * (qap + 2.0 * m_f64));
        let vd2 = 1.0 + aa * d;
        d = if vd2.abs() < tiny { tiny } else { vd2 }.recip();
        let vc2 = 1.0 + aa / c;
        c = if vc2.abs() < tiny { tiny } else { vc2 };
        let delta = d * c;
        h *= delta;
        if (delta - 1.0).abs() < eps {
            break;
        }
    }
    h
}

/// Log of the gamma function (Lanczos approximation).
fn lgamma(x: f64) -> f64 {
    let g = 7.0;
    let coeff = [
        0.99999999999980993,
        676.5203681218851,
        -1259.1392167224028,
        771.32342877765313,
        -176.61502916214059,
        12.507343278686905,
        -0.13857109526572012,
        9.9843695780195716e-6,
        1.5056327351493116e-7,
    ];
    if x < 0.5 {
        let z = 1.0 - x;
        (PI / (PI * z).sin()).ln() - lgamma(z)
    } else {
        let z = x - 1.0;
        let mut acc = coeff[0];
        for (i, &c) in coeff.iter().enumerate().skip(1) {
            acc += c / (z + i as f64);
        }
        let t = z + g + 0.5;
        0.5 * LN_2PI + (t.ln()) * (z + 0.5) - t + acc.ln()
    }
}

/// Log beta function.
#[allow(dead_code)]
fn lbeta(a: f64, b: f64) -> f64 {
    lgamma(a) + lgamma(b) - lgamma(a + b)
}

/// Regularized incomplete beta function I_x(a,b).
fn betai(a: f64, b: f64, x: f64) -> f64 {
    if x <= 0.0 { return 0.0; }
    if x >= 1.0 { return 1.0; }
    let bt = (lgamma(a + b) - lgamma(a) - lgamma(b) + a * x.ln() + b * (1.0 - x).ln()).exp();
    if x < (a + 1.0) / (a + b + 2.0) {
        bt * betacf(a, b, x) / a
    } else {
        1.0 - bt * betacf(b, a, 1.0 - x) / b
    }
}

/// Student-t CDF with nu degrees of freedom.
fn student_t_cdf(t: f64, nu: f64) -> f64 {
    let x = nu / (nu + t * t);
    let ib = betai(nu / 2.0, 0.5, x);
    if t >= 0.0 {
        1.0 - 0.5 * ib
    } else {
        0.5 * ib
    }
}

/// Inverse Student-t CDF via bisection.
fn student_t_cdf_inv(p: f64, nu: f64) -> f64 {
    if p <= 0.0 { return -30.0; }
    if p >= 1.0 { return 30.0; }
    let mut lo = -30.0_f64;
    let mut hi = 30.0_f64;
    for _ in 0..100 {
        let mid = 0.5 * (lo + hi);
        if student_t_cdf(mid, nu) < p {
            lo = mid;
        } else {
            hi = mid;
        }
    }
    0.5 * (lo + hi)
}

// ===========================================================================
// Xoshiro256++ PRNG (deterministic seeded)
// ===========================================================================

struct Xoshiro256 {
    s: [u64; 4],
}

impl Xoshiro256 {
    fn new(seed: u64) -> Self {
        // SplitMix64 to expand seed
        let mut z = seed;
        let mut s = [0u64; 4];
        for si in &mut s {
            z = z.wrapping_add(0x9e3779b97f4a7c15);
            let mut x = z;
            x = (x ^ (x >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
            x = (x ^ (x >> 27)).wrapping_mul(0x94d049bb133111eb);
            *si = x ^ (x >> 31);
        }
        Self { s }
    }

    fn next_u64(&mut self) -> u64 {
        let result = (self.s[0].wrapping_add(self.s[3]))
            .rotate_left(23)
            .wrapping_add(self.s[0]);
        let t = self.s[1] << 17;
        self.s[2] ^= self.s[0];
        self.s[3] ^= self.s[1];
        self.s[1] ^= self.s[2];
        self.s[0] ^= self.s[3];
        self.s[2] ^= t;
        self.s[3] = self.s[3].rotate_left(45);
        result
    }

    fn next_f64(&mut self) -> f64 {
        (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
    }

    /// Box-Muller: return pair of independent standard normals.
    fn next_normal_pair(&mut self) -> (f64, f64) {
        loop {
            let u1 = self.next_f64();
            let u2 = self.next_f64();
            if u1 > 1e-30 {
                let r = (-2.0 * u1.ln()).sqrt();
                let theta = 2.0 * PI * u2;
                return (r * theta.cos(), r * theta.sin());
            }
        }
    }

    fn next_normal(&mut self) -> f64 {
        self.next_normal_pair().0
    }
}

// ===========================================================================
// Copula family enum
// ===========================================================================

/// Copula family for bivariate dependence modeling.
#[pyclass(eq, eq_int, frozen, hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CopulaFamily {
    /// Gaussian (normal) copula
    Gaussian = 0,
    /// Student-t copula (nu=4 fixed)
    StudentT = 1,
    /// Clayton copula (lower tail dependence)
    Clayton = 2,
    /// Gumbel copula (upper tail dependence)
    Gumbel = 3,
    /// Frank copula (symmetric, no tail dependence)
    Frank = 4,
}

#[pymethods]
impl CopulaFamily {
    fn __repr__(&self) -> String {
        match self {
            CopulaFamily::Gaussian => "CopulaFamily.Gaussian".to_string(),
            CopulaFamily::StudentT => "CopulaFamily.StudentT".to_string(),
            CopulaFamily::Clayton => "CopulaFamily.Clayton".to_string(),
            CopulaFamily::Gumbel => "CopulaFamily.Gumbel".to_string(),
            CopulaFamily::Frank => "CopulaFamily.Frank".to_string(),
        }
    }
}

// ===========================================================================
// Frozen result type
// ===========================================================================

/// Result of copula fitting: family, estimated parameter, and goodness-of-fit.
#[pyclass(frozen)]
#[derive(Debug, Clone)]
pub struct CopulaFit {
    /// Selected copula family.
    #[pyo3(get)]
    pub family: CopulaFamily,
    /// Estimated dependence parameter (rho for Gaussian/t, theta for Archimedean).
    #[pyo3(get)]
    pub parameter: f64,
    /// Maximized log-likelihood.
    #[pyo3(get)]
    pub log_likelihood: f64,
    /// Akaike information criterion (lower is better).
    #[pyo3(get)]
    pub aic: f64,
    /// Kendall's tau implied by the fitted copula.
    #[pyo3(get)]
    pub kendall_tau: f64,
}

#[pymethods]
impl CopulaFit {
    fn __repr__(&self) -> String {
        format!(
            "CopulaFit(family={:?}, param={:.4}, ll={:.4}, aic={:.4}, tau={:.4})",
            self.family, self.parameter, self.log_likelihood, self.aic, self.kendall_tau
        )
    }
}

// ===========================================================================
// Copula CDF functions (public for vine_copula.rs)
// ===========================================================================

/// Gaussian copula CDF: C(u,v; rho) = Phi2(Phi^{-1}(u), Phi^{-1}(v); rho).
/// Bivariate normal CDF approximated via product formula when rho is moderate.
pub fn copula_cdf_gaussian(rho: f64, u: f64, v: f64) -> f64 {
    let x = normal_cdf_inv(clamp_unit(u));
    let y = normal_cdf_inv(clamp_unit(v));
    bvn_cdf(x, y, rho)
}

/// Clayton copula CDF: C(u,v; theta) = (u^{-theta} + v^{-theta} - 1)^{-1/theta}.
fn copula_cdf_clayton(theta: f64, u: f64, v: f64) -> f64 {
    let u = clamp_unit(u);
    let v = clamp_unit(v);
    if theta < 1e-10 {
        return u * v; // independence
    }
    let val = u.powf(-theta) + v.powf(-theta) - 1.0;
    if val <= 0.0 {
        return 0.0;
    }
    finite_or_zero(val.powf(-1.0 / theta).clamp(0.0, 1.0))
}

/// Gumbel copula CDF: C(u,v; theta) = exp(-((-ln u)^theta + (-ln v)^theta)^{1/theta}).
fn copula_cdf_gumbel(theta: f64, u: f64, v: f64) -> f64 {
    let u = clamp_unit(u);
    let v = clamp_unit(v);
    if theta <= 1.0 + 1e-10 {
        return u * v;
    }
    let lu = (-u.ln()).powf(theta);
    let lv = (-v.ln()).powf(theta);
    finite_or_zero((-(lu + lv).powf(1.0 / theta)).exp().clamp(0.0, 1.0))
}

/// Frank copula CDF: C(u,v; theta) = -1/theta * ln(1 + (e^{-theta*u}-1)(e^{-theta*v}-1)/(e^{-theta}-1)).
fn copula_cdf_frank(theta: f64, u: f64, v: f64) -> f64 {
    let u = clamp_unit(u);
    let v = clamp_unit(v);
    if theta.abs() < 1e-10 {
        return u * v;
    }
    let e_u = (-theta * u).exp() - 1.0;
    let e_v = (-theta * v).exp() - 1.0;
    let e_t = (-theta).exp() - 1.0;
    if e_t.abs() < 1e-30 {
        return u * v;
    }
    finite_or_zero((-1.0 / theta * (1.0 + e_u * e_v / e_t).ln()).clamp(0.0, 1.0))
}

/// Student-t copula CDF (bivariate t with rho and nu=STUDENT_T_NU).
/// Approximated via Gaussian copula CDF with correction.
fn copula_cdf_student_t(rho: f64, u: f64, v: f64) -> f64 {
    // Use t-quantiles instead of normal quantiles
    let x = student_t_cdf_inv(clamp_unit(u), STUDENT_T_NU);
    let y = student_t_cdf_inv(clamp_unit(v), STUDENT_T_NU);
    // Bivariate t CDF approximated by mapping through bivariate normal structure
    // then adjusting with t-marginals
    bvn_cdf(
        normal_cdf_inv(student_t_cdf(x, STUDENT_T_NU)),
        normal_cdf_inv(student_t_cdf(y, STUDENT_T_NU)),
        rho,
    )
}

// ===========================================================================
// Copula PDF functions (public for vine_copula.rs)
// ===========================================================================

/// Gaussian copula density: c(u,v; rho).
pub fn copula_pdf_gaussian(rho: f64, u: f64, v: f64) -> f64 {
    let x = normal_cdf_inv(clamp_unit(u));
    let y = normal_cdf_inv(clamp_unit(v));
    let rho2 = rho * rho;
    if rho2 >= 1.0 - 1e-12 {
        return 1e6; // degenerate
    }
    let denom = 1.0 - rho2;
    let exponent = -(rho2 * (x * x + y * y) - 2.0 * rho * x * y) / (2.0 * denom);
    finite_or_zero((exponent.exp() / denom.sqrt()).max(1e-30))
}

/// Clayton copula density.
fn copula_pdf_clayton(theta: f64, u: f64, v: f64) -> f64 {
    let u = clamp_unit(u);
    let v = clamp_unit(v);
    if theta < 1e-10 {
        return 1.0; // independence
    }
    let um = u.powf(-theta);
    let vm = v.powf(-theta);
    let s = um + vm - 1.0;
    if s <= 0.0 {
        return 1e-30;
    }
    let pdf = (1.0 + theta) * u.powf(-theta - 1.0) * v.powf(-theta - 1.0) * s.powf(-1.0 / theta - 2.0);
    finite_or_zero(pdf.max(1e-30))
}

/// Gumbel copula density.
fn copula_pdf_gumbel(theta: f64, u: f64, v: f64) -> f64 {
    let u = clamp_unit(u);
    let v = clamp_unit(v);
    if theta <= 1.0 + 1e-10 {
        return 1.0;
    }
    let lu = -u.ln();
    let lv = -v.ln();
    let lu_t = lu.powf(theta);
    let lv_t = lv.powf(theta);
    let s = lu_t + lv_t;
    let s_inv_t = s.powf(1.0 / theta);

    let cdf = (-s_inv_t).exp();
    // Derivative of Gumbel copula CDF
    let t1 = cdf / (u * v);
    let t2 = (lu * lv).powf(theta - 1.0);
    let t3 = s.powf(1.0 / theta - 2.0);
    let t4 = s_inv_t + theta - 1.0;
    finite_or_zero((t1 * t2 * t3 * t4).max(1e-30))
}

/// Frank copula density.
fn copula_pdf_frank(theta: f64, u: f64, v: f64) -> f64 {
    let u = clamp_unit(u);
    let v = clamp_unit(v);
    if theta.abs() < 1e-10 {
        return 1.0;
    }
    let e_t = (-theta).exp();
    let numer = -theta * e_t * (-theta * (u + v)).exp();
    let denom_base = e_t - (-theta * u).exp() - (-theta * v).exp() + (-theta * (u + v)).exp();
    if denom_base.abs() < 1e-30 {
        return 1e-30;
    }
    let denom = denom_base * denom_base;
    finite_or_zero((-numer / denom).abs().max(1e-30))
}

/// Student-t copula density.
fn copula_pdf_student_t(rho: f64, u: f64, v: f64) -> f64 {
    let nu = STUDENT_T_NU;
    let x = student_t_cdf_inv(clamp_unit(u), nu);
    let y = student_t_cdf_inv(clamp_unit(v), nu);
    let rho2 = rho * rho;
    if rho2 >= 1.0 - 1e-12 {
        return 1e6;
    }
    // Bivariate t density / product of marginal t densities
    let q = x * x + y * y - 2.0 * rho * x * y;
    let denom_rho = 1.0 - rho2;

    // log c(u,v) = log f_{2,nu}(x,y;rho) - log f_{1,nu}(x) - log f_{1,nu}(y)
    // Bivariate t: (nu+2)/2 terms
    let log_biv = lgamma((nu + 2.0) / 2.0) - lgamma(nu / 2.0) - (nu * PI).ln()
        - 0.5 * denom_rho.ln()
        - ((nu + 2.0) / 2.0) * (1.0 + q / (nu * denom_rho)).ln();

    // Marginal t: f(x; nu) = gamma((nu+1)/2)/(sqrt(nu*pi)*gamma(nu/2)) * (1+x^2/nu)^(-(nu+1)/2)
    let log_marg_x = lgamma((nu + 1.0) / 2.0) - lgamma(nu / 2.0)
        - 0.5 * (nu * PI).ln()
        - ((nu + 1.0) / 2.0) * (1.0 + x * x / nu).ln();
    let log_marg_y = lgamma((nu + 1.0) / 2.0) - lgamma(nu / 2.0)
        - 0.5 * (nu * PI).ln()
        - ((nu + 1.0) / 2.0) * (1.0 + y * y / nu).ln();

    let log_copula = log_biv - log_marg_x - log_marg_y;
    finite_or_zero(log_copula.exp().max(1e-30))
}

// ===========================================================================
// Bivariate normal CDF (Drezner-Wesolowsky 1990)
// ===========================================================================

fn bvn_cdf(x: f64, y: f64, rho: f64) -> f64 {
    if rho.abs() < 1e-12 {
        return normal_cdf(x) * normal_cdf(y);
    }
    if rho.abs() > 1.0 - 1e-12 {
        if rho > 0.0 {
            return normal_cdf(x.min(y));
        } else {
            return (normal_cdf(x) + normal_cdf(-y) - 1.0).max(0.0);
        }
    }
    // Gauss-Legendre quadrature approximation
    // Use Drezner's formula: BVN = Phi(x)*Phi(y) + integral
    bvn_drezner(x, y, rho)
}

/// Drezner-Wesolowsky bivariate normal CDF approximation.
fn bvn_drezner(dh: f64, dk: f64, r: f64) -> f64 {
    // Gauss-Legendre 5-point weights and abscissae for [0,1]
    let w = [0.0469100770, 0.2307653449, 0.5000000000, 0.7692346551, 0.9530899230];
    let x_gl = [0.0181191564, 0.0668228378, 0.1065853389, 0.0668228378, 0.0181191564];

    let h = -dh;
    let k = -dk;
    let hk = h * k;

    let mut bvn = 0.0;

    if r.abs() < 0.925 {
        let _hs = (h * h + k * k) / 2.0;
        let asr = r.asin();
        for i in 0..5 {
            let sn = (asr * w[i]).sin();
            bvn += x_gl[i] * (sn * hk / (1.0 - sn * sn)).exp();
            let sn2 = (asr * (1.0 - w[i])).sin();
            bvn += x_gl[i] * (sn2 * hk / (1.0 - sn2 * sn2)).exp();
        }
        bvn *= asr / (2.0 * PI);
        bvn += normal_cdf(-h) * normal_cdf(-k);
    } else {
        // Large |rho| case
        if r < 0.0 {
            let kk = -k;
            let hkk = -hk;
            // Use complementary form
            let ass = (1.0 - r) * (1.0 + r);
            let a = ass.sqrt();
            let bs = (h - kk) * (h - kk);
            let c = (4.0 - hkk) / 8.0;
            let d = (12.0 - hkk) / 16.0;
            let asr = -(bs / ass + hkk) / 2.0;
            if asr > -100.0 {
                bvn = a * asr.exp() * (1.0 - c * (bs - ass) * (1.0 - d * bs / 5.0) / 3.0 + c * d * ass * ass / 5.0);
            }
            if -hkk < 100.0 {
                let b = a.sqrt();
                bvn -= (-hkk / 2.0).exp() * (2.0 * PI).sqrt() * normal_cdf(-a.sqrt() * (h - kk) / a) * b
                    * (1.0 - c * bs * (1.0 - d * bs / 5.0) / 3.0);
            }
            let a_new = a / 2.0;
            for i in 0..5 {
                let xs = (a_new * w[i]) * (a_new * w[i]);
                let rs = (1.0 - xs).sqrt();
                let asr2 = -(bs / xs + hkk) / 2.0;
                if asr2 > -100.0 {
                    bvn += a_new * x_gl[i] * asr2.exp()
                        * (((-hkk * xs / (2.0 * (1.0 + rs) * (1.0 + rs))).exp()) / rs
                            - (1.0 + c * xs * (1.0 + d * xs)));
                }
                let xs2 = (a_new * (1.0 - w[i])) * (a_new * (1.0 - w[i]));
                let rs2 = (1.0 - xs2).sqrt();
                let asr3 = -(bs / xs2 + hkk) / 2.0;
                if asr3 > -100.0 {
                    bvn += a_new * x_gl[i] * asr3.exp()
                        * (((-hkk * xs2 / (2.0 * (1.0 + rs2) * (1.0 + rs2))).exp()) / rs2
                            - (1.0 + c * xs2 * (1.0 + d * xs2)));
                }
            }
            bvn /= -(2.0 * PI);
            if bvn > 0.0 { bvn = 0.0; }
            bvn += normal_cdf(-h.max(kk)) - (if h < kk { normal_cdf(kk) - normal_cdf(h) } else { 0.0 });
        } else {
            // r > 0.925, map to r < 0 case
            bvn = bvn_drezner(dh, -dk, -r);
            bvn = normal_cdf(-dh) - bvn;
        }
    }

    finite_or_zero(bvn.clamp(0.0, 1.0))
}

// ===========================================================================
// Log-likelihood and golden section search
// ===========================================================================

fn copula_log_likelihood(family: CopulaFamily, param: f64, u: &[f64], v: &[f64]) -> f64 {
    let n = u.len();
    let mut ll = 0.0;
    for i in 0..n {
        let pdf = match family {
            CopulaFamily::Gaussian => copula_pdf_gaussian(param, u[i], v[i]),
            CopulaFamily::StudentT => copula_pdf_student_t(param, u[i], v[i]),
            CopulaFamily::Clayton => copula_pdf_clayton(param, u[i], v[i]),
            CopulaFamily::Gumbel => copula_pdf_gumbel(param, u[i], v[i]),
            CopulaFamily::Frank => copula_pdf_frank(param, u[i], v[i]),
        };
        ll += pdf.max(1e-30).ln();
    }
    if ll.is_finite() { ll } else { -1e18 }
}

/// Kendall's tau from copula parameter.
fn kendall_tau_from_param(family: CopulaFamily, param: f64) -> f64 {
    match family {
        CopulaFamily::Gaussian | CopulaFamily::StudentT => {
            // tau = 2/pi * arcsin(rho)
            finite_or_zero(2.0 / PI * param.asin())
        }
        CopulaFamily::Clayton => {
            // tau = theta / (theta + 2)
            finite_or_zero(param / (param + 2.0))
        }
        CopulaFamily::Gumbel => {
            // tau = 1 - 1/theta
            finite_or_zero(1.0 - 1.0 / param)
        }
        CopulaFamily::Frank => {
            // tau = 1 - 4/theta * (1 - D_1(theta))
            // D_1(theta) = 1/theta * integral_0^theta t/(exp(t)-1) dt (Debye function)
            if param.abs() < 1e-10 {
                return 0.0;
            }
            let debye = debye_1(param);
            finite_or_zero(1.0 - 4.0 / param * (1.0 - debye))
        }
    }
}

/// First Debye function D_1(x) = (1/x) * integral_0^x t/(e^t - 1) dt.
/// Numerical integration via Simpson's rule.
/// D_1 is only defined for positive arguments. For the Frank copula Kendall tau
/// formula, we always use D_1(|theta|), so we take the absolute value up front.
fn debye_1(x: f64) -> f64 {
    let x = x.abs();
    if x < 1e-10 {
        return 1.0;
    }
    let n = 200;
    let h = x / n as f64;

    let mut sum = 0.0;
    for i in 1..n {
        let t = i as f64 * h;
        let val = t / (t.exp() - 1.0);
        if val.is_finite() {
            let w = if i % 2 == 0 { 2.0 } else { 4.0 };
            sum += w * val;
        }
    }
    // Endpoints: t=0 contributes 1 (L'Hopital), t=x contributes x/(e^x-1)
    let end_val = x / (x.exp() - 1.0);
    if end_val.is_finite() {
        sum += 1.0 + end_val; // f(0)=1 via L'Hopital
    } else {
        sum += 1.0;
    }
    sum *= h / 3.0;

    finite_or_zero(sum / x)
}

/// Golden section search for maximum of f on [a, b].
fn golden_section_max<F: Fn(f64) -> f64>(f: F, mut a: f64, mut b: f64, tol: f64) -> f64 {
    let mut c = b - GOLDEN_RATIO * (b - a);
    let mut d = a + GOLDEN_RATIO * (b - a);
    let mut fc = f(c);
    let mut fd = f(d);

    for _ in 0..200 {
        if (b - a).abs() < tol {
            break;
        }
        if fc < fd {
            a = c;
            c = d;
            fc = fd;
            d = a + GOLDEN_RATIO * (b - a);
            fd = f(d);
        } else {
            b = d;
            d = c;
            fd = fc;
            c = b - GOLDEN_RATIO * (b - a);
            fc = f(c);
        }
    }
    0.5 * (a + b)
}

/// Parameter bounds for each copula family.
fn param_bounds(family: CopulaFamily) -> (f64, f64) {
    match family {
        CopulaFamily::Gaussian => (-0.999, 0.999),
        CopulaFamily::StudentT => (-0.999, 0.999),
        CopulaFamily::Clayton => (0.01, 20.0),
        CopulaFamily::Gumbel => (1.01, 20.0),
        CopulaFamily::Frank => (-20.0, 20.0),
    }
}

/// Fit a single copula family to data using MLE.
fn fit_single_copula(family: CopulaFamily, u: &[f64], v: &[f64]) -> CopulaFit {
    let (lo, hi) = param_bounds(family);
    let best_param = golden_section_max(
        |p| copula_log_likelihood(family, p, u, v),
        lo,
        hi,
        1e-8,
    );
    let ll = copula_log_likelihood(family, best_param, u, v);
    let aic = 2.0 - 2.0 * ll; // 1 parameter
    let tau = kendall_tau_from_param(family, best_param);

    CopulaFit {
        family,
        parameter: finite_or_zero(best_param),
        log_likelihood: finite_or_zero(ll),
        aic: finite_or_zero(aic),
        kendall_tau: finite_or_zero(tau),
    }
}

// ===========================================================================
// Validation
// ===========================================================================

fn validate_uv(u: &[f64], v: &[f64]) -> PyResult<()> {
    if u.len() != v.len() {
        return Err(pyo3::exceptions::PyValueError::new_err(
            "u and v must have the same length",
        ));
    }
    if u.len() < 5 {
        return Err(pyo3::exceptions::PyValueError::new_err(
            "need at least 5 observations",
        ));
    }
    for (i, (&ui, &vi)) in u.iter().zip(v.iter()).enumerate() {
        if !ui.is_finite() || !vi.is_finite() {
            return Err(pyo3::exceptions::PyValueError::new_err(format!(
                "non-finite value at index {}", i
            )));
        }
        if ui <= 0.0 || ui >= 1.0 || vi <= 0.0 || vi >= 1.0 {
            return Err(pyo3::exceptions::PyValueError::new_err(format!(
                "values must be in (0, 1) at index {} (got u={}, v={})", i, ui, vi
            )));
        }
    }
    Ok(())
}

// ===========================================================================
// Public copula dispatch (for vine_copula.rs)
// ===========================================================================

/// Evaluate copula PDF for any family (used by vine_copula.rs).
#[allow(dead_code)]
pub fn copula_pdf_dispatch(family: CopulaFamily, param: f64, u: f64, v: f64) -> f64 {
    match family {
        CopulaFamily::Gaussian => copula_pdf_gaussian(param, u, v),
        CopulaFamily::StudentT => copula_pdf_student_t(param, u, v),
        CopulaFamily::Clayton => copula_pdf_clayton(param, u, v),
        CopulaFamily::Gumbel => copula_pdf_gumbel(param, u, v),
        CopulaFamily::Frank => copula_pdf_frank(param, u, v),
    }
}

/// Evaluate copula CDF for any family (used by vine_copula.rs).
pub fn copula_cdf_dispatch(family: CopulaFamily, param: f64, u: f64, v: f64) -> f64 {
    match family {
        CopulaFamily::Gaussian => copula_cdf_gaussian(param, u, v),
        CopulaFamily::StudentT => copula_cdf_student_t(param, u, v),
        CopulaFamily::Clayton => copula_cdf_clayton(param, u, v),
        CopulaFamily::Gumbel => copula_cdf_gumbel(param, u, v),
        CopulaFamily::Frank => copula_cdf_frank(param, u, v),
    }
}

/// Conditional copula CDF: C(v|u) = dC(u,v)/du via numerical differentiation.
/// Used by vine_copula.rs for h-functions.
pub fn copula_h_function(family: CopulaFamily, param: f64, u: f64, v: f64) -> f64 {
    let eps = 1e-5;
    let u_lo = clamp_unit(u - eps);
    let u_hi = clamp_unit(u + eps);
    let c_hi = copula_cdf_dispatch(family, param, u_hi, v);
    let c_lo = copula_cdf_dispatch(family, param, u_lo, v);
    finite_or_zero(((c_hi - c_lo) / (u_hi - u_lo)).clamp(1e-10, 1.0 - 1e-10))
}

/// Fit a single copula to pseudo-observations (public for vine_copula.rs).
#[allow(dead_code)]
pub fn fit_single_copula_pub(family: CopulaFamily, u: &[f64], v: &[f64]) -> CopulaFit {
    fit_single_copula(family, u, v)
}

/// Find best-fitting copula family (public for vine_copula.rs).
pub fn best_copula_fit(u: &[f64], v: &[f64]) -> CopulaFit {
    let families = [
        CopulaFamily::Gaussian,
        CopulaFamily::StudentT,
        CopulaFamily::Clayton,
        CopulaFamily::Gumbel,
        CopulaFamily::Frank,
    ];
    let mut best: Option<CopulaFit> = None;
    for &family in &families {
        let fit = fit_single_copula(family, u, v);
        if best.is_none() || fit.aic < best.as_ref().unwrap().aic {
            best = Some(fit);
        }
    }
    best.unwrap()
}

// ===========================================================================
// Exported pyfunction implementations
// ===========================================================================

/// Fit a copula of the specified family to pseudo-uniform data.
///
/// Parameters
/// ----------
/// u : list[float]
///     First marginal pseudo-observations in (0, 1).
/// v : list[float]
///     Second marginal pseudo-observations in (0, 1).
/// family : CopulaFamily
///     Which copula family to fit.
///
/// Returns
/// -------
/// CopulaFit
#[pyfunction]
#[pyo3(signature = (u, v, family))]
fn fit_copula(u: Vec<f64>, v: Vec<f64>, family: CopulaFamily) -> PyResult<CopulaFit> {
    crate::auth::require_pro()
        .map_err(|e| pyo3::exceptions::PyPermissionError::new_err(e.to_string()))?;
    validate_uv(&u, &v)?;
    Ok(fit_single_copula(family, &u, &v))
}

/// Fit all 5 copula families and return the one with best AIC.
///
/// Parameters
/// ----------
/// u : list[float]
///     First marginal pseudo-observations in (0, 1).
/// v : list[float]
///     Second marginal pseudo-observations in (0, 1).
///
/// Returns
/// -------
/// CopulaFit
#[pyfunction]
#[pyo3(signature = (u, v))]
fn best_copula(u: Vec<f64>, v: Vec<f64>) -> PyResult<CopulaFit> {
    crate::auth::require_pro()
        .map_err(|e| pyo3::exceptions::PyPermissionError::new_err(e.to_string()))?;
    validate_uv(&u, &v)?;
    Ok(best_copula_fit(&u, &v))
}

/// Sample from a bivariate copula.
///
/// Parameters
/// ----------
/// family : CopulaFamily
/// parameter : float
/// n : int
/// seed : int
///
/// Returns
/// -------
/// list[tuple[float, float]]
#[pyfunction]
#[pyo3(signature = (family, parameter, n, seed))]
fn copula_sample(family: CopulaFamily, parameter: f64, n: usize, seed: u64) -> PyResult<Vec<(f64, f64)>> {
    crate::auth::require_pro()
        .map_err(|e| pyo3::exceptions::PyPermissionError::new_err(e.to_string()))?;

    if n == 0 {
        return Ok(vec![]);
    }
    if !parameter.is_finite() {
        return Err(pyo3::exceptions::PyValueError::new_err("parameter must be finite"));
    }

    let mut rng = Xoshiro256::new(seed);
    let mut samples = Vec::with_capacity(n);

    match family {
        CopulaFamily::Gaussian => {
            let rho = parameter.clamp(-0.999, 0.999);
            for _ in 0..n {
                let (z1, z2) = rng.next_normal_pair();
                let x = z1;
                let y = rho * z1 + (1.0 - rho * rho).sqrt() * z2;
                let u = clamp_unit(normal_cdf(x));
                let v = clamp_unit(normal_cdf(y));
                samples.push((u, v));
            }
        }
        CopulaFamily::StudentT => {
            let rho = parameter.clamp(-0.999, 0.999);
            let nu = STUDENT_T_NU;
            for _ in 0..n {
                let (z1, z2) = rng.next_normal_pair();
                // chi-squared with nu d.f. = sum of nu standard normals squared
                let mut chi2 = 0.0;
                for _ in 0..(nu as usize) {
                    let g = rng.next_normal();
                    chi2 += g * g;
                }
                let s = (nu / chi2).sqrt();
                let x = s * z1;
                let y = s * (rho * z1 + (1.0 - rho * rho).sqrt() * z2);
                let u = clamp_unit(student_t_cdf(x, nu));
                let v = clamp_unit(student_t_cdf(y, nu));
                samples.push((u, v));
            }
        }
        CopulaFamily::Clayton => {
            let theta = parameter.max(0.01);
            for _ in 0..n {
                let u1 = rng.next_f64().clamp(1e-10, 1.0 - 1e-10);
                let u2 = rng.next_f64().clamp(1e-10, 1.0 - 1e-10);
                // Conditional inversion: v = ((u1^(-theta))*(u2^(-theta/(1+theta)) - 1) + 1)^(-1/theta)
                let v = (u1.powf(-theta) * (u2.powf(-theta / (1.0 + theta)) - 1.0) + 1.0).powf(-1.0 / theta);
                let u_out = clamp_unit(u1);
                let v_out = clamp_unit(v);
                samples.push((u_out, v_out));
            }
        }
        CopulaFamily::Gumbel => {
            let theta = parameter.max(1.01);
            // Marshall-Olkin algorithm with stable distribution
            for _ in 0..n {
                let u1 = rng.next_f64().clamp(1e-10, 1.0 - 1e-10);
                let u2 = rng.next_f64().clamp(1e-10, 1.0 - 1e-10);
                let e1 = -u1.ln();
                let e2 = -u2.ln();
                // Approximate stable(1/theta) variate via Chambers-Mallows-Stuck
                let w = rng.next_f64().clamp(1e-10, 1.0 - 1e-10);
                let alpha = 1.0 / theta;
                let phi = PI * (w - 0.5);
                let exp_w = (-rng.next_f64().clamp(1e-10, 1.0 - 1e-10).ln()).max(1e-30);
                let stable = if (alpha - 1.0).abs() < 1e-10 {
                    1.0
                } else {
                    let t1 = (alpha * phi).sin() / phi.cos().powf(1.0 / alpha);
                    let t2 = (phi * (1.0 - alpha)).cos() / exp_w;
                    (t1 * t2.powf((1.0 - alpha) / alpha)).max(1e-30)
                };
                let s1 = (-e1 / stable).exp();
                let s2 = (-e2 / stable).exp();
                samples.push((clamp_unit(s1), clamp_unit(s2)));
            }
        }
        CopulaFamily::Frank => {
            let theta = parameter;
            for _ in 0..n {
                let u1 = rng.next_f64().clamp(1e-10, 1.0 - 1e-10);
                let u2 = rng.next_f64().clamp(1e-10, 1.0 - 1e-10);
                if theta.abs() < 1e-10 {
                    samples.push((u1, u2));
                } else {
                    // Conditional inversion
                    let e_t = (-theta).exp();
                    let v = -1.0 / theta * (1.0 + (e_t - 1.0) / ((-theta * u1).exp() * (1.0 / u2 - 1.0) + 1.0)).ln();
                    samples.push((clamp_unit(u1), clamp_unit(v)));
                }
            }
        }
    }

    Ok(samples)
}

/// Evaluate the copula CDF C(u, v; parameter) for a given family.
///
/// Parameters
/// ----------
/// family : CopulaFamily
/// parameter : float
/// u : float
/// v : float
///
/// Returns
/// -------
/// float
#[pyfunction]
#[pyo3(signature = (family, parameter, u, v))]
fn copula_cdf(family: CopulaFamily, parameter: f64, u: f64, v: f64) -> f64 {
    finite_or_zero(copula_cdf_dispatch(family, parameter, u, v))
}

/// Transform raw data to pseudo-uniform [0, 1] via rank transform.
///
/// Parameters
/// ----------
/// data : list[float]
///     Raw observations.
///
/// Returns
/// -------
/// list[float]
///     Pseudo-uniform values in (0, 1).
#[pyfunction]
#[pyo3(signature = (data,))]
fn rank_transform(data: Vec<f64>) -> Vec<f64> {
    let n = data.len();
    if n == 0 {
        return vec![];
    }
    // Create (value, original_index) pairs
    let mut indexed: Vec<(f64, usize)> = data.iter().copied().enumerate().map(|(i, v)| (v, i)).collect();
    indexed.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));

    let mut ranks = vec![0.0_f64; n];
    // Handle ties by averaging ranks
    let mut i = 0;
    while i < n {
        let mut j = i + 1;
        while j < n && (indexed[j].0 - indexed[i].0).abs() < 1e-15 {
            j += 1;
        }
        let avg_rank = (i + j) as f64 / 2.0; // average of (i+1) .. j
        for item in indexed.iter().take(j).skip(i) {
            ranks[item.1] = avg_rank;
        }
        i = j;
    }

    // Scale to (0, 1): rank / (n + 1)
    let denom = (n + 1) as f64;
    ranks.iter().map(|&r| finite_or_zero((r / denom).clamp(1e-10, 1.0 - 1e-10))).collect()
}

// ===========================================================================
// Module registration
// ===========================================================================

pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<CopulaFamily>()?;
    m.add_class::<CopulaFit>()?;
    m.add_function(wrap_pyfunction!(fit_copula, m)?)?;
    m.add_function(wrap_pyfunction!(best_copula, m)?)?;
    m.add_function(wrap_pyfunction!(copula_sample, m)?)?;
    m.add_function(wrap_pyfunction!(copula_cdf, m)?)?;
    m.add_function(wrap_pyfunction!(rank_transform, m)?)?;
    Ok(())
}

// ===========================================================================
// Tests
// ===========================================================================

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

    #[test]
    fn test_normal_cdf() {
        assert!((normal_cdf(0.0) - 0.5).abs() < 1e-6);
        assert!((normal_cdf(1.0) - 0.8413).abs() < 1e-3);
        assert!((normal_cdf(-1.0) - 0.1587).abs() < 1e-3);
    }

    #[test]
    fn test_normal_cdf_inv() {
        assert!((normal_cdf_inv(0.5)).abs() < 1e-4);
        assert!((normal_cdf_inv(0.8413) - 1.0).abs() < 1e-2);
        assert!((normal_cdf_inv(0.1587) + 1.0).abs() < 1e-2);
    }

    #[test]
    fn test_normal_roundtrip() {
        for &x in &[-2.0, -1.0, 0.0, 0.5, 1.5, 2.5] {
            let p = normal_cdf(x);
            let x2 = normal_cdf_inv(p);
            assert!((x - x2).abs() < 1e-4, "roundtrip failed for x={}", x);
        }
    }

    #[test]
    fn test_finite_or_zero_nan() {
        assert_eq!(finite_or_zero(f64::NAN), 0.0);
        assert_eq!(finite_or_zero(f64::INFINITY), 0.0);
        assert_eq!(finite_or_zero(1.5), 1.5);
    }

    #[test]
    fn test_rank_transform() {
        let data = vec![3.0, 1.0, 2.0, 5.0, 4.0];
        let ranks = rank_transform(data);
        assert_eq!(ranks.len(), 5);
        // All should be in (0, 1)
        for &r in &ranks {
            assert!(r > 0.0 && r < 1.0);
        }
    }

    #[test]
    fn test_gaussian_copula_cdf_independence() {
        // rho=0 => C(u,v) = u*v
        let c = copula_cdf_gaussian(0.0, 0.5, 0.5);
        assert!((c - 0.25).abs() < 0.02);
    }

    #[test]
    fn test_clayton_copula_cdf() {
        let c = copula_cdf_clayton(1.0, 0.5, 0.5);
        // (0.5^{-1} + 0.5^{-1} - 1)^{-1} = (2+2-1)^{-1} = 1/3
        assert!((c - 1.0 / 3.0).abs() < 1e-6);
    }

    #[test]
    fn test_gumbel_copula_cdf() {
        let c = copula_cdf_gumbel(2.0, 0.5, 0.5);
        // Should be between u*v=0.25 and min(u,v)=0.5
        assert!(c > 0.25 && c < 0.5);
    }

    #[test]
    fn test_frank_copula_cdf_zero_theta() {
        let c = copula_cdf_frank(0.0, 0.5, 0.5);
        assert!((c - 0.25).abs() < 1e-6);
    }

    #[test]
    fn test_kendall_tau_gaussian() {
        let tau = kendall_tau_from_param(CopulaFamily::Gaussian, 0.0);
        assert!(tau.abs() < 1e-6);
    }

    #[test]
    fn test_kendall_tau_clayton() {
        let tau = kendall_tau_from_param(CopulaFamily::Clayton, 2.0);
        // 2/(2+2) = 0.5
        assert!((tau - 0.5).abs() < 1e-6);
    }
}
