mod cli;
mod commands;
mod config;
mod core;
mod output;
mod ui;

use crate::commands::remote_copy::{handle_remote_copy, is_plain_mode};
use crate::config::{is_json_mode, set_json_mode, UpdateCheck, CONFIG};
use crate::core::error::BcmrError;
use crate::ui::runner::ProgressRunner;
use anyhow::{bail, Result};
use cli::Commands;
use std::io::{self, Write};
use std::sync::mpsc;
use std::sync::Arc;
use ui::utils::format_bytes;

fn prompt_yes_no(message: &str) -> Result<bool> {
    if is_json_mode() {
        return Ok(true);
    }
    print!("{} [y/N] ", message);
    io::stdout().flush()?;
    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    Ok(input.trim().eq_ignore_ascii_case("y"))
}

fn confirm_overwrite(files: &[commands::copy::FileToOverwrite]) -> Result<bool> {
    if is_json_mode() {
        return Ok(true);
    }
    println!("\nThe following items will be overwritten:");
    for file in files {
        println!(
            "  {} {}",
            if file.is_dir { "DIR:" } else { "FILE:" },
            file.path.display()
        );
    }
    prompt_yes_no("\nDo you want to proceed?")
}

fn confirm_removal(files: &[commands::remove::FileToRemove]) -> Result<bool> {
    if is_json_mode() {
        return Ok(true);
    }
    let mut total_size = 0u64;
    let mut file_count = 0;
    let mut dir_count = 0;

    for file in files {
        if file.is_dir {
            dir_count += 1;
        } else {
            file_count += 1;
            total_size += file.size;
        }
    }

    println!("\nThe following items will be removed:");
    println!("  Files: {}", file_count);
    println!("  Directories: {}", dir_count);
    if total_size > 0 {
        println!("  Total size: {}", format_bytes(total_size as f64));
    }

    for file in files {
        println!(
            "  {} {}{}",
            if file.is_dir { "DIR:" } else { "FILE:" },
            file.path.display(),
            if !file.is_dir && file.size > 0 {
                format!(" ({})", format_bytes(file.size as f64))
            } else {
                String::new()
            }
        );
    }

    prompt_yes_no("\nDo you want to proceed?")
}

/// First filename from a list, as a short display label.
fn first_display_name(paths: &[std::path::PathBuf]) -> Option<String> {
    paths.first().map(|p| {
        p.file_name()
            .unwrap_or_default()
            .to_string_lossy()
            .into_owned()
    })
}

/// In JSON mode, create a ProgressRunner with `scanning=true` so the ticker
/// emits heartbeat events to the log while the plan/check phase runs. Long
/// scans on slow filesystems (NFS, HPC, many files) would otherwise leave
/// the log with only the parent-written descriptor line, making the job
/// look frozen. Returns `None` in non-JSON mode so interactive handlers
/// keep their existing prompt flow.
fn start_scanning_runner(
    args: &Commands,
    operation: &str,
    first_display: Option<&str>,
) -> Result<Option<ProgressRunner>> {
    if !is_json_mode() {
        return Ok(None);
    }
    let runner = ProgressRunner::new(
        0,
        is_plain_mode(args),
        false,
        true,
        commands::copy::cleanup_partial_files,
    )?;
    {
        let mut p = runner.progress().lock();
        p.set_operation_type(operation);
        p.set_scanning(true);
        if let Some(name) = first_display {
            p.set_current_file(name, 0);
        }
    }
    Ok(Some(runner))
}

/// Either take over an early scanning runner (setting total and clearing
/// `scanning`) or create a fresh runner now that the total is known.
fn resume_or_new_runner(
    early: Option<ProgressRunner>,
    args: &Commands,
    operation: &str,
    first_display: Option<&str>,
    total_size: u64,
    silent: bool,
) -> Result<ProgressRunner> {
    if let Some(r) = early {
        {
            let mut p = r.progress().lock();
            p.set_total_bytes(total_size);
            p.set_scanning(false);
            if let Some(name) = first_display {
                p.set_current_file(name, total_size);
            }
        }
        return Ok(r);
    }
    let r = ProgressRunner::new(
        total_size,
        is_plain_mode(args),
        silent,
        is_json_mode(),
        commands::copy::cleanup_partial_files,
    )?;
    {
        let mut p = r.progress().lock();
        p.set_operation_type(operation);
        if let Some(name) = first_display {
            p.set_current_file(name, total_size);
        }
    }
    Ok(r)
}

