//! Main meka struct and methods

use crate::{
    args::{Commands, MekaArgs, RunArgs},
    errors::MekaError,
    meka_file::MekaFile,
    process::ProcessOptions,
    utils::{convert_to_relative, correct_args, get_path_var},
};
use std::path::PathBuf;

/// Main struct for meka
#[derive(Default)]
pub struct Meka {
    /// Debug level (0 = no debug, higher = more debug)
    debug_level: u8,
}

/// Default meka output file
pub const MEKA_FILE: &str = "meka_recording.json";

impl Meka {
    /// Build meka from args
    pub fn from_args(args: &MekaArgs) -> Self {
        Meka::default().with_debug(args.debug)
    }

    /// Create a new Meka instance
    /// # Errors
    /// Returns `MekaError` if there is an error
    pub fn run_from_args(args: MekaArgs) -> Result<(), MekaError> {
        let meka = Self::from_args(&args);
        match args.command {
            Commands::Record(run_args) => meka.meka_run(run_args),
            Commands::Analyze(analysis_args) => meka.meka_analyze(analysis_args.input_file),
        }
    }

    /// Set the debug level (0 = no debug, higher = more debug)
    pub fn with_debug(mut self, level: u8) -> Self {
        self.debug_level = level;
        self
    }

    /// Should show debug
    pub(crate) fn should_show_debug(&self, debug_level: u8) -> bool {
        debug_level <= self.debug_level
    }

    /// Log a message if the debug level is high enough
    pub(crate) fn logger<P>(&self, debug_level: u8, msg: P)
    where
        P: AsRef<str>,
    {
        let msg = msg.as_ref();
        if self.should_show_debug(debug_level) {
            println!("[meka] {}", &msg);
        }
    }

    /// Process a command and return a map of process data
    /// # Errors
    /// Returns `MekaError` if there is an error
    pub fn meka_run(&self, run_args: RunArgs) -> Result<(), MekaError> {
        let RunArgs {
            capture_environ,
            capture_status,
            cwd,
            path_var,
            output_file,
            inputs,
            wrap_shell,
            ..
        } = run_args;
        let print_json = run_args.json_output;
        let path_var = path_var.unwrap_or(get_path_var());
        let command = correct_args(inputs);
        let options = ProcessOptions {
            capture_environ,
            capture_status,
            cwd,
            wrap_shell,
        };
        let map = self.process_command(path_var, command, options)?;
        let map = if run_args.relative_pid {
            convert_to_relative(map)
        } else {
            map
        };
        let json_data = serde_json::to_string_pretty(&map)
            .map_err(|_| MekaError::new("Cannot stringify json"))?;
        if print_json || output_file == "-" {
            println!("{}", &json_data);
        }
        if output_file != "-" {
            let meka_file = MekaFile { processes: map };
            meka_file.write_json(&PathBuf::from(output_file))?;
        }

        Ok(())
    }

    /// Analyze a meka recording file and print a tree of processes
    /// # Errors
    /// Returns `MekaError` if there is an error
    pub fn meka_analyze(&self, input_file: String) -> Result<(), MekaError> {
        let meka_file = MekaFile::try_from_file(PathBuf::from(&input_file))?;
        let map = meka_file.processes;
        // find index where parent_pid is 0
        let root = map
            .iter()
            .find(|(_, v)| v.parent_pid == 0)
            .ok_or(MekaError::new(format!(
                "Cannot find root process in {}",
                input_file
            )))?;

        let mut stack: Vec<(&i32, usize)> = vec![(root.0, 0)];

        while let Some((pid, space)) = stack.pop() {
            if let Some(proc) = map.get(pid) {
                let cmd = if proc.after_exec.is_empty() {
                    proc.cmdline.join(" ")
                } else {
                    proc.after_exec
                        .iter()
                        .map(|cmd| cmd.join(" "))
                        .collect::<Vec<String>>()
                        .join("THEN")
                };
                println!(
                    "{}{} - {}",
                    " ".repeat(space),
                    proc.pid,
                    &cmd.replace('\n', " ")[..50.min(cmd.len())],
                );

                // Push children in *reverse order* so they print in the same order
                // as the recursive version
                for child_pid in proc.subprograms.iter().rev() {
                    stack.push((child_pid, space + 4));
                }
            }
        }

        // print tree
        Ok(())
    }
}
