use pyo3::prelude::*;
use std::error::Error;
use std::future::Future;
use std::io::{Error as IoError, ErrorKind};
use std::sync::OnceLock;
use tokio::runtime::Runtime;

mod db;
mod export;
mod process;

fn shared_runtime() -> &'static Runtime {
    static RUNTIME: OnceLock<Runtime> = OnceLock::new();
    RUNTIME.get_or_init(|| {
        tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .expect("failed to initialize shared tokio runtime")
    })
}

fn run_async_with_runtime<T, F>(future: F, context: &'static str) -> PyResult<T>
where
    F: Future<Output = Result<T, Box<dyn Error>>>,
{
    match shared_runtime().block_on(future) {
        Ok(data) => Ok(data),
        Err(e) => {
            eprintln!("{context}: {e:?}");
            Err(PyErr::new::<pyo3::exceptions::PyException, _>(context))
        }
    }
}

#[pymodule]
fn sdb_connector(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(select_additional_info_data, m)?)?;
    m.add_function(wrap_pyfunction!(select_measurement_data, m)?)?;
    m.add_function(wrap_pyfunction!(select_raw_data, m)?)?;
    m.add_function(wrap_pyfunction!(select_ai_data, m)?)?;
    m.add_function(wrap_pyfunction!(select_di_data, m)?)?;
    // m.add_function(wrap_pyfunction!(select_general_info_data, m)?)?;
    Ok(())
}

#[pyfunction]
fn select_ai_data(
    ip: &str,
    port: &str,
    user: &str,
    pw: &str,
    namespace: &str,
    db_name: &str,
    table_name: &str,
    run_id: &str,
    path_name: &str,
    select_type: u8,
) -> PyResult<Vec<(u8, u8, String, u16, u64)>> {
    run_async_with_runtime(
        select_ai_data_async(
            ip,
            port,
            user,
            pw,
            namespace,
            db_name,
            table_name,
            run_id,
            path_name,
            select_type,
        ),
        "Error selecting AI data",
    )
}

#[pyfunction]
fn select_di_data(
    ip: &str,
    port: &str,
    user: &str,
    pw: &str,
    namespace: &str,
    db_name: &str,
    table_name: &str,
    run_id: &str,
    path_name: &str,
    select_type: u8,
) -> PyResult<Vec<(u8, u8, String, bool, u64)>> {
    run_async_with_runtime(
        select_di_data_async(
            ip,
            port,
            user,
            pw,
            namespace,
            db_name,
            table_name,
            run_id,
            path_name,
            select_type,
        ),
        "Error selecting DI data",
    )
}

#[pyfunction]
fn select_additional_info_data(
    ip: &str,
    port: &str,
    user: &str,
    pw: &str,
    namespace: &str,
    db_name: &str,
    table_name: &str,
    run_id: &str,
    path_name: &str,
    select_type: u8,
) -> PyResult<Vec<(u64, u16, u8, u16, u16, u16, u16, u16, u16, String)>> {
    run_async_with_runtime(
        select_additional_info_data_async(
            ip,
            port,
            user,
            pw,
            namespace,
            db_name,
            table_name,
            run_id,
            path_name,
            select_type,
        ),
        "Error selecting additional info data",
    )
}

#[pyfunction]
fn select_measurement_data(
    ip: &str,
    port: &str,
    user: &str,
    pw: &str,
    namespace: &str,
    db_name: &str,
    table_name: &str,
    run_id: &str,
    path_name: &str,
    select_type: u8,
) -> PyResult<
    Vec<(
        u64,
        u8,
        u64,
        u64,
        u16,
        u16,
        u16,
        u16,
        u16,
        String,
        Vec<String>,
    )>,
> {
    run_async_with_runtime(
        select_measurement_data_async(
            ip,
            port,
            user,
            pw,
            namespace,
            db_name,
            table_name,
            run_id,
            path_name,
            select_type,
        ),
        "Error selecting measurement data",
    )
}

#[pyfunction]
fn select_raw_data(
    ip: &str,
    port: &str,
    user: &str,
    pw: &str,
    namespace: &str,
    db_name: &str,
    table_name: &str,
    run_id: &str,
) -> PyResult<Vec<(u64, u8, i32, String, u32)>> {
    run_async_with_runtime(
        select_raw_data_async(ip, port, user, pw, namespace, db_name, table_name, run_id),
        "Error selecting raw data",
    )
}

