//! Autoregressive Conditional Duration (ACD) models.
//!
//! Models the time between events (trades, quotes, etc.) using
//! an GARCH-like structure for durations: E[d_i] = omega + alpha*d_{i-1} + beta*E[d_{i-1}].
//! Useful for detecting unusual inter-trade times and activity regime changes.

use pyo3::prelude::*;
use std::sync::Mutex;

// ===========================================================================
// Result types
// ===========================================================================

#[pyclass(frozen)]
#[derive(Debug, Clone)]
pub struct AcdState {
    #[pyo3(get)]
    pub expected_duration: f64,
    #[pyo3(get)]
    pub actual_duration: f64,
    #[pyo3(get)]
    pub surprise: f64,
    #[pyo3(get)]
    pub is_unusual: bool,
}

#[pymethods]
impl AcdState {
    fn __repr__(&self) -> String {
        format!(
            "AcdState(expected={:.4}, actual={:.4}, surprise={:.4}, unusual={})",
            self.expected_duration, self.actual_duration, self.surprise, self.is_unusual
        )
    }
}

// ===========================================================================
// ACD Model (streaming)
// ===========================================================================

struct AcdInner {
    omega: f64,
    alpha: f64,
    beta: f64,
    expected_duration: f64,
    last_duration: f64,
    last_event_time: f64,
    n_events: usize,
}

impl AcdInner {
    fn new(omega: f64, alpha: f64, beta: f64) -> Self {
        Self {
            omega,
            alpha,
            beta,
            expected_duration: omega / (1.0 - alpha - beta).max(1e-10),
            last_duration: 0.0,
            last_event_time: -1.0,
            n_events: 0,
        }
    }
}

#[pyclass]
pub struct AcdModel {
    inner: Mutex<AcdInner>,
}

#[pymethods]
impl AcdModel {
    #[new]
    #[pyo3(signature = (omega=0.1, alpha=0.1, beta=0.8))]
    fn new(omega: f64, alpha: f64, beta: f64) -> PyResult<Self> {
        crate::auth::require_pro()
            .map_err(|e| pyo3::exceptions::PyPermissionError::new_err(e.to_string()))?;

        if omega <= 0.0 || alpha < 0.0 || beta < 0.0 {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "omega must be positive, alpha and beta must be non-negative",
            ));
        }
        if alpha + beta >= 1.0 {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "alpha + beta must be < 1 for stationarity",
            ));
        }

        Ok(Self {
            inner: Mutex::new(AcdInner::new(omega, alpha, beta)),
        })
    }

    /// Update with a new event time, return the ACD state.
    fn update(&self, event_time: f64) -> PyResult<AcdState> {
        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());

        if !event_time.is_finite() {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "event_time must be finite",
            ));
        }

        if inner.last_event_time < 0.0 {
            // First event
            inner.last_event_time = event_time;
            inner.n_events = 1;
            return Ok(AcdState {
                expected_duration: inner.expected_duration,
                actual_duration: 0.0,
                surprise: 1.0,
                is_unusual: false,
            });
        }

        let actual = (event_time - inner.last_event_time).max(0.0);
        inner.last_event_time = event_time;
        inner.n_events += 1;

        // ACD update: E[d_i] = omega + alpha * d_{i-1} + beta * E[d_{i-1}]
        let expected = inner.omega + inner.alpha * inner.last_duration + inner.beta * inner.expected_duration;
        let expected = if expected.is_finite() && expected > 0.0 { expected } else { inner.omega };

        let surprise = if expected > 1e-15 {
            actual / expected
        } else {
            1.0
        };

        inner.expected_duration = expected;
        inner.last_duration = actual;

        Ok(AcdState {
            expected_duration: expected,
            actual_duration: actual,
            surprise: if surprise.is_finite() { surprise } else { 1.0 },
            is_unusual: surprise > 2.0 || surprise < 0.5,
        })
    }

    /// Current expected duration.
    fn expected_duration(&self) -> f64 {
        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        inner.expected_duration
    }

    /// Surprise ratio: actual / expected.
    fn surprise(&self) -> f64 {
        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        if inner.expected_duration > 1e-15 {
            let s = inner.last_duration / inner.expected_duration;
            if s.is_finite() { s } else { 1.0 }
        } else {
            1.0
        }
    }

    /// Hazard rate: probability of event given elapsed time.
    /// h(t) = 1 / E[d] for exponential ACD.
    fn hazard_rate(&self, elapsed: f64) -> f64 {
        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        if inner.expected_duration > 1e-15 && elapsed >= 0.0 {
            let rate = 1.0 / inner.expected_duration;
            if rate.is_finite() { rate } else { 0.0 }
        } else {
            0.0
        }
    }

    fn __repr__(&self) -> String {
        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        format!(
            "AcdModel(omega={:.4}, alpha={:.4}, beta={:.4}, events={})",
            inner.omega, inner.alpha, inner.beta, inner.n_events
        )
    }
}

