use std::{collections::HashMap, error::Error, str::FromStr};

use capctl::Cap;
use chrono::Duration;
use indexmap::IndexSet;
use log::{debug, warn};
use pest::iterators::Pair;

use crate::cli::data::{Convertion, RoleType, TaskType};
use rar_common::{
    database::{
        actor::{SActor, SGroupType},
        options::{EnvBehavior, OptType, PathBehavior, TimestampType, WorkdirBehavior},
        structs::{IdTask, SetBehavior},
    },
    util::StorageMethod,
};

use super::data::{InputAction, Inputs, Rule, SetListType, TimeoutOpt};

type MatchingFunction = dyn Fn(&Pair<Rule>, &mut Inputs) -> Result<(), Box<dyn Error>>;

fn recurse_pair_with_action(
    pair: Pair<Rule>,
    inputs: &mut Inputs,
    do_matching: &MatchingFunction,
) -> Result<(), Box<dyn Error>> {
    for inner_pair in pair.into_inner() {
        do_matching(&inner_pair, inputs)?;
        recurse_pair(inner_pair, inputs)?;
    }
    Ok(())
}

pub fn recurse_pair(pair: Pair<Rule>, inputs: &mut Inputs) -> Result<(), Box<dyn Error>> {
    recurse_pair_with_action(pair, inputs, &match_pair)
}

