use anyhow::{Context, Result};
use bmux_cli_schema::SandboxEnvModeArg;
use bmux_config::ConfigPaths;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Command as ProcessCommand, ExitStatus, Stdio};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use crate::sandbox_meta::{
    LOCK_FILE, MANIFEST_FILE, SandboxManifest, SandboxManifestPaths,
    clear_lock as clear_sandbox_lock, prune_missing_index_entries,
    read_index_entries as read_sandbox_index_entries, read_lock as read_sandbox_lock,
    read_manifest as read_sandbox_manifest, remove_index_entry as remove_sandbox_index_entry,
    replace_index_entries as replace_sandbox_index_entries,
    sandbox_id_from_root as sandbox_id_from_root_meta,
    sandbox_index_exists as sandbox_index_exists_meta, unix_millis_now as unix_millis_now_meta,
    upsert_index_entry as upsert_sandbox_index_entry, write_lock as write_sandbox_lock,
    write_manifest as write_sandbox_manifest,
};

const SANDBOX_PREFIX: &str = "bmux-sbx-";
const PID_MARKER_FILE: &str = "sandbox.pid";
const DEFAULT_CLEANUP_MIN_AGE: Duration = Duration::from_mins(5);
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(1);
const LOCK_FRESHNESS: Duration = Duration::from_secs(15);
const SANDBOX_JSON_SCHEMA_VERSION: u32 = 1;

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
struct CleanupEntry {
    path: String,
    source: String,
    age_secs: u64,
    status: String,
    reason: String,
    removed: bool,
}

#[derive(Debug, Clone, Copy)]
struct CleanupSkipped {
    source_mismatch: usize,
    running: usize,
    recent: usize,
    not_failed: usize,
    missing_manifest: usize,
    delete_failed: usize,
}

#[derive(Debug)]
struct CleanupScan {
    scanned: usize,
    entries: Vec<CleanupEntry>,
    skipped: CleanupSkipped,
}

#[derive(Debug, Clone, Copy, Serialize)]
struct IndexRebuildReport {
    scanned: usize,
    rebuilt_count: usize,
    pruned_count: usize,
    missing_manifest: usize,
    scan_fallback_used: bool,
}

#[derive(Debug, Clone)]
struct SandboxCandidate {
    root: PathBuf,
    updated_at_unix_ms: Option<u128>,
}

#[derive(Debug, Clone, Serialize)]
struct SandboxListEntry {
    id: String,
    source: String,
    root: String,
    status: String,
    age_secs: u64,
    exit_code: Option<i32>,
    kept: Option<bool>,
}

#[derive(Debug, Clone, Copy, Serialize)]
struct StatusCounts {
    total: usize,
    running: usize,
    failed: usize,
    stopped: usize,
}

#[derive(Debug, Clone, Serialize)]
struct SourceStatusCounts {
    source: String,
    total: usize,
    running: usize,
    failed: usize,
    stopped: usize,
}

#[derive(Debug, Clone, Serialize)]
struct SandboxStatusHealth {
    stale_lock_count: usize,
    missing_manifest_count: usize,
    index_exists: bool,
    index_entries: usize,
    index_missing_roots: usize,
    index_read_error: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
struct SandboxStatusSnapshot {
    totals: StatusCounts,
    by_source: Vec<SourceStatusCounts>,
    health: SandboxStatusHealth,
    reconcile: ReconcileReport,
}

#[derive(Debug, Clone, Default, Serialize)]
struct ReconcileReport {
    healed_entries: usize,
    pruned_entries: usize,
    rebuilt_entries: usize,
    normalized_running: usize,
    cleared_stale_locks: usize,
    scan_fallback_used: bool,
    index_read_error: Option<String>,
}

#[derive(Debug, Clone, Copy, Default)]
struct RecoveryReport {
    normalized_running: usize,
    cleared_stale_locks: usize,
}

#[derive(Debug, Clone)]
struct SandboxCandidateCollection {
    candidates: Vec<SandboxCandidate>,
    reconcile: ReconcileReport,
}

#[derive(Debug, Clone, Copy, Serialize)]
struct TriageSelection {
    defaulted_to_latest_failed: bool,
    latest: bool,
    latest_failed: bool,
}

#[derive(Debug, Clone, Serialize)]
struct TriageTarget {
    id: String,
    source: String,
    status: String,
    running: bool,
    root: String,
    log_dir: String,
    latest_log: Option<String>,
    repro: String,
    log_tail: Vec<String>,
}

#[derive(Debug, Clone, Serialize)]
struct TriageSnapshot {
    totals: StatusCounts,
    health: SandboxStatusHealth,
    reconcile: ReconcileReport,
    selection: TriageSelection,
    source_filter: Option<String>,
    target: TriageTarget,
}

#[derive(Debug, Clone, Serialize)]
struct TriageBundleSnapshot {
    requested: bool,
    executed: bool,
    bundle_dir: Option<String>,
    bundle_manifest: Option<String>,
    verify: Option<BundleVerifyReport>,
}

#[derive(Debug, Clone, Serialize)]
struct DoctorCheck {
    name: String,
    ok: bool,
    detail: String,
}

#[derive(Debug, Clone, Serialize)]
struct DoctorFixReport {
    applied: bool,
    dry_run: bool,
    target: Option<String>,
    scanned: usize,
    normalized_running: usize,
    cleared_stale_locks: usize,
    index_rebuilt: bool,
    rebuilt_count: usize,
    pruned_count: usize,
    missing_manifest: usize,
}

#[derive(Debug, Clone)]
struct SandboxPaths {
    root_dir: PathBuf,
    config_home: PathBuf,
    data_home: PathBuf,
    runtime_dir: PathBuf,
    state_dir: PathBuf,
    log_dir: PathBuf,
    tmp_dir: PathBuf,
    home_dir: PathBuf,
    config_paths: ConfigPaths,
}

impl SandboxPaths {
    fn new(name: Option<&str>) -> Self {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map_or(0, |duration| duration.as_nanos());
        let suffix = name
            .map(sanitize_component)
            .filter(|value| !value.is_empty())
            .map_or_else(String::new, |value| format!("-{value}"));
        let root_dir = std::env::temp_dir().join(format!(
            "{SANDBOX_PREFIX}{nanos:x}-{}{}",
            std::process::id(),
            suffix
        ));

        let config_home = root_dir.join("config");
        let data_home = root_dir.join("data");
        let runtime_dir = root_dir.join("runtime");
        let state_dir = root_dir.join("state");
        let log_dir = root_dir.join("logs");
        let tmp_dir = root_dir.join("tmp");
        let home_dir = root_dir.join("home");

        let config_paths = ConfigPaths::new(
            config_home.join("bmux"),
            runtime_dir.clone(),
            data_home.join("bmux"),
            state_dir.clone(),
        );

        Self {
            root_dir,
            config_home,
            data_home,
            runtime_dir,
            state_dir,
            log_dir,
            tmp_dir,
            home_dir,
            config_paths,
        }
    }

    fn ensure_dirs(&self) -> Result<()> {
        std::fs::create_dir_all(&self.config_home)
            .with_context(|| format!("failed creating {}", self.config_home.display()))?;
        std::fs::create_dir_all(&self.data_home)
            .with_context(|| format!("failed creating {}", self.data_home.display()))?;
        std::fs::create_dir_all(&self.runtime_dir)
            .with_context(|| format!("failed creating {}", self.runtime_dir.display()))?;
        std::fs::create_dir_all(&self.state_dir)
            .with_context(|| format!("failed creating {}", self.state_dir.display()))?;
        std::fs::create_dir_all(&self.log_dir)
            .with_context(|| format!("failed creating {}", self.log_dir.display()))?;
        std::fs::create_dir_all(&self.tmp_dir)
            .with_context(|| format!("failed creating {}", self.tmp_dir.display()))?;
        std::fs::create_dir_all(&self.home_dir)
            .with_context(|| format!("failed creating {}", self.home_dir.display()))?;
        std::fs::create_dir_all(&self.config_paths.config_dir).with_context(|| {
            format!("failed creating {}", self.config_paths.config_dir.display())
        })?;
        std::fs::create_dir_all(&self.config_paths.data_dir)
            .with_context(|| format!("failed creating {}", self.config_paths.data_dir.display()))?;
        Ok(())
    }
}

#[derive(Debug, Serialize)]
struct SandboxRunReport {
    schema_version: u32,
    sandbox_id: String,
    sandbox_root: String,
    bmux_bin: String,
    env_mode: String,
    status: String,
    keep_requested: bool,
    kept: bool,
    exit_code: i32,
    duration_ms: u128,
}

struct RunOutcome<'a> {
    binary: &'a Path,
    options: &'a RunSandboxOptions<'a>,
    kept: bool,
    keep_on_failure: bool,
    raw_exit_code: i32,
    final_status: &'a str,
    duration_ms: u128,
}

struct SandboxLockGuard {
    root_dir: PathBuf,
}

impl SandboxLockGuard {
    fn new(root_dir: &Path) -> Self {
        Self {
            root_dir: root_dir.to_path_buf(),
        }
    }
}

impl Drop for SandboxLockGuard {
    fn drop(&mut self) {
        clear_lock(&self.root_dir);
    }
}

pub(super) struct RunSandboxOptions<'a> {
    pub(super) bmux_bin: Option<&'a str>,
    pub(super) env_mode: SandboxEnvModeArg,
    pub(super) keep: bool,
    pub(super) json: bool,
    pub(super) print_env: bool,
    pub(super) timeout_secs: Option<u64>,
    pub(super) name: Option<&'a str>,
}

pub(super) struct RerunSandboxOptions<'a> {
    pub(super) inspect: InspectTargetOptions<'a>,
    pub(super) run: RunSandboxOptions<'a>,
    pub(super) bmux_bin_override: Option<&'a str>,
    pub(super) env_mode_override: Option<SandboxEnvModeArg>,
}

pub(super) struct InspectTargetOptions<'a> {
    pub(super) target: Option<&'a str>,
    pub(super) latest: bool,
    pub(super) latest_failed: bool,
    pub(super) source_filter: Option<&'a str>,
}

pub(super) struct TriageSandboxOptions<'a> {
    pub(super) inspect: InspectTargetOptions<'a>,
    pub(super) tail: usize,
    pub(super) rerun: bool,
    pub(super) run: RunSandboxOptions<'a>,
    pub(super) bmux_bin_override: Option<&'a str>,
    pub(super) env_mode_override: Option<SandboxEnvModeArg>,
    pub(super) bundle: Option<TriageBundleOptions<'a>>,
    pub(super) json: bool,
}

pub(super) struct TriageBundleOptions<'a> {
    pub(super) output: Option<&'a str>,
    pub(super) strict_verify: bool,
}

#[derive(Debug, Clone, Copy)]
pub(super) struct BundleIncludeOptions {
    pub(super) env: bool,
    pub(super) index_state: bool,
    pub(super) doctor: bool,
}