pub async fn select_ai_data_async(
    ip: &str,
    port: &str,
    user: &str,
    pw: &str,
    namespace: &str,
    db_name: &str,
    table_name: &str,
    run_id: &str,
    path_name: &str,
    select_type: u8,
) -> Result<Vec<(u8, u8, String, u16, u64)>, Box<dyn Error>> {
    let db = db::connect_to_db(ip, port, user, pw, namespace, db_name).await?;
    let result = db::query_ai_data(&db, table_name, run_id).await?;
    //println!("Result before processing: {:?}", result);
    let data = process::process_ai_data(result, path_name, select_type).await?;
    //println!("Processed data: {:?}", data);
    Ok(data)
}

pub async fn select_di_data_async(
    ip: &str,
    port: &str,
    user: &str,
    pw: &str,
    namespace: &str,
    db_name: &str,
    table_name: &str,
    run_id: &str,
    path_name: &str,
    select_type: u8,
) -> Result<Vec<(u8, u8, String, bool, u64)>, Box<dyn Error>> {
    let db = db::connect_to_db(ip, port, user, pw, namespace, db_name).await?;
    let result = db::query_di_data(&db, table_name, run_id).await?;
    //println!("Result before processing: {:?}", result);

    let data = process::process_di_data(result, path_name, select_type).await?;
    //println!("Processed data: {:?}", data);
    Ok(data)
}

// Main function that uses both helper functions
pub async fn select_additional_info_data_async(
    ip: &str,
    port: &str,
    user: &str,
    pw: &str,
    namespace: &str,
    db_name: &str,
    table_name: &str,
    run_id: &str,
    path_name: &str,
    select_type: u8,
) -> Result<Vec<(u64, u16, u8, u16, u16, u16, u16, u16, u16, String)>, Box<dyn Error>> {
    let db = db::connect_to_db(ip, port, user, pw, namespace, db_name).await?;
    let (static_info, result) = tokio::try_join!(
        db::query_amv_static_info(&db, "amv_static_info", run_id),
        db::query_additonal_info_data(&db, table_name, run_id)
    )?;
    let static_info = static_info.ok_or_else(|| {
        IoError::new(
            ErrorKind::NotFound,
            "no amv_static_info record found for run_id and no fallback record available",
        )
    })?;
    let ip = format!(
        "{}.{}.{}.{}",
        static_info.ip_address[0],
        static_info.ip_address[1],
        static_info.ip_address[2],
        static_info.ip_address[3]
    );

    let number_of_channels = static_info.number_of_channels;
    let data = process::process_additonal_info_data(
        result,
        &ip,
        path_name,
        select_type,
        number_of_channels,
    )
    .await?;
    Ok(data)
}

// Main function that uses both helper functions
pub async fn select_measurement_data_async(
    ip: &str,
    port: &str,
    user: &str,
    pw: &str,
    namespace: &str,
    db_name: &str,
    table_name: &str,
    run_id: &str,
    path_name: &str,
    select_type: u8,
) -> Result<
    Vec<(
        u64,
        u8,
        u64,
        u64,
        u16,
        u16,
        u16,
        u16,
        u16,
        String,
        Vec<String>,
    )>,
    Box<dyn Error>,
> {
    let db = db::connect_to_db(ip, port, user, pw, namespace, db_name).await?;

    let (static_info, result) = tokio::try_join!(
        db::query_amv_static_info(&db, "amv_static_info", run_id),
        db::query_measurement_data(&db, table_name, run_id)
    )?;
    let static_info = static_info.ok_or_else(|| {
        IoError::new(
            ErrorKind::NotFound,
            "no amv_static_info record found for run_id and no fallback record available",
        )
    })?;
    let ip = format!(
        "{}.{}.{}.{}",
        static_info.ip_address[0],
        static_info.ip_address[1],
        static_info.ip_address[2],
        static_info.ip_address[3]
    );
    let number_of_channels = static_info.number_of_channels;
    let data =
        process::process_measurement_data(result, &ip, path_name, select_type, number_of_channels)
            .await?;
    Ok(data)
}

// Main function that uses both helper functions
pub async fn select_raw_data_async(
    ip: &str,
    port: &str,
    user: &str,
    pw: &str,
    namespace: &str,
    db_name: &str,
    table_name: &str,
    run_id: &str,
) -> Result<Vec<(u64, u8, i32, String, u32)>, Box<dyn Error>> {
    let db = db::connect_to_db(ip, port, user, pw, namespace, db_name).await?;
    let result = db::query_raw_data(&db, table_name, run_id).await?;
    let data = process::process_raw_data(result).await?;
    Ok(data)
}
