use std::{
    fs::{self, File},
    io::{Read, Write},
    path::{Path, PathBuf},
    process::{Command, Stdio},
    thread,
};

use crate::model::{BuildTarget, MxrunConfig, ResultMirrorPlan};
use portable_pty::{CommandBuilder, PtySize, PtySystem, native_pty_system};

pub struct BuildPlan {
    jobs: Vec<BuildJob>,
}

impl BuildPlan {
    pub fn new(
        config: &MxrunConfig,
        entry: &str,
        root_dir: &Path,
        log_root: &Path,
        local_make: &str,
        mirror_plan: ResultMirrorPlan,
    ) -> Self {
        Self {
            jobs: config
                .targets()
                .iter()
                .map(|target| {
                    BuildJob::build(target, entry, root_dir, log_root, local_make, &mirror_plan)
                })
                .collect(),
        }
    }

    pub(crate) fn init(config: &MxrunConfig, root_dir: &Path) -> Self {
        Self {
            jobs: config
                .targets()
                .iter()
                .map(|target| BuildJob::init_for(target, root_dir))
                .collect(),
        }
    }

    pub fn jobs(&self) -> &[BuildJob] {
        &self.jobs
    }
}

pub struct BuildJob {
    target: BuildTarget,
    command: BuildCommand,
    log_path: PathBuf,
    root_dir: PathBuf,
    mirror_plan: ResultMirrorPlan,
    mode: RunMode,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum RunMode {
    Run,
    Init,
}

impl Clone for BuildJob {
    fn clone(&self) -> Self {
        Self {
            target: self.target.clone(),
            command: self.command.clone(),
            log_path: self.log_path.clone(),
            root_dir: self.root_dir.clone(),
            mirror_plan: self.mirror_plan.clone(),
            mode: self.mode,
        }
    }
}

impl BuildJob {
    pub fn new(
        target: BuildTarget,
        command: BuildCommand,
        log_path: PathBuf,
        root_dir: PathBuf,
        mirror_plan: ResultMirrorPlan,
        mode: RunMode,
    ) -> Self {
        Self {
            target,
            command,
            log_path,
            root_dir,
            mirror_plan,
            mode,
        }
    }

    pub fn build(
        target: &BuildTarget,
        entry: &str,
        root_dir: &Path,
        log_root: &Path,
        local_make: &str,
        mirror_plan: &ResultMirrorPlan,
    ) -> Self {
        Self::new(
            target.clone(),
            BuildCommand::for_target(target, entry, root_dir, local_make),
            log_root.join(format!("{}.log", target.log_key())),
            root_dir.to_path_buf(),
            mirror_plan.clone(),
            RunMode::Run,
        )
    }

    pub fn target(&self) -> &BuildTarget {
        &self.target
    }

    pub fn is_init(&self) -> bool {
        matches!(self.mode, RunMode::Init)
    }

    pub(crate) fn init_for(target: &BuildTarget, root_dir: &Path) -> Self {
        Self::new(
            target.clone(),
            BuildCommand::new("", vec![], None),
            root_dir
                .join(".mxrun")
                .join("logs")
                .join("init")
                .join(format!("{}.log", target.log_key())),
            root_dir.to_path_buf(),
            ResultMirrorPlan::new(false, PathBuf::new(), ""),
            RunMode::Init,
        )
    }

    #[cfg(test)]
    pub fn command(&self) -> &BuildCommand {
        &self.command
    }

    pub fn log_path(&self) -> &Path {
        &self.log_path
    }

    #[cfg(test)]
    pub fn run(&self) -> Result<JobResult, String> {
        fs::create_dir_all(self.log_path.parent().unwrap_or_else(|| Path::new(".")))
            .map_err(|err| format!("mxrun: failed to create log directory: {err}"))?;
        self.reset_log_file()?;
        self.prepare().and_then(|_| {
            self.run_build().and_then(|status| {
                if status == 0 {
                    self.run_mirror()
                        .map(|_| JobResult::new(self.log_path.clone(), status))
                } else {
                    Ok(JobResult::new(self.log_path.clone(), status))
                }
            })
        })
    }