pub(super) struct BundleSandboxOptions<'a> {
    pub(super) target: &'a str,
    pub(super) output: Option<&'a str>,
    pub(super) include: BundleIncludeOptions,
    pub(super) verify: bool,
    pub(super) json: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct BundleArtifactMetadata {
    path: String,
    kind: String,
    bytes: u64,
    file_count: usize,
    exists: bool,
    #[serde(default)]
    sha256: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
struct BundleVerifyManifest {
    #[serde(default)]
    bundle_version: Option<u32>,
    #[serde(default)]
    schema_version: Option<u32>,
    #[serde(default)]
    artifact_metadata: Vec<BundleArtifactMetadata>,
    #[serde(default)]
    artifacts: Vec<String>,
}

#[derive(Debug, Clone, Serialize)]
struct BundleVerifyIssue {
    path: String,
    field: String,
    expected: String,
    actual: String,
}

#[derive(Debug, Clone, Serialize)]
struct BundleVerifyReport {
    ok: bool,
    strict: bool,
    mode: String,
    bundle_dir: String,
    bundle_manifest: String,
    unexpected_artifacts: Vec<String>,
    version_check: BundleVersionCheck,
    checked_artifacts: usize,
    issue_count: usize,
    issues: Vec<BundleVerifyIssue>,
}

#[derive(Debug, Clone, Serialize)]
struct BundleVersionCheck {
    ok: bool,
    bundle_version: Option<u32>,
    schema_version: Option<u32>,
    reason: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
struct BundleCreateResult {
    bundle_dir: String,
    sandbox_root: String,
    sandbox_id: String,
    bundle_manifest: String,
    verify: Option<BundleVerifyReport>,
}

pub(super) async fn run_sandbox_run(
    options: RunSandboxOptions<'_>,
    command_args: &[String],
) -> Result<u8> {
    let sandbox = SandboxPaths::new(options.name);
    sandbox.ensure_dirs()?;
    write_pid_marker(&sandbox.root_dir)?;

    let binary = resolve_bmux_binary(options.bmux_bin)?;
    let mut manifest = build_manifest(
        &sandbox,
        &binary,
        command_args,
        options.env_mode,
        options.keep,
    );
    write_manifest(&sandbox.root_dir, &manifest)?;
    let _ = upsert_sandbox_index_entry(&manifest);
    write_lock(&sandbox.root_dir)?;
    let lock_guard = SandboxLockGuard::new(&sandbox.root_dir);

    if options.print_env {
        let env = collect_effective_sandbox_env(&sandbox, options.env_mode);
        println!("{}", serde_json::to_string_pretty(&env)?);
    }

    let (status, timed_out, duration_ms) = match spawn_and_wait(
        &binary,
        &sandbox,
        options.env_mode,
        command_args,
        options.timeout_secs.map(Duration::from_secs),
    )
    .await
    {
        Ok(values) => values,
        Err(error) => {
            let _ = mark_manifest_aborted(&sandbox.root_dir, &mut manifest);
            let _ = upsert_sandbox_index_entry(&manifest);
            return Err(error);
        }
    };

    let raw_exit_code = status.code().unwrap_or(if timed_out { 124 } else { 1 });
    let keep_on_failure = !status.success() || timed_out;
    let kept = options.keep || keep_on_failure;
    let final_status = if timed_out {
        "timed_out"
    } else if status.success() {
        "succeeded"
    } else {
        "failed"
    };

    manifest.updated_at_unix_ms = unix_millis_now_meta();
    manifest.status = final_status.to_string();
    manifest.exit_code = Some(raw_exit_code);
    manifest.kept = kept;
    write_manifest(&sandbox.root_dir, &manifest)?;
    let _ = upsert_sandbox_index_entry(&manifest);

    emit_run_output(
        &sandbox,
        &RunOutcome {
            binary: &binary,
            options: &options,
            kept,
            keep_on_failure,
            raw_exit_code,
            final_status,
            duration_ms,
        },
        &manifest,
    )?;

    if !kept {
        drop(lock_guard);
        let _ = std::fs::remove_dir_all(&sandbox.root_dir);
        let _ = remove_sandbox_index_entry(&sandbox.root_dir);
    }

    Ok(exit_code_to_u8(raw_exit_code))
}

fn mark_manifest_aborted(root_dir: &Path, manifest: &mut SandboxManifest) -> Result<()> {
    manifest.updated_at_unix_ms = unix_millis_now_meta();
    manifest.status = "aborted".to_string();
    manifest.exit_code = Some(1);
    manifest.kept = true;
    write_manifest(root_dir, manifest)
}

fn build_manifest(
    sandbox: &SandboxPaths,
    binary: &Path,
    command_args: &[String],
    env_mode: SandboxEnvModeArg,
    keep: bool,
) -> SandboxManifest {
    SandboxManifest {
        id: sandbox_id_from_root_meta(&sandbox.root_dir),
        source: "sandbox-cli".to_string(),
        created_at_unix_ms: unix_millis_now_meta(),
        updated_at_unix_ms: unix_millis_now_meta(),
        pid: std::process::id(),
        bmux_bin: binary.to_string_lossy().to_string(),
        command: command_args.to_vec(),
        env_mode: sandbox_env_mode_name(env_mode).to_string(),
        status: "running".to_string(),
        exit_code: None,
        kept: keep,
        paths: SandboxManifestPaths {
            root: sandbox.root_dir.to_string_lossy().to_string(),
            logs: sandbox.log_dir.to_string_lossy().to_string(),
            runtime: sandbox.runtime_dir.to_string_lossy().to_string(),
            state: sandbox.state_dir.to_string_lossy().to_string(),
        },
    }
}

async fn spawn_and_wait(
    binary: &Path,
    sandbox: &SandboxPaths,
    env_mode: SandboxEnvModeArg,
    command_args: &[String],
    timeout: Option<Duration>,
) -> Result<(ExitStatus, bool, u128)> {
    let mut command = ProcessCommand::new(binary);
    command.args(command_args);
    command.stdin(Stdio::inherit());
    command.stdout(Stdio::inherit());
    command.stderr(Stdio::inherit());
    apply_sandbox_env(&mut command, sandbox, env_mode);

    let start = Instant::now();
    let mut last_heartbeat = Instant::now();
    let mut child = command.spawn().with_context(|| {
        format!(
            "failed spawning sandbox command with {}",
            binary.to_string_lossy()
        )
    })?;

    let mut timed_out = false;
    let status = loop {
        if last_heartbeat.elapsed() >= HEARTBEAT_INTERVAL {
            let _ = write_lock(&sandbox.root_dir);
            last_heartbeat = Instant::now();
        }

        if let Some(status) = child
            .try_wait()
            .context("failed waiting for sandbox child")?
        {
            break status;
        }

        if timeout.is_some_and(|limit| start.elapsed() >= limit) {
            timed_out = true;
            let _ = child.kill();
            break child
                .wait()
                .context("failed waiting after sandbox timeout")?;
        }

        tokio::time::sleep(Duration::from_millis(100)).await;
    };

    Ok((status, timed_out, start.elapsed().as_millis()))
}

fn emit_run_output(
    sandbox: &SandboxPaths,
    outcome: &RunOutcome<'_>,
    manifest: &SandboxManifest,
) -> Result<()> {
    let repro = format_repro_command(outcome.options, manifest.command.as_slice());
    if outcome.options.json {
        let report = SandboxRunReport {
            schema_version: SANDBOX_JSON_SCHEMA_VERSION,
            sandbox_id: manifest.id.clone(),
            sandbox_root: sandbox.root_dir.to_string_lossy().to_string(),
            bmux_bin: outcome.binary.to_string_lossy().to_string(),
            env_mode: sandbox_env_mode_name(outcome.options.env_mode).to_string(),
            status: outcome.final_status.to_string(),
            keep_requested: outcome.options.keep,
            kept: outcome.kept,
            exit_code: outcome.raw_exit_code,
            duration_ms: outcome.duration_ms,
        };
        println!("{}", serde_json::to_string_pretty(&report)?);
    } else {
        if outcome.keep_on_failure {
            eprintln!(
                "sandbox command exited with {}; keeping sandbox at {}",
                outcome.raw_exit_code,
                sandbox.root_dir.display()
            );
            eprintln!("repro: {repro}");
        } else if outcome.kept {
            println!("kept sandbox at {}", sandbox.root_dir.display());
        }

        if outcome.kept {
            eprintln!("sandbox logs: {}", sandbox.log_dir.display());
        }
    }
    Ok(())
}

pub(super) fn run_sandbox_list(
    status_filter: Option<&str>,
    source_filter: Option<&str>,
    limit: usize,
    json: bool,
) -> Result<u8> {
    let collection = collect_sandbox_candidates();
    let mut entries = collection
        .candidates
        .into_iter()
        .map(|candidate| sandbox_list_entry(&candidate))
        .collect::<Vec<_>>();

    entries.sort_by_key(|entry| entry.age_secs);

    if let Some(filter) = status_filter {
        entries.retain(|entry| entry.status == filter);
    }
    if let Some(filter) = source_filter {
        entries.retain(|entry| entry.source == filter);
    }
    if limit > 0 {
        entries.truncate(limit);
    }

    if json {
        let payload = serde_json::json!({
            "schema_version": SANDBOX_JSON_SCHEMA_VERSION,
            "reconcile": collection.reconcile,
            "sandboxes": entries,
        });
        println!("{}", serde_json::to_string_pretty(&payload)?);
    } else if entries.is_empty() {
        println!("no sandboxes found");
    } else {
        println!(
            "ID                               SOURCE            STATUS   AGE   EXIT  KEPT  ROOT"
        );
        for entry in entries {
            println!(
                "{:<32} {:<17} {:<8} {:>4}s {:>5} {:>5}  {}",
                entry.id,
                entry.source,
                entry.status,
                entry.age_secs,
                entry
                    .exit_code
                    .map_or_else(|| "-".to_string(), |value| value.to_string()),
                entry.kept.map_or_else(
                    || "-".to_string(),
                    |value| if value { "yes" } else { "no" }.to_string()
                ),
                entry.root,
            );
        }
        print_reconcile_report_text(&collection.reconcile);
    }

    Ok(0)
}

pub(super) fn run_sandbox_status(json: bool) -> Result<u8> {
    let snapshot = collect_sandbox_status_snapshot();

    if json {
        let payload = serde_json::json!({
            "schema_version": SANDBOX_JSON_SCHEMA_VERSION,
            "totals": snapshot.totals,
            "by_source": snapshot.by_source,
            "health": snapshot.health,
            "reconcile": snapshot.reconcile,
        });
        println!("{}", serde_json::to_string_pretty(&payload)?);
    } else {
        print_sandbox_status_text(&snapshot);
        if let Some(error) = snapshot.health.index_read_error.as_deref() {
            println!("health: index_read_error={error}");
        }
    }

    Ok(0)
}

fn collect_sandbox_status_snapshot() -> SandboxStatusSnapshot {
    let collection = collect_sandbox_candidates();
    let entries = collection
        .candidates
        .iter()
        .map(sandbox_list_entry)
        .collect::<Vec<_>>();
    let totals = summarize_status_counts(&entries);
    let by_source = summarize_source_status_counts(&entries);
    let health = summarize_sandbox_health(&collection.candidates);

    SandboxStatusSnapshot {
        totals,
        by_source,
        health,
        reconcile: collection.reconcile,
    }
}

fn summarize_status_counts(entries: &[SandboxListEntry]) -> StatusCounts {
    entries.iter().fold(
        StatusCounts {
            total: 0,
            running: 0,
            failed: 0,
            stopped: 0,
        },
        |mut acc, entry| {
            acc.total += 1;
            match entry.status.as_str() {
                "running" => acc.running += 1,
                "failed" => acc.failed += 1,
                _ => acc.stopped += 1,
            }
            acc
        },
    )
}

fn summarize_source_status_counts(entries: &[SandboxListEntry]) -> Vec<SourceStatusCounts> {
    let mut by_source_map = std::collections::BTreeMap::<String, StatusCounts>::new();
    for entry in entries {
        let counts = by_source_map
            .entry(entry.source.clone())
            .or_insert(StatusCounts {
                total: 0,
                running: 0,
                failed: 0,
                stopped: 0,
            });
        counts.total += 1;
        match entry.status.as_str() {
            "running" => counts.running += 1,
            "failed" => counts.failed += 1,
            _ => counts.stopped += 1,
        }
    }

    by_source_map
        .into_iter()
        .map(|(source, counts)| SourceStatusCounts {
            source,
            total: counts.total,
            running: counts.running,
            failed: counts.failed,
            stopped: counts.stopped,
        })
        .collect::<Vec<_>>()
}

fn summarize_sandbox_health(candidates: &[SandboxCandidate]) -> SandboxStatusHealth {
    let stale_lock_count = candidates
        .iter()
        .filter(|candidate| sandbox_lock_is_stale(&candidate.root))
        .count();
    let missing_manifest_count = candidates
        .iter()
        .filter(|candidate| read_manifest(&candidate.root).is_err())
        .count();
    let index_exists = sandbox_index_exists_meta();
    let (index_entries, index_missing_roots, index_read_error) = match read_sandbox_index_entries()
    {
        Ok(index) => {
            let missing_roots = index
                .iter()
                .filter(|entry| !Path::new(&entry.root).is_dir())
                .count();
            (index.len(), missing_roots, None)
        }
        Err(error) => (0, 0, Some(error.to_string())),
    };

    SandboxStatusHealth {
        stale_lock_count,
        missing_manifest_count,
        index_exists,
        index_entries,
        index_missing_roots,
        index_read_error,
    }
}

fn print_sandbox_status_text(snapshot: &SandboxStatusSnapshot) {
    println!(
        "sandboxes: total={} running={} failed={} stopped={}",
        snapshot.totals.total,
        snapshot.totals.running,
        snapshot.totals.failed,
        snapshot.totals.stopped
    );
    if snapshot.by_source.is_empty() {
        println!("by source: (none)");
    } else {
        println!("by source:");
        for source in &snapshot.by_source {
            println!(
                "  {:<17} total={} running={} failed={} stopped={}",
                source.source, source.total, source.running, source.failed, source.stopped
            );
        }
    }
    println!(
        "health: stale_locks={} missing_manifest={} index_exists={} index_entries={} index_missing_roots={}",
        snapshot.health.stale_lock_count,
        snapshot.health.missing_manifest_count,
        snapshot.health.index_exists,
        snapshot.health.index_entries,
        snapshot.health.index_missing_roots
    );
    print_reconcile_report_text(&snapshot.reconcile);
}

fn print_reconcile_report_text(reconcile: &ReconcileReport) {
    if reconcile.healed_entries > 0 {
        println!(
            "reconcile: healed={} (rebuilt={}, pruned={}, normalized_running={}, cleared_stale_locks={}, fallback={})",
            reconcile.healed_entries,
            reconcile.rebuilt_entries,
            reconcile.pruned_entries,
            reconcile.normalized_running,
            reconcile.cleared_stale_locks,
            reconcile.scan_fallback_used
        );
    }
}

pub(super) fn run_sandbox_inspect(
    target: Option<&str>,
    latest: bool,
    latest_failed: bool,
    source_filter: Option<&str>,
    tail: usize,
    json: bool,
) -> Result<u8> {
    let collection = collect_sandbox_candidates();
    let root = resolve_inspect_target(
        "inspect",
        target,
        latest,
        latest_failed,
        source_filter,
        &collection.candidates,
    )?;
    let manifest = read_manifest(&root)?;
    let log_dir = root.join("logs");
    let latest_log = newest_regular_file(&log_dir);
    let log_tail = read_log_tail(&root, tail);
    let running = sandbox_process_alive(&root) || sandbox_socket_alive(&root);

    if json {
        let payload = serde_json::json!({
            "schema_version": SANDBOX_JSON_SCHEMA_VERSION,
            "reconcile": collection.reconcile,
            "root": root.to_string_lossy().to_string(),
            "log_dir": log_dir.to_string_lossy().to_string(),
            "latest_log": latest_log.as_ref().map(|path| path.to_string_lossy().to_string()),
            "manifest": manifest,
            "running": running,
            "log_tail": log_tail,
        });
        println!("{}", serde_json::to_string_pretty(&payload)?);
    } else {
        println!("id: {}", manifest.id);
        println!("source: {}", manifest.source);
        println!("status: {}", manifest.status);
        println!("running: {running}");
        println!(
            "exit_code: {}",
            manifest
                .exit_code
                .map_or_else(|| "-".to_string(), |value| value.to_string())
        );
        println!("kept: {}", if manifest.kept { "yes" } else { "no" });
        println!("env_mode: {}", manifest.env_mode);
        println!("root: {}", root.display());
        println!("log_dir: {}", log_dir.display());
        println!(
            "latest_log: {}",
            latest_log
                .as_ref()
                .map_or_else(|| "(none)".to_string(), |path| path.display().to_string())
        );
        println!("bmux_bin: {}", manifest.bmux_bin);
        println!("command: {}", manifest.command.join(" "));
        println!("repro: {}", format_repro_command_from_manifest(&manifest));
        println!("logs:");
        for line in log_tail {
            println!("  {line}");
        }
        print_reconcile_report_text(&collection.reconcile);
    }

    Ok(0)
}

pub(super) fn run_sandbox_tail(
    target: Option<&str>,
    latest: bool,
    latest_failed: bool,
    source_filter: Option<&str>,
    tail: usize,
    json: bool,
) -> Result<u8> {
    let collection = collect_sandbox_candidates();
    let root = resolve_inspect_target(
        "tail",
        target,
        latest,
        latest_failed,
        source_filter,
        &collection.candidates,
    )?;
    let manifest = read_manifest(&root)?;
    let log_dir = root.join("logs");
    let latest_log = newest_regular_file(&log_dir);
    let log_tail = read_log_tail(&root, tail);

    if json {
        let payload = serde_json::json!({
            "schema_version": SANDBOX_JSON_SCHEMA_VERSION,
            "reconcile": collection.reconcile,
            "id": manifest.id,
            "source": manifest.source,
            "status": manifest.status,
            "root": root.to_string_lossy().to_string(),
            "log_dir": log_dir.to_string_lossy().to_string(),
            "latest_log": latest_log.as_ref().map(|path| path.to_string_lossy().to_string()),
            "log_tail": log_tail,
        });
        println!("{}", serde_json::to_string_pretty(&payload)?);
    } else {
        println!("id: {}", manifest.id);
        println!("source: {}", manifest.source);
        println!("status: {}", manifest.status);
        println!("logs: {}", log_dir.display());
        println!(
            "latest_log: {}",
            latest_log
                .as_ref()
                .map_or_else(|| "(none)".to_string(), |path| path.display().to_string())
        );
        for line in log_tail {
            println!("{line}");
        }
    }

    Ok(0)
}

pub(super) fn run_sandbox_open(
    target: Option<&str>,
    latest: bool,
    latest_failed: bool,
    source_filter: Option<&str>,
    json: bool,
) -> Result<u8> {
    let collection = collect_sandbox_candidates();
    let root = resolve_inspect_target(
        "open",
        target,
        latest,
        latest_failed,
        source_filter,
        &collection.candidates,
    )?;
    let manifest = read_manifest(&root)?;
    let log_dir = root.join("logs");
    let latest_log = newest_regular_file(&log_dir);
    let repro = format_repro_command_from_manifest(&manifest);

    if json {
        let payload = serde_json::json!({
            "schema_version": SANDBOX_JSON_SCHEMA_VERSION,
            "reconcile": collection.reconcile,
            "id": manifest.id,
            "source": manifest.source,
            "status": manifest.status,
            "root": root.to_string_lossy().to_string(),
            "log_dir": log_dir.to_string_lossy().to_string(),
            "latest_log": latest_log.as_ref().map(|path| path.to_string_lossy().to_string()),
            "repro": repro,
        });
        println!("{}", serde_json::to_string_pretty(&payload)?);
    } else {
        println!("id: {}", manifest.id);
        println!("source: {}", manifest.source);
        println!("status: {}", manifest.status);
        println!("root: {}", root.display());
        println!("logs: {}", log_dir.display());
        println!(
            "latest_log: {}",
            latest_log
                .as_ref()
                .map_or_else(|| "(none)".to_string(), |path| path.display().to_string())
        );
        println!("repro: {repro}");
    }

    Ok(0)
}

pub(super) async fn run_sandbox_triage(options: TriageSandboxOptions<'_>) -> Result<u8> {
    anyhow::ensure!(
        !(options.json && options.rerun),
        "sandbox triage --json cannot be combined with --rerun"
    );

    let collection = collect_sandbox_candidates();
    let selection = resolve_triage_selector(&options);

    let root = resolve_inspect_target(
        "triage",
        options.inspect.target,
        selection.latest,
        selection.latest_failed,
        options.inspect.source_filter,
        &collection.candidates,
    )?;
    let manifest = read_manifest(&root)?;
    let snapshot = build_triage_snapshot(&options, &collection, selection, &root, &manifest);
    let (bundle_snapshot, bundle_exit_code) =
        triage_bundle_snapshot(&options, &root, &manifest, &snapshot)?;

    if options.json {
        let payload = triage_json_payload(&snapshot, &bundle_snapshot);
        println!("{}", serde_json::to_string_pretty(&payload)?);
        return Ok(bundle_exit_code);
    }

    print_triage_text(&snapshot);
    print_triage_bundle_text(&bundle_snapshot);

    if options.rerun {
        println!("rerun: executing manifest command");
        let target = snapshot.target.root.clone();
        let rerun_code = run_sandbox_rerun(RerunSandboxOptions {
            inspect: InspectTargetOptions {
                target: Some(target.as_str()),
                latest: false,
                latest_failed: false,
                source_filter: None,
            },
            run: RunSandboxOptions {
                bmux_bin: None,
                env_mode: SandboxEnvModeArg::Inherit,
                keep: options.run.keep,
                json: false,
                print_env: options.run.print_env,
                timeout_secs: options.run.timeout_secs,
                name: options.run.name,
            },
            bmux_bin_override: options.bmux_bin_override,
            env_mode_override: options.env_mode_override,
        })
        .await?;
        println!("rerun_exit_code: {rerun_code}");
        return Ok(std::cmp::max(bundle_exit_code, rerun_code));
    }

    Ok(bundle_exit_code)
}

fn build_triage_snapshot(
    options: &TriageSandboxOptions<'_>,
    collection: &SandboxCandidateCollection,
    selection: TriageSelection,
    root: &Path,
    manifest: &SandboxManifest,
) -> TriageSnapshot {
    let running = sandbox_process_alive(root) || sandbox_socket_alive(root);
    let log_dir = root.join("logs");
    let latest_log = newest_regular_file(&log_dir);
    let log_tail = read_log_tail(root, options.tail);
    let repro = format_repro_command_from_manifest(manifest);

    let entries = collection
        .candidates
        .iter()
        .map(sandbox_list_entry)
        .collect::<Vec<_>>();

    TriageSnapshot {
        totals: summarize_status_counts(&entries),
        health: summarize_sandbox_health(&collection.candidates),
        reconcile: collection.reconcile.clone(),
        selection,
        source_filter: options.inspect.source_filter.map(ToOwned::to_owned),
        target: TriageTarget {
            id: manifest.id.clone(),
            source: manifest.source.clone(),
            status: manifest.status.clone(),
            running,
            root: root.to_string_lossy().to_string(),
            log_dir: log_dir.to_string_lossy().to_string(),
            latest_log: latest_log
                .as_ref()
                .map(|path| path.to_string_lossy().to_string()),
            repro,
            log_tail,
        },
    }
}

fn triage_bundle_snapshot(
    options: &TriageSandboxOptions<'_>,
    root: &Path,
    manifest: &SandboxManifest,
    snapshot: &TriageSnapshot,
) -> Result<(TriageBundleSnapshot, u8)> {
    let mut bundle_snapshot = TriageBundleSnapshot {
        requested: options.bundle.is_some(),
        executed: false,
        bundle_dir: None,
        bundle_manifest: None,
        verify: None,
    };

    let bundle_exit_code = if let Some(bundle_options) = &options.bundle {
        let created = create_bundle_from_manifest(
            root,
            manifest,
            &snapshot.target.root,
            bundle_options.output,
            BundleIncludeOptions {
                env: false,
                index_state: false,
                doctor: false,
            },
            true,
            bundle_options.strict_verify,
        )?;
        bundle_snapshot.executed = true;
        bundle_snapshot.bundle_dir = Some(created.bundle_dir);
        bundle_snapshot.bundle_manifest = Some(created.bundle_manifest);
        bundle_snapshot.verify = created.verify;
        u8::from(
            bundle_snapshot
                .verify
                .as_ref()
                .is_some_and(|report| !report.ok),
        )
    } else {
        0
    };

    Ok((bundle_snapshot, bundle_exit_code))
}

fn print_triage_bundle_text(bundle: &TriageBundleSnapshot) {
    if !bundle.executed {
        return;
    }

    println!(
        "bundle: {}",
        bundle.bundle_dir.as_deref().unwrap_or("(none)")
    );
    if let Some(verify) = &bundle.verify {
        print_bundle_verify_text(verify);
    }
}

const fn resolve_triage_selector(options: &TriageSandboxOptions<'_>) -> TriageSelection {
    let defaulted = options.inspect.target.is_none()
        && !options.inspect.latest
        && !options.inspect.latest_failed;
    if defaulted {
        TriageSelection {
            defaulted_to_latest_failed: true,
            latest: false,
            latest_failed: true,
        }
    } else {
        TriageSelection {
            defaulted_to_latest_failed: false,
            latest: options.inspect.latest,
            latest_failed: options.inspect.latest_failed,
        }
    }
}

fn triage_json_payload(
    snapshot: &TriageSnapshot,
    bundle: &TriageBundleSnapshot,
) -> serde_json::Value {
    serde_json::json!({
        "schema_version": SANDBOX_JSON_SCHEMA_VERSION,
        "totals": snapshot.totals,
        "health": snapshot.health,
        "reconcile": snapshot.reconcile,
        "selection": {
            "defaulted_to_latest_failed": snapshot.selection.defaulted_to_latest_failed,
            "latest": snapshot.selection.latest,
            "latest_failed": snapshot.selection.latest_failed,
            "source_filter": snapshot.source_filter,
        },
        "target": snapshot.target,
        "bundle": bundle,
        "rerun": {
            "requested": false,
            "executed": false,
        },
    })
}

fn print_triage_text(snapshot: &TriageSnapshot) {
    println!(
        "sandboxes: total={} running={} failed={} stopped={}",
        snapshot.totals.total,
        snapshot.totals.running,
        snapshot.totals.failed,
        snapshot.totals.stopped
    );
    println!(
        "triage_target: {} source={} status={} running={}",
        snapshot.target.id, snapshot.target.source, snapshot.target.status, snapshot.target.running
    );
    if snapshot.selection.defaulted_to_latest_failed {
        println!("selection: defaulted to --latest-failed");
    }
    println!("root: {}", snapshot.target.root);
    println!("log_dir: {}", snapshot.target.log_dir);
    println!(
        "latest_log: {}",
        snapshot.target.latest_log.as_deref().unwrap_or("(none)")
    );
    println!("repro: {}", snapshot.target.repro);
    println!("logs:");
    for line in &snapshot.target.log_tail {
        println!("  {line}");
    }
    print_reconcile_report_text(&snapshot.reconcile);
}

pub(super) async fn run_sandbox_rerun(options: RerunSandboxOptions<'_>) -> Result<u8> {
    let collection = collect_sandbox_candidates();
    let root = resolve_inspect_target(
        "rerun",
        options.inspect.target,
        options.inspect.latest,
        options.inspect.latest_failed,
        options.inspect.source_filter,
        &collection.candidates,
    )?;
    let manifest = read_manifest(&root)?;
    anyhow::ensure!(
        !manifest.command.is_empty(),
        "sandbox manifest '{}' has no command to rerun",
        manifest.id
    );

    let resolved_env_mode = options
        .env_mode_override
        .unwrap_or_else(|| manifest_env_mode(&manifest));
    let bmux_bin_owned = options
        .bmux_bin_override
        .map_or_else(|| manifest.bmux_bin.clone(), ToOwned::to_owned);

    run_sandbox_run(
        RunSandboxOptions {
            bmux_bin: Some(bmux_bin_owned.as_str()),
            env_mode: resolved_env_mode,
            keep: options.run.keep,
            json: options.run.json,
            print_env: options.run.print_env,
            timeout_secs: options.run.timeout_secs,
            name: options.run.name,
        },
        &manifest.command,
    )
    .await
}

fn manifest_env_mode(manifest: &SandboxManifest) -> SandboxEnvModeArg {
    match manifest.env_mode.as_str() {
        "clean" => SandboxEnvModeArg::Clean,
        "hermetic" => SandboxEnvModeArg::Hermetic,
        _ => SandboxEnvModeArg::Inherit,
    }
}

fn resolve_inspect_target(
    command: &str,
    target: Option<&str>,
    latest: bool,
    latest_failed: bool,
    source_filter: Option<&str>,
    candidates: &[SandboxCandidate],
) -> Result<PathBuf> {
    if let Some(target) = target {
        return resolve_sandbox_target(target);
    }

    if latest || latest_failed {
        return resolve_latest_sandbox(latest_failed, source_filter, candidates);
    }

    anyhow::bail!("{command} target required (provide <id|path>, --latest, or --latest-failed)")
}

fn resolve_latest_sandbox(
    failed_only: bool,
    source_filter: Option<&str>,
    candidates: &[SandboxCandidate],
) -> Result<PathBuf> {
    let mut sorted = candidates.to_vec();
    sorted.sort_by_key(|candidate| std::cmp::Reverse(candidate_sort_key(candidate)));

    let mut source_total = 0usize;

    for candidate in sorted {
        let path = candidate.root;
        if let Some(source) = source_filter {
            if sandbox_source_for_dir(&path) != source {
                continue;
            }
            source_total += 1;
        }
        if !failed_only || matches!(sandbox_status_for_dir(&path), "failed") {
            return Ok(path);
        }
    }

    if let Some(source) = source_filter {
        let known_sources = format_known_sources(candidates);
        if source_total == 0 {
            if known_sources.is_empty() {
                anyhow::bail!("no sandboxes found for source {source}");
            }
            anyhow::bail!(
                "no sandboxes found for source {source}; available sources: {known_sources}"
            );
        }
        if failed_only {
            anyhow::bail!(
                "no failed sandboxes found for source {source}; try --latest --source {source}"
            );
        }
        anyhow::bail!("no sandboxes found for source {source}");
    }

    if failed_only {
        anyhow::bail!("no failed sandboxes found");
    }

    anyhow::bail!("no sandboxes found")
}

pub(super) fn run_sandbox_doctor(
    id: Option<&str>,
    fix: bool,
    dry_run: bool,
    json: bool,
) -> Result<u8> {
    let mut checks = Vec::new();

    let temp_dir = std::env::temp_dir();
    checks.push(DoctorCheck {
        name: "temp_dir_writable".to_string(),
        ok: std::fs::create_dir_all(&temp_dir).is_ok(),
        detail: temp_dir.display().to_string(),
    });

    let current_exe = std::env::current_exe();
    checks.push(DoctorCheck {
        name: "current_bmux_executable".to_string(),
        ok: current_exe.is_ok(),
        detail: current_exe.ok().map_or_else(
            || "unavailable".to_string(),
            |path| path.display().to_string(),
        ),
    });

    if let Some(target) = id {
        append_target_doctor_checks(&mut checks, target);
    }

    let fix_report = if fix {
        Some(run_sandbox_doctor_fix(id, dry_run)?)
    } else {
        None
    };

    let ok = checks.iter().all(|check| check.ok);
    if json {
        let payload = serde_json::json!({
            "schema_version": SANDBOX_JSON_SCHEMA_VERSION,
            "ok": ok,
            "checks": checks,
            "fix": fix_report,
        });
        println!("{}", serde_json::to_string_pretty(&payload)?);
    } else {
        println!("sandbox doctor: {}", if ok { "ok" } else { "issues found" });
        for check in checks {
            println!(
                "  {} {} ({})",
                if check.ok { "OK" } else { "FAIL" },
                check.name,
                check.detail
            );
        }
        if let Some(report) = fix_report {
            println!(
                "doctor fix: applied={} dry_run={} scanned={} normalized_running={} cleared_stale_locks={} index_rebuilt={} rebuilt={} pruned={} missing_manifest={}",
                report.applied,
                report.dry_run,
                report.scanned,
                report.normalized_running,
                report.cleared_stale_locks,
                report.index_rebuilt,
                report.rebuilt_count,
                report.pruned_count,
                report.missing_manifest
            );
        }
    }

    Ok(u8::from(!ok))
}

fn run_sandbox_doctor_fix(target: Option<&str>, dry_run: bool) -> Result<DoctorFixReport> {
    let (roots, target_value) = if let Some(target) = target {
        (
            vec![resolve_sandbox_target(target)?],
            Some(target.to_string()),
        )
    } else {
        (collect_sandbox_directories(), None)
    };

    let candidates = roots
        .iter()
        .cloned()
        .map(|root| SandboxCandidate {
            root,
            updated_at_unix_ms: None,
        })
        .collect::<Vec<_>>();

    let recovery = if dry_run {
        preview_candidate_lifecycle_recovery(&candidates)
    } else {
        recover_candidate_lifecycle_state(&candidates)
    };

    let mut report = DoctorFixReport {
        applied: !dry_run,
        dry_run,
        target: target_value,
        scanned: roots.len(),
        normalized_running: recovery.normalized_running,
        cleared_stale_locks: recovery.cleared_stale_locks,
        index_rebuilt: false,
        rebuilt_count: 0,
        pruned_count: 0,
        missing_manifest: 0,
    };

    if target.is_none() {
        let rebuild = if dry_run {
            preview_rebuild_sandbox_index_from_roots(&roots)
        } else {
            rebuild_sandbox_index_from_roots(&roots, !sandbox_index_exists_meta())
        };
        report.index_rebuilt = true;
        report.rebuilt_count = rebuild.rebuilt_count;
        report.pruned_count = rebuild.pruned_count;
        report.missing_manifest = rebuild.missing_manifest;
    } else if !dry_run {
        for root in &roots {
            if let Ok(manifest) = read_manifest(root) {
                let _ = upsert_sandbox_index_entry(&manifest);
            }
        }
    }

    Ok(report)
}

fn preview_candidate_lifecycle_recovery(candidates: &[SandboxCandidate]) -> RecoveryReport {
    let mut report = RecoveryReport::default();
    for candidate in candidates {
        let root = &candidate.root;
        if sandbox_lock_is_fresh(root) || sandbox_process_alive(root) || sandbox_socket_alive(root)
        {
            continue;
        }

        if let Ok(manifest) = read_manifest(root)
            && manifest.status == "running"
        {
            report.normalized_running += 1;
        }

        if sandbox_lock_is_stale(root) {
            report.cleared_stale_locks += 1;
        }
    }
    report
}

pub(super) fn run_sandbox_bundle(options: &BundleSandboxOptions<'_>) -> Result<u8> {
    let root = resolve_sandbox_target(options.target)?;
    let manifest = read_manifest(&root)?;

    let created = create_bundle_from_manifest(
        &root,
        &manifest,
        options.target,
        options.output,
        options.include,
        options.verify,
        false,
    )?;
    let exit_code = u8::from(created.verify.as_ref().is_some_and(|report| !report.ok));

    if options.json {
        let payload = serde_json::json!({
            "schema_version": SANDBOX_JSON_SCHEMA_VERSION,
            "bundle_dir": created.bundle_dir,
            "sandbox_root": created.sandbox_root,
            "sandbox_id": created.sandbox_id,
            "bundle_manifest": created.bundle_manifest,
            "verify": created.verify,
        });
        println!("{}", serde_json::to_string_pretty(&payload)?);
    } else {
        println!("sandbox bundle created: {}", created.bundle_dir);
        if let Some(report) = &created.verify {
            print_bundle_verify_text(report);
        }
    }

    Ok(exit_code)
}

fn create_bundle_from_manifest(
    root: &Path,
    manifest: &SandboxManifest,
    doctor_target: &str,
    output: Option<&str>,
    include: BundleIncludeOptions,
    verify: bool,
    verify_strict: bool,
) -> Result<BundleCreateResult> {
    let output_root = output.map_or_else(
        || {
            std::env::current_dir()
                .unwrap_or_else(|_| PathBuf::from("."))
                .join("sandbox-bundles")
        },
        PathBuf::from,
    );
    std::fs::create_dir_all(&output_root)
        .with_context(|| format!("failed creating {}", output_root.display()))?;

    let bundle_dir = output_root.join(format!("{}-{}", manifest.id, unix_millis_now_meta()));
    std::fs::create_dir_all(&bundle_dir)
        .with_context(|| format!("failed creating {}", bundle_dir.display()))?;

    let mut artifacts = write_bundle_core_artifacts(root, manifest, &bundle_dir)?;
    let index_status = write_bundle_optional_artifacts(
        manifest,
        doctor_target,
        &bundle_dir,
        include,
        &mut artifacts,
    )?;
    let artifact_metadata = bundle_artifact_metadata(&bundle_dir, &artifacts);

    let bundle_manifest_path = bundle_dir.join("bundle_manifest.json");
    let bundle_manifest_payload = serde_json::json!({
        "schema_version": SANDBOX_JSON_SCHEMA_VERSION,
        "bundle_version": 1,
        "created_at_unix_ms": unix_millis_now_meta(),
        "sandbox": {
            "id": manifest.id,
            "source": manifest.source,
            "status": manifest.status,
            "root": root.to_string_lossy().to_string(),
        },
        "includes": {
            "env": include.env,
            "index_state": include.index_state,
            "doctor": include.doctor,
        },
        "index_state": {
            "status": index_status,
        },
        "artifacts": artifacts,
        "artifact_metadata": artifact_metadata,
    });
    std::fs::write(
        &bundle_manifest_path,
        serde_json::to_vec_pretty(&bundle_manifest_payload)?,
    )
    .with_context(|| format!("failed writing {}", bundle_manifest_path.display()))?;

    let verification_report = if verify {
        Some(load_bundle_verify_report(&bundle_dir, verify_strict)?)
    } else {
        None
    };

    Ok(BundleCreateResult {
        bundle_dir: bundle_dir.to_string_lossy().to_string(),
        sandbox_root: root.to_string_lossy().to_string(),
        sandbox_id: manifest.id.clone(),
        bundle_manifest: bundle_manifest_path.to_string_lossy().to_string(),
        verify: verification_report,
    })
}

pub(super) fn run_sandbox_verify_bundle(bundle_dir: &str, strict: bool, json: bool) -> Result<u8> {
    let report = load_bundle_verify_report(Path::new(bundle_dir), strict)?;
    if json {
        let payload = serde_json::json!({
            "schema_version": SANDBOX_JSON_SCHEMA_VERSION,
            "ok": report.ok,
            "strict": report.strict,
            "mode": report.mode,
            "bundle_dir": report.bundle_dir,
            "bundle_manifest": report.bundle_manifest,
            "unexpected_artifacts": report.unexpected_artifacts,
            "version_check": report.version_check,
            "checked_artifacts": report.checked_artifacts,
            "issue_count": report.issue_count,
            "issues": report.issues,
        });
        println!("{}", serde_json::to_string_pretty(&payload)?);
    } else {
        print_bundle_verify_text(&report);
    }

    Ok(u8::from(!report.ok))
}

fn load_bundle_verify_report(bundle_root: &Path, strict: bool) -> Result<BundleVerifyReport> {
    anyhow::ensure!(
        bundle_root.is_dir(),
        "sandbox bundle directory not found: {}",
        bundle_root.display()
    );

    let bundle_manifest_path = bundle_root.join("bundle_manifest.json");
    let manifest_bytes = std::fs::read(&bundle_manifest_path)
        .with_context(|| format!("failed reading {}", bundle_manifest_path.display()))?;
    let manifest: BundleVerifyManifest = serde_json::from_slice(&manifest_bytes)
        .with_context(|| format!("failed parsing {}", bundle_manifest_path.display()))?;

    let version_check = validate_bundle_version(&manifest);

    let (mode, issues, checked_artifacts) = if manifest.artifact_metadata.is_empty() {
        (
            "existence_only".to_string(),
            verify_bundle_artifacts_existence(bundle_root, &manifest.artifacts),
            manifest.artifacts.len(),
        )
    } else {
        (
            "strict_metadata".to_string(),
            verify_bundle_artifacts_metadata(bundle_root, &manifest.artifact_metadata),
            manifest.artifact_metadata.len(),
        )
    };

    let expected_artifacts = expected_bundle_artifact_paths(&manifest);
    let unexpected_artifacts = bundle_unexpected_artifacts(bundle_root, &expected_artifacts)?;

    let mut issues = issues;
    if strict {
        for artifact in &unexpected_artifacts {
            issues.push(BundleVerifyIssue {
                path: artifact.clone(),
                field: "unexpected_artifact".to_string(),
                expected: "absent".to_string(),
                actual: "present".to_string(),
            });
        }
    }

    if !version_check.ok {
        issues.push(BundleVerifyIssue {
            path: "bundle_manifest.json".to_string(),
            field: "version_check".to_string(),
            expected: "supported".to_string(),
            actual: version_check
                .reason
                .clone()
                .unwrap_or_else(|| "unsupported version".to_string()),
        });
    }

    let issue_count = issues.len();
    let ok = issue_count == 0;
    Ok(BundleVerifyReport {
        ok,
        strict,
        mode,
        bundle_dir: bundle_root.to_string_lossy().to_string(),
        bundle_manifest: bundle_manifest_path.to_string_lossy().to_string(),
        unexpected_artifacts,
        version_check,
        checked_artifacts,
        issue_count,
        issues,
    })
}

fn validate_bundle_version(manifest: &BundleVerifyManifest) -> BundleVersionCheck {
    let bundle_version = manifest.bundle_version;
    let schema_version = manifest.schema_version;

    if bundle_version != Some(1) {
        return BundleVersionCheck {
            ok: false,
            bundle_version,
            schema_version,
            reason: Some(format!(
                "unsupported bundle_version={}; expected 1",
                bundle_version.map_or_else(|| "missing".to_string(), |value| value.to_string())
            )),
        };
    }

    if schema_version != Some(SANDBOX_JSON_SCHEMA_VERSION) {
        return BundleVersionCheck {
            ok: false,
            bundle_version,
            schema_version,
            reason: Some(format!(
                "unsupported schema_version={}; expected {}",
                schema_version.map_or_else(|| "missing".to_string(), |value| value.to_string()),
                SANDBOX_JSON_SCHEMA_VERSION
            )),
        };
    }

    BundleVersionCheck {
        ok: true,
        bundle_version,
        schema_version,
        reason: None,
    }
}

fn expected_bundle_artifact_paths(manifest: &BundleVerifyManifest) -> Vec<String> {
    if manifest.artifact_metadata.is_empty() {
        return manifest.artifacts.clone();
    }

    manifest
        .artifact_metadata
        .iter()
        .map(|entry| entry.path.clone())
        .collect()
}

fn bundle_unexpected_artifacts(bundle_root: &Path, expected: &[String]) -> Result<Vec<String>> {
    let mut expected_set = std::collections::BTreeSet::new();
    expected_set.insert("bundle_manifest.json".to_string());
    for artifact in expected {
        expected_set.insert(artifact.clone());
    }

    let mut unexpected = Vec::new();
    for entry in std::fs::read_dir(bundle_root)
        .with_context(|| format!("failed reading {}", bundle_root.display()))?
    {
        let entry = entry.with_context(|| format!("failed reading {}", bundle_root.display()))?;
        let file_type = entry
            .file_type()
            .with_context(|| format!("failed reading type for {}", entry.path().display()))?;

        let Some(name) = entry.file_name().to_str().map(ToOwned::to_owned) else {
            continue;
        };

        let normalized = if file_type.is_dir() {
            format!("{name}/")
        } else {
            name
        };

        if !expected_set.contains(&normalized) {
            unexpected.push(normalized);
        }
    }

    unexpected.sort();
    Ok(unexpected)
}

fn print_bundle_verify_text(report: &BundleVerifyReport) {
    println!(
        "bundle verify: {}",
        if report.ok { "ok" } else { "drift detected" }
    );
    println!("mode: {}", report.mode);
    println!("strict: {}", report.strict);
    println!("checked_artifacts: {}", report.checked_artifacts);
    if !report.unexpected_artifacts.is_empty() {
        println!("unexpected_artifacts:");
        for artifact in &report.unexpected_artifacts {
            println!("  {artifact}");
        }
    }
    if !report.version_check.ok {
        println!(
            "version_check: unsupported ({})",
            report
                .version_check
                .reason
                .as_deref()
                .unwrap_or("unknown reason")
        );
    }
    if !report.issues.is_empty() {
        println!("issues:");
        for issue in &report.issues {
            println!(
                "  path={} field={} expected={} actual={}",
                issue.path, issue.field, issue.expected, issue.actual
            );
        }
    }
}

fn verify_bundle_artifacts_metadata(
    bundle_root: &Path,
    expected: &[BundleArtifactMetadata],
) -> Vec<BundleVerifyIssue> {
    let mut issues = Vec::new();
    for expected_entry in expected {
        let actual = bundle_artifact_metadata_for_expected(bundle_root, expected_entry);
        if expected_entry.kind != actual.kind {
            issues.push(BundleVerifyIssue {
                path: expected_entry.path.clone(),
                field: "kind".to_string(),
                expected: expected_entry.kind.clone(),
                actual: actual.kind,
            });
        }
        if expected_entry.exists != actual.exists {
            issues.push(BundleVerifyIssue {
                path: expected_entry.path.clone(),
                field: "exists".to_string(),
                expected: expected_entry.exists.to_string(),
                actual: actual.exists.to_string(),
            });
        }
        if expected_entry.bytes != actual.bytes {
            issues.push(BundleVerifyIssue {
                path: expected_entry.path.clone(),
                field: "bytes".to_string(),
                expected: expected_entry.bytes.to_string(),
                actual: actual.bytes.to_string(),
            });
        }
        if expected_entry.file_count != actual.file_count {
            issues.push(BundleVerifyIssue {
                path: expected_entry.path.clone(),
                field: "file_count".to_string(),
                expected: expected_entry.file_count.to_string(),
                actual: actual.file_count.to_string(),
            });
        }
        if let Some(expected_hash) = expected_entry.sha256.as_ref()
            && actual.sha256.as_deref() != Some(expected_hash.as_str())
        {
            issues.push(BundleVerifyIssue {
                path: expected_entry.path.clone(),
                field: "sha256".to_string(),
                expected: expected_hash.clone(),
                actual: actual.sha256.unwrap_or_else(|| "missing".to_string()),
            });
        }
    }

    issues
}

fn verify_bundle_artifacts_existence(
    bundle_root: &Path,
    artifacts: &[String],
) -> Vec<BundleVerifyIssue> {
    let mut issues = Vec::new();
    for artifact in artifacts {
        let expected_kind = if artifact.ends_with('/') {
            "directory"
        } else {
            "file"
        };
        let expected = BundleArtifactMetadata {
            path: artifact.clone(),
            kind: expected_kind.to_string(),
            bytes: 0,
            file_count: 0,
            exists: true,
            sha256: None,
        };
        let actual = bundle_artifact_metadata_for_expected(bundle_root, &expected);
        if !actual.exists {
            issues.push(BundleVerifyIssue {
                path: artifact.clone(),
                field: "exists".to_string(),
                expected: "true".to_string(),
                actual: "false".to_string(),
            });
        }
        if actual.kind != expected_kind {
            issues.push(BundleVerifyIssue {
                path: artifact.clone(),
                field: "kind".to_string(),
                expected: expected_kind.to_string(),
                actual: actual.kind,
            });
        }
    }

    issues
}

fn bundle_artifact_metadata_for_expected(
    bundle_root: &Path,
    expected: &BundleArtifactMetadata,
) -> BundleArtifactMetadata {
    let relative = expected.path.trim_end_matches('/');
    let path = bundle_root.join(relative);
    if expected.kind == "directory" {
        let (bytes, file_count, exists) = directory_stats(&path);
        let sha256 = if expected.sha256.is_some() {
            directory_sha256_hex(&path)
        } else {
            None
        };
        return BundleArtifactMetadata {
            path: expected.path.clone(),
            kind: "directory".to_string(),
            bytes,
            file_count,
            exists,
            sha256,
        };
    }

    let (bytes, exists) = file_bytes(&path);
    let sha256 = if expected.sha256.is_some() {
        file_sha256_hex(&path)
    } else {
        None
    };
    BundleArtifactMetadata {
        path: expected.path.clone(),
        kind: "file".to_string(),
        bytes,
        file_count: usize::from(exists),
        exists,
        sha256,
    }
}

fn write_bundle_core_artifacts(
    root: &Path,
    manifest: &SandboxManifest,
    bundle_dir: &Path,
) -> Result<Vec<String>> {
    copy_if_exists(&root.join(MANIFEST_FILE), &bundle_dir.join(MANIFEST_FILE))?;
    copy_if_exists(
        &root.join(PID_MARKER_FILE),
        &bundle_dir.join(PID_MARKER_FILE),
    )?;
    copy_if_exists(&root.join(LOCK_FILE), &bundle_dir.join(LOCK_FILE))?;

    let mut artifacts = vec![
        MANIFEST_FILE.to_string(),
        PID_MARKER_FILE.to_string(),
        LOCK_FILE.to_string(),
    ];

    let logs_src = root.join("logs");
    if logs_src.exists() {
        copy_directory_recursive(&logs_src, &bundle_dir.join("logs"))?;
        artifacts.push("logs/".to_string());
    }

    let repro_path = bundle_dir.join("repro.txt");
    std::fs::write(&repro_path, format_repro_command_from_manifest(manifest))
        .with_context(|| format!("failed writing {}", repro_path.display()))?;
    artifacts.push("repro.txt".to_string());

    Ok(artifacts)
}

fn write_bundle_optional_artifacts(
    manifest: &SandboxManifest,
    target: &str,
    bundle_dir: &Path,
    include: BundleIncludeOptions,
    artifacts: &mut Vec<String>,
) -> Result<String> {
    let mut index_status = "not_included".to_string();

    if include.env {
        let env_path = bundle_dir.join("env.json");
        let payload = bundle_env_payload(manifest);
        std::fs::write(&env_path, serde_json::to_vec_pretty(&payload)?)
            .with_context(|| format!("failed writing {}", env_path.display()))?;
        artifacts.push("env.json".to_string());
    }

    if include.index_state {
        index_status = write_bundle_index_state(manifest, bundle_dir, artifacts)?;
    }

    if include.doctor {
        let doctor_path = bundle_dir.join("doctor.json");
        let doctor_payload = bundle_doctor_payload(target);
        std::fs::write(&doctor_path, serde_json::to_vec_pretty(&doctor_payload)?)
            .with_context(|| format!("failed writing {}", doctor_path.display()))?;
        artifacts.push("doctor.json".to_string());
    }

    Ok(index_status)
}

fn bundle_artifact_metadata(
    bundle_dir: &Path,
    artifacts: &[String],
) -> Vec<BundleArtifactMetadata> {
    artifacts
        .iter()
        .map(|artifact| {
            let is_dir = artifact.ends_with('/');
            let relative = artifact.trim_end_matches('/');
            let path = bundle_dir.join(relative);

            if is_dir {
                let (bytes, file_count, exists) = directory_stats(&path);
                return BundleArtifactMetadata {
                    path: artifact.clone(),
                    kind: "directory".to_string(),
                    bytes,
                    file_count,
                    exists,
                    sha256: directory_sha256_hex(&path),
                };
            }

            let (bytes, exists) = file_bytes(&path);
            BundleArtifactMetadata {
                path: artifact.clone(),
                kind: "file".to_string(),
                bytes,
                file_count: usize::from(exists),
                exists,
                sha256: file_sha256_hex(&path),
            }
        })
        .collect()
}

fn file_sha256_hex(path: &Path) -> Option<String> {
    let mut file = std::fs::File::open(path).ok()?;
    let mut hasher = Sha256::new();
    let mut buffer = [0u8; 8192];

    loop {
        let bytes_read = file.read(&mut buffer).ok()?;
        if bytes_read == 0 {
            break;
        }
        hasher.update(&buffer[..bytes_read]);
    }

    Some(hex_encode_digest(hasher.finalize()))
}

fn directory_sha256_hex(path: &Path) -> Option<String> {
    if !path.is_dir() {
        return None;
    }

    let mut entries = Vec::new();
    collect_directory_hash_entries(path, path, &mut entries).ok()?;
    entries.sort_by(|left, right| left.0.cmp(&right.0));

    let mut hasher = Sha256::new();
    for (relative, digest) in entries {
        hasher.update(relative.as_bytes());
        hasher.update([0]);
        hasher.update(digest.as_bytes());
        hasher.update([b'\n']);
    }

    Some(hex_encode_digest(hasher.finalize()))
}

fn collect_directory_hash_entries(
    root: &Path,
    current: &Path,
    entries: &mut Vec<(String, String)>,
) -> Result<()> {
    for entry in std::fs::read_dir(current)
        .with_context(|| format!("failed reading {}", current.display()))?
    {
        let entry = entry.with_context(|| format!("failed reading {}", current.display()))?;
        let entry_path = entry.path();
        let file_type = entry
            .file_type()
            .with_context(|| format!("failed reading type for {}", entry_path.display()))?;

        if file_type.is_dir() {
            collect_directory_hash_entries(root, &entry_path, entries)?;
            continue;
        }

        if !file_type.is_file() {
            continue;
        }

        let digest = file_sha256_hex(&entry_path)
            .ok_or_else(|| anyhow::anyhow!("failed hashing {}", entry_path.display()))?;
        let relative = entry_path
            .strip_prefix(root)
            .map_or_else(|_| entry_path.clone(), PathBuf::from)
            .to_string_lossy()
            .replace('\\', "/");
        entries.push((relative, digest));
    }

    Ok(())
}

fn hex_encode_digest(digest: impl AsRef<[u8]>) -> String {
    let bytes = digest.as_ref();
    let mut output = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        use std::fmt::Write as _;
        let _ = write!(&mut output, "{byte:02x}");
    }
    output
}

fn file_bytes(path: &Path) -> (u64, bool) {
    path.metadata().map_or((0, false), |metadata| {
        if metadata.is_file() {
            (metadata.len(), true)
        } else {
            (0, false)
        }
    })
}

fn directory_stats(path: &Path) -> (u64, usize, bool) {
    if !path.is_dir() {
        return (0, 0, false);
    }

    let Ok(entries) = std::fs::read_dir(path) else {
        return (0, 0, false);
    };

    let mut bytes = 0u64;
    let mut file_count = 0usize;
    for entry in entries.flatten() {
        let entry_path = entry.path();
        if entry_path.is_dir() {
            let (dir_bytes, dir_files, dir_exists) = directory_stats(&entry_path);
            if dir_exists {
                bytes = bytes.saturating_add(dir_bytes);
                file_count += dir_files;
            }
            continue;
        }

        if let Ok(metadata) = entry.metadata()
            && metadata.is_file()
        {
            bytes = bytes.saturating_add(metadata.len());
            file_count += 1;
        }
    }

    (bytes, file_count, true)
}

fn write_bundle_index_state(
    manifest: &SandboxManifest,
    bundle_dir: &Path,
    artifacts: &mut Vec<String>,
) -> Result<String> {
    let index_path = ConfigPaths::default()
        .state_dir()
        .join("sandbox")
        .join("index.json");
    let index_bundle_path = bundle_dir.join("sandbox-index.json");
    let status = if index_path.exists() {
        std::fs::copy(&index_path, &index_bundle_path)
            .with_context(|| format!("failed copying {}", index_path.display()))?;
        artifacts.push("sandbox-index.json".to_string());
        "copied".to_string()
    } else {
        "missing".to_string()
    };

    let index_entry_path = bundle_dir.join("sandbox-index-entry.json");
    let index_entry_payload = match read_sandbox_index_entries() {
        Ok(entries) => {
            let entry = entries.iter().find(|entry| entry.id == manifest.id);
            serde_json::json!({ "entry": entry })
        }
        Err(error) => serde_json::json!({ "index_read_error": error.to_string() }),
    };
    std::fs::write(
        &index_entry_path,
        serde_json::to_vec_pretty(&index_entry_payload)?,
    )
    .with_context(|| format!("failed writing {}", index_entry_path.display()))?;
    artifacts.push("sandbox-index-entry.json".to_string());

    Ok(status)
}

fn bundle_env_payload(manifest: &SandboxManifest) -> serde_json::Value {
    let root = PathBuf::from(&manifest.paths.root);
    serde_json::json!({
        "schema_version": SANDBOX_JSON_SCHEMA_VERSION,
        "env_mode": manifest.env_mode,
        "bmux_bin": manifest.bmux_bin,
        "command": manifest.command,
        "paths": {
            "root": manifest.paths.root,
            "logs": manifest.paths.logs,
            "runtime": manifest.paths.runtime,
            "state": manifest.paths.state,
            "config_home": root.join("config").to_string_lossy().to_string(),
            "data_home": root.join("data").to_string_lossy().to_string(),
            "home": root.join("home").to_string_lossy().to_string(),
            "tmp": root.join("tmp").to_string_lossy().to_string(),
        }
    })
}

fn bundle_doctor_payload(target: &str) -> serde_json::Value {
    let mut checks = Vec::new();
    let temp_dir = std::env::temp_dir();
    checks.push(DoctorCheck {
        name: "temp_dir_writable".to_string(),
        ok: std::fs::create_dir_all(&temp_dir).is_ok(),
        detail: temp_dir.display().to_string(),
    });

    let current_exe = std::env::current_exe();
    checks.push(DoctorCheck {
        name: "current_bmux_executable".to_string(),
        ok: current_exe.is_ok(),
        detail: current_exe.ok().map_or_else(
            || "unavailable".to_string(),
            |path| path.display().to_string(),
        ),
    });

    append_target_doctor_checks(&mut checks, target);
    let ok = checks.iter().all(|check| check.ok);

    serde_json::json!({
        "schema_version": SANDBOX_JSON_SCHEMA_VERSION,
        "ok": ok,
        "checks": checks,
    })
}

fn append_target_doctor_checks(checks: &mut Vec<DoctorCheck>, target: &str) {
    match resolve_sandbox_target(target) {
        Ok(root) => {
            let manifest_exists = root.join(MANIFEST_FILE).exists();
            let runtime_exists = root.join("runtime").exists();
            checks.push(DoctorCheck {
                name: "target_exists".to_string(),
                ok: root.exists(),
                detail: root.display().to_string(),
            });
            checks.push(DoctorCheck {
                name: "target_manifest_present".to_string(),
                ok: manifest_exists,
                detail: root.join(MANIFEST_FILE).display().to_string(),
            });
            checks.push(DoctorCheck {
                name: "target_runtime_dir_present".to_string(),
                ok: runtime_exists,
                detail: root.join("runtime").display().to_string(),
            });
        }
        Err(error) => checks.push(DoctorCheck {
            name: "target_resolve".to_string(),
            ok: false,
            detail: error.to_string(),
        }),
    }
}

fn copy_if_exists(source: &Path, destination: &Path) -> Result<()> {
    if source.exists() {
        std::fs::copy(source, destination)
            .with_context(|| format!("failed copying {}", source.display()))?;
    }
    Ok(())
}

fn copy_directory_recursive(source: &Path, destination: &Path) -> Result<()> {
    std::fs::create_dir_all(destination)
        .with_context(|| format!("failed creating {}", destination.display()))?;

    for entry in
        std::fs::read_dir(source).with_context(|| format!("failed reading {}", source.display()))?
    {
        let entry = entry.with_context(|| "failed reading directory entry".to_string())?;
        let source_path = entry.path();
        let destination_path = destination.join(entry.file_name());
        if source_path.is_dir() {
            copy_directory_recursive(&source_path, &destination_path)?;
        } else {
            std::fs::copy(&source_path, &destination_path)
                .with_context(|| format!("failed copying {}", source_path.display()))?;
        }
    }

    Ok(())
}

pub(super) fn run_sandbox_cleanup(
    dry_run: bool,
    failed_only: bool,
    older_than_secs: Option<u64>,
    source_filter: Option<&str>,
    json: bool,
) -> Result<u8> {
    let older_than = older_than_secs.map_or(DEFAULT_CLEANUP_MIN_AGE, Duration::from_secs);
    let collection = collect_sandbox_candidates();
    let scan = cleanup_orphaned_sandboxes(
        collection.candidates,
        dry_run,
        failed_only,
        older_than,
        source_filter,
    );
    let orphaned = scan
        .entries
        .iter()
        .filter(|entry| cleanup_reason_is_orphaned(&entry.reason))
        .count();

    if json {
        let report = serde_json::json!({
            "schema_version": SANDBOX_JSON_SCHEMA_VERSION,
            "scanned": scan.scanned,
            "orphaned": orphaned,
            "dry_run": dry_run,
            "failed_only": failed_only,
            "older_than_secs": older_than.as_secs(),
            "source": source_filter,
            "reconcile": collection.reconcile,
            "skipped_source_mismatch": scan.skipped.source_mismatch,
            "skipped_running": scan.skipped.running,
            "skipped_recent": scan.skipped.recent,
            "skipped_not_failed": scan.skipped.not_failed,
            "skipped_missing_manifest": scan.skipped.missing_manifest,
            "delete_failed": scan.skipped.delete_failed,
            "entries": scan.entries,
        });
        println!("{}", serde_json::to_string_pretty(&report)?);
    } else if !scan.entries.is_empty() {
        for entry in &scan.entries {
            let status = if entry.removed { "removed" } else { "kept" };
            println!(
                "  {status}: {} (source: {}, status: {}, age: {}s, reason: {})",
                entry.path, entry.source, entry.status, entry.age_secs, entry.reason
            );
        }
        if dry_run {
            println!("{orphaned} orphaned sandbox(es) found (dry run, not removed)");
        } else {
            let removed = scan.entries.iter().filter(|entry| entry.removed).count();
            println!(
                "{removed} orphaned sandbox(es) removed ({} failed removals)",
                scan.skipped.delete_failed
            );
        }
        println!(
            "reasons: source_mismatch={}, running={}, recent={}, not_failed={}, missing_manifest={}, delete_failed={}",
            scan.skipped.source_mismatch,
            scan.skipped.running,
            scan.skipped.recent,
            scan.skipped.not_failed,
            scan.skipped.missing_manifest,
            scan.skipped.delete_failed
        );
        print_reconcile_report_text(&collection.reconcile);
    } else {
        println!(
            "no orphaned sandboxes found ({} scanned; reasons source_mismatch={}, running={}, recent={}, not_failed={}, missing_manifest={}, delete_failed={})",
            scan.scanned,
            scan.skipped.source_mismatch,
            scan.skipped.running,
            scan.skipped.recent,
            scan.skipped.not_failed,
            scan.skipped.missing_manifest,
            scan.skipped.delete_failed
        );
        print_reconcile_report_text(&collection.reconcile);
    }

    Ok(0)
}

pub(super) fn run_sandbox_rebuild_index(json: bool) -> Result<u8> {
    let roots = collect_sandbox_directories();
    let report = rebuild_sandbox_index_from_roots(&roots, true);

    if json {
        let payload = serde_json::json!({
            "schema_version": SANDBOX_JSON_SCHEMA_VERSION,
            "scanned": report.scanned,
            "rebuilt_count": report.rebuilt_count,
            "pruned_count": report.pruned_count,
            "missing_manifest": report.missing_manifest,
            "scan_fallback_used": report.scan_fallback_used,
        });
        println!("{}", serde_json::to_string_pretty(&payload)?);
    } else {
        println!(
            "sandbox index rebuilt: scanned={}, rebuilt={}, pruned={}, missing_manifest={}, fallback={}",
            report.scanned,
            report.rebuilt_count,
            report.pruned_count,
            report.missing_manifest,
            report.scan_fallback_used
        );
    }

    Ok(0)
}

fn cleanup_orphaned_sandboxes(
    candidates: Vec<SandboxCandidate>,
    dry_run: bool,
    failed_only: bool,
    older_than: Duration,
    source_filter: Option<&str>,
) -> CleanupScan {
    let now = SystemTime::now();
    let mut scanned = 0;
    let mut entries = Vec::new();
    let mut skipped = CleanupSkipped {
        source_mismatch: 0,
        running: 0,
        recent: 0,
        not_failed: 0,
        missing_manifest: 0,
        delete_failed: 0,
    };

    for candidate in candidates {
        scanned += 1;
        entries.push(evaluate_cleanup_candidate(
            candidate,
            now,
            dry_run,
            failed_only,
            older_than,
            source_filter,
            &mut skipped,
        ));
    }

    CleanupScan {
        scanned,
        entries,
        skipped,
    }
}

fn evaluate_cleanup_candidate(
    candidate: SandboxCandidate,
    now: SystemTime,
    dry_run: bool,
    failed_only: bool,
    older_than: Duration,
    source_filter: Option<&str>,
    skipped: &mut CleanupSkipped,
) -> CleanupEntry {
    let age = candidate_age(&candidate, now);
    let root_path = candidate.root;
    let source = sandbox_source_for_dir(&root_path);
    let status = sandbox_status_for_dir(&root_path).to_string();

    if let Some(filter) = source_filter
        && source != filter
    {
        skipped.source_mismatch += 1;
        return cleanup_entry(&root_path, source, age, status, "source_mismatch", false);
    }

    if age < older_than {
        skipped.recent += 1;
        return cleanup_entry(&root_path, source, age, status, "recent", false);
    }

    let manifest = read_manifest(&root_path).ok();
    if status == "running" {
        skipped.running += 1;
        return cleanup_entry(&root_path, source, age, status, "running", false);
    }

    if failed_only && status != "failed" {
        if manifest.is_some() {
            skipped.not_failed += 1;
            return cleanup_entry(&root_path, source, age, status, "not_failed", false);
        }
        skipped.missing_manifest += 1;
        return cleanup_entry(&root_path, source, age, status, "missing_manifest", false);
    }

    if dry_run {
        return cleanup_entry(&root_path, source, age, status, "would_remove", false);
    }

    if sandbox_lock_is_fresh(&root_path)
        || sandbox_process_alive(&root_path)
        || sandbox_socket_alive(&root_path)
    {
        skipped.running += 1;
        return cleanup_entry(&root_path, source, age, status, "running", false);
    }

    let removed = std::fs::remove_dir_all(&root_path).is_ok();
    if removed || !root_path.exists() {
        let _ = remove_sandbox_index_entry(&root_path);
        return cleanup_entry(&root_path, source, age, status, "removed", true);
    }

    skipped.delete_failed += 1;
    cleanup_entry(&root_path, source, age, status, "delete_failed", false)
}

fn cleanup_entry(
    root_path: &Path,
    source: &str,
    age: Duration,
    status: String,
    reason: &str,
    removed: bool,
) -> CleanupEntry {
    CleanupEntry {
        path: root_path.to_string_lossy().to_string(),
        source: source.to_string(),
        age_secs: age.as_secs(),
        status,
        reason: reason.to_string(),
        removed,
    }
}

fn cleanup_reason_is_orphaned(reason: &str) -> bool {
    matches!(reason, "removed" | "would_remove" | "delete_failed")
}

fn collect_sandbox_directories() -> Vec<PathBuf> {
    let Ok(dir_entries) = std::fs::read_dir(std::env::temp_dir()) else {
        return Vec::new();
    };

    dir_entries
        .flatten()
        .map(|entry| entry.path())
        .filter(|path| {
            if !path.is_dir() {
                return false;
            }

            path.file_name().is_some_and(|name| {
                let name = name.to_string_lossy();
                name.starts_with(SANDBOX_PREFIX)
                    || name.starts_with("bpb-")
                    || name.starts_with("brv-")
            })
        })
        .collect()
}

fn collect_sandbox_directories_index_first() -> Vec<PathBuf> {
    collect_sandbox_candidates()
        .candidates
        .into_iter()
        .map(|candidate| candidate.root)
        .collect()
}

fn collect_sandbox_candidates() -> SandboxCandidateCollection {
    let mut reconcile = ReconcileReport::default();
    let index_exists = sandbox_index_exists_meta();
    match read_sandbox_index_entries() {
        Ok(_) => {
            reconcile.pruned_entries = prune_missing_index_entries().unwrap_or(0);
            if let Ok(entries) = read_sandbox_index_entries() {
                let indexed = index_entries_to_candidates(entries);
                if !indexed.is_empty() {
                    let recovery = recover_candidate_lifecycle_state(&indexed);
                    reconcile.normalized_running = recovery.normalized_running;
                    reconcile.cleared_stale_locks = recovery.cleared_stale_locks;
                    reconcile.healed_entries = reconcile.pruned_entries
                        + reconcile.normalized_running
                        + reconcile.cleared_stale_locks;
                    return SandboxCandidateCollection {
                        candidates: indexed,
                        reconcile,
                    };
                }
            }
        }
        Err(error) => {
            reconcile.scan_fallback_used = true;
            reconcile.index_read_error = Some(error.to_string());
        }
    }

    let roots = collect_sandbox_directories();
    if !roots.is_empty() {
        let rebuild_report = rebuild_sandbox_index_from_roots(&roots, !index_exists);
        reconcile.rebuilt_entries = rebuild_report.rebuilt_count;
        reconcile.pruned_entries = reconcile.pruned_entries.max(rebuild_report.pruned_count);
        reconcile.scan_fallback_used =
            reconcile.scan_fallback_used || rebuild_report.scan_fallback_used;
        if let Ok(entries) = read_sandbox_index_entries() {
            let indexed = index_entries_to_candidates(entries);
            if !indexed.is_empty() {
                let recovery = recover_candidate_lifecycle_state(&indexed);
                reconcile.normalized_running = recovery.normalized_running;
                reconcile.cleared_stale_locks = recovery.cleared_stale_locks;
                reconcile.healed_entries = reconcile.rebuilt_entries
                    + reconcile.pruned_entries
                    + reconcile.normalized_running
                    + reconcile.cleared_stale_locks;
                return SandboxCandidateCollection {
                    candidates: indexed,
                    reconcile,
                };
            }
        }
    }

    let candidates = roots
        .into_iter()
        .map(|root| SandboxCandidate {
            root,
            updated_at_unix_ms: None,
        })
        .collect::<Vec<_>>();

    let recovery = recover_candidate_lifecycle_state(&candidates);
    reconcile.normalized_running = recovery.normalized_running;
    reconcile.cleared_stale_locks = recovery.cleared_stale_locks;
    reconcile.healed_entries = reconcile.rebuilt_entries
        + reconcile.pruned_entries
        + reconcile.normalized_running
        + reconcile.cleared_stale_locks;
    SandboxCandidateCollection {
        candidates,
        reconcile,
    }
}

fn recover_candidate_lifecycle_state(candidates: &[SandboxCandidate]) -> RecoveryReport {
    let mut report = RecoveryReport::default();
    for candidate in candidates {
        let root = &candidate.root;
        if sandbox_lock_is_fresh(root) || sandbox_process_alive(root) || sandbox_socket_alive(root)
        {
            continue;
        }

        if let Ok(mut manifest) = read_manifest(root)
            && manifest.status == "running"
        {
            manifest.updated_at_unix_ms = unix_millis_now_meta();
            manifest.status = "aborted".to_string();
            if manifest.exit_code.is_none() {
                manifest.exit_code = Some(1);
            }
            manifest.kept = true;
            if write_manifest(root, &manifest).is_ok() {
                let _ = upsert_sandbox_index_entry(&manifest);
                report.normalized_running += 1;
            }
        }

        if sandbox_lock_is_stale(root) {
            clear_sandbox_lock(root);
            report.cleared_stale_locks += 1;
        }
    }
    report
}

fn index_entries_to_candidates(
    entries: Vec<crate::sandbox_meta::SandboxIndexEntry>,
) -> Vec<SandboxCandidate> {
    entries
        .into_iter()
        .filter_map(|entry| {
            let root = PathBuf::from(entry.root);
            if root.is_dir() {
                Some(SandboxCandidate {
                    root,
                    updated_at_unix_ms: Some(entry.updated_at_unix_ms),
                })
            } else {
                None
            }
        })
        .collect::<Vec<_>>()
}

fn rebuild_sandbox_index_from_roots(
    roots: &[PathBuf],
    scan_fallback_used: bool,
) -> IndexRebuildReport {
    let existing = read_sandbox_index_entries().unwrap_or_default();
    let previous_roots = existing
        .iter()
        .map(|entry| entry.root.clone())
        .collect::<std::collections::BTreeSet<_>>();

    let mut rebuilt_entries = Vec::new();
    let mut seen_roots = std::collections::BTreeSet::new();
    let mut missing_manifest = 0usize;

    for root in roots {
        seen_roots.insert(root.to_string_lossy().to_string());
        if let Ok(manifest) = read_manifest(root) {
            rebuilt_entries.push(crate::sandbox_meta::SandboxIndexEntry {
                id: manifest.id,
                root: manifest.paths.root,
                source: manifest.source,
                status: manifest.status,
                created_at_unix_ms: manifest.created_at_unix_ms,
                updated_at_unix_ms: manifest.updated_at_unix_ms,
                last_seen_unix_ms: unix_millis_now_meta(),
            });
        } else {
            missing_manifest += 1;
        }
    }

    let _ = replace_sandbox_index_entries(rebuilt_entries.clone());

    let pruned_count = previous_roots
        .iter()
        .filter(|root| !seen_roots.contains(*root))
        .count();

    IndexRebuildReport {
        scanned: roots.len(),
        rebuilt_count: rebuilt_entries.len(),
        pruned_count,
        missing_manifest,
        scan_fallback_used,
    }
}

fn preview_rebuild_sandbox_index_from_roots(roots: &[PathBuf]) -> IndexRebuildReport {
    let existing = read_sandbox_index_entries().unwrap_or_default();
    let previous_roots = existing
        .iter()
        .map(|entry| entry.root.clone())
        .collect::<std::collections::BTreeSet<_>>();

    let mut rebuilt_count = 0usize;
    let mut missing_manifest = 0usize;
    let mut seen_roots = std::collections::BTreeSet::new();
    for root in roots {
        seen_roots.insert(root.to_string_lossy().to_string());
        if read_manifest(root).is_ok() {
            rebuilt_count += 1;
        } else {
            missing_manifest += 1;
        }
    }

    let pruned_count = previous_roots
        .iter()
        .filter(|root| !seen_roots.contains(*root))
        .count();

    IndexRebuildReport {
        scanned: roots.len(),
        rebuilt_count,
        pruned_count,
        missing_manifest,
        scan_fallback_used: !sandbox_index_exists_meta(),
    }
}

fn candidate_sort_key(candidate: &SandboxCandidate) -> u128 {
    candidate.updated_at_unix_ms.unwrap_or_else(|| {
        candidate
            .root
            .metadata()
            .ok()
            .and_then(|metadata| metadata.modified().ok())
            .and_then(|modified| modified.duration_since(UNIX_EPOCH).ok())
            .map_or(0, |duration| duration.as_millis())
    })
}

fn candidate_age(candidate: &SandboxCandidate, now: SystemTime) -> Duration {
    if let Some(updated_at) = candidate.updated_at_unix_ms {
        let now_unix_ms = now
            .duration_since(UNIX_EPOCH)
            .map_or(0, |duration| duration.as_millis());
        let elapsed_ms = now_unix_ms.saturating_sub(updated_at);
        let elapsed_ms_u64 = u64::try_from(elapsed_ms).unwrap_or(u64::MAX);
        return Duration::from_millis(elapsed_ms_u64);
    }

    candidate
        .root
        .metadata()
        .ok()
        .and_then(|metadata| metadata.modified().ok())
        .and_then(|modified| now.duration_since(modified).ok())
        .unwrap_or_default()
}

fn sandbox_list_entry(candidate: &SandboxCandidate) -> SandboxListEntry {
    let root = &candidate.root;
    let age = candidate_age(candidate, SystemTime::now());

    let manifest = read_manifest(root).ok();
    SandboxListEntry {
        id: sandbox_id_from_root_meta(root),
        source: manifest.as_ref().map_or_else(
            || sandbox_source_for_dir(root).to_string(),
            |value| value.source.clone(),
        ),
        root: root.to_string_lossy().to_string(),
        status: sandbox_status_for_dir(root).to_string(),
        age_secs: age.as_secs(),
        exit_code: manifest.as_ref().and_then(|value| value.exit_code),
        kept: manifest.as_ref().map(|value| value.kept),
    }
}

fn sandbox_source_for_dir(root: &Path) -> &'static str {
    if let Ok(manifest) = read_manifest(root) {
        return match manifest.source.as_str() {
            "playbook" => "playbook",
            "recording-verify" => "recording-verify",
            _ => "sandbox-cli",
        };
    }

    let Some(name) = root.file_name() else {
        return "sandbox-cli";
    };
    let name = name.to_string_lossy();
    if name.starts_with("bpb-") {
        "playbook"
    } else if name.starts_with("brv-") {
        "recording-verify"
    } else {
        "sandbox-cli"
    }
}

