use std::path::PathBuf;

use crate::help_msg::HelpMessage;
use crate::handlers::{adapt_path, read_lines};

/// Function which creates the Bad USB script contents for executing a file on either Windows or Unix systems.
///
///Takes the OS type as &str, the source_file as &[`std::path::PathBuf`], the destination as &str,
///and 2 *booleans* 'd_flag' and 'm_flag' which are set to true respectively if their options '-d'
///or '-m' were used. 
///
/// Function runs after [fn@make_file_boilerplate] but before [fn@end_boilerplate].
pub fn executable_boilerplate(os_type: &str, source_file: &PathBuf, destination : &str, d_flag: bool, m_flag: bool) -> Result<String, HelpMessage> {
    let mut execute_string : String = String::new();

    let mod_dest : Vec<&str> = destination.split('.').collect();
    let no_suffix_dest : &str = mod_dest[0];

    let file_name : &str = match &source_file.file_name() {
        Some(x) => x.to_str().unwrap(),
        None => panic!("No file name. Unrecoverable error."),
    };
    let adapted_path : PathBuf = match adapt_path(&source_file, os_type) {
        Ok(x) => x,
        Err(e) => return Err(e),
    };
    let mut no_prefix_adapted_path : &str = "";
    if os_type.to_lowercase() == "windows" {
        no_prefix_adapted_path = match adapted_path.strip_prefix("\\") {
            Ok(x) => x.to_str().unwrap(),
            Err(_e) => {eprintln!("No prefix: Continuing..."); adapted_path.to_str().unwrap()},
        };

    } else if os_type.to_lowercase() == "unix" {
        no_prefix_adapted_path = match adapted_path.strip_prefix("/") {
            Ok(x) => x.to_str().unwrap(),
            Err(_e) => {eprintln!("No prefix: Continuing..."); adapted_path.to_str().unwrap()},
        };

    }

    if os_type.to_lowercase() == "windows" {
        execute_string.push_str(
                "DELAY 100\n\
                STRINGLN Set-ExecutionPolicy RemoteSigned -Scope CurrentUser\n\
                DELAY 200\n\
                ");

    } 
    if d_flag {
        if os_type.to_lowercase() == "windows" {
            execute_string.push_str(format!(
                    "STRINGLN $code = Get-Content $HOME\\{}\\{} -Raw\n\
                    DELAY 400\n\
                    STRINGLN Invoke-Expression $code\n\
                    ", no_suffix_dest, no_prefix_adapted_path).as_str());

        } else if os_type.to_lowercase() == "unix" {
            execute_string.push_str(format!(
                    "STRINGLN chmod +x $HOME/{}/{}\n\
                    DELAY 100\n\
                    STRINGLN $HOME/{}/{}\n\
                    ",no_suffix_dest, no_prefix_adapted_path, no_suffix_dest , no_prefix_adapted_path).as_str());

        } else {
            return Err(HelpMessage::WrongArgOS)
        }

    } else if m_flag {
        if os_type.to_lowercase() == "windows" {
            execute_string.push_str(format!(
                    "STRINGLN $code = Get-Content $HOME\\{}\\{} -Raw\n\
                    DELAY 400\n\
                    STRINGLN Invoke-Expression $code\n\
                    ", no_suffix_dest, file_name).as_str());

        } else if os_type.to_lowercase() == "unix" {
            execute_string.push_str(format!(
                    "STRINGLN chmod +x $HOME/{}/{}\n\
                    DELAY 100\n\
                    STRINGLN $HOME/{}/{}\n\
                    ", no_suffix_dest, file_name, no_suffix_dest, file_name).as_str());

        } else {
            return Err(HelpMessage::WrongArgOS)
        }

        //Note that even in the below standard case where we just generate 1 file, we still need to
        //specify the name of the file we want to execute as that is the name of the file that will
        //be added to the execution busbi script. 
    } else {
        if os_type.to_lowercase() == "windows" {
            execute_string.push_str(format!(
                    "STRINGLN $code = Get-Content $HOME\\{} -Raw\n\
                    DELAY 400\n\
                    STRINGLN Invoke-Expression $code\n\
                    ",file_name).as_str());

        } else if os_type.to_lowercase() == "unix" {
            execute_string.push_str(format!(
                    "STRINGLN chmod +x $HOME/{}\n\
                    DELAY 100\n\
                    STRINGLN $HOME/{}\n\
                    ", file_name, file_name).as_str());

        } else {
            return Err(HelpMessage::WrongArgOS)
        }

    }
    Ok(execute_string)
}

