use std::{
    fs::File,
    io::{BufRead, BufReader, Write},
    process::{Child, Command, Stdio},
    sync::{
        mpsc::{self, TryRecvError},
        Arc, Mutex,
    },
    thread,
};

use bgzip::{BGZFWriter, Compression};
use chrono::{DateTime, Utc};
use log::{info, warn};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::DOCKER_ID;

pub trait Run {
    fn run(&mut self) -> anyhow::Result<()>;
}

pub trait Wait {
    fn wait(&mut self) -> anyhow::Result<()>;
}

pub trait RunWait: Run + Wait + Log {}
impl<T: Run + Wait + Log> RunWait for T {}

pub fn run_wait<T: RunWait>(item: &mut T) -> anyhow::Result<RunReport> {
    let mut run_report = RunReport {
        start: Utc::now(),
        ..Default::default()
    };
    item.run()?;
    item.wait()?;
    run_report.log = item.log();
    run_report.end = Utc::now();
    Ok(run_report)
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct RunReport {
    pub start: DateTime<Utc>,
    pub end: DateTime<Utc>,
    pub log: String,
}

impl RunReport {
    // pub fn save_to_file(&self, file_prefix: &str) -> std::io::Result<()> {
    //     let json_data = serde_json::to_string_pretty(self).expect("Failed to serialize RunReport");
    //     let uuid = Uuid::new_v4().to_string()[..5].to_string();
    //     let file_path = format!("{}{}.log", file_prefix, uuid);
    //     let mut file = File::create(&file_path)?;
    //     file.write_all(json_data.as_bytes())?;
    //     Ok(())
    // }

    /// Serialize the RunReport to a JSON string and save to file_prefix.log.gz
    pub fn save_to_file(&self, file_prefix: &str) -> std::io::Result<()> {
        let json_data = serde_json::to_string_pretty(self).expect("Failed to serialize RunReport");
        let uuid = Uuid::new_v4().to_string()[..5].to_string();
        let file_path = format!("{}{}.log.gz", file_prefix, uuid);
        let file = File::create(&file_path)?;
        let mut writer = BGZFWriter::new(file, Compression::default());
        writer.write_all(json_data.as_bytes())?;
        writer.close()?;
        Ok(())
    }
}

pub trait Log {
    fn log(&self) -> String;
}

#[derive(Debug, Default)]
pub struct DockerRun {
    pub args: Vec<String>,
    pub container_id: String,
    pub start: DateTime<Utc>,
    pub logs: Arc<Mutex<String>>,
}

impl DockerRun {
    pub fn new(args: &[&str]) -> Self {
        DockerRun {
            args: args.iter().map(|e| e.to_string()).collect(),
            start: Utc::now(),
            ..Default::default()
        }
    }
}

impl Run for DockerRun {
    fn run(&mut self) -> anyhow::Result<()> {
        // Setup Ctrl-C handler cant be defined two times
        let _ = ctrlc::try_set_handler(move || {
            if let Ok(container_id) = DOCKER_ID.lock() {
                for id in container_id.iter() {
                    warn!("Stopping Docker container {id}...");
                    let _ = Command::new("docker").args(["stop", id]).status();
                }
            }
            std::process::exit(1);
        });

        // Spawn the main command
        let output = Command::new("docker")
            .args(&self.args)
            .stdout(Stdio::piped())
            .stderr(Stdio::inherit())
            .output()
            .expect("Failed to run Docker command");

        let info = format!("Running with docker: {}", &self.args.join(" "));
        info!("{info}");
        {
            let mut logs_lock = self.logs.lock().unwrap();
            logs_lock.push_str(&format!("{info}\n"));
        }

        // add id to Arc
        let id = String::from_utf8_lossy(&output.stdout).trim().to_string();
        {
            DOCKER_ID.lock().unwrap().push(id.clone());
            self.container_id = id.clone();
        }

        // Spawn a thread to follow the logs
        let log_id = id.clone();
        let logs_clone = Arc::clone(&self.logs);
        let _loger_thread = thread::spawn(move || {
            let output = Command::new("docker")
                .args(["inspect", "--format='{{.Config.Image}}'", &log_id])
                .output()
                .expect("Failed to execute Docker inspect");
            let container_name = if output.status.success() {
                String::from_utf8(output.stdout)
                    .expect("Failed to convert output to string")
                    .trim() // Trim to remove any trailing newlines
                    .to_string()
            } else {
                "?".to_string()
            };
            let mut child = Command::new("docker")
                .args(["logs", "--follow", &log_id])
                .stdout(Stdio::piped())
                .stderr(Stdio::inherit())
                .spawn()
                .expect("Failed to follow Docker logs");

            if let Some(stdout) = child.stdout.take() {
                let reader = BufReader::new(stdout);
                for line in reader.lines().map_while(Result::ok) {
                    info!("[{container_name}] {line}");
                    let mut logs_lock = logs_clone.lock().unwrap();
                    logs_lock.push_str(&line);
                    logs_lock.push('\n');
                }
            }
        });
        Ok(())
    }
}

impl Wait for DockerRun {
    fn wait(&mut self) -> anyhow::Result<()> {
        let status = Command::new("docker")
            .args(["wait", &self.container_id])
            .status()
            .expect("Failed to wait on Docker container");
        if !status.success() {
            let warn = format!("Docker command failed with status: {status}");
            warn!("{warn}");
            {
                let mut logs_lock = self.logs.lock().unwrap();
                logs_lock.push_str(&format!("\n{warn}\n"));
            }
        }

        let mut container_id_lock = DOCKER_ID.lock().unwrap();
        container_id_lock.retain(|entry| *entry != self.container_id);

        Ok(())
    }
}

impl Log for DockerRun {
    fn log(&self) -> String {
        let logs_lock = self.logs.lock().unwrap();
        logs_lock.to_string()
    }
}

pub struct CommandRun {
    pub bin: String,
    pub args: Vec<String>,
    pub child: Option<Child>,
    pub tx: mpsc::Sender<(String, String)>,
    pub rx: mpsc::Receiver<(String, String)>,
    pub log: String,
}

impl CommandRun {
    pub fn new(bin: &str, args: &[&str]) -> Self {
        let (tx, rx) = mpsc::channel();

        CommandRun {
            bin: bin.to_string(),
            args: args.iter().map(|e| e.to_string()).collect(),
            child: None,
            log: String::default(),
            tx,
            rx,
        }
    }
}

impl Run for CommandRun {
    fn run(&mut self) -> anyhow::Result<()> {
        let info = format!("Running command: {} {}", &self.bin, &self.args.join(" "));
        info!("{info}");
        let mut child = Command::new(&self.bin)
            .args(&self.args)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()?;

        self.log.push_str(&format!("{info}\n"));

        let stdout = child.stdout.take().expect("Failed to capture stdout");
        let stderr = child.stderr.take().expect("Failed to capture stderr");

        let stdout_reader = BufReader::new(stdout);
        let stderr_reader = BufReader::new(stderr);

        let tx = self.tx.clone();
        std::thread::scope(|s| {
            s.spawn(|| {
                for line in stdout_reader.lines().map_while(Result::ok) {
                    info!("[{}:stdout] {}", self.bin, line);
                    tx.send(("stdout".to_string(), line.to_string()))
                        .expect("Channel send failed");
                }
            });

            s.spawn(|| {
                for line in stderr_reader.lines().map_while(Result::ok) {
                    info!("[{}:stderr] {}", self.bin, line);
                    tx.send(("stderr".to_string(), line.to_string()))
                        .expect("Channel send failed");
                }
            });
        });

        self.child = Some(child);
        Ok(())
    }
}

impl Wait for CommandRun {
    fn wait(&mut self) -> anyhow::Result<()> {
        if let Some(child) = &mut self.child {
            loop {
                match self.rx.try_recv() {
                    Ok((stream, line)) => {
                        self.log
                            .push_str(&format!("[{}] {}: {}\n", self.bin, stream, line));
                    }
                    Err(TryRecvError::Empty) => match child.try_wait()? {
                        Some(_) => {
                            break;
                        }
                        None => {
                            std::thread::sleep(std::time::Duration::from_millis(100));
                        }
                    },
                    Err(TryRecvError::Disconnected) => {
                        break;
                    }
                }
            }

            // Ensure child process has exited
            let status = child.wait()?;
            info!("{} {}", self.bin, status);
        }
        Ok(())
    }
}

impl Log for CommandRun {
    fn log(&self) -> String {
        self.log.clone()
    }
}