fn sandbox_status_for_dir(root: &Path) -> &'static str {
    if sandbox_lock_is_fresh(root) || sandbox_process_alive(root) || sandbox_socket_alive(root) {
        return "running";
    }

    if let Ok(manifest) = read_manifest(root) {
        if matches!(
            manifest.status.as_str(),
            "failed" | "timed_out" | "killed" | "aborted" | "running"
        ) {
            "failed"
        } else {
            "stopped"
        }
    } else {
        "stopped"
    }
}

fn read_manifest(root: &Path) -> Result<SandboxManifest> {
    read_sandbox_manifest(root)
}

fn write_manifest(root: &Path, manifest: &SandboxManifest) -> Result<()> {
    write_sandbox_manifest(root, manifest)
}

fn resolve_sandbox_target(target: &str) -> Result<PathBuf> {
    let maybe_path = PathBuf::from(target);
    if maybe_path.is_absolute() && maybe_path.exists() {
        return Ok(maybe_path);
    }

    let direct = std::env::temp_dir().join(target);
    if direct.exists() {
        return Ok(direct);
    }

    let directories = collect_sandbox_directories_index_first();

    if let Some(found) = directories
        .iter()
        .find(|candidate| sandbox_id_from_root_meta(candidate) == target)
    {
        return Ok(found.clone());
    }

    let prefix_matches = directories
        .iter()
        .filter_map(|candidate| {
            let id = sandbox_id_from_root_meta(candidate);
            id.starts_with(target).then(|| (candidate.clone(), id))
        })
        .collect::<Vec<_>>();

    if prefix_matches.len() == 1 {
        return Ok(prefix_matches[0].0.clone());
    }
    if prefix_matches.len() > 1 {
        let suggestions = prefix_matches
            .into_iter()
            .map(|(_, id)| id)
            .take(5)
            .collect::<Vec<_>>()
            .join(", ");
        anyhow::bail!(
            "sandbox target '{target}' is ambiguous; matches: {suggestions}; use a full id or absolute path"
        );
    }

    let suggestions = directories
        .iter()
        .map(|candidate| sandbox_id_from_root_meta(candidate))
        .filter(|id| id.contains(target))
        .take(5)
        .collect::<Vec<_>>();

    if suggestions.is_empty() {
        anyhow::bail!("sandbox target not found: {target}");
    }
    anyhow::bail!(
        "sandbox target not found: {target}; did you mean: {}",
        suggestions.join(", ")
    )
}