    #[cfg(test)]
    fn reset_log_file(&self) -> Result<(), String> {
        File::options()
            .create(true)
            .write(true)
            .truncate(true)
            .open(&self.log_path)
            .map_err(|err| format!("mxrun: failed to reset log file: {err}"))
            .map(|_| ())
    }

    pub(crate) fn prepare(&self) -> Result<(), String> {
        fs::create_dir_all(self.log_path.parent().unwrap_or_else(|| Path::new(".")))
            .map_err(|err| format!("mxrun: failed to create log directory: {err}"))?;
        if self.target.is_local() {
            Ok(())
        } else {
            RemoteSync::new(&self.root_dir, &self.target).run(&self.log_path)
        }
    }

    pub(crate) fn run_build(&self) -> Result<i32, String> {
        self.command.run(&self.log_path)
    }

    pub(crate) fn run_mirror(&self) -> Result<(), String> {
        if self.mirror_plan.is_enabled() {
            ResultMirror::new(&self.root_dir, &self.target, &self.mirror_plan).run(&self.log_path)
        } else {
            Ok(())
        }
    }

    pub(crate) fn should_mirror_results(&self) -> bool {
        self.mirror_plan.is_enabled()
    }
}

pub struct BuildCommand {
    program: String,
    args: Vec<String>,
    cwd: Option<PathBuf>,
}

impl Clone for BuildCommand {
    fn clone(&self) -> Self {
        Self {
            program: self.program.clone(),
            args: self.args.clone(),
            cwd: self.cwd.clone(),
        }
    }
}

impl BuildCommand {
    pub fn new(program: &str, args: Vec<String>, cwd: Option<PathBuf>) -> Self {
        Self {
            program: program.to_string(),
            args,
            cwd,
        }
    }

    pub fn for_target(
        target: &BuildTarget,
        entry: &str,
        root_dir: &Path,
        local_make: &str,
    ) -> Self {
        if target.is_local() {
            Self::local(entry, root_dir, local_make)
        } else {
            Self::remote(target, entry, root_dir)
        }
    }

    pub fn program(&self) -> &str {
        &self.program
    }

    pub fn args(&self) -> &[String] {
        &self.args
    }

    pub fn cwd(&self) -> Option<&Path> {
        self.cwd.as_deref()
    }

    pub fn run(&self, log_path: &Path) -> Result<i32, String> {
        PtySession::new(self)?.run(log_path)
    }

    fn local(entry: &str, root_dir: &Path, local_make: &str) -> Self {
        Self::new(
            "sh",
            vec![
                "-lc".to_string(),
                format!("MXRUN_CONFIG= MXRUN_LOCAL_MAKE= {} {}", local_make, entry),
            ],
            Some(root_dir.to_path_buf()),
        )
    }

    fn remote(target: &BuildTarget, entry: &str, root_dir: &Path) -> Self {
        let _ = root_dir;
        Self::new(
            "ssh",
            vec![
                "-o".to_string(),
                "StrictHostKeyChecking=accept-new".to_string(),
                "-o".to_string(),
                "UpdateHostKeys=yes".to_string(),
                "-tt".to_string(),
                target.host().to_string(),
                format!(
                    "cd '{}' && {} {}",
                    target.remote_path(),
                    target.make_cmd(),
                    entry
                ),
            ],
            None,
        )
    }
}

struct RemoteSync<'a> {
    root_dir: &'a Path,
    target: &'a BuildTarget,
}

impl<'a> RemoteSync<'a> {
    fn new(root_dir: &'a Path, target: &'a BuildTarget) -> Self {
        Self { root_dir, target }
    }

    fn run(&self, log_path: &Path) -> Result<(), String> {
        self.ensure_remote_dir(log_path)?;
        self.rsync_tree(log_path)
    }

    fn ensure_remote_dir(&self, log_path: &Path) -> Result<(), String> {
        self.command("ssh", self.ensure_remote_dir_args())
            .status(log_path)
            .and_then(Self::ensure_success)
    }