#[allow(clippy::too_many_lines)]
fn match_pair(pair: &Pair<Rule>, inputs: &mut Inputs) -> Result<(), Box<dyn Error>> {
    match pair.as_rule() {
        Rule::help => {
            inputs.action = InputAction::Help;
        }
        Rule::list => {
            inputs.action = InputAction::List;
        }
        Rule::set => {
            inputs.action = InputAction::Set;
        }
        Rule::add | Rule::grant => {
            inputs.action = InputAction::Add;
        }
        Rule::del | Rule::revoke => {
            inputs.action = InputAction::Del;
        }
        Rule::purge => {
            inputs.action = InputAction::Purge;
        }
        Rule::whitelist => {
            inputs.setlist_type = Some(SetListType::White);
        }
        Rule::blacklist => {
            inputs.setlist_type = Some(SetListType::Black);
        }
        Rule::checklist => {
            inputs.setlist_type = Some(SetListType::Check);
        }
        Rule::setlist => {
            inputs.setlist_type = Some(SetListType::Set);
        }
        Rule::convert => {
            inputs.action = InputAction::Convert;
        }
        Rule::to => {
            let mut inner = pair.clone().into_inner();
            let temp_convertion = Convertion::default();
            let convertion = inputs.convertion.get_or_insert(temp_convertion);
            convertion.to_type = inner
                .next()
                .ok_or("No type specified for convertion")?
                .as_str()
                .to_lowercase()
                .parse()
                .inspect_err(|e| {
                    warn!(
                        "Unknown type {}, types available : {}",
                        e,
                        StorageMethod::VARIANTS.join(", ")
                    );
                })?;
            convertion.to = inner.next().ok_or("to_value not found")?.as_str().into();
        }
        Rule::from => {
            let mut inner = pair.clone().into_inner();
            let temp_convertion = Convertion::default();
            let convertion = inputs.convertion.get_or_insert(temp_convertion);
            convertion.from_type = Some(
                inner
                    .next()
                    .expect("from_type not found")
                    .as_str()
                    .to_lowercase()
                    .parse()
                    .inspect_err(|e| {
                        warn!(
                            "Unknown type {}, types available : {}",
                            e,
                            StorageMethod::VARIANTS.join(", ")
                        );
                    })?,
            );
            convertion.from = Some(inner.next().expect("from_value not found").as_str().into());
        }
        Rule::convert_reconfigure => {
            inputs.convert_reconfigure = true;
        }
        // === setpolicies ===
        Rule::cmd_policy => {
            inputs.action = InputAction::Set;
            if pair.as_str() == "deny-all" {
                inputs.cmd_policy = Some(SetBehavior::None);
            } else if pair.as_str() == "allow-all" {
                inputs.cmd_policy = Some(SetBehavior::All);
            } else {
                unreachable!("Unknown cmd policy: {}", pair.as_str())
            }
        }
        Rule::caps_policy => {
            inputs.action = InputAction::Set;
            if pair.as_str() == "deny-all" {
                inputs.cred_policy = Some(SetBehavior::None);
            } else if pair.as_str() == "allow-all" {
                inputs.cred_policy = Some(SetBehavior::All);
            } else {
                unreachable!("Unknown caps policy: {}", pair.as_str())
            }
        }
        Rule::path_policy => {
            inputs.action = InputAction::Set;
            if pair.as_str() == "delete-all" {
                inputs.options_path_policy = Some(PathBehavior::Delete);
            } else if pair.as_str() == "keep-safe" {
                inputs.options_path_policy = Some(PathBehavior::KeepSafe);
            } else if pair.as_str() == "keep-unsafe" {
                inputs.options_path_policy = Some(PathBehavior::KeepUnsafe);
            } else if pair.as_str() == "inherit" {
                inputs.options_path_policy = Some(PathBehavior::Inherit);
            } else {
                unreachable!("Unknown path policy: {}", pair.as_str())
            }
        }
        Rule::env_policy => {
            inputs.action = InputAction::Set;
            if pair.as_str() == "delete-all" {
                inputs.options_env_policy = Some(EnvBehavior::Delete);
            } else if pair.as_str() == "keep-all" {
                inputs.options_env_policy = Some(EnvBehavior::Keep);
            } else if pair.as_str() == "inherit" {
                inputs.options_env_policy = Some(EnvBehavior::Inherit);
            } else {
                unreachable!("Unknown env policy: {}", pair.as_str())
            }
        }
        Rule::workdir_policy => {
            inputs.action = InputAction::Set;
            inputs.options_workdir_policy = Some(WorkdirBehavior::const_parse(pair.as_str()));
        }
        // === timeout ===
        Rule::time => {
            let mut reversed = pair.as_str().split(':').rev();
            let mut duration: Duration = Duration::try_seconds(
                reversed
                    .next()
                    .ok_or("No seconds in time")?
                    .parse::<i64>()
                    .unwrap_or(0),
            )
            .unwrap_or_default();
            if let Some(mins) = reversed.next() {
                duration = duration
                    .checked_add(
                        &Duration::try_minutes(mins.parse::<i64>().unwrap_or(0))
                            .unwrap_or_default(),
                    )
                    .expect("Invalid minutes");
                if let Some(hours) = reversed.next() {
                    duration = duration
                        .checked_add(
                            &Duration::try_hours(hours.parse::<i64>().unwrap_or(0))
                                .unwrap_or_default(),
                        )
                        .expect("Invalid hours");
                }
            }
            inputs.timeout_duration = Some(duration);
        }
        Rule::opt_timeout_type => {
            if pair.as_str() == "tty" {
                inputs.timeout_type = Some(TimestampType::TTY);
            } else if pair.as_str() == "ppid" {
                inputs.timeout_type = Some(TimestampType::PPID);
            } else if pair.as_str() == "uid" {
                inputs.timeout_type = Some(TimestampType::UID);
            } else {
                unreachable!("Unknown timeout type: {}", pair.as_str())
            }
        }
        Rule::opt_timeout_t_arg => {
            let mut timeout_arg = inputs.timeout_arg.unwrap_or_default();
            timeout_arg[TimeoutOpt::Type as usize] = true;
            inputs.timeout_arg.replace(timeout_arg);
        }
        Rule::opt_timeout_d_arg => {
            let mut timeout_arg = inputs.timeout_arg.unwrap_or_default();
            timeout_arg[TimeoutOpt::Duration as usize] = true;
            inputs.timeout_arg.replace(timeout_arg);
        }
        Rule::opt_timeout_m_arg => {
            let mut timeout_arg = inputs.timeout_arg.unwrap_or_default();
            timeout_arg[TimeoutOpt::MaxUsage as usize] = true;
            inputs.timeout_arg.replace(timeout_arg);
        }
        Rule::opt_timeout_max_usage => {
            inputs.timeout_max_usage = Some(pair.as_str().parse::<u64>()?);
        }
        // === file ===
        Rule::file => {
            inputs.policy = true;
        }
        Rule::policy_path => {
            inputs.policy_path = Some(pair.as_str().to_string());
        }
        // === roles ===
        Rule::role_id => {
            inputs.role_id = Some(pair.as_str().to_string());
        }
        Rule::role_type_arg => {
            if pair.as_str() == "all" {
                inputs.role_type = Some(RoleType::All);
            } else if pair.as_str() == "actors" {
                inputs.role_type = Some(RoleType::Actors);
            } else if pair.as_str() == "tasks" {
                inputs.role_type = Some(RoleType::Tasks);
            } else {
                unreachable!("Unknown role type: {}", pair.as_str())
            }
        }
        // === actors ===
        Rule::user => {
            for pair in pair.clone().into_inner() {
                debug!("user {:?}", pair.as_str());
                inputs
                    .actors
                    .get_or_insert_default()
                    .push(SActor::user(pair.as_str()).build());
            }
        }

        Rule::group => {
            fn inner_recurse(pair: &Pair<Rule>, vec: &mut Vec<String>) {
                for pair in pair.clone().into_inner() {
                    if pair.as_rule() == Rule::actor_name {
                        vec.push(pair.as_str().into());
                    }
                    inner_recurse(&pair, vec);
                }
            }
            let mut vec: Vec<String> = Vec::new();
            for pair in pair.clone().into_inner() {
                inner_recurse(&pair, &mut vec);
            }
            if vec.is_empty() {
                warn!("No group specified");
            } else if vec.len() == 1 {
                inputs
                    .actors
                    .get_or_insert_default()
                    .push(SActor::group(vec[0].as_str()).build());
            } else {
                inputs.actors.get_or_insert_default().push(
                    SActor::group(
                        vec.iter()
                            .map(|s| SGroupType::from(s.as_str()))
                            .collect::<Vec<_>>(),
                    )
                    .build(),
                );
            }
            debug!("actors: {:?}", inputs.actors);
        }
        // === tasks ===
        Rule::task_id => {
            inputs.task_id = Some(IdTask::Name(pair.as_str().to_string()));
        }
        Rule::task_type_arg => {
            if pair.as_str() == "all" {
                inputs.task_type = Some(TaskType::All);
            } else if pair.as_str() == "commands" || pair.as_str() == "cmd" {
                inputs.task_type = Some(TaskType::Commands);
            } else if pair.as_str().starts_with("cred") {
                inputs.task_type = Some(TaskType::Credentials);
            } else {
                unreachable!("Unknown task type: {}", pair.as_str());
            }
        }
        // === commands ===
        Rule::cmd => {
            inputs.cmd_id = Some(shell_words::split(pair.as_str())?);
        }
        // === credentials ===
        Rule::capability => {
            if let Ok(cap) = Cap::from_str(pair.as_str()) {
                inputs.cred_caps.get_or_insert_default().add(cap);
            } else {
                warn!("Unknown capability: {}", pair.as_str());
            }
        }
        Rule::cred_u => {
            inputs.cred_setuid = Some(
                pair.as_str()
                    .chars()
                    .skip(9)
                    .collect::<String>()
                    .as_str()
                    .into(),
            );
        }
        Rule::cred_g => {
            let mut vec: Vec<SGroupType> = Vec::new();
            for pair in pair.clone().into_inner() {
                if pair.as_rule() == Rule::actor_name {
                    vec.push(pair.as_str().into());
                }
            }
            if vec.is_empty() {
                warn!("No group specified");
            }
            inputs.cred_setgid = Some(vec.into());
        }
        // === options ===
        Rule::options_operations => {
            inputs.options = true;
        }
        Rule::opt_env => {
            inputs.options_type = Some(OptType::Env);
        }
        Rule::opt_env_listing => {
            inputs.options_key_env = Some(IndexSet::new());
        }
        Rule::opt_env_setlisting => {
            inputs.setlist_type = Some(SetListType::Set);
            inputs.options_env_values = Some(HashMap::new());
            inputs.options_key_env = Some(IndexSet::new());
        }
        Rule::opt_env_keep => {
            inputs.action = InputAction::Set;
            inputs.options_env_policy = Some(EnvBehavior::Delete);
            inputs.options_key_env = Some(IndexSet::new());
        }
        Rule::opt_env_delete => {
            inputs.action = InputAction::Set;
            inputs.options_env_policy = Some(EnvBehavior::Keep);
            inputs.options_key_env = Some(IndexSet::new());
        }
        Rule::opt_env_set => {
            inputs.action = InputAction::Set;
            inputs.setlist_type = Some(SetListType::Set);
            inputs.options_env_values = Some(HashMap::new());
        }

        Rule::opt_path => {
            inputs.options_type = Some(OptType::Path);
        }
        Rule::opt_workdir => {
            inputs.options_type = Some(OptType::Workdir);
        }
        Rule::opt_show_arg => {
            if pair.as_str() == "all" {
                inputs.options_type = None;
            } else if pair.as_str() == "path" {
                inputs.options_type = Some(OptType::Path);
            } else if pair.as_str() == "env" {
                inputs.options_type = Some(OptType::Env);
            } else if pair.as_str() == "root" {
                inputs.options_type = Some(OptType::Root);
            } else if pair.as_str() == "bounding" {
                inputs.options_type = Some(OptType::Bounding);
            } else if pair.as_str() == "timeout" {
                inputs.options_type = Some(OptType::Timeout);
            } else if pair.as_str() == "authentication" {
                inputs.options_type = Some(OptType::Authentication);
            } else if pair.as_str() == "execinfo" {
                inputs.options_type = Some(OptType::ExecInfo);
            } else if pair.as_str() == "umask" {
                inputs.options_type = Some(OptType::UMask);
            } else {
                unreachable!("Unknown option type: {}", pair.as_str())
            }
        }
        Rule::path => {
            inputs.options_path = Some(pair.as_str().to_string());
        }
        Rule::env_key_value => {
            if let Some(options_env_values) = inputs.options_env_values.as_mut() {
                let mut inner = pair.clone().into_inner();
                let key = inner
                    .next()
                    .expect("env_key_value parsing must have a key, implementation error")
                    .as_str()
                    .to_string();
                let value = inner
                    .next()
                    .expect("env_key_value parsing must have a value, implementation error")
                    .into_inner()
                    .next()
                    .expect("env_key_value parsing must have a value, implementation error")
                    .as_str()
                    .to_string();
                debug!("env_key_value: {key}={value}");
                options_env_values.insert(key, value);
            }
        }
        Rule::env_key => {
            if let Some(options_env) = inputs.options_key_env.as_mut() {
                options_env.insert(pair.as_str().into());
            }
        }
        Rule::opt_root_args => {
            inputs.action = InputAction::Set; // If del it will be overwritten by the parse loop
            inputs.options_root = Some(pair.as_str().parse().unwrap_or_default());
        }
        Rule::opt_bounding_args => {
            inputs.action = InputAction::Set; // If del it will be overwritten by the parse loop
            inputs.options_bounding = Some(pair.as_str().parse().unwrap_or_default());
        }
        Rule::opt_skip_auth_args => {
            inputs.action = InputAction::Set; // If del it will be overwritten by the parse loop
            inputs.options_auth = Some(pair.as_str().parse().unwrap_or_default());
        }
        Rule::opt_execinfo_args => {
            inputs.action = InputAction::Set; // If del it will be overwritten by the parse loop
            inputs.options_execinfo = Some(pair.as_str().parse().unwrap_or_default());
        }
        Rule::opt_mask_args => {
            inputs.action = InputAction::Set; // If del it will be overwritten by the parse loop
            inputs.options_umask = Some(pair.as_str().parse().unwrap_or_default());
        }
        Rule::all => {
            if inputs.role_id.is_some() && inputs.task_id.is_none() {
                inputs.role_type = Some(RoleType::All);
            } else if inputs.task_id.is_some() {
                inputs.task_type = Some(TaskType::All);
            }
        }
        Rule::editor => {
            debug!("Editor mode enabled");
            inputs.editor = true;
        }
        Rule::editor_type => {
            let mut inner = pair.clone().into_inner();
            inputs.editor_type = Some(
                inner
                    .next()
                    .expect("from_type not found")
                    .as_str()
                    .to_lowercase()
                    .parse()
                    .inspect_err(|e| {
                        warn!(
                            "Unknown type {}, types available : {}",
                            e,
                            StorageMethod::VARIANTS.join(", ")
                        );
                    })?,
            );
        }
        Rule::editor_path => {
            inputs.editor_path = Some(pair.as_str().into());
        }
        _ => {
            debug!("Unmatched rule: {:?}", pair.as_rule());
        }
    }
    Ok(())
}

