use std::process::Output;

use async_trait::async_trait;
use serde::Deserialize;
use util::command::Command;

use crate::devcontainer_api::DevContainerError;

pub(crate) struct DefaultCommandRunner;

impl DefaultCommandRunner {
    pub(crate) fn new() -> Self {
        Self
    }
}

#[async_trait]
impl CommandRunner for DefaultCommandRunner {
    async fn run_command(&self, command: &mut Command) -> Result<Output, std::io::Error> {
        command.output().await
    }
}

#[async_trait]
pub(crate) trait CommandRunner: Send + Sync {
    async fn run_command(&self, command: &mut Command) -> Result<Output, std::io::Error>;
}

pub(crate) async fn evaluate_json_command<T>(
    mut command: Command,
) -> Result<Option<T>, DevContainerError>
where
    T: for<'de> Deserialize<'de>,
{
    let output = command.output().await.map_err(|e| {
        log::error!("Error running command {:?}: {e}", command);
        DevContainerError::CommandFailed(command.get_program().display().to_string())
    })?;

    deserialize_json_output(output).map_err(|e| {
        log::error!("Error running command {:?}: {e}", command);
        DevContainerError::CommandFailed(command.get_program().display().to_string())
    })
}

pub(crate) async fn evaluate_yaml_command<T>(
    mut command: Command,
) -> Result<Option<T>, DevContainerError>
where
    T: for<'de> Deserialize<'de>,
{
    let output = command.output().await.map_err(|e| {
        log::error!("Error running command {:?}: {e}", command);
        DevContainerError::CommandFailed(command.get_program().display().to_string())
    })?;

    deserialize_yaml_output(output).map_err(|e| {
        log::error!("Error running command {:?}: {e}", command);
        DevContainerError::CommandFailed(command.get_program().display().to_string())
    })
}

pub(crate) fn deserialize_yaml_output<T>(output: Output) -> Result<Option<T>, String>
where
    T: for<'de> Deserialize<'de>,
{
    if output.status.success() {
        let raw = String::from_utf8_lossy(&output.stdout);
        if raw.is_empty() || raw.trim() == "[]" || raw.trim() == "{}" {
            return Ok(None);
        }
        serde_yaml::from_str(&raw)
            .map(Some)
            .map_err(|e| format!("Error deserializing from raw yaml: {e}"))
    } else {
        let std_err = String::from_utf8_lossy(&output.stderr);
        Err(format!(
            "Sent non-successful output; cannot deserialize. StdErr: {std_err}"
        ))
    }
}

pub(crate) fn deserialize_json_output<T>(output: Output) -> Result<Option<T>, String>
where
    T: for<'de> Deserialize<'de>,
{
    if output.status.success() {
        let raw = String::from_utf8_lossy(&output.stdout);
        if raw.is_empty() || raw.trim() == "[]" || raw.trim() == "{}" {
            return Ok(None);
        }
        serde_json_lenient::from_str(&raw)
            .map_err(|e| format!("Error deserializing from raw json: {e}"))
    } else {
        let std_err = String::from_utf8_lossy(&output.stderr);
        Err(format!(
            "Sent non-successful output; cannot deserialize. StdErr: {std_err}"
        ))
    }
}

#[cfg(test)]
mod tests {
    use std::process::ExitStatus;

    use crate::docker::{DockerComposeConfig, DockerComposeServiceBuild};

    use super::*;

    fn success_output(stdout: &str) -> Output {
        Output {
            status: ExitStatus::default(),
            stdout: stdout.as_bytes().to_vec(),
            stderr: Vec::new(),
        }
    }

    #[derive(Debug, Deserialize, PartialEq)]
    struct TestItem {
        id: String,
    }

    #[test]
    fn test_deserialize_newline_delimited_json_rejected() {
        // Strict single-value contract: NDJSON must be rejected. Commands that
        // may legitimately return multiple rows (e.g. `docker ps`) parse their
        // output themselves rather than routing through this helper.
        let output = success_output("{\"id\":\"first\"}\n{\"id\":\"second\"}\n");
        let result: Result<Option<TestItem>, String> = deserialize_json_output(output);
        assert!(result.is_err(), "expected parse error, got {result:?}");
    }

    #[test]
    fn test_deserialize_empty_output() {
        let output = success_output("");
        let result: Option<TestItem> = deserialize_json_output(output).unwrap();
        assert_eq!(result, None);
    }

    #[test]
    fn test_deserialize_empty_object() {
        let output = success_output("{}");
        let result: Option<TestItem> = deserialize_json_output(output).unwrap();
        assert_eq!(result, None);
    }

    #[test]
    fn test_deserialize_yaml_docker_compose_config() {
        let yaml = indoc::indoc! {"
            name: my-project
            services:
              app:
                image: node:18
                command:
                  - sleep
                  - infinity
                build:
                  context: .
                  dockerfile: Dockerfile
              db:
                image: postgres:15
            volumes: {}
        "};
        let output = success_output(yaml);
        let result: DockerComposeConfig = deserialize_yaml_output(output)
            .expect("deserialization should succeed")
            .expect("result should not be None");

        assert_eq!(result.name, Some("my-project".to_string()));
        assert_eq!(result.services.len(), 2);

        let app = result
            .services
            .get("app")
            .expect("app service should exist");
        assert_eq!(app.image, Some("node:18".to_string()));
        assert_eq!(
            app.command,
            vec!["sleep".to_string(), "infinity".to_string()]
        );
        assert_eq!(
            app.build,
            Some(DockerComposeServiceBuild {
                context: Some(".".to_string()),
                dockerfile: Some("Dockerfile".to_string()),
                ..Default::default()
            })
        );

        let db = result.services.get("db").expect("db service should exist");
        assert_eq!(db.image, Some("postgres:15".to_string()));
    }

    #[test]
    fn test_deserialize_yaml_empty_output() {
        let output = success_output("");
        let result: Option<DockerComposeConfig> = deserialize_yaml_output(output).unwrap();
        assert_eq!(result, None);
    }

    #[test]
    fn test_deserialize_yaml_empty_object() {
        let output = success_output("{}");
        let result: Option<DockerComposeConfig> = deserialize_yaml_output(output).unwrap();
        assert_eq!(result, None);
    }
}