    fn rsync_tree(&self, log_path: &Path) -> Result<(), String> {
        self.command("rsync", self.rsync_args())
            .status(log_path)
            .and_then(Self::ensure_success)
    }

    fn ensure_remote_dir_args(&self) -> Vec<String> {
        vec![
            "-o".to_string(),
            "StrictHostKeyChecking=accept-new".to_string(),
            "-o".to_string(),
            "UpdateHostKeys=yes".to_string(),
            self.target.host().to_string(),
            format!("mkdir -p '{}'", self.target.remote_path()),
        ]
    }

    fn rsync_args(&self) -> Vec<String> {
        vec![
            "-az".to_string(),
            "--exclude".to_string(),
            ".git".to_string(),
            "--exclude".to_string(),
            ".github".to_string(),
            "--exclude".to_string(),
            ".vscode".to_string(),
            "--exclude".to_string(),
            ".idea".to_string(),
            "--exclude".to_string(),
            ".mxrun".to_string(),
            "--exclude".to_string(),
            "target".to_string(),
            "--exclude".to_string(),
            "build/stage".to_string(),
            "--exclude".to_string(),
            "build/modules-dist".to_string(),
            format!("{}/", self.root_dir.display()),
            format!("{}:{}/", self.target.host(), self.target.remote_path()),
        ]
    }

    fn command(&self, program: &str, args: Vec<String>) -> LoggedCommand {
        LoggedCommand::new(program, args)
    }

    fn ensure_success(status: i32) -> Result<(), String> {
        (status == 0)
            .then_some(())
            .ok_or_else(|| "mxrun: remote sync failed".to_string())
    }
}

struct ResultMirror<'a> {
    root_dir: &'a Path,
    target: &'a BuildTarget,
    mirror_plan: &'a ResultMirrorPlan,
}

impl<'a> ResultMirror<'a> {
    fn new(root_dir: &'a Path, target: &'a BuildTarget, mirror_plan: &'a ResultMirrorPlan) -> Self {
        Self {
            root_dir,
            target,
            mirror_plan,
        }
    }

    fn run(&self, log_path: &Path) -> Result<(), String> {
        let mirror_target = self.resolve_target()?;
        let manifest_entries = self.manifest_entries()?;

        if manifest_entries.is_empty() {
            Ok(())
        } else {
            {
                self.log_mirror_header(&mirror_target, log_path)
                    .and_then(|_| {
                        manifest_entries
                            .iter()
                            .enumerate()
                            .try_for_each(|(index, path)| {
                                self.ensure_target_subdir(&mirror_target, path)
                                    .and_then(|_| {
                                        self.log_mirror_root(
                                            &mirror_target,
                                            index + 1,
                                            path,
                                            log_path,
                                        )
                                    })
                                    .and_then(|_| self.mirror_path(&mirror_target, path, log_path))
                            })
                    })
            }
        }
    }

    fn manifest_entries(&self) -> Result<Vec<PathBuf>, String> {
        if self.target.is_local() {
            self.local_manifest_entries()
        } else {
            self.remote_manifest_entries()
        }
    }

    fn local_manifest_entries(&self) -> Result<Vec<PathBuf>, String> {
        fs::read_to_string(self.local_manifest_path())
            .map_err(|err| format!("mxrun: failed to read mirror manifest: {err}"))
            .map(|text| Self::parse_manifest_entries(&text))
    }

