use std::io::{Read, Write};
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::time::Duration;

use regex::Regex;

use crate::parse::{self, ShoutFile};

#[derive(Debug, Clone)]
pub struct CommandResult {
    pub command: parse::Command,
    pub actual: Vec<String>,
    pub exit_code: i32,
}

#[derive(Debug)]
#[allow(dead_code)]
pub struct FileResult {
    pub file: ShoutFile,
    pub results: Vec<CommandResult>,
    pub tmp_dir: String,
    pub error: Option<String>,
}

pub struct RunOptions {
    pub clean_env: bool,
    pub path_dirs: Vec<String>,
    pub env_vars: Vec<(String, String)>,
    pub source_dir: Option<String>,
    pub project_dir: Option<String>,
    pub timeout_ms: u64,
    pub verbose: bool,
}

const SENTINEL_PREFIX: &str = "__SHOUT_SENTINEL_";
const VERBOSE_MARKER: &str = "__SHOUT_CMD_";

fn build_script(commands: &[parse::Command], verbose: bool) -> String {
    let mut lines = Vec::new();

    if verbose {
        lines.push("exec 3>&2 2>&1 9>&1".to_string());
    } else {
        lines.push("exec 2>&1 9>&1".to_string());
    }

    for (i, cmd) in commands.iter().enumerate() {
        if verbose {
            lines.push(format!("printf '{VERBOSE_MARKER}{i}\\n' >&3"));
        }
        lines.push("__shout_out=$(mktemp)".to_string());
        lines.push("exec 1>\"$__shout_out\" 2>&1".to_string());
        lines.push(cmd.command.clone());
        lines.push("__shout_ec=$?".to_string());
        lines.push("exec 1>&9 2>&1".to_string());
        lines.push("cat \"$__shout_out\"".to_string());
        lines.push("rm -f \"$__shout_out\"".to_string());
        lines.push(format!(
            "printf '\\n{SENTINEL_PREFIX}%s_{i}__\\n' \"$__shout_ec\""
        ));
    }

    lines.join("\n") + "\n"
}

fn strip_ansi(line: &str) -> String {
    // Same regex as the TS version
    let re = Regex::new(r"[\x1b\x9b][\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]").unwrap();
    re.replace_all(line, "").to_string()
}

fn parse_sentinel_output(raw: &str, command_count: usize) -> (Vec<Vec<String>>, Vec<i32>) {
    let mut outputs = Vec::new();
    let mut exit_codes = Vec::new();

    let sentinel_re = Regex::new(&format!(r"{}(\d+)_(\d+)__", regex::escape(SENTINEL_PREFIX))).unwrap();

    let mut remaining = raw;

    for _i in 0..command_count {
        if let Some(m) = sentinel_re.find(remaining) {
            let caps = sentinel_re.captures(&remaining[m.start()..]).unwrap();
            let exit_code: i32 = caps[1].parse().unwrap_or(1);

            let before = &remaining[..m.start()];
            let mut lines: Vec<String> = before.split('\n').map(|s| s.to_string()).collect();

            // Remove leading empty line (from printf \n prefix)
            if !lines.is_empty() && lines[0].is_empty() {
                lines.remove(0);
            }
            // Remove trailing empty lines
            lines = parse::trim_trailing_empty(&lines);
            if lines.len() == 1 && lines[0].is_empty() {
                lines.clear();
            }

            outputs.push(lines);
            exit_codes.push(exit_code);

            // Skip past sentinel
            let after = &remaining[m.end()..];
            remaining = if after.starts_with('\n') {
                &after[1..]
            } else {
                after
            };
        } else {
            // No sentinel found — rest is output for this command
            let mut lines: Vec<String> = remaining.split('\n').map(|s| s.to_string()).collect();
            if !lines.is_empty() && lines[0].is_empty() {
                lines.remove(0);
            }
            lines = parse::trim_trailing_empty(&lines);
            outputs.push(lines);
            exit_codes.push(1); // assume failure
            break;
        }
    }

    // Fill missing entries
    while outputs.len() < command_count {
        outputs.push(vec![]);
        exit_codes.push(1);
    }

    (outputs, exit_codes)
}

fn make_tmp_dir() -> std::io::Result<String> {
    let base = std::env::temp_dir();
    // Create a unique temp directory
    loop {
        let suffix: u64 = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos() as u64;
        let dir = base.join(format!("shout-{suffix}"));
        if !dir.exists() {
            std::fs::create_dir_all(&dir)?;
            return Ok(dir.to_string_lossy().to_string());
        }
    }
}