// ===========================================================================
// Standalone functions
// ===========================================================================

/// Log-likelihood of ACD model given durations.
#[pyfunction]
pub fn acd_log_likelihood(durations: Vec<f64>, omega: f64, alpha: f64, beta: f64) -> f64 {
    crate::auth::require_pro()
        .map_err(|e| pyo3::exceptions::PyPermissionError::new_err(e.to_string()))
        .ok();

    if durations.len() < 2 || omega <= 0.0 || alpha < 0.0 || beta < 0.0 || alpha + beta >= 1.0 {
        return f64::NEG_INFINITY;
    }

    let mut expected = omega / (1.0 - alpha - beta).max(1e-10);
    let mut ll = 0.0;

    for i in 0..durations.len() {
        let d = durations[i];
        if d <= 0.0 || !d.is_finite() {
            continue;
        }

        // Exponential ACD log-likelihood: log(1/E) - d/E
        if expected > 1e-15 {
            ll += -(expected.ln()) - d / expected;
        }

        if i > 0 {
            expected = omega + alpha * durations[i - 1] + beta * expected;
            if !expected.is_finite() || expected <= 0.0 {
                expected = omega;
            }
        }
    }

    if ll.is_finite() { ll } else { f64::NEG_INFINITY }
}

/// Fit ACD model to durations via grid search MLE.
#[pyfunction]
pub fn fit_acd(durations: Vec<f64>) -> PyResult<AcdModel> {
    crate::auth::require_pro()
        .map_err(|e| pyo3::exceptions::PyPermissionError::new_err(e.to_string()))?;

    if durations.len() < 5 {
        return Err(pyo3::exceptions::PyValueError::new_err(
            "need at least 5 durations to fit ACD model",
        ));
    }

    // Grid search over omega, alpha, beta
    let mean_dur: f64 = {
        let valid: Vec<f64> = durations.iter().filter(|d| d.is_finite() && **d > 0.0).copied().collect();
        if valid.is_empty() {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "no valid durations",
            ));
        }
        valid.iter().sum::<f64>() / valid.len() as f64
    };

    let mut best_ll = f64::NEG_INFINITY;
    let mut best_params = (mean_dur * 0.1, 0.1, 0.8);

    for &omega_frac in &[0.01, 0.05, 0.1, 0.2] {
        for &alpha in &[0.05, 0.1, 0.15, 0.2, 0.3] {
            for &beta in &[0.5, 0.6, 0.7, 0.8, 0.85, 0.9] {
                if alpha + beta >= 0.99 {
                    continue;
                }
                let omega = mean_dur * omega_frac * (1.0 - alpha - beta);
                let ll = acd_log_likelihood(durations.clone(), omega, alpha, beta);
                if ll > best_ll {
                    best_ll = ll;
                    best_params = (omega, alpha, beta);
                }
            }
        }
    }

    AcdModel::new(best_params.0, best_params.1, best_params.2)
}

// ===========================================================================
// Registration
// ===========================================================================

pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<AcdState>()?;
    m.add_class::<AcdModel>()?;
    m.add_function(wrap_pyfunction!(acd_log_likelihood, m)?)?;
    m.add_function(wrap_pyfunction!(fit_acd, m)?)?;
    Ok(())
}
