use collections::HashMap;
use serde::Deserialize;
use util::ResultExt as _;

use crate::{
    DebugScenario, DebugTaskFile, EnvVariableReplacer, TcpArgumentsTemplate, VariableName,
};

// TODO support preLaunchTask linkage with other tasks
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct VsCodeDebugTaskDefinition {
    r#type: String,
    name: String,
    #[serde(default)]
    port: Option<u16>,
    #[serde(flatten)]
    other_attributes: serde_json::Value,
}

impl VsCodeDebugTaskDefinition {
    fn try_to_zed(mut self, replacer: &EnvVariableReplacer) -> anyhow::Result<DebugScenario> {
        let label = replacer.replace(&self.name);
        let mut config = replacer.replace_value(self.other_attributes);
        let adapter = task_type_to_adapter_name(&self.r#type);
        if let Some(config) = config.as_object_mut()
            && adapter == "JavaScript"
        {
            config.insert("type".to_owned(), self.r#type.clone().into());
            if let Some(port) = self.port.take() {
                config.insert("port".to_owned(), port.into());
            }
        }
        let definition = DebugScenario {
            label: label.into(),
            build: None,
            adapter: adapter.into(),
            tcp_connection: self.port.map(|port| TcpArgumentsTemplate {
                port: Some(port),
                host: None,
                timeout: None,
            }),
            config,
        };
        Ok(definition)
    }
}

#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VsCodeDebugTaskFile {
    #[serde(default)]
    version: Option<String>,
    configurations: Vec<VsCodeDebugTaskDefinition>,
}

impl TryFrom<VsCodeDebugTaskFile> for DebugTaskFile {
    type Error = anyhow::Error;

    fn try_from(file: VsCodeDebugTaskFile) -> Result<Self, Self::Error> {
        let replacer = EnvVariableReplacer::new(HashMap::from_iter([
            (
                "workspaceFolder".to_owned(),
                VariableName::WorktreeRoot.to_string(),
            ),
            (
                "relativeFile".to_owned(),
                VariableName::RelativeFile.to_string(),
            ),
            ("file".to_owned(), VariableName::File.to_string()),
        ]))
        .with_commands([(
            "pickMyProcess".to_owned(),
            VariableName::PickProcessId.to_string(),
        )]);
        let templates = file
            .configurations
            .into_iter()
            .filter_map(|config| config.try_to_zed(&replacer).log_err())
            .collect::<Vec<_>>();
        Ok(DebugTaskFile(templates))
    }
}

fn task_type_to_adapter_name(task_type: &str) -> String {
    match task_type {
        "pwa-node" | "node" | "node-terminal" | "chrome" | "pwa-chrome" | "edge" | "pwa-edge"
        | "msedge" | "pwa-msedge" => "JavaScript",
        "go" => "Delve",
        "php" => "Xdebug",
        "cppdbg" | "lldb" => "CodeLLDB",
        "debugpy" => "Debugpy",
        "rdbg" => "rdbg",
        _ => task_type,
    }
    .to_owned()
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use crate::{DebugScenario, DebugTaskFile, VariableName};

    use super::VsCodeDebugTaskFile;

    #[test]
    fn test_parsing_vscode_launch_json() {
        let raw = r#"
            {
                "version": "0.2.0",
                "configurations": [
                    {
                        "name": "Debug my JS app",
                        "request": "launch",
                        "type": "node",
                        "program": "${workspaceFolder}/xyz.js",
                        "showDevDebugOutput": false,
                        "stopOnEntry": true,
                        "args": ["--foo", "${workspaceFolder}/thing"],
                        "cwd": "${workspaceFolder}/${env:FOO}/sub",
                        "env": {
                            "X": "Y"
                        },
                        "port": 17
                    },
                ]
            }
        "#;
        let parsed: VsCodeDebugTaskFile =
            serde_json_lenient::from_str(raw).expect("deserializing launch.json");
        let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates");
        pretty_assertions::assert_eq!(
            zed,
            DebugTaskFile(vec![DebugScenario {
                label: "Debug my JS app".into(),
                adapter: "JavaScript".into(),
                config: json!({
                    "request": "launch",
                    "program": "${ZED_WORKTREE_ROOT}/xyz.js",
                    "showDevDebugOutput": false,
                    "stopOnEntry": true,
                    "args": [
                        "--foo",
                        "${ZED_WORKTREE_ROOT}/thing",
                    ],
                    "cwd": "${ZED_WORKTREE_ROOT}/${FOO}/sub",
                    "env": {
                        "X": "Y",
                    },
                    "type": "node",
                    "port": 17,
                }),
                tcp_connection: None,
                build: None
            }])
        );
    }

    #[test]
    fn test_command_pickmyprocess_replacement() {
        let raw = r#"
            {
                "version": "0.2.0",
                "configurations": [
                    {
                        "name": "Attach to Process",
                        "request": "attach",
                        "type": "cppdbg",
                        "processId": "${command:pickMyProcess}"
                    }
                ]
            }
        "#;
        let parsed: VsCodeDebugTaskFile =
            serde_json_lenient::from_str(raw).expect("deserializing launch.json");
        let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates");

        let expected_placeholder = format!("${{{}}}", VariableName::PickProcessId);
        pretty_assertions::assert_eq!(
            zed,
            DebugTaskFile(vec![DebugScenario {
                label: "Attach to Process".into(),
                adapter: "CodeLLDB".into(),
                config: json!({
                    "request": "attach",
                    "processId": expected_placeholder,
                }),
                tcp_connection: None,
                build: None
            }])
        );
    }
}