fn format_known_sources(candidates: &[SandboxCandidate]) -> String {
    let mut known = candidates
        .iter()
        .map(|candidate| sandbox_source_for_dir(&candidate.root))
        .collect::<Vec<_>>();
    known.sort_unstable();
    known.dedup();
    known.join(", ")
}

fn read_log_tail(root: &Path, tail: usize) -> Vec<String> {
    let log_dir = root.join("logs");
    let Some(log_path) = newest_regular_file(&log_dir) else {
        return vec!["<no log files found>".to_string()];
    };

    let Ok(contents) = std::fs::read_to_string(&log_path) else {
        return vec![format!("<failed reading {}>", log_path.display())];
    };

    let mut lines = contents.lines().map(ToOwned::to_owned).collect::<Vec<_>>();
    let keep = tail.max(1);
    if lines.len() > keep {
        lines.drain(0..(lines.len() - keep));
    }
    lines
}

fn newest_regular_file(dir: &Path) -> Option<PathBuf> {
    let entries = std::fs::read_dir(dir).ok()?;
    entries
        .flatten()
        .filter_map(|entry| {
            let path = entry.path();
            let modified = entry.metadata().ok()?.modified().ok()?;
            if path.is_file() {
                Some((path, modified))
            } else {
                None
            }
        })
        .max_by_key(|(_, modified)| *modified)
        .map(|(path, _)| path)
}