async fn handle_copy_command(args: &Commands) -> Result<()> {
    use crate::core::remote::parse_remote_path;

    let excludes = args.compile_excludes()?;
    let (sources, dest) = args.get_sources_and_dest().map_err(anyhow::Error::msg)?;

    if let Some(mode) = args.get_reflink_mode() {
        validate_mode(&mode, "reflink")?;
    }
    if let Some(mode) = args.get_sparse_mode() {
        validate_mode(&mode, "sparse")?;
    }

    let dest_str = dest.to_string_lossy();
    let remote_dest = parse_remote_path(&dest_str);
    let any_remote_source = sources
        .iter()
        .any(|s| parse_remote_path(&s.to_string_lossy()).is_some());

    if remote_dest.is_some() || any_remote_source {
        return handle_remote_copy(args, sources, dest, &excludes).await;
    }

    if sources.len() > 1 && (!dest.exists() || !dest.is_dir()) {
        bail!(
            "When copying multiple sources, destination '{}' must be an existing directory",
            dest.display()
        );
    }

    let needs_overwrite_prompt = args.is_force() && args.should_prompt_for_overwrite();

    if needs_overwrite_prompt || args.is_dry_run() {
        let first_display = first_display_name(sources);
        // Only start an early heartbeat runner for the real execute path.
        // Dry-run in JSON mode already emits nothing to the log; starting
        // a runner there would leak its ticker task.
        let early = if !args.is_dry_run() {
            start_scanning_runner(args, "Copying", first_display.as_deref())?
        } else {
            None
        };

        let plan =
            match commands::copy::plan_copy(sources, dest, args.is_recursive(), &excludes).await {
                Ok(p) => p,
                Err(e) => {
                    if let Some(r) = early {
                        r.finish_with_error(&e.to_string());
                    }
                    return Err(e.into());
                }
            };

        if args.is_force()
            && !plan.overwrites.is_empty()
            && args.should_prompt_for_overwrite()
            && !confirm_overwrite(&plan.overwrites)?
        {
            if let Some(r) = early {
                r.finish_with_error("cancelled by user");
            }
            return Err(BcmrError::Cancelled.into());
        }

        if args.is_dry_run() {
            if !is_json_mode() {
                println!("DRY RUN MODE: No changes will be made.\n");
                commands::copy::dry_run_plan(&plan, args)?;
                println!(
                    "\nSummary: {} sources, {}",
                    sources.len(),
                    format_bytes(plan.total_size as f64)
                );
            }
            return Ok(());
        }

        let runner = resume_or_new_runner(
            early,
            args,
            "Copying",
            first_display.as_deref(),
            plan.total_size,
            false,
        )?;

        let result = commands::copy::execute_plan(
            &plan,
            args,
            runner.inc_callback(),
            runner.file_callback(),
        )
        .await;

        if let Err(e) = result {
            return runner.finish_err(e.to_string());
        }

        runner.finish_ok()
    } else {
        let runner = ProgressRunner::new(
            0,
            is_plain_mode(args),
            false,
            is_json_mode(),
            commands::copy::cleanup_partial_files,
        )?;

        {
            let mut p = runner.progress().lock();
            p.set_operation_type("Copying");
            p.set_scanning(true);
            if let Some(first) = sources.first() {
                let display_name = first.file_name().unwrap_or_default().to_string_lossy();
                p.set_current_file(&display_name, 0);
            }
        }

        let total_cb = {
            let p = Arc::clone(runner.progress());
            move |total: u64| p.lock().set_total_bytes(total)
        };
        let scan_done_cb = {
            let p = Arc::clone(runner.progress());
            move || p.lock().set_scanning(false)
        };
        let files_found_cb = {
            let p = Arc::clone(runner.progress());
            move |count: u64| p.lock().set_files_found(count)
        };

        let result = commands::copy::pipeline_copy(
            sources,
            dest,
            args,
            &excludes,
            commands::copy::PipelineCallbacks {
                on_progress: runner.inc_callback(),
                on_new_file: Box::new(runner.file_callback()),
                on_total_update: Box::new(total_cb),
                on_scan_complete: Box::new(scan_done_cb),
                on_file_found: Box::new(files_found_cb),
            },
        )
        .await;

        if let Err(e) = result {
            return runner.finish_err(e.to_string());
        }

        runner.finish_ok()
    }
}

