use crate::alpha::{self, Distance};
use crate::bradley_terry::{bradley_terry, newman};
use crate::counting::{average_win_rate, counting};
use crate::elo::elo;
use crate::linalg::{eigen, pagerank};
use crate::utils::{matrices, win_plus_tie_matrix};
use ndarray::{Array1, Array2};
use wasm_bindgen::prelude::*;

#[wasm_bindgen(js_name = "counting")]
/// Runs the counting method in WebAssembly.
///
/// # Errors
///
/// Returns an error string when inputs have mismatched lengths or invalid indices.
pub fn counting_wasm(
    xs: &[usize],
    ys: &[usize],
    winners: &[u8],
    weights: &[f64],
    total: usize,
    win_weight: f64,
    tie_weight: f64,
) -> Result<Vec<f64>, String> {
    counting(
        &Array1::from_vec(xs.to_vec()).view(),
        &Array1::from_vec(ys.to_vec()).view(),
        &Array1::from_vec(winners.to_vec()).view(),
        &Array1::from_vec(weights.to_vec()).view(),
        total,
        win_weight,
        tie_weight,
    )
    .map(|a| a.to_vec())
    .map_err(|e| e.to_string())
}

#[wasm_bindgen(js_name = "averageWinRate")]
/// Runs the average win rate method in WebAssembly.
///
/// # Errors
///
/// Returns an error string when inputs have mismatched lengths or invalid indices.
pub fn average_win_rate_wasm(
    xs: &[usize],
    ys: &[usize],
    winners: &[u8],
    weights: &[f64],
    total: usize,
    win_weight: f64,
    tie_weight: f64,
) -> Result<Vec<f64>, String> {
    average_win_rate(
        &Array1::from_vec(xs.to_vec()).view(),
        &Array1::from_vec(ys.to_vec()).view(),
        &Array1::from_vec(winners.to_vec()).view(),
        &Array1::from_vec(weights.to_vec()).view(),
        total,
        win_weight,
        tie_weight,
    )
    .map(|a| a.to_vec())
    .map_err(|e| e.to_string())
}

#[wasm_bindgen(js_name = "bradleyTerry")]
/// Runs the Bradley-Terry method in WebAssembly.
///
/// # Errors
///
/// Returns an error string when inputs have mismatched lengths, invalid indices, or matrix errors.
pub fn bradley_terry_wasm(
    xs: &[usize],
    ys: &[usize],
    winners: &[u8],
    weights: &[f64],
    total: usize,
    win_weight: f64,
    tie_weight: f64,
    tolerance: f64,
    limit: usize,
) -> Result<Vec<f64>, String> {
    let (wins, ties) = matrices(
        &Array1::from_vec(xs.to_vec()).view(),
        &Array1::from_vec(ys.to_vec()).view(),
        &Array1::from_vec(winners.to_vec()).view(),
        &Array1::from_vec(weights.to_vec()).view(),
        total,
    )
    .map_err(|e| e.to_string())?;

    let matrix = win_plus_tie_matrix(&wins.view(), &ties.view(), win_weight, tie_weight, 0.0);
    bradley_terry(&matrix.view(), tolerance, limit)
        .map(|(a, _)| a.to_vec())
        .map_err(|e| e.to_string())
}

#[wasm_bindgen(js_name = "newman")]
/// Runs the Newman method in WebAssembly.
///
/// # Errors
///
/// Returns an error string when inputs have mismatched lengths, invalid indices, or matrix errors.
pub fn newman_wasm(
    xs: &[usize],
    ys: &[usize],
    winners: &[u8],
    weights: &[f64],
    total: usize,
    v_init: f64,
    tolerance: f64,
    limit: usize,
) -> Result<Vec<f64>, String> {
    let (wins, ties) = matrices(
        &Array1::from_vec(xs.to_vec()).view(),
        &Array1::from_vec(ys.to_vec()).view(),
        &Array1::from_vec(winners.to_vec()).view(),
        &Array1::from_vec(weights.to_vec()).view(),
        total,
    )
    .map_err(|e| e.to_string())?;

    newman(&wins.view(), &ties.view(), v_init, tolerance, limit)
        .map(|(a, _, _)| a.to_vec())
        .map_err(|e| e.to_string())
}

#[wasm_bindgen(js_name = "eigen")]
/// Runs the eigenvector method in WebAssembly.
///
/// # Errors
///
/// Returns an error string when inputs have mismatched lengths, invalid indices, or matrix errors.
pub fn eigen_wasm(
    xs: &[usize],
    ys: &[usize],
    winners: &[u8],
    weights: &[f64],
    total: usize,
    win_weight: f64,
    tie_weight: f64,
    tolerance: f64,
    limit: usize,
) -> Result<Vec<f64>, String> {
    let (wins, ties) = matrices(
        &Array1::from_vec(xs.to_vec()).view(),
        &Array1::from_vec(ys.to_vec()).view(),
        &Array1::from_vec(winners.to_vec()).view(),
        &Array1::from_vec(weights.to_vec()).view(),
        total,
    )
    .map_err(|e| e.to_string())?;

    let matrix = win_plus_tie_matrix(&wins.view(), &ties.view(), win_weight, tie_weight, 0.0);
    eigen(&matrix.view(), tolerance, limit)
        .map(|(a, _)| a.to_vec())
        .map_err(|e| e.to_string())
}