    fn remote_manifest_entries(&self) -> Result<Vec<PathBuf>, String> {
        let temp_manifest = self
            .root_dir
            .join(".mxrun")
            .join("manifest-cache")
            .join(format!("{}.paths", self.target.log_key()));

        temp_manifest
            .parent()
            .map(Path::to_path_buf)
            .into_iter()
            .try_for_each(|dir| {
                fs::create_dir_all(dir).map_err(|err| {
                    format!("mxrun: failed to create manifest cache directory: {err}")
                })
            })
            .and_then(|_| {
                LoggedCommand::new(
                    "rsync",
                    vec![
                        "-az".to_string(),
                        format!("{}:{}", self.target.host(), self.remote_manifest_path()),
                        temp_manifest.display().to_string(),
                    ],
                )
                .status(&self.root_dir.join(".mxrun").join("manifest-fetch.log"))
                .and_then(|status| {
                    (status == 0)
                        .then_some(())
                        .ok_or_else(|| "mxrun: failed to fetch remote mirror manifest".to_string())
                })
            })
            .and_then(|_| {
                fs::read_to_string(&temp_manifest).map_err(|err| {
                    format!("mxrun: failed to read fetched remote mirror manifest: {err}")
                })
            })
            .map(|text| Self::parse_manifest_entries(&text))
    }

    fn parse_manifest_entries(text: &str) -> Vec<PathBuf> {
        text.lines()
            .map(str::trim)
            .filter(|line| !line.is_empty())
            .filter(|line| !line.starts_with('#'))
            .map(PathBuf::from)
            .collect()
    }

    fn resolve_target(&self) -> Result<MirrorTarget, String> {
        self.resolve_os_label()
            .map(|label| MirrorTarget::new(label, self.mirror_plan.root()))
    }

    fn resolve_os_label(&self) -> Result<String, String> {
        if self.is_linux_target() {
            self.resolve_linux_label()
        } else {
            self.resolve_bsd_label()
        }
    }

    fn resolve_linux_label(&self) -> Result<String, String> {
        self.kernel_major().and_then(|kernel| {
            self.libc_label()
                .map(|libc| format!("Linux {kernel} {libc}"))
        })
    }

    fn resolve_bsd_label(&self) -> Result<String, String> {
        self.release_major_minor()
            .map(|release| format!("{} {release}", self.target.os()))
    }

    fn kernel_major(&self) -> Result<String, String> {
        self.capture("uname", &["-r"]).map(|release| {
            release
                .split('.')
                .next()
                .unwrap_or(release.as_str())
                .to_string()
        })
    }

    fn release_major_minor(&self) -> Result<String, String> {
        self.capture("uname", &["-r"]).map(|release| {
            release
                .split(|ch: char| !ch.is_ascii_digit() && ch != '.')
                .find(|part| !part.is_empty())
                .map(Self::take_major_minor)
                .unwrap_or(release)
        })
    }

    fn libc_label(&self) -> Result<String, String> {
        self.glibc_label().or_else(|_| self.musl_label())
    }

    fn glibc_label(&self) -> Result<String, String> {
        self.capture("getconf", &["GNU_LIBC_VERSION"])
            .and_then(|value| {
                value
                    .strip_prefix("glibc ")
                    .map(|version| format!("glibc {version}"))
                    .ok_or_else(|| "mxrun: could not parse glibc version".to_string())
            })
    }

    fn musl_label(&self) -> Result<String, String> {
        self.capture("ldd", &["--version"]).and_then(|value| {
            Self::first_version(&value)
                .map(|version| format!("musl {version}"))
                .ok_or_else(|| "mxrun: could not parse musl version".to_string())
        })
    }

    fn capture(&self, program: &str, args: &[&str]) -> Result<String, String> {
        if self.target.is_local() {
            Self::capture_local(program, args)
        } else {
            Self::capture_remote(self.target.host(), program, args)
        }
    }

    fn capture_local(program: &str, args: &[&str]) -> Result<String, String> {
        Command::new(program)
            .args(args)
            .output()
            .map_err(|err| format!("mxrun: failed to run '{program}': {err}"))
            .and_then(|output| Self::capture_output(program, output))
    }

    fn capture_remote(host: &str, program: &str, args: &[&str]) -> Result<String, String> {
        Command::new("ssh")
            .arg("-o")
            .arg("StrictHostKeyChecking=accept-new")
            .arg("-o")
            .arg("UpdateHostKeys=yes")
            .arg(host)
            .arg(program)
            .args(args)
            .output()
            .map_err(|err| format!("mxrun: failed to query {host}: {err}"))
            .and_then(|output| Self::capture_output(program, output))
    }