async fn handle_move_command(args: &Commands) -> Result<()> {
    let excludes = args.compile_excludes()?;
    let (sources, dest) = args.get_sources_and_dest().map_err(anyhow::Error::msg)?;

    if sources.len() > 1 && (!dest.exists() || !dest.is_dir()) {
        bail!(
            "When moving multiple sources, destination '{}' must be an existing directory",
            dest.display()
        );
    }

    let first_display = first_display_name(sources);
    let early = if !args.is_dry_run() {
        start_scanning_runner(args, "Moving", first_display.as_deref())?
    } else {
        None
    };

    // Any Err before the runner is constructed must go through this
    // closure so the early scanning runner (if any) emits a terminal
    // error event to the log before we propagate.
    let bail_early = |early: Option<ProgressRunner>, e: anyhow::Error| -> Result<()> {
        if let Some(r) = early {
            r.finish_with_error(&e.to_string());
        }
        Err(e)
    };

    if args.is_force() {
        let files_to_overwrite = match commands::r#move::check_overwrites(
            sources,
            dest,
            args.is_recursive(),
            args,
            &excludes,
        )
        .await
        {
            Ok(v) => v,
            Err(e) => return bail_early(early, e.into()),
        };

        if !files_to_overwrite.is_empty()
            && args.should_prompt_for_overwrite()
            && !confirm_overwrite(&files_to_overwrite)?
        {
            return bail_early(early, BcmrError::Cancelled.into());
        }
    }

    let total_size =
        match commands::r#move::get_total_size(sources, args.is_recursive(), args, &excludes).await
        {
            Ok(v) => v,
            Err(e) => return bail_early(early, e.into()),
        };

    if args.is_dry_run() {
        if !is_json_mode() {
            println!("DRY RUN MODE: No changes will be made.\n");
        }

        for src in sources {
            commands::r#move::move_path(src, dest, args, &excludes, |_| {}, |_, _| {}).await?;
        }

        if !is_json_mode() {
            println!(
                "\nSummary: {} sources, {}",
                sources.len(),
                format_bytes(total_size as f64)
            );
        }
        return Ok(());
    }

    let runner = resume_or_new_runner(
        early,
        args,
        "Moving",
        first_display.as_deref(),
        total_size,
        false,
    )?;

    for src in sources {
        let result = commands::r#move::move_path(
            src,
            dest,
            args,
            &excludes,
            runner.inc_callback(),
            runner.file_callback(),
        )
        .await;

        if let Err(e) = result {
            if !is_json_mode() {
                eprintln!("Error moving '{}': {}", src.display(), e);
            }
            return runner.finish_err(format!("Error moving '{}': {}", src.display(), e));
        }
    }

    runner.finish_ok()
}

async fn handle_remove_command(args: &Commands) -> Result<()> {
    let excludes = args.compile_excludes()?;
    let paths = args.get_remove_paths().map_err(anyhow::Error::msg)?;

    let first_display = first_display_name(paths);
    let early = start_scanning_runner(args, "Removing", first_display.as_deref())?;

    let files_to_remove =
        match commands::remove::check_removes(paths, args.is_recursive(), args, &excludes).await {
            Ok(v) => v,
            Err(e) => {
                if let Some(r) = early {
                    r.finish_with_error(&e.to_string());
                }
                return Err(e.into());
            }
        };

    if args.is_dry_run() {
        if !is_json_mode() {
            println!("DRY RUN MODE: No changes will be made.\n");
        }

        let total_size: u64 = files_to_remove.iter().map(|f| f.size).sum();
        let file_count = files_to_remove.iter().filter(|f| !f.is_dir).count();
        let dir_count = files_to_remove.iter().filter(|f| f.is_dir).count();

        let runner = resume_or_new_runner(
            early,
            args,
            "Removing",
            first_display.as_deref(),
            total_size,
            true,
        )?;
        let result = commands::remove::remove_paths(
            paths,
            args,
            &excludes,
            Arc::clone(runner.progress()),
            runner.inc_callback(),
            Box::new(runner.file_callback()),
            files_to_remove.len(),
        )
        .await;

        match result {
            Ok(()) => {
                runner.finish_ok()?;
            }
            Err(e) => {
                runner.finish_with_error(&e.to_string());
                return Err(e.into());
            }
        }

        if !is_json_mode() {
            print!("\nSummary: {} files", file_count);
            if dir_count > 0 {
                print!(", {} directories", dir_count);
            }
            println!(", {}", format_bytes(total_size as f64));
        }
        return Ok(());
    }

    if !files_to_remove.is_empty()
        && !args.is_force()
        && !args.is_yes()
        && (!args.is_interactive() || files_to_remove.len() > 1)
        && !confirm_removal(&files_to_remove)?
    {
        if let Some(r) = early {
            r.finish_with_error("cancelled by user");
        }
        return Err(BcmrError::Cancelled.into());
    }

    let total_size: u64 = files_to_remove.iter().map(|f| f.size).sum();
    let runner = resume_or_new_runner(
        early,
        args,
        "Removing",
        first_display.as_deref(),
        total_size,
        false,
    )?;

    let result = commands::remove::remove_paths(
        paths,
        args,
        &excludes,
        Arc::clone(runner.progress()),
        runner.inc_callback(),
        Box::new(runner.file_callback()),
        files_to_remove.len(),
    )
    .await;

    match result {
        Ok(()) => runner.finish_ok(),
        Err(e) => {
            runner.finish_with_error(&e.to_string());
            Err(e.into())
        }
    }
}