#[wasm_bindgen(js_name = "pagerank")]
/// Runs the `PageRank` method in WebAssembly.
///
/// # Errors
///
/// Returns an error string when inputs have mismatched lengths, invalid indices, or matrix errors.
pub fn pagerank_wasm(
    xs: &[usize],
    ys: &[usize],
    winners: &[u8],
    weights: &[f64],
    total: usize,
    win_weight: f64,
    tie_weight: f64,
    alpha: f64,
    tolerance: f64,
    limit: usize,
) -> Result<Vec<f64>, String> {
    let (wins, ties) = matrices(
        &Array1::from_vec(xs.to_vec()).view(),
        &Array1::from_vec(ys.to_vec()).view(),
        &Array1::from_vec(winners.to_vec()).view(),
        &Array1::from_vec(weights.to_vec()).view(),
        total,
    )
    .map_err(|e| e.to_string())?;

    let matrix = win_plus_tie_matrix(&wins.view(), &ties.view(), win_weight, tie_weight, 0.0);
    pagerank(&matrix.view(), alpha, tolerance, limit)
        .map(|(a, _)| a.to_vec())
        .map_err(|e| e.to_string())
}

#[wasm_bindgen(js_name = "elo")]
/// Runs the Elo method in WebAssembly.
///
/// # Errors
///
/// Returns an error string when inputs have mismatched lengths or invalid indices.
pub fn elo_wasm(
    xs: &[usize],
    ys: &[usize],
    winners: &[u8],
    weights: &[f64],
    total: usize,
    initial: f64,
    base: f64,
    scale: f64,
    k: f64,
    win_weight: f64,
    tie_weight: f64,
) -> Result<Vec<f64>, String> {
    elo(
        &Array1::from_vec(xs.to_vec()).view(),
        &Array1::from_vec(ys.to_vec()).view(),
        &Array1::from_vec(winners.to_vec()).view(),
        &Array1::from_vec(weights.to_vec()).view(),
        total,
        initial,
        base,
        scale,
        k,
        win_weight,
        tie_weight,
    )
    .map(|a| a.to_vec())
    .map_err(|e| e.to_string())
}

#[wasm_bindgen(js_name = "alpha")]
/// Runs Krippendorff's alpha in WebAssembly.
///
/// # Errors
///
/// Returns an error string when distance parsing fails or input shapes are invalid.
pub fn alpha_wasm(
    codes: &[i64],
    unique_values: &[f64],
    n_units: usize,
    n_raters: usize,
    distance: &str,
) -> Result<Vec<f64>, String> {
    let distance_enum = Distance::parse(distance)?;

    let codes_array =
        Array2::from_shape_vec((n_units, n_raters), codes.to_vec()).map_err(|e| e.to_string())?;

    let (alpha, observed, expected) =
        alpha::alpha_from_factorized(&codes_array.view(), unique_values, distance_enum)?;

    Ok(vec![alpha, observed, expected])
}

#[cfg(test)]
mod tests {
    use approx::assert_abs_diff_eq as assert_approx_eq;
    use super::*;
    use wasm_bindgen_test::*;

    #[wasm_bindgen_test]
    fn test_counting() {
        let xs = vec![0, 1, 2];
        let ys = vec![1, 2, 0];
        let winners = vec![1, 1, 1]; // X wins
        let weights = vec![1.0, 1.0, 1.0];
        let result = counting_wasm(&xs, &ys, &winners, &weights, 3, 1.0, 0.5).unwrap();
        assert_eq!(result.len(), 3);
        for &score in &result {
            assert!(score > 0.0);
        }
    }

    #[wasm_bindgen_test]
    fn test_bradley_terry() {
        let xs = vec![0, 1, 2];
        let ys = vec![1, 2, 0];
        let winners = vec![1, 1, 1];
        let weights = vec![1.0, 1.0, 1.0];
        let result =
            bradley_terry_wasm(&xs, &ys, &winners, &weights, 3, 1.0, 0.5, 1e-6, 100).unwrap();
        assert_eq!(result.len(), 3);
    }

    #[wasm_bindgen_test]
    fn test_elo() {
        let xs = vec![0, 1, 2];
        let ys = vec![1, 2, 0];
        let winners = vec![1, 1, 1];
        let weights = vec![1.0, 1.0, 1.0];
        let result = elo_wasm(
            &xs, &ys, &winners, &weights, 3, 1500.0, 10.0, 400.0, 32.0, 1.0, 0.5,
        )
        .unwrap();
        assert_eq!(result.len(), 3);
    }

    #[wasm_bindgen_test]
    fn test_alpha() {
        // Data:
        //      Unit 1: 1, 1
        //      Unit 2: 2, 2
        //      Unit 3: 3, 3
        // Perfect agreement, alpha should be 1.0.
        // Coded:
        //      Unit 1: 0, 0
        //      Unit 2: 1, 1
        //      Unit 3: 2, 2
        // Unique values: [1.0, 2.0, 3.0]

        let codes = vec![
            0, 0, // Unit 1
            1, 1, // Unit 2
            2, 2, // Unit 3
        ];
        let unique_values = vec![1.0, 2.0, 3.0];

        let result = alpha_wasm(&codes, &unique_values, 3, 2, "nominal").unwrap();

        assert_eq!(result.len(), 3);
        assert_approx_eq!(result[0], 1.0, epsilon = 1e-6); // Alpha
        assert_approx_eq!(result[1], 0.0, epsilon = 1e-12); // Observed disagreement
    }
}