fn write_pid_marker(root_dir: &Path) -> Result<()> {
    let marker_path = root_dir.join(PID_MARKER_FILE);
    std::fs::write(&marker_path, std::process::id().to_string())
        .with_context(|| format!("failed writing {}", marker_path.display()))
}

fn write_lock(root_dir: &Path) -> Result<()> {
    write_sandbox_lock(root_dir, std::process::id())
}

fn clear_lock(root_dir: &Path) {
    clear_sandbox_lock(root_dir);
}

fn sandbox_lock_is_fresh(root_dir: &Path) -> bool {
    let Some(lock) = read_sandbox_lock(root_dir) else {
        return false;
    };
    let now = unix_millis_now_meta();
    if now.saturating_sub(lock.updated_at_unix_ms) > LOCK_FRESHNESS.as_millis() {
        return false;
    }
    is_pid_alive(lock.pid)
}

fn sandbox_lock_is_stale(root_dir: &Path) -> bool {
    let lock_path = root_dir.join(LOCK_FILE);
    lock_path.exists()
        && !sandbox_lock_is_fresh(root_dir)
        && !sandbox_process_alive(root_dir)
        && !sandbox_socket_alive(root_dir)
}

fn format_repro_command(options: &RunSandboxOptions<'_>, command_args: &[String]) -> String {
    let mut parts = vec![
        "bmux".to_string(),
        "sandbox".to_string(),
        "run".to_string(),
        "--env-mode".to_string(),
        sandbox_env_mode_name(options.env_mode).to_string(),
    ];
    if let Some(bin) = options.bmux_bin {
        parts.push("--bmux-bin".to_string());
        parts.push(shell_quote(bin));
    }
    if options.keep {
        parts.push("--keep".to_string());
    }
    parts.push("--".to_string());
    parts.extend(command_args.iter().map(|arg| shell_quote(arg)));
    parts.join(" ")
}