#[cfg(test)]
mod test {
    use crate::{
        cli::{
            data::{Cli, RoleType},
            pair::{InputAction, Rule, recurse_pair},
        },
        util::underline,
    };
    use pest::Parser;

    use rar_common::{
        database::actor::SActor,
        util::{BOLD, RED, RST},
    };

    use super::Inputs;

    fn make_args(args: &str) -> String {
        shell_words::join(shell_words::split(args).expect("Failed to split args"))
    }

    fn get_inputs(args: &str) -> Inputs {
        let binding = make_args(args);
        let args = Cli::parse(Rule::cli, &binding);
        let args = match args {
            Ok(v) => v,
            Err(e) => {
                println!(
                    "{RED}{BOLD}Unrecognized command line:\n| {RST}{}{RED}{BOLD}\n| {}\n= {}{RST}",
                    e.line(),
                    underline(&e),
                    e.variant.message(),
                    RED = RED,
                    BOLD = BOLD,
                    RST = RST
                );
                panic!("Error parsing args");
            }
        };
        let mut inputs = Inputs::default();
        for pair in args {
            assert!(recurse_pair(pair, &mut inputs).is_ok());
        }
        inputs
    }

    #[test]
    fn test_grant() {
        let inputs = get_inputs("role r1 grant -u u1 -u u2 -g g1,g2");
        assert_eq!(inputs.role_id, Some("r1".to_string()));
        assert_eq!(inputs.action, InputAction::Add);
        assert_eq!(
            inputs.actors,
            Some(vec![
                SActor::user("u1").build(),
                SActor::user("u2").build(),
                SActor::group(["g1", "g2"]).build()
            ])
        );
    }