    fn capture_output(program: &str, output: std::process::Output) -> Result<String, String> {
        output
            .status
            .success()
            .then_some(String::from_utf8_lossy(&output.stdout).trim().to_string())
            .filter(|text| !text.is_empty())
            .ok_or_else(|| {
                let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
                format!("mxrun: failed to query '{program}': {stderr}")
            })
    }

    fn first_version(text: &str) -> Option<String> {
        text.split_whitespace()
            .find(|part| {
                part.chars().any(|ch| ch.is_ascii_digit())
                    && part.chars().all(|ch| ch.is_ascii_digit() || ch == '.')
            })
            .map(|part| part.to_string())
    }

    fn take_major_minor(release: &str) -> String {
        release.split('.').take(2).collect::<Vec<_>>().join(".")
    }

    fn is_linux_target(&self) -> bool {
        matches!(self.target.os(), "GNU/Linux" | "Linux")
            || self.target.os().starts_with("GNU/Linux")
            || self.target.os().starts_with("Linux")
    }

    fn log_mirror_header(
        &self,
        mirror_target: &MirrorTarget,
        log_path: &Path,
    ) -> Result<(), String> {
        LogAppendFile::open(log_path).and_then(|mut log_file| {
            log_file.write_text(&format!(
                "\n\nMirroring {} artifacts:\n",
                Self::blue_bold(mirror_target.label())
            ))
        })
    }

    fn ensure_target_subdir(
        &self,
        mirror_target: &MirrorTarget,
        path: &Path,
    ) -> Result<(), String> {
        fs::create_dir_all(
            mirror_target
                .root()
                .join(path)
                .parent()
                .map(Path::to_path_buf)
                .unwrap_or_else(|| mirror_target.root().to_path_buf()),
        )
        .map_err(|err| format!("mxrun: failed to create mirror directory: {err}"))
    }

    fn mirror_path(
        &self,
        mirror_target: &MirrorTarget,
        path: &Path,
        log_path: &Path,
    ) -> Result<(), String> {
        if self.target.is_local() {
            self.mirror_local_path(mirror_target, path, log_path)
        } else {
            self.mirror_remote_path(mirror_target, path, log_path)
        }
    }

    fn mirror_local_path(
        &self,
        mirror_target: &MirrorTarget,
        path: &Path,
        log_path: &Path,
    ) -> Result<(), String> {
        self.command("rsync", self.local_rsync_args(mirror_target, path))
            .status(log_path)
            .and_then(Self::ensure_success)
    }

    fn mirror_remote_path(
        &self,
        mirror_target: &MirrorTarget,
        path: &Path,
        log_path: &Path,
    ) -> Result<(), String> {
        self.command("rsync", self.remote_rsync_args(mirror_target, path))
            .status(log_path)
            .and_then(Self::ensure_success)
    }

    fn log_mirror_root(
        &self,
        mirror_target: &MirrorTarget,
        index: usize,
        path: &Path,
        log_path: &Path,
    ) -> Result<(), String> {
        LogAppendFile::open(log_path).and_then(|mut log_file| {
            log_file.write_text(&format!(
                "    {index}. src: {}\n         dst: {}\n\n",
                Self::green(&self.source_label(path)),
                Self::green(&mirror_target.root().join(path).display().to_string())
            ))
        })
    }

    fn local_rsync_args(&self, mirror_target: &MirrorTarget, path: &Path) -> Vec<String> {
        vec![
            "-az".to_string(),
            self.source_path(path).display().to_string(),
            mirror_target.root().join(path).display().to_string(),
        ]
    }

    fn remote_rsync_args(&self, mirror_target: &MirrorTarget, path: &Path) -> Vec<String> {
        vec![
            "-az".to_string(),
            format!("{}:{}", self.target.host(), self.remote_source_path(path)),
            mirror_target.root().join(path).display().to_string(),
        ]
    }

    fn source_path(&self, path: &Path) -> PathBuf {
        self.root_dir.join(path)
    }