/// Function reads the File it is given and turns a copy of the contents into Bad USB script, 
/// mostly by prepending 'STRINGLN' to each line. 
/// Function is also in charge of creating Bad USB script that will create directories and folders
/// if necessary, as specified by the flag options the user uses.  
///
/// The function takes the OS type as &str, the file path as &[`std::path::PathBuf`] (source_file), the
/// dest as &str (AKA the destination we specified), and 2 *booleans* 'd_flag' and 'm_flag' to
/// indicate if either the 'm' or 'd' flags were used.     
///
/// Funtion Returns a string of Bad USB script based on the modified contents of the 'source_file' 
/// and any additional Bad USB script for creating files and folders.
///
/// Function runs after [fn@start_boilerplate] but before [fn@executable_boilerplate] and [fn@end_boilerplate].
pub fn make_file_boilerplate(os_type: &str, source_file: &PathBuf, dest: &str, d_flag: bool, m_flag: bool) -> String {
    let mut mf_string: String = String::new();
    // Can only call .parent() on a PathBuf that is valid for the current OS (on which the command
    // runs). It doesn't work if i modify the path to the target OS first and then try to call
    // .parent() on it. That's why I do this before here, to create 2 seperate PathBuf's, one for
    // creating directories and the other for making the file itself.  
    let path_parent : PathBuf = match &source_file.parent() {
        Some(x) => PathBuf::from(x),
        None => panic!("Unrecoverable, failed to get parent path."),
    };
    let mod_path_parent :PathBuf = match adapt_path(&path_parent, os_type) {
        Ok(x) => x,
        Err(e) => panic!("Unrecoverable error processing changes to path. Got: {:?}", e),
    };   
    let mod_path : PathBuf = match adapt_path(&source_file, os_type) {
        Ok(x) => x,
        Err(e) => panic!("Unrecoverable error processing changes to path. Got: {:?}", e),
    };
    let file_name : &str = match &source_file.file_name() {
        Some(x) => x.to_str().unwrap(),
        None => panic!("No file name. Unrecoverable error."),
    };
    // To get rid of the file extensions as it looks weird to call a directory 'script.txt'.
    let mod_dest : Vec<&str> = dest.split('.').collect();
    let no_suffix_dest : &str = mod_dest[0];

    if os_type.to_lowercase() == "windows" {
        if d_flag {
            mf_string.push_str(format!(
                "STRINGLN New-Item -ItemType Directory -Path \"$HOME\\{}\\{}\" -Force\n\
                " , no_suffix_dest, mod_path_parent.display()).as_str())
        }
        mf_string.push_str(
        "STRINGLN $file = @'\n\
        ");
    } else if os_type.to_lowercase() == "unix" {
        if d_flag {
            mf_string.push_str(format!(
                "STRINGLN mkdir -p $HOME/{}/{}\n\
                STRINGLN cat > $HOME/{}/{}\n\
                " ,no_suffix_dest, mod_path_parent.display(), 
                &no_suffix_dest, mod_path.display()
                ).as_str());

        } else if m_flag {
            mf_string.push_str(format!(
                "STRINGLN cat > $HOME/{}/{}\n\
                ", &no_suffix_dest, file_name).as_str());
        } else {
            //This should use regular 'dest' as it's the normal base case where the user doesn't do
            //multi file creation. 
            mf_string.push_str(format!(
                "STRINGLN cat > $HOME/{}\n\
                ", file_name).as_str());

        }
    }
    if let Ok(lines) = read_lines(&source_file) {
        for line in lines.map_while(Result::ok) {
            let mut mod_line : String = line.clone();
            // Its necessary to check if the line is empty. The ducky script runs so fast that
            // especially on windows a STRINGLN with nothing after will be printed out as
            // "TRINGLN", without the "S" at the start, which powershell just loses for some
            // reason. this check is to mitigate that and try to write the lines correctly.
            match line.trim().is_empty() {
                true => {
                    mod_line.push_str("ENTER\n");
                    mf_string.push_str(mod_line.as_str());
                },
                false => {
                    //Changes to the line applied here before being written to the buffer.
                    mod_line.insert_str(0, "STRINGLN ");
                    mod_line.push_str("\n");
                    mf_string.push_str(mod_line.as_str());
                },
            }

        }
    } else {
        println!("Err: File you specified doesn't exist or something else went wrong. Your file: {}", &source_file.display());
        panic!("Stopped due to above error.")
    }
    if os_type.to_lowercase() == "windows" {
        if m_flag {
            mf_string.push_str(format!(
                "STRINGLN '@\n\
                STRINGLN Set-Content -Path $HOME\\{}\\{} -Value $file\n\
                ", &no_suffix_dest, file_name).as_str());

        } else if d_flag {
            mf_string.push_str(format!(
                "STRINGLN '@\n\
                STRINGLN Set-Content -Path $HOME\\{}\\{} -Value $file\n\
                ", &no_suffix_dest, mod_path.display()).as_str());
        } else {
            mf_string.push_str(format!(
                "STRINGLN '@\n\
                STRINGLN Set-Content -Path $HOME\\{} -Value $file\n\
                ", dest).as_str());

        }
    } else if os_type.to_lowercase() == "unix" {
        mf_string.push_str(
        "CTRL d\n\
        ");
    }
    mf_string
}