fn format_repro_command_from_manifest(manifest: &SandboxManifest) -> String {
    let env_mode = if matches!(manifest.env_mode.as_str(), "clean") {
        SandboxEnvModeArg::Clean
    } else if matches!(manifest.env_mode.as_str(), "hermetic") {
        SandboxEnvModeArg::Hermetic
    } else {
        SandboxEnvModeArg::Inherit
    };
    let options = RunSandboxOptions {
        bmux_bin: Some(&manifest.bmux_bin),
        env_mode,
        keep: manifest.kept,
        json: false,
        print_env: false,
        timeout_secs: None,
        name: None,
    };
    format_repro_command(&options, manifest.command.as_slice())
}

fn shell_quote(value: &str) -> String {
    if value
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '/' | '.' | ':'))
    {
        return value.to_string();
    }
    format!("'{}'", value.replace('\'', "'\"'\"'"))
}

fn apply_sandbox_env(
    command: &mut ProcessCommand,
    sandbox: &SandboxPaths,
    env_mode: SandboxEnvModeArg,
) {
    if matches!(
        env_mode,
        SandboxEnvModeArg::Clean | SandboxEnvModeArg::Hermetic
    ) {
        command.env_clear();
    }

    command
        .env("BMUX_CONFIG_DIR", &sandbox.config_paths.config_dir)
        .env("BMUX_RUNTIME_DIR", &sandbox.config_paths.runtime_dir)
        .env("BMUX_DATA_DIR", &sandbox.config_paths.data_dir)
        .env("BMUX_STATE_DIR", &sandbox.config_paths.state_dir)
        .env("BMUX_LOG_DIR", &sandbox.log_dir)
        .env("XDG_CONFIG_HOME", &sandbox.config_home)
        .env("XDG_DATA_HOME", &sandbox.data_home)
        .env("XDG_RUNTIME_DIR", &sandbox.runtime_dir)
        .env("TMPDIR", &sandbox.tmp_dir)
        .env("HOME", &sandbox.home_dir)
        .env("TERM", "xterm-256color")
        .env("LANG", "C.UTF-8")
        .env("LC_ALL", "C.UTF-8");

    if matches!(env_mode, SandboxEnvModeArg::Clean) {
        if let Ok(path) = std::env::var("PATH") {
            command.env("PATH", path);
        }
        if let Ok(user) = std::env::var("USER") {
            command.env("USER", user);
        }
        if let Ok(shell) = std::env::var("SHELL") {
            command.env("SHELL", shell);
        }
    }

    if matches!(env_mode, SandboxEnvModeArg::Hermetic) {
        command.env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin");
    }
}