fn kill_tree(pid: u32) {
    // Find processes in the same process group
    if let Ok(output) = Command::new("ps")
        .args(["-eo", "pid,pgid"])
        .output()
    {
        let text = String::from_utf8_lossy(&output.stdout);
        let pgid = pid.to_string();
        for line in text.lines() {
            let parts: Vec<&str> = line.trim().split_whitespace().collect();
            if parts.len() >= 2 && parts[1] == pgid {
                if let Ok(p) = parts[0].parse::<i32>() {
                    if p as u32 != pid && p > 1 {
                        unsafe { libc::kill(p, libc::SIGKILL); }
                    }
                }
            }
        }
    }
    // Kill the process group
    unsafe { libc::kill(-(pid as i32), libc::SIGKILL); }
}

pub fn run_file(
    file: &ShoutFile,
    options: &RunOptions,
    on_command: Option<&dyn Fn(&parse::Command)>,
    on_command_result: Option<&dyn Fn(usize, &CommandResult)>,
) -> FileResult {
    let tmp_dir = match make_tmp_dir() {
        Ok(d) => d,
        Err(e) => {
            return FileResult {
                file: file.clone(),
                results: vec![],
                tmp_dir: String::new(),
                error: Some(format!("Failed to create temp dir: {e}")),
            };
        }
    };

    if file.commands.is_empty() {
        return FileResult {
            file: file.clone(),
            results: vec![],
            tmp_dir,
            error: None,
        };
    }

    let verbose = options.verbose && on_command.is_some();
    let script = build_script(&file.commands, verbose);

    let mut cmd = Command::new("/bin/sh");
    cmd.stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .current_dir(&tmp_dir);

    // Set up process group (detached)
    unsafe {
        use std::os::unix::process::CommandExt;
        cmd.pre_exec(|| {
            libc::setpgid(0, 0);
            Ok(())
        });
    }

    // Environment
    if options.clean_env {
        cmd.env_clear();
    }

    cmd.env("HOME", &tmp_dir);
    cmd.env("SHOUT_DIR", &tmp_dir);

    if let Some(ref source_dir) = options.source_dir {
        cmd.env("SHOUT_SOURCE_DIR", source_dir);
    }
    if let Some(ref project_dir) = options.project_dir {
        cmd.env("SHOUT_PROJECT_DIR", project_dir);
    }

    for (key, value) in &options.env_vars {
        cmd.env(key, value);
    }

    if !options.path_dirs.is_empty() {
        let existing = std::env::var("PATH").unwrap_or_default();
        let new_path = format!("{}:{existing}", options.path_dirs.join(":"));
        cmd.env("PATH", new_path);
    }

    let mut child = match cmd.spawn() {
        Ok(c) => c,
        Err(e) => {
            return FileResult {
                file: file.clone(),
                results: vec![],
                tmp_dir,
                error: Some(format!("Failed to spawn shell: {e}")),
            };
        }
    };

    let pid = child.id();

    // Stream verbose markers from stderr in a separate thread
    if verbose {
        let stderr = child.stderr.take().unwrap();
        let commands = file.commands.clone();
        let _handle = std::thread::spawn(move || {
            let mut reader = std::io::BufReader::new(stderr);
            let mut buf = String::new();
            let mut byte = [0u8; 1];
            while reader.read(&mut byte).unwrap_or(0) > 0 {
                if byte[0] == b'\n' {
                    if buf.starts_with(VERBOSE_MARKER) {
                        if let Ok(idx) = buf[VERBOSE_MARKER.len()..].parse::<usize>() {
                            if idx < commands.len() {
                                let _ = write!(
                                    std::io::stderr(),
                                    "\x1b[2m  $ {}\x1b[0m\n",
                                    commands[idx].command
                                );
                            }
                        }
                    }
                    buf.clear();
                } else {
                    buf.push(byte[0] as char);
                }
            }
        });
    }

    // Write script to stdin
    if let Some(mut stdin) = child.stdin.take() {
        let _ = stdin.write_all(script.as_bytes());
        // stdin drops here, closing the pipe
    }

    // Read stdout with timeout
    let total_timeout_ms = options.timeout_ms * file.commands.len() as u64;
    let stdout = child.stdout.take().unwrap();
    let (tx, rx) = mpsc::channel::<Vec<u8>>();

    let last_sentinel_suffix = format!("_{}_", file.commands.len() - 1);
    let sentinel_prefix = SENTINEL_PREFIX.to_string();

    let reader_thread = std::thread::spawn(move || {
        let mut reader = std::io::BufReader::new(stdout);
        let mut buf = [0u8; 4096];
        loop {
            match reader.read(&mut buf) {
                Ok(0) => break,
                Ok(n) => {
                    if tx.send(buf[..n].to_vec()).is_err() {
                        break;
                    }
                }
                Err(_) => break,
            }
        }
    });

    let mut accumulated = String::new();
    let deadline = std::time::Instant::now() + Duration::from_millis(total_timeout_ms);
    let mut timed_out = false;
    let mut sentinels_reported: usize = 0;
    let mut last_sentinel_end: usize = 0;

    let sentinel_re = Regex::new(&format!(r"{}(\d+)_(\d+)__", regex::escape(SENTINEL_PREFIX))).unwrap();

    loop {
        let remaining = deadline.saturating_duration_since(std::time::Instant::now());
        if remaining.is_zero() {
            timed_out = true;
            break;
        }

        match rx.recv_timeout(remaining) {
            Ok(chunk) => {
                accumulated.push_str(&String::from_utf8_lossy(&chunk));

                // Stream command results as they come in
                if let Some(on_result) = on_command_result {
                    for caps in sentinel_re.captures_iter(&accumulated[last_sentinel_end..]) {
                        let idx: usize = caps[2].parse().unwrap_or(0);
                        if idx >= sentinels_reported {
                            let exit_code: i32 = caps[1].parse().unwrap_or(1);
                            let sentinel_match = caps.get(0).unwrap();
                            let abs_start = last_sentinel_end + sentinel_match.start();
                            let abs_end = last_sentinel_end + sentinel_match.end();
                            let output_slice = &accumulated[last_sentinel_end..abs_start];
                            let mut lines: Vec<String> = output_slice.split('\n').map(|s| s.to_string()).collect();
                            if !lines.is_empty() && lines[0].is_empty() {
                                lines.remove(0);
                            }
                            lines = parse::trim_trailing_empty(&lines);
                            if lines.len() == 1 && lines[0].is_empty() {
                                lines.clear();
                            }
                            let result = CommandResult {
                                command: file.commands[idx].clone(),
                                actual: lines.iter().map(|l| strip_ansi(l)).collect(),
                                exit_code,
                            };
                            on_result(idx, &result);
                            sentinels_reported = idx + 1;
                            last_sentinel_end = abs_end;
                            if accumulated.as_bytes().get(last_sentinel_end) == Some(&b'\n') {
                                last_sentinel_end += 1;
                            }
                        }
                    }
                }

                // Check if we've seen the last sentinel
                if let Some(prefix_idx) = accumulated.rfind(&sentinel_prefix) {
                    if accumulated[prefix_idx..].contains(&last_sentinel_suffix) {
                        break;
                    }
                }
            }
            Err(mpsc::RecvTimeoutError::Timeout) => {
                timed_out = true;
                break;
            }
            Err(mpsc::RecvTimeoutError::Disconnected) => break,
        }
    }

    let _ = reader_thread.join();

    // Kill the process tree
    kill_tree(pid);
    let _ = child.wait();

    if timed_out {
        return FileResult {
            file: file.clone(),
            results: vec![],
            tmp_dir,
            error: Some("Timeout reading output".to_string()),
        };
    }

    let (outputs, exit_codes) = parse_sentinel_output(&accumulated, file.commands.len());

    let results: Vec<CommandResult> = file
        .commands
        .iter()
        .enumerate()
        .map(|(i, cmd)| CommandResult {
            command: cmd.clone(),
            actual: outputs
                .get(i)
                .unwrap_or(&vec![])
                .iter()
                .map(|l| strip_ansi(l))
                .collect(),
            exit_code: exit_codes.get(i).copied().unwrap_or(1),
        })
        .collect();

    FileResult {
        file: file.clone(),
        results,
        tmp_dir,
        error: None,
    }
}

pub fn cleanup_tmp_dir(dir: &str) {
    let _ = std::fs::remove_dir_all(dir);
}

/// Check if a command result passes.
pub fn command_passes(result: &CommandResult) -> bool {
    use crate::matching::match_output;

    let output_matches = match_output(&result.command.expected, &result.actual);
    let exit_code_mismatch = match &result.command.exit_code {
        parse::ExitCode::Default => result.exit_code != 0,
        parse::ExitCode::Any => result.exit_code == 0,
        parse::ExitCode::Code(expected) => result.exit_code != *expected,
    };
    output_matches && !exit_code_mismatch
}