/// Function which creates the string added at the start of a Bad USB script.
///
/// Takes the OS type as &str, a *boolean* 'is_dir' to set wether the target file is a directory if
/// true, and a &str dest, which is either the file or directory busbi is targeting for turning into a
/// Bad USB script.  
///
/// Function runs before all other boilerplate functions, after which they run in this order:
/// 1) [fn@make_file_boilerplate] 
/// 2) [fn@executable_boilerplate]
/// 3) [fn@end_boilerplate].
pub fn start_boilerplate(os_type: &str, is_dir : bool ,dest: &str) -> String {
    
    let mut os_start_string :String = String::new(); 
    let mod_dest : Vec<&str> = dest.split('.').collect();
    let no_suffix_dest : &str = mod_dest[0];

    if os_type.to_lowercase() == "windows" {
        os_start_string.push_str(
            "WINDOWS\n\
            DELAY 400\n\
            STRING powershell\n\
            DELAY 200\n\
            ENTER\n\
            DELAY 1000\n\
        ");
        if is_dir {
            os_start_string.push_str(format!(
            "STRINGLN New-Item -Path \"$HOME\\{}\" -Type Directory\n\
            ", no_suffix_dest).as_str());
        }
        
    } else if os_type.to_lowercase() == "unix" {
        os_start_string.push_str(
            "GUI\n\
            DELAY 400\n\
            STRING terminal\n\
            DELAY 200\n\
            ENTER\n\
            DELAY 400\n\
        ");
        if is_dir {
            os_start_string.push_str(format!(
                "STRINGLN mkdir $HOME/{}\n\
            ", no_suffix_dest).as_str());
        }
    } 
    os_start_string

}

//Whatever String this function produces will be added to the end of any file produced.
//Use it as you see fit. Currently only runs if '-x' flag is NOT true.
//TODO make this more soffisticated with '-x' so both can be true at the same time. 
/// Function that outputs a string to append at the end of the bad USB file. 
/// Takes a &str which defines the OS type and a *boolean* used to decide wether the script 
/// should close the powershell or terminal window at the end of execution. 
///
/// This function currently is not run if the `-x` flag is used, as it could easily cause 
/// conflicts. Since we don't know how long a called script will take to run we can't arbitrarily
/// close a window. 
/// Possible future feature where execution spawns another process to run the script instead of
/// being tied to the current terminal/shell window.
///
/// # Example 
///
/// ```
/// let end_string = end_boilerplate("windows", true);
/// println!(end_string);
/// // Will output as String:
/// "STRINGLN exit\n"
///
/// // OR 
/// let end_string = end_boilerplate("windows", false);
/// println!(end_string);
/// //Will output as String:
/// "STRING end of programn ! Do what you want now...\n"
/// ```
pub fn end_boilerplate(os_type: &str , close_window : bool) -> String {
    let mut os_end_string: String = String::new();
    if close_window {
        os_end_string.push_str("STRINGLN exit\n");


    } else {
        if os_type.to_lowercase() == "windows" {
            os_end_string.push_str(
                "STRING end of programn ! Do what you want now...\n");

        } else if os_type.to_lowercase() == "unix" {
            os_end_string.push_str(
                "STRING end of programn! Do other stuff here if you want now...\n");

        } else {
            panic!("Something went wrong the boilerplate start function got the wrong OS type: {}", os_type);
        }
    }
    os_end_string
}