fn collect_effective_sandbox_env(
    sandbox: &SandboxPaths,
    env_mode: SandboxEnvModeArg,
) -> serde_json::Value {
    let mut vars = serde_json::Map::new();
    vars.insert(
        "BMUX_CONFIG_DIR".to_string(),
        serde_json::Value::String(
            sandbox
                .config_paths
                .config_dir
                .to_string_lossy()
                .to_string(),
        ),
    );
    vars.insert(
        "BMUX_RUNTIME_DIR".to_string(),
        serde_json::Value::String(
            sandbox
                .config_paths
                .runtime_dir
                .to_string_lossy()
                .to_string(),
        ),
    );
    vars.insert(
        "BMUX_DATA_DIR".to_string(),
        serde_json::Value::String(sandbox.config_paths.data_dir.to_string_lossy().to_string()),
    );
    vars.insert(
        "BMUX_STATE_DIR".to_string(),
        serde_json::Value::String(sandbox.config_paths.state_dir.to_string_lossy().to_string()),
    );
    vars.insert(
        "BMUX_LOG_DIR".to_string(),
        serde_json::Value::String(sandbox.log_dir.to_string_lossy().to_string()),
    );
    vars.insert(
        "XDG_CONFIG_HOME".to_string(),
        serde_json::Value::String(sandbox.config_home.to_string_lossy().to_string()),
    );
    vars.insert(
        "XDG_DATA_HOME".to_string(),
        serde_json::Value::String(sandbox.data_home.to_string_lossy().to_string()),
    );
    vars.insert(
        "XDG_RUNTIME_DIR".to_string(),
        serde_json::Value::String(sandbox.runtime_dir.to_string_lossy().to_string()),
    );
    vars.insert(
        "TMPDIR".to_string(),
        serde_json::Value::String(sandbox.tmp_dir.to_string_lossy().to_string()),
    );
    vars.insert(
        "HOME".to_string(),
        serde_json::Value::String(sandbox.home_dir.to_string_lossy().to_string()),
    );
    vars.insert(
        "TERM".to_string(),
        serde_json::Value::String("xterm-256color".to_string()),
    );
    vars.insert(
        "LANG".to_string(),
        serde_json::Value::String("C.UTF-8".to_string()),
    );
    vars.insert(
        "LC_ALL".to_string(),
        serde_json::Value::String("C.UTF-8".to_string()),
    );

    if matches!(env_mode, SandboxEnvModeArg::Clean) {
        if let Ok(path) = std::env::var("PATH") {
            vars.insert("PATH".to_string(), serde_json::Value::String(path));
        }
        if let Ok(user) = std::env::var("USER") {
            vars.insert("USER".to_string(), serde_json::Value::String(user));
        }
        if let Ok(shell) = std::env::var("SHELL") {
            vars.insert("SHELL".to_string(), serde_json::Value::String(shell));
        }
    }

    if matches!(env_mode, SandboxEnvModeArg::Hermetic) {
        vars.insert(
            "PATH".to_string(),
            serde_json::Value::String("/usr/bin:/bin:/usr/sbin:/sbin".to_string()),
        );
    }

    serde_json::Value::Object(vars)
}