    #[test]
    fn test_list_roles() {
        let inputs = get_inputs("list");
        assert_eq!(inputs.action, InputAction::List);
    }

    #[test]
    fn test_list_role() {
        let inputs = get_inputs("role r1 show");
        assert_eq!(inputs.action, InputAction::List);
        assert_eq!(inputs.role_id, Some("r1".to_string()));
    }

    #[test]
    fn test_list_role_actors() {
        let inputs = get_inputs("r r1 l actors");
        assert_eq!(inputs.action, InputAction::List);
        assert_eq!(inputs.role_id, Some("r1".to_string()));
        assert_eq!(inputs.role_type, Some(RoleType::Actors));
    }

    #[test]
    fn test_list_role_tasks() {
        let inputs = get_inputs("r r1 l tasks");
        assert_eq!(inputs.action, InputAction::List);
        assert_eq!(inputs.role_id, Some("r1".to_string()));
        assert_eq!(inputs.role_type, Some(RoleType::Tasks));
    }

    #[test]
    fn test_list_role_all() {
        let inputs = get_inputs("r r1 l all");
        assert_eq!(inputs.action, InputAction::List);
        assert_eq!(inputs.role_id, Some("r1".to_string()));
        assert_eq!(inputs.role_type, Some(RoleType::All));
    }
}
