use super::{path_to_content, toml_old};
use crate::model::{
    histories::{self},
    runner_type,
};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::{
    env,
    fs::{self, File},
    io::Write,
    path::PathBuf,
};

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub struct Histories {
    histories: Vec<History>,
}

impl Histories {
    pub fn get_history() -> Histories {
        match history_file_path() {
            Some((history_file_dir, history_file_name)) => {
                match path_to_content::path_to_content(&history_file_dir.join(history_file_name)) {
                    Ok(c) => Histories::parse_history_in_considering_history_file_format_version(c),
                    Err(_) => Histories::default(),
                }
            }
            None => Histories::default(),
        }
    }

    fn parse_history_in_considering_history_file_format_version(content: String) -> Histories {
        // NOTE: The history file format has changed after https://github.com/kyu08/fzf-make/pull/324.
        // So at first we try to parse it as the new format, and then try to parse it as the old format.
        match parse_history(content.to_string()) {
            Ok(h) => h,
            Err(_) => toml_old::parse_history(content.to_string()).unwrap_or_default(),
        }
    }

    pub fn new(histories: Vec<History>) -> Self {
        Self { histories }
    }

    pub fn into(self) -> histories::Histories {
        let mut result: Vec<histories::History> = vec![];
        for h in self.histories {
            result.push(h.into());
        }
        histories::Histories { histories: result }
    }
}

impl From<histories::Histories> for Histories {
    fn from(histories: histories::Histories) -> Histories {
        let mut result: Vec<History> = vec![];
        for h in histories.histories {
            result.push(History::from(h));
        }
        Self { histories: result }
    }
}

#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct History {
    path: PathBuf,
    commands: Vec<HistoryCommand>,
}

impl History {
    // Implementing From<file::toml::History> on the model::History side is not desirable due to dependency direction, so the into method is manually defined.
    fn into(self) -> histories::History {
        let mut commands: Vec<histories::HistoryCommand> = vec![];
        for h in self.commands {
            commands.push(HistoryCommand::into(h));
        }

        histories::History {
            path: self.path,
            commands,
        }
    }

    pub fn new(path: PathBuf, commands: Vec<HistoryCommand>) -> Self {
        Self { path, commands }
    }
}

impl From<histories::History> for History {
    fn from(history: histories::History) -> History {
        let mut commands: Vec<HistoryCommand> = vec![];
        for h in history.commands {
            commands.push(HistoryCommand::from(h));
        }

        History {
            path: history.path,
            commands,
        }
    }
}

/// toml representation of histories::HistoryCommand.
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct HistoryCommand {
    runner_type: runner_type::RunnerType,
    args: String,
}

impl HistoryCommand {
    pub fn new(runner_type: runner_type::RunnerType, args: String) -> Self {
        Self { runner_type, args }
    }

    fn into(self) -> histories::HistoryCommand {
        histories::HistoryCommand {
            runner_type: self.runner_type,
            args: self.args,
        }
    }
}

impl From<histories::HistoryCommand> for HistoryCommand {
    fn from(command: histories::HistoryCommand) -> HistoryCommand {
        Self {
            runner_type: command.runner_type,
            args: command.args.clone(),
        }
    }
}

// TODO: should return Result not Option(returns when it fails to get the home dir)
pub fn history_file_path() -> Option<(PathBuf, String)> {
    const HISTORY_FILE_NAME: &str = "history.toml";

    match env::var("FZF_MAKE_IS_TESTING") {
        Ok(_) => {
            // When testing
            let cwd = env::current_dir().unwrap();
            Some((cwd.join(PathBuf::from("test_data/history")), HISTORY_FILE_NAME.to_string()))
        }
        // This will be undeprecated in the next version(1.87.0), so we ignore it for now
        #[allow(deprecated)]
        _ => std::env::home_dir()
            .map(|home_dir| (home_dir.join(PathBuf::from(".config/fzf-make")), HISTORY_FILE_NAME.to_string())),
    }
}

pub fn parse_history(content: String) -> Result<Histories> {
    let histories = toml::from_str(&content)?;
    Ok(histories)
}

