use crate::sim::{EventData, Simulation};
use crate::util::model::FormatType;
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::io::{BufWriter, Write};
use std::path::Path;
use std::process::{Command, Stdio};

pub struct SimulationSink {
    entities: HashMap<String, Entity>,
    system_sinks: HashMap<String, Box<dyn Write>>,
    stream: bool,
}

#[derive(Debug)]
struct Entity {
    system_key: String,
    format_type: FormatType,
}

impl SimulationSink {
    pub fn stream() -> Self {
        SimulationSink {
            system_sinks: HashMap::new(),
            entities: HashMap::new(),
            stream: true,
        }
    }

    pub fn write_event(&mut self, event_data: EventData) {
        if let EventData::Error { .. } = event_data {
            match serde_json::to_string(&event_data) {
                Ok(str) => eprintln!("Error: {}", str),
                Err(_) => (),
            }
        } else if self.stream {
            match serde_json::to_string(&event_data) {
                Ok(str) => println!("{}", str),
                Err(_) => (),
            }
        } else if let EventData::Create { entity, value, .. } = event_data {
            if let Some(entity) = self.entities.get(&entity) {
                if let Some(system_sink) = self.system_sinks.get_mut(&entity.system_key) {
                    let value = match entity.format_type {
                        FormatType::Json => &value.to_string(),
                        _ => value.as_str().unwrap(),
                    };

                    let _ = writeln!(system_sink, "{}", value);
                }
            }
        }
    }
}

impl TryFrom<Simulation> for SimulationSink {
    type Error = anyhow::Error;

    fn try_from(simulation: Simulation) -> Result<Self> {
        let mut simulation_sink = SimulationSink {
            system_sinks: HashMap::new(),
            entities: HashMap::new(),
            stream: false,
        };

        let simulation_directory = format!(".rngo/simulations/{}", simulation.id);
        let simulation_directory = Path::new(&simulation_directory);

        for (key, entity) in simulation.spec.entities.iter() {
            if let Some(entity_system) = &entity.system {
                let system = simulation
                    .spec
                    .systems
                    .get(&entity_system.stype)
                    .with_context(|| {
                        format!("Could not resolve system type {}", entity_system.stype)
                    })?;

                #[cfg(target_os = "windows")]
                let (shell, flag) = ("cmd", "/C");

                #[cfg(not(target_os = "windows"))]
                let (shell, flag) = ("sh", "-c");

                let mut child = Command::new(shell)
                    .arg(flag)
                    .arg(system.import.command.clone())
                    .stdin(Stdio::piped())
                    .stdout(Stdio::null())
                    .stderr(Stdio::inherit())
                    .spawn()
                    .with_context(|| {
                        format!(
                            "Could not run import command for system {}:\n\n{}",
                            entity_system.stype, system.import.command
                        )
                    })?;

                let child_stdin = child.stdin.take().expect("No stdin");

                let system_key = entity_system.stype.clone();

                simulation_sink.entities.insert(
                    key.clone(),
                    Entity {
                        system_key: system_key.clone(),
                        format_type: system.format.otype.clone(),
                    },
                );

                simulation_sink
                    .system_sinks
                    .insert(system_key, Box::new(child_stdin));
            } else if let Some(format) = &entity.format {
                let (extension, system_type) = match format.otype {
                    FormatType::Sql => ("sql", "sql"),
                    FormatType::Json => ("jsonl", "json"),
                };

                let file_path = simulation_directory.join(format!("{}.{}", key, extension));

                let file = OpenOptions::new()
                    .create(true)
                    .append(true)
                    .open(file_path.clone())
                    .expect(&format!("Failed to open file at {}", file_path.display()));

                let system_key = format!("{}_{}", system_type, key);

                simulation_sink.entities.insert(
                    key.clone(),
                    Entity {
                        system_key: system_key.clone(),
                        format_type: format.otype.clone(),
                    },
                );

                simulation_sink
                    .system_sinks
                    .insert(system_key, Box::new(BufWriter::new(file)));
            }
        }

        Ok(simulation_sink)
    }
}