    fn remote_source_path(&self, path: &Path) -> String {
        format!("{}/{}", self.target.remote_path(), path.display())
    }

    fn source_label(&self, path: &Path) -> String {
        if self.target.is_local() {
            self.source_path(path).display().to_string()
        } else {
            format!("{}:{}", self.target.host(), self.remote_source_path(path))
        }
    }

    fn local_manifest_path(&self) -> PathBuf {
        self.root_dir.join(self.mirror_plan.manifest())
    }

    fn remote_manifest_path(&self) -> String {
        format!(
            "{}/{}",
            self.target.remote_path(),
            self.mirror_plan.manifest().display()
        )
    }

    fn blue_bold(text: &str) -> String {
        format!("\u{1b}[1;94m{text}\u{1b}[0m")
    }

    fn green(text: &str) -> String {
        format!("\u{1b}[92m{text}\u{1b}[0m")
    }

    fn command(&self, program: &str, args: Vec<String>) -> LoggedCommand {
        LoggedCommand::new(program, args)
    }

    fn ensure_success(status: i32) -> Result<(), String> {
        (status == 0)
            .then_some(())
            .ok_or_else(|| "mxrun: result mirroring failed".to_string())
    }
}

struct MirrorTarget {
    label: String,
    root: PathBuf,
}

impl MirrorTarget {
    fn new(label: String, mirror_root: &Path) -> Self {
        Self {
            root: mirror_root.join(Self::dir_name(&label)),
            label,
        }
    }

    fn label(&self) -> &str {
        &self.label
    }

    fn root(&self) -> &Path {
        &self.root
    }

    fn dir_name(label: &str) -> String {
        label
            .chars()
            .map(|ch| {
                if ch.is_ascii_alphanumeric() || ch == '.' {
                    ch.to_ascii_lowercase()
                } else {
                    '_'
                }
            })
            .collect::<String>()
            .trim_matches('_')
            .to_string()
    }
}

struct LoggedCommand {
    program: String,
    args: Vec<String>,
}

impl LoggedCommand {
    fn new(program: &str, args: Vec<String>) -> Self {
        Self {
            program: program.to_string(),
            args,
        }
    }

    fn status(&self, log_path: &Path) -> Result<i32, String> {
        LogAppendFile::open(log_path).and_then(|log_file| {
            log_file
                .spawn(&self.program, &self.args)
                .and_then(|mut child| {
                    child
                        .wait()
                        .map_err(|err| format!("mxrun: failed to wait for command: {err}"))
                })
                .map(|status| status.code().unwrap_or(1))
        })
    }
}

struct LogAppendFile {
    file: File,
}

impl LogAppendFile {
    fn open(log_path: &Path) -> Result<Self, String> {
        File::options()
            .create(true)
            .append(true)
            .open(log_path)
            .map_err(|err| format!("mxrun: failed to open log file for append: {err}"))
            .map(|file| Self { file })
    }

    fn spawn(&self, program: &str, args: &[String]) -> Result<std::process::Child, String> {
        let stdout = self
            .file
            .try_clone()
            .map_err(|err| format!("mxrun: failed to clone log file handle: {err}"))?;
        let stderr = self
            .file
            .try_clone()
            .map_err(|err| format!("mxrun: failed to clone log file handle: {err}"))?;

        Command::new(program)
            .args(args)
            .stdout(Stdio::from(stdout))
            .stderr(Stdio::from(stderr))
            .spawn()
            .map_err(|err| format!("mxrun: failed to spawn command '{program}': {err}"))
    }

    fn write_text(&mut self, text: &str) -> Result<(), String> {
        self.file
            .write_all(text.as_bytes())
            .map_err(|err| format!("mxrun: failed to append log file: {err}"))
            .and_then(|_| {
                self.file
                    .flush()
                    .map_err(|err| format!("mxrun: failed to flush log file: {err}"))
            })
    }
}

#[cfg(test)]
pub struct JobResult {
    log_path: PathBuf,
    status: i32,
}