pub fn create_or_update_history_file(
    history_directory_path: PathBuf,
    history_file_name: String,
    new_history: histories::Histories,
) -> Result<()> {
    if !history_directory_path.is_dir() {
        fs::create_dir_all(history_directory_path.clone())?;
    }
    let mut history_file = File::create(history_directory_path.join(history_file_name))?;
    history_file.write_all(toml::to_string(&Histories::from(new_history)).unwrap().as_bytes())?;
    history_file.flush()?;

    Ok(())
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::model::runner_type;
    use anyhow::Result;
    use pretty_assertions::assert_eq;

    // TODO: test serialize
    #[test]
    fn serialize_test() {}

    #[test]
    fn parse_history_test() {
        struct Case {
            title: &'static str,
            content: String,
            expect: Result<Histories>,
        }
        let cases = vec![
            Case {
                title: "Success",
                content: r#"
[[histories]]
path = "/Users/user/code/fzf-make"

[[histories.commands]]
runner-type = "make"
args = "test"

[[histories.commands]]
runner-type = "make"
args = "check"

[[histories.commands]]
runner-type = "make"
args = "spell-check"

[[histories]]
path = "/Users/user/code/react"

[[histories.commands]]
runner-type = "pnpm"
args = "test"

[[histories.commands]]
runner-type = "pnpm"
args = "app1 build"
                "#
                .to_string(),
                expect: Ok(Histories {
                    histories: vec![
                        History {
                            path: PathBuf::from("/Users/user/code/fzf-make"),
                            commands: vec![
                                HistoryCommand {
                                    runner_type: runner_type::RunnerType::Make,
                                    args: "test".to_string(),
                                },
                                HistoryCommand {
                                    runner_type: runner_type::RunnerType::Make,
                                    args: "check".to_string(),
                                },
                                HistoryCommand {
                                    runner_type: runner_type::RunnerType::Make,
                                    args: "spell-check".to_string(),
                                },
                            ],
                        },
                        History {
                            path: PathBuf::from("/Users/user/code/react"),
                            commands: vec![
                                HistoryCommand {
                                    runner_type: runner_type::RunnerType::JsPackageManager(
                                        runner_type::JsPackageManager::Pnpm,
                                    ),
                                    args: "test".to_string(),
                                },
                                HistoryCommand {
                                    runner_type: runner_type::RunnerType::JsPackageManager(
                                        runner_type::JsPackageManager::Pnpm,
                                    ),
                                    args: "app1 build".to_string(),
                                },
                            ],
                        },
                    ],
                }),
            },
            Case {
                title: "Error",
                content: r#"
                "#
                .to_string(),
                expect: Err(anyhow::anyhow!(
                    "TOML parse error at line 1, column 1\n  |\n1 | \n  | ^\nmissing field `histories`\n"
                )),
            },
        ];

        for case in cases {
            match case.expect {
                Ok(v) => assert_eq!(v, parse_history(case.content).unwrap(), "\nFailed: 🚨{:?}🚨\n", case.title,),
                Err(e) => assert_eq!(
                    e.to_string(),
                    parse_history(case.content).unwrap_err().to_string(),
                    "\nFailed: 🚨{:?}🚨\n",
                    case.title,
                ),
            }
        }
    }

    #[test]
    fn parse_history_in_considering_history_file_format_version_test() {
        struct Case {
            title: &'static str,
            content: String,
            expect: Histories,
        }
        let cases = vec![
            Case {
                title: "Success(new format)",
                content: r#"
[[histories]]
path = "/Users/user/code/fzf-make"

[[histories.commands]]
runner-type = "make"
args = "test"

[[histories.commands]]
runner-type = "make"
args = "check"

[[histories.commands]]
runner-type = "make"
args = "spell-check"

[[histories]]
path = "/Users/user/code/golang/go-playground"

[[histories.commands]]
runner-type = "make"
args = "run"

[[histories.commands]]
runner-type = "make"
args = "echo1"
                "#
                .to_string(),
                expect: Histories {
                    histories: vec![
                        History {
                            path: PathBuf::from("/Users/user/code/fzf-make"),
                            commands: vec![
                                HistoryCommand {
                                    runner_type: runner_type::RunnerType::Make,
                                    args: "test".to_string(),
                                },
                                HistoryCommand {
                                    runner_type: runner_type::RunnerType::Make,
                                    args: "check".to_string(),
                                },
                                HistoryCommand {
                                    runner_type: runner_type::RunnerType::Make,
                                    args: "spell-check".to_string(),
                                },
                            ],
                        },
                        History {
                            path: PathBuf::from("/Users/user/code/golang/go-playground"),
                            commands: vec![
                                HistoryCommand {
                                    runner_type: runner_type::RunnerType::Make,
                                    args: "run".to_string(),
                                },
                                HistoryCommand {
                                    runner_type: runner_type::RunnerType::Make,
                                    args: "echo1".to_string(),
                                },
                            ],
                        },
                    ],
                },
            },
            Case {
                title: "Success(old format)",
                content: r#"
[[history]]
path = "/Users/user/code/fzf-make/Makefile"
executed-targets = ["test", "check", "spell-check"]

[[history]]
path = "/Users/user/code/golang/go-playground/Makefile"
executed-targets = ["run", "echo1"]
                "#
                .to_string(),
                expect: Histories {
                    histories: vec![
                        History {
                            path: PathBuf::from("/Users/user/code/fzf-make"),
                            commands: vec![
                                HistoryCommand {
                                    runner_type: runner_type::RunnerType::Make,
                                    args: "test".to_string(),
                                },
                                HistoryCommand {
                                    runner_type: runner_type::RunnerType::Make,
                                    args: "check".to_string(),
                                },
                                HistoryCommand {
                                    runner_type: runner_type::RunnerType::Make,
                                    args: "spell-check".to_string(),
                                },
                            ],
                        },
                        History {
                            path: PathBuf::from("/Users/user/code/golang/go-playground"),
                            commands: vec![
                                HistoryCommand {
                                    runner_type: runner_type::RunnerType::Make,
                                    args: "run".to_string(),
                                },
                                HistoryCommand {
                                    runner_type: runner_type::RunnerType::Make,
                                    args: "echo1".to_string(),
                                },
                            ],
                        },
                    ],
                },
            },
            Case {
                title: "Error",
                content: r#"
                "#
                .to_string(),
                expect: Histories::default(),
            },
        ];

        for case in cases {
            assert_eq!(
                case.expect,
                Histories::parse_history_in_considering_history_file_format_version(case.content),
                "\nFailed: 🚨{:?}🚨\n",
                case.title,
            )
        }
    }
}