async fn handle_check_command(args: &Commands) -> Result<output::CheckResult> {
    let excludes = args.compile_excludes()?;
    let (sources, dest) = args.get_sources_and_dest().map_err(anyhow::Error::msg)?;
    Ok(commands::check::run(sources, dest, args.is_recursive(), &excludes).await?)
}

fn handle_init_command(args: &Commands) -> Result<()> {
    match args {
        Commands::Init {
            shell,
            cmd,
            prefix,
            suffix,
            path,
            no_cmd,
        } => {
            let script = commands::init::generate_init_script(
                shell,
                cmd.as_deref().unwrap_or(""),
                prefix.as_deref(),
                suffix.as_deref(),
                path.as_ref(),
                *no_cmd,
            );
            print!("{}", script);
            Ok(())
        }
        _ => unreachable!(),
    }
}

fn validate_mode(mode: &str, name: &str) -> Result<()> {
    match mode.to_lowercase().as_str() {
        "force" | "disable" | "never" | "auto" => Ok(()),
        other => Err(BcmrError::InvalidInput(format!(
            "Invalid {} mode '{}'. Supported modes: force, disable, never, auto.",
            name, other
        ))
        .into()),
    }
}

const POWERSHELL_REMOTE_INJECT: &str = r#"    $tokens = $commandAst.ToString() -split '\s+'
    if ($wordToComplete -match '.+:.+' -and $tokens.Count -ge 2 -and ($tokens[1] -eq 'copy' -or $tokens[1] -eq 'move')) {
        $results = bcmr __complete-remote $wordToComplete 2>$null
        if ($results) {
            $results | ForEach-Object {
                [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
            }
            return
        }
    }"#;

fn build_completion_command() -> clap::Command {
    let full = <cli::Cli as clap::CommandFactory>::command();
    let visible: Vec<clap::Command> = full
        .get_subcommands()
        .filter(|s| !s.is_hide_set())
        .cloned()
        .collect();
    let mut cmd = clap::Command::new("bcmr");
    for sub in visible {
        cmd = cmd.subcommand(sub);
    }
    cmd
}

fn remote_completion_script(shell: &clap_complete::Shell) -> &'static str {
    use clap_complete::Shell;
    match shell {
        Shell::Zsh => {
            r#"

_bcmr_with_remote() {
    local cur="${words[CURRENT]}"
    if [[ "$cur" == *:* ]] && [[ "${words[2]}" == "copy" || "${words[2]}" == "move" ]]; then
        local -a results
        results=("${(@f)$(bcmr __complete-remote "$cur" 2>/dev/null)}")
        if [[ ${#results[@]} -gt 0 && -n "${results[1]}" ]]; then
            compadd -U -S '' -- "${results[@]}"
            return
        fi
    fi
    _bcmr "$@"
}
compdef _bcmr_with_remote bcmr
"#
        }
        Shell::Bash => {
            r#"

_bcmr_with_remote() {
    local cur="${COMP_WORDS[COMP_CWORD]}"
    local cmd="${COMP_WORDS[1]}"
    if [[ "$cur" == *:* ]] && [[ "$cmd" == "copy" || "$cmd" == "move" ]]; then
        local IFS=$'\n'
        COMPREPLY=($(bcmr __complete-remote "$cur" 2>/dev/null))
        if [[ ${#COMPREPLY[@]} -gt 0 ]]; then
            compopt -o nospace
            return
        fi
    fi
    _bcmr "$@"
}
complete -F _bcmr_with_remote bcmr
"#
        }
        Shell::Fish => {
            r#"

complete -c bcmr -n '__fish_seen_subcommand_from copy move; and string match -q "*:*" -- (commandline -ct)' -f -a '(bcmr __complete-remote (commandline -ct) 2>/dev/null)'
"#
        }
        Shell::PowerShell => "", // handled via injection into clap-generated script
        _ => "",
    }
}

fn background_update_check(command: &Commands) -> Option<mpsc::Receiver<Option<String>>> {
    if matches!(
        command,
        Commands::Update
            | Commands::Completions { .. }
            | Commands::CompleteRemote { .. }
            | Commands::Serve
            | Commands::Deploy { .. }
    ) {
        return None;
    }
    if CONFIG.update_check != UpdateCheck::Notify {
        return None;
    }

    let (tx, rx) = mpsc::channel();
    std::thread::spawn(move || {
        let _ = tx.send(commands::update::check_for_update());
    });
    Some(rx)
}

/// For copy/move/remove with --json: detach to background and write progress to log file.
/// Returns true if this process should exit (parent after fork), false if it should continue (child or no detach).
fn maybe_detach(cli: &cli::Cli) -> Result<bool> {
    let is_operation = matches!(
        cli.command,
        Commands::Copy { .. } | Commands::Move { .. } | Commands::Remove { .. }
    );

    if !cli.json || !is_operation {
        return Ok(false);
    }

    // If --_bg is set, we ARE the background child: set up log file and continue.
    if let Some(ref job_id) = cli._bg {
        let log_path = commands::jobs::log_path(job_id);
        config::set_log_file(log_path);
        return Ok(false);
    }

    // Parent: spawn background child with --_bg flag.
    commands::jobs::ensure_jobs_dir()?;
    let job_id = commands::jobs::new_job_id();
    let log_path = commands::jobs::log_path(&job_id);

    let exe = std::env::current_exe()?;
    let original_args: Vec<String> = std::env::args().skip(1).collect();
    // --_bg must come before the subcommand (global flag)
    let mut args = vec!["--_bg".to_string(), job_id.clone()];
    args.extend(original_args);

    let child = std::process::Command::new(exe)
        .args(&args)
        .stdin(std::process::Stdio::null())
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn()?;

    // Write job start info to log file
    let job_info = commands::jobs::JobInfo {
        job_id: job_id.clone(),
        pid: child.id(),
        log: log_path.to_string_lossy().to_string(),
    };
    let mut f = std::fs::File::create(&log_path)?;
    serde_json::to_writer(&mut f, &job_info)?;
    use std::io::Write;
    f.write_all(b"\n")?;

    // Print job info to stdout for the caller
    println!("{}", serde_json::to_string(&job_info)?);

    // Clean up old job logs (7 days)
    commands::jobs::cleanup_old_jobs(7 * 24 * 3600);

    Ok(true) // parent exits
}

fn status_detail(latest: &str) -> String {
    serde_json::from_str::<serde_json::Value>(latest)
        .ok()
        .and_then(|v| {
            let ty = v.get("type")?.as_str()?;
            match ty {
                "progress" => v
                    .get("percent")
                    .and_then(|p| p.as_f64())
                    .map(|p| format!("{:.1}%", p)),
                "result" => match v.get("status").and_then(|s| s.as_str()) {
                    Some("success") => Some("complete".to_string()),
                    Some(_) => v
                        .get("error")
                        .and_then(|e| e.as_str())
                        .map(String::from)
                        .or_else(|| Some("error".to_string())),
                    None => None,
                },
                _ => None,
            }
        })
        .unwrap_or_default()
}

fn handle_status_command(job_id: &Option<String>) {
    match job_id {
        Some(id) => {
            let (state, latest) = match commands::jobs::job_state(id) {
                Ok(t) => t,
                Err(e) => {
                    eprintln!("Error: {}", e);
                    std::process::exit(1);
                }
            };

            if is_json_mode() {
                // Emit a single JSON object wrapping state + latest line.
                let latest_val: serde_json::Value =
                    serde_json::from_str(&latest).unwrap_or(serde_json::Value::Null);
                let wrapper = serde_json::json!({
                    "job_id": id,
                    "state": state.as_str(),
                    "latest": latest_val,
                });
                println!("{}", wrapper);
            } else {
                println!("{}\t{}\t{}", id, state.as_str(), status_detail(&latest));
            }
        }
        None => {
            let jobs = commands::jobs::list_jobs();
            if jobs.is_empty() {
                if !is_json_mode() {
                    println!("No jobs found.");
                }
                return;
            }
            if is_json_mode() {
                let arr: Vec<_> = jobs
                    .iter()
                    .map(|j| {
                        let latest_val: serde_json::Value =
                            serde_json::from_str(&j.latest).unwrap_or(serde_json::Value::Null);
                        serde_json::json!({
                            "job_id": j.id,
                            "state": j.state.as_str(),
                            "latest": latest_val,
                        })
                    })
                    .collect();
                println!("{}", serde_json::Value::Array(arr));
            } else {
                for j in &jobs {
                    println!(
                        "{}\t{}\t{}",
                        j.id,
                        j.state.as_str(),
                        status_detail(&j.latest)
                    );
                }
            }
        }
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let cli = cli::parse_args();

    // Handle detach for --json operations
    if maybe_detach(&cli)? {
        return Ok(()); // parent exits after spawning background child
    }

    set_json_mode(cli.json || cli._bg.is_some());

    let update_rx = background_update_check(&cli.command);

    match &cli.command {
        Commands::Copy { .. } => handle_copy_command(&cli.command).await?,
        Commands::Move { .. } => handle_move_command(&cli.command).await?,
        Commands::Remove { .. } => handle_remove_command(&cli.command).await?,
        Commands::Check { .. } => {
            let result = handle_check_command(&cli.command).await;
            match result {
                Ok(r) => {
                    if is_json_mode() {
                        let out = output::CommandOutput::Check(r);
                        println!("{}", out.to_json());
                        let code = out.exit_code();
                        if code != 0 {
                            std::process::exit(code);
                        }
                    } else {
                        let in_sync = r.in_sync;
                        output::print_check_human(&r);
                        if !in_sync {
                            std::process::exit(1);
                        }
                    }
                }
                Err(e) => {
                    if is_json_mode() {
                        let out = output::error_output("check", &e);
                        println!("{}", out.to_json());
                        std::process::exit(2);
                    } else {
                        return Err(e);
                    }
                }
            }
        }
        Commands::Status { job_id } => {
            handle_status_command(job_id);
        }
        Commands::Init { .. } => handle_init_command(&cli.command)?,
        Commands::Update => commands::update::run()?,
        Commands::Serve => {
            commands::serve::run().await?;
        }
        Commands::Deploy { target, path } => {
            let remote_path = path.as_deref().unwrap_or("~/.local/bin/bcmr");
            commands::deploy::run(target, remote_path).await?;
        }
        Commands::CompleteRemote { partial } => {
            for entry in crate::core::remote::complete_remote_path(partial).await {
                println!("{}", entry);
            }
        }
        Commands::Completions { shell } => {
            let mut cmd = build_completion_command();
            let mut buf = Vec::new();
            clap_complete::generate(*shell, &mut cmd, "bcmr", &mut buf);
            let base = String::from_utf8(buf).expect("clap generated invalid UTF-8");

            if *shell == clap_complete::Shell::PowerShell {
                let injected = base.replacen(
                    "param($wordToComplete, $commandAst, $cursorPosition)\n",
                    &format!(
                        "param($wordToComplete, $commandAst, $cursorPosition)\n{}\n",
                        POWERSHELL_REMOTE_INJECT
                    ),
                    1,
                );
                print!("{}", injected);
            } else {
                print!("{}", base);
                print!("{}", remote_completion_script(shell));
            }
        }
    }

    if !is_json_mode() {
        show_update_hint(update_rx);
    }

    Ok(())
}

fn show_update_hint(update_rx: Option<mpsc::Receiver<Option<String>>>) {
    if let Some(rx) = update_rx {
        if let Ok(Some(version)) = rx.try_recv() {
            eprintln!(
                "\x1b[33m↑ Update available: v{} → v{} (run `bcmr update`)\x1b[0m",
                env!("CARGO_PKG_VERSION"),
                version
            );
        }
    }
}