#[cfg(test)]
impl JobResult {
    pub fn new(log_path: PathBuf, status: i32) -> Self {
        Self { log_path, status }
    }

    pub fn log_path(&self) -> &Path {
        &self.log_path
    }

    pub fn status(&self) -> i32 {
        self.status
    }

    pub fn is_success(&self) -> bool {
        self.status == 0
    }
}

struct PtySession<'a> {
    command: &'a BuildCommand,
}

impl<'a> PtySession<'a> {
    fn new(command: &'a BuildCommand) -> Result<Self, String> {
        Ok(Self { command })
    }

    fn run(&self, log_path: &Path) -> Result<i32, String> {
        self.pty_system()
            .openpty(self.size())
            .map_err(|err| format!("mxrun: failed to open PTY: {err}"))
            .and_then(|pty| self.spawn(pty, log_path))
    }

    fn pty_system(&self) -> Box<dyn PtySystem + Send> {
        native_pty_system()
    }

    fn size(&self) -> PtySize {
        PtySize {
            rows: 40,
            cols: 160,
            pixel_width: 0,
            pixel_height: 0,
        }
    }

    fn spawn(&self, pty: portable_pty::PtyPair, log_path: &Path) -> Result<i32, String> {
        pty.slave
            .spawn_command(self.command_builder())
            .map_err(|err| format!("mxrun: failed to spawn PTY child: {err}"))
            .and_then(|mut child| {
                drop(pty.slave);
                pty.master
                    .try_clone_reader()
                    .map_err(|err| format!("mxrun: failed to clone PTY reader: {err}"))
                    .and_then(|reader| {
                        Self::capture(reader, log_path).and_then(|capture| {
                            child
                                .wait()
                                .map_err(|err| {
                                    format!("mxrun: failed to wait for PTY child: {err}")
                                })
                                .map(|status| (capture, status))
                        })
                    })
                    .and_then(|(capture, status)| capture.join().map(|_| status))
            })
            .map(|status| i32::try_from(status.exit_code()).unwrap_or(1))
    }

    fn command_builder(&self) -> CommandBuilder {
        self.command
            .cwd()
            .map(|cwd| {
                let mut builder = CommandBuilder::new(self.command.program());
                builder.args(self.command.args());
                builder.cwd(cwd);
                builder.env("TERM", "xterm-256color");
                if let Ok(sock) = std::env::var("SSH_AUTH_SOCK") {
                    builder.env("SSH_AUTH_SOCK", sock);
                }
                builder
            })
            .unwrap_or_else(|| {
                let mut builder = CommandBuilder::new(self.command.program());
                builder.args(self.command.args());
                builder.env("TERM", "xterm-256color");
                if let Ok(sock) = std::env::var("SSH_AUTH_SOCK") {
                    builder.env("SSH_AUTH_SOCK", sock);
                }
                builder
            })
    }

    fn capture(reader: Box<dyn Read + Send>, log_path: &Path) -> Result<LogCapture, String> {
        File::create(log_path)
            .map_err(|err| format!("mxrun: failed to create log file: {err}"))
            .map(|file| LogCapture::new(reader, file))
    }
}

struct LogCapture {
    thread: thread::JoinHandle<Result<(), String>>,
}

impl LogCapture {
    fn new(mut reader: Box<dyn Read + Send>, mut file: File) -> Self {
        Self {
            thread: thread::spawn(move || {
                let mut buffer = [0_u8; 4096];
                loop {
                    match reader
                        .read(&mut buffer)
                        .map_err(|err| format!("mxrun: failed to read PTY stream: {err}"))?
                    {
                        0 => return Ok(()),
                        read_len => {
                            file.write_all(&buffer[..read_len])
                                .map_err(|err| format!("mxrun: failed to write log file: {err}"))?;
                            file.flush()
                                .map_err(|err| format!("mxrun: failed to flush log file: {err}"))?;
                        }
                    }
                }
            }),
        }
    }

    fn join(self) -> Result<(), String> {
        self.thread
            .join()
            .map_err(|_| "mxrun: PTY log capture thread panicked".to_string())?
    }
}