fn resolve_bmux_binary(bmux_bin: Option<&str>) -> Result<PathBuf> {
    bmux_bin.map_or_else(
        || std::env::current_exe().context("failed resolving current bmux executable"),
        resolve_explicit_binary,
    )
}

fn resolve_explicit_binary(path: &str) -> Result<PathBuf> {
    let trimmed = path.trim();
    if trimmed.is_empty() {
        anyhow::bail!("--bmux-bin requires a non-empty path");
    }
    let candidate = PathBuf::from(trimmed);
    let resolved = if candidate.is_absolute() {
        candidate
    } else {
        std::env::current_dir()
            .context("failed resolving current directory for --bmux-bin")?
            .join(candidate)
    };
    if !resolved.exists() {
        anyhow::bail!("--bmux-bin path does not exist: {}", resolved.display());
    }
    Ok(resolved)
}

const fn sandbox_env_mode_name(mode: SandboxEnvModeArg) -> &'static str {
    match mode {
        SandboxEnvModeArg::Clean => "clean",
        SandboxEnvModeArg::Inherit => "inherit",
        SandboxEnvModeArg::Hermetic => "hermetic",
    }
}

fn exit_code_to_u8(code: i32) -> u8 {
    if code < 0 {
        1
    } else if code > i32::from(u8::MAX) {
        u8::MAX
    } else {
        u8::try_from(code).unwrap_or(1)
    }
}

fn sanitize_component(value: &str) -> String {
    value
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_') {
                ch
            } else {
                '-'
            }
        })
        .collect::<String>()
        .trim_matches('-')
        .to_string()
}

fn sandbox_process_alive(root_dir: &Path) -> bool {
    let marker_path = root_dir.join(PID_MARKER_FILE);
    std::fs::read_to_string(marker_path)
        .ok()
        .and_then(|contents| contents.trim().parse::<u32>().ok())
        .is_some_and(is_pid_alive)
}

fn sandbox_socket_alive(root_dir: &Path) -> bool {
    #[cfg(unix)]
    {
        let socket_path = root_dir.join("runtime").join("server.sock");
        if !socket_path.exists() {
            return false;
        }
        std::os::unix::net::UnixStream::connect(socket_path).is_ok()
    }

    #[cfg(not(unix))]
    {
        let _ = root_dir;
        false
    }
}

fn is_pid_alive(pid: u32) -> bool {
    if pid == 0 {
        return false;
    }

    #[cfg(unix)]
    {
        ProcessCommand::new("kill")
            .args(["-0", &pid.to_string()])
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status()
            .is_ok_and(|status| status.success())
    }

    #[cfg(windows)]
    {
        ProcessCommand::new("tasklist")
            .args(["/FI", &format!("PID eq {pid}"), "/NH"])
            .stdout(Stdio::piped())
            .stderr(Stdio::null())
            .output()
            .is_ok_and(|output| {
                output.status.success()
                    && String::from_utf8_lossy(&output.stdout).contains(&pid.to_string())
            })
    }

    #[cfg(not(any(unix, windows)))]
    {
        let _ = pid;
        false
    }
}

#[cfg(test)]
mod tests {
    use super::{
        SandboxEnvModeArg, SandboxPaths, apply_sandbox_env, directory_sha256_hex, exit_code_to_u8,
        file_sha256_hex, sanitize_component,
    };

    fn env_value(command: &std::process::Command, key: &str) -> Option<std::ffi::OsString> {
        command.get_envs().find_map(|(name, value)| {
            if name == std::ffi::OsStr::new(key) {
                value.map(std::ffi::OsStr::to_os_string)
            } else {
                None
            }
        })
    }

    #[test]
    fn sanitize_component_rewrites_non_alnum() {
        assert_eq!(sanitize_component("my sandbox/test"), "my-sandbox-test");
    }

    #[test]
    fn sandbox_env_sets_clean_defaults() {
        let sandbox = SandboxPaths::new(None);
        sandbox
            .ensure_dirs()
            .expect("sandbox dirs should be created");
        let mut command = std::process::Command::new("sh");
        apply_sandbox_env(&mut command, &sandbox, SandboxEnvModeArg::Clean);
        let expected_log_dir = sandbox.log_dir.clone();
        let root_dir = sandbox.root_dir.clone();

        assert_eq!(
            env_value(&command, "BMUX_LOG_DIR"),
            Some(expected_log_dir.into_os_string())
        );
        assert_eq!(
            env_value(&command, "TERM"),
            Some(std::ffi::OsString::from("xterm-256color"))
        );

        let _ = std::fs::remove_dir_all(root_dir);
    }

    #[test]
    fn sandbox_env_sets_inherit_defaults() {
        let sandbox = SandboxPaths::new(None);
        sandbox
            .ensure_dirs()
            .expect("sandbox dirs should be created");
        let mut command = std::process::Command::new("sh");
        apply_sandbox_env(&mut command, &sandbox, SandboxEnvModeArg::Inherit);
        let expected_runtime_dir = sandbox.runtime_dir.clone();
        let root_dir = sandbox.root_dir.clone();

        assert_eq!(
            env_value(&command, "XDG_RUNTIME_DIR"),
            Some(expected_runtime_dir.into_os_string())
        );

        let _ = std::fs::remove_dir_all(root_dir);
    }

    #[test]
    fn sandbox_env_sets_hermetic_path() {
        let sandbox = SandboxPaths::new(None);
        sandbox
            .ensure_dirs()
            .expect("sandbox dirs should be created");
        let mut command = std::process::Command::new("sh");
        apply_sandbox_env(&mut command, &sandbox, SandboxEnvModeArg::Hermetic);
        let root_dir = sandbox.root_dir.clone();

        assert_eq!(
            env_value(&command, "PATH"),
            Some(std::ffi::OsString::from("/usr/bin:/bin:/usr/sbin:/sbin"))
        );

        let _ = std::fs::remove_dir_all(root_dir);
    }

    #[test]
    fn exit_code_conversion_clamps() {
        assert_eq!(exit_code_to_u8(-1), 1);
        assert_eq!(exit_code_to_u8(300), 255);
        assert_eq!(exit_code_to_u8(7), 7);
    }

    #[test]
    fn file_sha256_hex_matches_known_digest() {
        let root = std::env::temp_dir().join(format!(
            "bmux-file-sha-test-{}-{}",
            std::process::id(),
            super::unix_millis_now_meta()
        ));
        std::fs::create_dir_all(&root).expect("create temp test root");
        let file = root.join("sample.txt");
        std::fs::write(&file, "abc").expect("write sample file");

        let digest = file_sha256_hex(&file).expect("file hash should exist");
        assert_eq!(
            digest,
            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
        );

        let _ = std::fs::remove_dir_all(root);
    }

    #[test]
    fn directory_sha256_hex_is_stable_and_detects_changes() {
        let base = std::env::temp_dir().join(format!(
            "bmux-dir-sha-test-{}-{}",
            std::process::id(),
            super::unix_millis_now_meta()
        ));
        let dir_one = base.join("one");
        let dir_two = base.join("two");
        std::fs::create_dir_all(dir_one.join("nested")).expect("create dir one");
        std::fs::create_dir_all(dir_two.join("nested")).expect("create dir two");

        std::fs::write(dir_one.join("b.txt"), "bbb").expect("write one b");
        std::fs::write(dir_one.join("nested").join("a.txt"), "aaa").expect("write one a");

        std::fs::write(dir_two.join("nested").join("a.txt"), "aaa").expect("write two a");
        std::fs::write(dir_two.join("b.txt"), "bbb").expect("write two b");

        let hash_one = directory_sha256_hex(&dir_one).expect("dir one hash should exist");
        let hash_two = directory_sha256_hex(&dir_two).expect("dir two hash should exist");
        assert_eq!(hash_one, hash_two, "directory hash should be order-stable");

        std::fs::write(dir_two.join("nested").join("a.txt"), "changed")
            .expect("mutate nested file");
        let hash_two_changed =
            directory_sha256_hex(&dir_two).expect("dir two changed hash should exist");
        assert_ne!(
            hash_one, hash_two_changed,
            "directory hash should change on content drift"
        );

        let _ = std::fs::remove_dir_all(base);
    }
}
