use clap::Parser;
use clap::builder::ArgPredicate;
use harmony;
use harmony::{score::Score, topology::Topology};
use inquire::Confirm;

#[cfg(feature = "tui")]
use harmony_tui;

#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Args {
    /// Run score(s) without prompt
    #[arg(short, long, default_value_t = false, conflicts_with = "interactive")]
    yes: bool,

    /// Filter query
    #[arg(short, long, conflicts_with = "interactive")]
    filter: Option<String>,

    /// Run interactive TUI or not
    #[arg(short, long, default_value_t = false)]
    interactive: bool,

    /// Run all or nth, defaults to all
    #[arg(
        short,
        long,
        default_value_t = true,
        default_value_if("number", ArgPredicate::IsPresent, "false"),
        conflicts_with = "number",
        conflicts_with = "interactive"
    )]
    all: bool,

    /// Run nth matching, zero indexed
    #[arg(short, long, default_value_t = 0, conflicts_with = "interactive")]
    number: usize,

    /// list scores, will also be affected by run filter
    #[arg(short, long, default_value_t = false, conflicts_with = "interactive")]
    list: bool,
}

fn maestro_scores_filter<T: Topology>(
    maestro: &harmony::maestro::Maestro<T>,
    all: bool,
    filter: Option<String>,
    number: usize,
) -> Vec<Box<dyn Score<T>>> {
    let scores = maestro.scores();
    let scores_read = scores.read().expect("Should be able to read scores");
    let mut scores_vec: Vec<Box<dyn Score<T>>> = match filter {
        Some(f) => scores_read
            .iter()
            .filter(|s| s.name().contains(&f))
            .map(|s| s.clone_box())
            .collect(),
        None => scores_read.iter().map(|s| s.clone_box()).collect(),
    };

    if !all {
        let score = scores_vec.get(number);
        match score {
            Some(s) => scores_vec = vec![s.clone_box()],
            None => return vec![],
        }
    };

    return scores_vec;
}

// TODO: consider adding doctest for this function
fn list_scores_with_index<T: Topology>(scores_vec: &Vec<Box<dyn Score<T>>>) -> String {
    let mut display_str = String::new();
    for (i, s) in scores_vec.iter().enumerate() {
        let name = s.name();
        display_str.push_str(&format!("\n{i}: {name}"));
    }
    return display_str;
}

pub async fn init<T: Topology + Send + Sync + 'static>(
    maestro: harmony::maestro::Maestro<T>,
    args_struct: Option<Args>,
) -> Result<(), Box<dyn std::error::Error>> {
    let args = match args_struct {
        Some(args) => args,
        None => Args::parse(),
    };

    #[cfg(feature = "tui")]
    if args.interactive {
        return harmony_tui::init(maestro).await;
    }

    #[cfg(not(feature = "tui"))]
    if args.interactive {
        return Err("Not compiled with interactive support".into());
    }

    let _ = env_logger::builder().try_init();

    let scores_vec = maestro_scores_filter(&maestro, args.all, args.filter, args.number);

    if scores_vec.len() == 0 {
        return Err("No score found".into());
    }

    // if list option is specified, print filtered list and exit
    if args.list {
        println!("Available scores:");
        println!("{}", list_scores_with_index(&scores_vec));
        return Ok(());
    }

    // prompt user if --yes is not specified
    if !args.yes {
        let confirmation = Confirm::new(
            format!(
                "This will run the following scores: {}\n",
                list_scores_with_index(&scores_vec)
            )
            .as_str(),
        )
        .with_default(true)
        .prompt()
        .expect("Unexpected prompt error");

        if !confirmation {
            return Ok(());
        }
    }

    // Run filtered scores
    for s in scores_vec {
        println!("Running: {}", s.name());
        maestro.interpret(s).await?;
    }

    Ok(())
}

#[cfg(test)]
mod test {
    use harmony::{
        inventory::Inventory,
        maestro::Maestro,
        modules::dummy::{ErrorScore, PanicScore, SuccessScore},
        topology::HAClusterTopology,
    };

    fn init_test_maestro() -> Maestro<HAClusterTopology> {
        let inventory = Inventory::autoload();
        let topology = HAClusterTopology::autoload();
        let mut maestro = Maestro::new(inventory, topology);

        maestro.register_all(vec![
            Box::new(SuccessScore {}),
            Box::new(ErrorScore {}),
            Box::new(PanicScore {}),
        ]);

        maestro
    }

    #[tokio::test]
    async fn test_init_success_score() {
        let maestro = init_test_maestro();
        let res = crate::init(
            maestro,
            Some(crate::Args {
                yes: true,
                filter: Some("SuccessScore".to_owned()),
                interactive: false,
                all: true,
                number: 0,
                list: false,
            }),
        )
        .await;

        assert!(res.is_ok());
    }

    #[tokio::test]
    async fn test_init_error_score() {
        let maestro = init_test_maestro();

        let res = crate::init(
            maestro,
            Some(crate::Args {
                yes: true,
                filter: Some("ErrorScore".to_owned()),
                interactive: false,
                all: true,
                number: 0,
                list: false,
            }),
        )
        .await;

        assert!(res.is_err());
    }

    #[tokio::test]
    async fn test_init_number_score() {
        let maestro = init_test_maestro();

        let res = crate::init(
            maestro,
            Some(crate::Args {
                yes: true,
                filter: None,
                interactive: false,
                all: false,
                number: 0,
                list: false,
            }),
        )
        .await;

        assert!(res.is_ok());
    }

    #[tokio::test]
    async fn test_filter_fn_all() {
        let maestro = init_test_maestro();

        let res = crate::maestro_scores_filter(&maestro, true, None, 0);

        assert!(res.len() == 3);
    }

    #[tokio::test]
    async fn test_filter_fn_all_success() {
        let maestro = init_test_maestro();

        let res = crate::maestro_scores_filter(&maestro, true, Some("Success".to_owned()), 0);

        assert!(res.len() == 1);

        assert!(
            maestro
                .interpret(res.get(0).unwrap().clone_box())
                .await
                .is_ok()
        );
    }

    #[tokio::test]
    async fn test_filter_fn_all_error() {
        let maestro = init_test_maestro();

        let res = crate::maestro_scores_filter(&maestro, true, Some("Error".to_owned()), 0);

        assert!(res.len() == 1);

        assert!(
            maestro
                .interpret(res.get(0).unwrap().clone_box())
                .await
                .is_err()
        );
    }

    #[tokio::test]
    async fn test_filter_fn_all_score() {
        let maestro = init_test_maestro();

        let res = crate::maestro_scores_filter(&maestro, true, Some("Score".to_owned()), 0);

        assert!(res.len() == 3);

        assert!(
            maestro
                .interpret(res.get(0).unwrap().clone_box())
                .await
                .is_ok()
        );
        assert!(
            maestro
                .interpret(res.get(1).unwrap().clone_box())
                .await
                .is_err()
        );
    }

    #[tokio::test]
    async fn test_filter_fn_number() {
        let maestro = init_test_maestro();

        let res = crate::maestro_scores_filter(&maestro, false, None, 0);

        assert!(res.len() == 1);

        assert!(
            maestro
                .interpret(res.get(0).unwrap().clone_box())
                .await
                .is_ok()
        );
    }

    #[tokio::test]
    async fn test_filter_fn_number_invalid() {
        let maestro = init_test_maestro();

        let res = crate::maestro_scores_filter(&maestro, false, None, 11);

        assert!(res.len() == 0);
    }
}
