#![allow(dead_code)]

pub mod builder;
mod error;
pub(crate) mod package_discovery;
pub(crate) mod scope;
pub mod task_access;
pub(crate) mod task_filter;
mod ui;
pub mod watch;

use std::{
    collections::{BTreeMap, HashSet},
    future::Future,
    io::{self, IsTerminal, Write},
    process::Command,
    sync::{
        atomic::{AtomicBool, Ordering},
        Arc,
    },
    time::Duration,
};

use chrono::{DateTime, Local};
use futures::StreamExt;
use itertools::Itertools;
use shared_child::SharedChild;
use tokio::{pin, select, task::JoinHandle};
use tracing::{debug, error, info, instrument, warn};
use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf};
use turborepo_api_client::APIAuth;
use turborepo_ci::Vendor;
use turborepo_env::EnvironmentVariableMap;
use turborepo_microfrontends_proxy::ProxyServer;
use turborepo_process::ProcessManager;
use turborepo_repository::package_graph::{PackageGraph, PackageName, PackageNode};
pub use turborepo_run_cache::{ConfigCache, RunCache, TaskCache};
use turborepo_run_summary::{ObservabilityHandle, RunTracker};
use turborepo_scm::{RepoGitIndex, SCM};
use turborepo_signals::{listeners::get_signal, ShutdownReason, SignalHandler};
use turborepo_task_hash::{
    collect_global_file_hash_inputs, get_external_deps_hash, get_internal_deps_hash,
    global_hash::GLOBAL_CACHE_KEY, GlobalHashableInputs, PackageInputsHashes,
};
use turborepo_telemetry::events::generic::GenericEventBuilder;
use turborepo_types::{EnvMode, UIMode};
use turborepo_ui::{sender::UISender, tui, tui::TuiSender, wui::sender::WebUISender, ColorConfig};

pub use crate::run::error::Error;
use crate::{
    engine::{Engine, EngineExt},
    microfrontends::MicrofrontendsConfigs,
    opts::{Opts, RemoteCacheDisabledReason},
    run::task_access::TaskAccess,
    task_graph::Visitor,
    turbo_json::{TurboJson, UnifiedTurboJsonLoader},
};

/// Live status of the remote cache, determined by a preflight API check
/// that runs concurrently with graph building.
#[derive(Debug, Clone, Copy)]
pub(crate) enum RemoteCacheUnavailableReason {
    CouldNotConnect,
    UsageLimitExceeded,
    SpendingPaused,
    DisabledForTeam,
    AuthenticationFailed,
    UnexpectedServerError,
}

/// Resolved remote cache status for the run prelude display.
#[derive(Debug, Clone, Copy)]
pub(crate) enum RemoteCacheStatus {
    Disabled(RemoteCacheDisabledReason),
    Enabled,
    Unavailable(RemoteCacheUnavailableReason),
}

#[derive(Clone)]
pub struct Run {
    version: &'static str,
    color_config: ColorConfig,
    start_at: DateTime<Local>,
    processes: ProcessManager,
    run_telemetry: GenericEventBuilder,
    repo_root: AbsoluteSystemPathBuf,
    opts: Arc<Opts>,
    api_auth: Option<APIAuth>,
    env_at_execution_start: EnvironmentVariableMap,
    filtered_pkgs: HashSet<PackageName>,
    pkg_dep_graph: Arc<PackageGraph>,
    turbo_json_loader: UnifiedTurboJsonLoader,
    root_turbo_json: TurboJson,
    scm: SCM,
    run_cache: Arc<RunCache>,
    signal_handler: SignalHandler,
    remote_cache_status: RemoteCacheStatus,
    engine: Arc<Engine>,
    task_access: TaskAccess,
    micro_frontend_configs: Option<MicrofrontendsConfigs>,
    repo_index: Arc<Option<RepoGitIndex>>,
    observability_handle: Option<ObservabilityHandle>,
    pub(crate) query_server: Option<Arc<dyn turborepo_query_api::QueryServer>>,
    shutdown_started_emitted: Arc<AtomicBool>,
}

type UIResult<T> = Result<Option<(T, JoinHandle<Result<(), turborepo_ui::Error>>)>, Error>;

type WuiResult = UIResult<WebUISender>;
type TuiResult = UIResult<TuiSender>;

#[derive(Debug, Clone, Copy)]
enum ForceShutdownReason {
    Signal,
    Timeout,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CacheShutdownOutcome {
    Complete,
    ForcedShutdown,
}

const SLOW_SHUTDOWN_WARNING_DELAY: Duration = Duration::from_secs(3);

impl Run {
    fn shutdown_started_message(force_shutdown_timeout: Option<Duration>) -> &'static str {
        #[cfg(windows)]
        {
            let _ = force_shutdown_timeout;
            "Shutting down Turborepo tasks..."
        }

        #[cfg(not(windows))]
        {
            match force_shutdown_timeout {
                Some(_) => "Shutting down Turborepo tasks...",
                None => "^C - Shutting down Turborepo tasks...",
            }
        }
    }

    fn force_shutdown_timeout() -> Option<Duration> {
        (!std::io::stdin().is_terminal()).then_some(Duration::from_secs(10))
    }

    fn emit_shutdown_started(force_shutdown_timeout: Option<Duration>) {
        turborepo_log::info(
            turborepo_log::Source::turbo(turborepo_log::Subsystem::Run),
            Self::shutdown_started_message(force_shutdown_timeout),
        )
        .emit();
    }

    fn emit_shutdown_started_once(
        shutdown_started_emitted: &AtomicBool,
        force_shutdown_timeout: Option<Duration>,
    ) {
        if shutdown_started_emitted
            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
            .is_ok()
        {
            Self::emit_shutdown_started(force_shutdown_timeout);
        }
    }

    fn emit_slow_tasks(task_names: &[String], force_shutdown_timeout: Option<Duration>) {
        if task_names.is_empty() {
            return;
        }

        let list = task_names.join(", ");
        turborepo_log::warn(
            turborepo_log::Source::turbo(turborepo_log::Subsystem::Run),
            format!("Some tasks in your Turborepo are taking awhile to shut down: {list}"),
        )
        .emit();

        if let Some(remaining) = force_shutdown_timeout
            .map(|timeout| timeout.saturating_sub(SLOW_SHUTDOWN_WARNING_DELAY))
        {
            turborepo_log::warn(
                turborepo_log::Source::turbo(turborepo_log::Subsystem::Run),
                format!("Shutting down forcibly in {}s...", remaining.as_secs()),
            )
            .emit();
        } else {
            turborepo_log::warn(
                turborepo_log::Source::turbo(turborepo_log::Subsystem::Run),
                "Press CTRL+C again to force shut down, or wait.",
            )
            .emit();
        }
    }

    fn emit_force_shutdown_message(
        reason: ForceShutdownReason,
        is_interactive: bool,
        task_names: &[String],
    ) {
        let message = match (reason, task_names.is_empty()) {
            (ForceShutdownReason::Signal, false) if is_interactive => {
                format!(
                    " - Force killing Turborepo tasks: {}",
                    task_names.join(", ")
                )
            }
            (ForceShutdownReason::Signal, false) => {
                format!("Force killing Turborepo tasks: {}", task_names.join(", "))
            }
            (ForceShutdownReason::Timeout, false) => format!(
                "Graceful shutdown timed out. Force killing Turborepo tasks: {}",
                task_names.join(", ")
            ),
            (ForceShutdownReason::Signal, true) if is_interactive => {
                " - Force killing remaining Turborepo tasks...".to_string()
            }
            (ForceShutdownReason::Signal, true) => {
                "Force killing remaining Turborepo tasks...".to_string()
            }
            (ForceShutdownReason::Timeout, true) => "Graceful shutdown timed out. Force killing \
                                                     remaining Turborepo tasks..."
                .to_string(),
        };
        turborepo_log::warn(
            turborepo_log::Source::turbo(turborepo_log::Subsystem::Run),
            message,
        )
        .emit();
    }

    async fn wait_for_forced_shutdown(
        force_shutdown_timeout: Option<Duration>,
    ) -> ForceShutdownReason {
        match force_shutdown_timeout {
            Some(timeout) => {
                tokio::time::sleep(timeout).await;
                ForceShutdownReason::Timeout
            }
            None => {
                let interrupt = async {
                    if let Ok(fut) = get_signal() {
                        pin!(fut);
                        fut.next().await;
                    } else {
                        tracing::warn!("could not register ctrl-c handler");
                        tokio::time::sleep(Duration::MAX).await;
                    }
                };
                interrupt.await;
                ForceShutdownReason::Signal
            }
        }
    }

    async fn wait_for_cache_shutdown<FClosed, FProgress>(
        shutdown_reason: Option<ShutdownReason>,
        force_shutdown_timeout: Option<Duration>,
        closed: FClosed,
        progress: FProgress,
    ) -> CacheShutdownOutcome
    where
        FClosed: Future<Output = ()> + Send,
        FProgress: Future<Output = ()> + Send,
    {
        if shutdown_reason == Some(ShutdownReason::Signal) {
            select! {
                _ = closed => CacheShutdownOutcome::Complete,
                _ = progress => CacheShutdownOutcome::Complete,
                _ = Self::wait_for_forced_shutdown(force_shutdown_timeout) => {
                    CacheShutdownOutcome::ForcedShutdown
                }
            }
        } else {
            select! {
                _ = closed => CacheShutdownOutcome::Complete,
                _ = progress => CacheShutdownOutcome::Complete,
            }
        }
    }

    async fn wait_for_process_manager_shutdown<F>(
        process_manager: ProcessManager,
        force_shutdown_timeout: Option<Duration>,
        graceful_shutdown: F,
    ) where
        F: Future<Output = ()> + Send,
    {
        let is_interactive = force_shutdown_timeout.is_none();
        let slow_task_delay = tokio::time::sleep(SLOW_SHUTDOWN_WARNING_DELAY);
        let force_shutdown = Self::wait_for_forced_shutdown(force_shutdown_timeout);
        pin!(graceful_shutdown, slow_task_delay, force_shutdown);
        let mut slow_tasks_emitted = false;

        loop {
            select! {
                _ = &mut graceful_shutdown => break,
                _ = &mut slow_task_delay, if !slow_tasks_emitted => {
                    slow_tasks_emitted = true;
                    let tasks = process_manager.running_task_ids();
                    Self::emit_slow_tasks(&tasks, force_shutdown_timeout);
                }
                reason = &mut force_shutdown => {
                    let tasks = process_manager.running_task_ids();
                    Self::emit_force_shutdown_message(reason, is_interactive, &tasks);
                    process_manager.kill_all().await;
                    break;
                }
            }
        }
    }

    /// Emit run prelude through `turborepo_log`. In stream mode,
    /// `TerminalSink` writes these to stdout; in TUI mode, `TuiSink`
    /// captures them for the log panel.
    pub fn emit_run_prelude_logs(&self) {
        let pad = "   ";
        turborepo_log::info(
            turborepo_log::Source::turbo(turborepo_log::Subsystem::Run),
            "",
        )
        .emit();

        let targets_list = self.opts.run_opts.tasks.join(", ");
        if self.opts.run_opts.single_package {
            turborepo_log::info(
                turborepo_log::Source::turbo(turborepo_log::Subsystem::Run),
                format!("{pad}• Running {targets_list}"),
            )
            .emit();
        } else {
            let mut packages = self
                .filtered_pkgs
                .iter()
                .map(|workspace_name| workspace_name.to_string())
                .collect::<Vec<String>>();
            packages.sort();
            turborepo_log::info(
                turborepo_log::Source::turbo(turborepo_log::Subsystem::Run),
                format!("{pad}• Packages in scope: {}", packages.join(", ")),
            )
            .emit();
            turborepo_log::info(
                turborepo_log::Source::turbo(turborepo_log::Subsystem::Run),
                format!(
                    "{pad}• Running {targets_list} in {} packages",
                    self.filtered_pkgs.len()
                ),
            )
            .emit();
        }

        let api_url = &self.opts.api_client_opts.api_url;
        let (base_msg, is_warning) = match self.remote_cache_status {
            RemoteCacheStatus::Enabled => ("Remote caching enabled".to_string(), false),
            RemoteCacheStatus::Disabled(reason) => {
                let msg = match reason {
                    RemoteCacheDisabledReason::NotLinked => "Remote caching disabled".to_string(),
                    RemoteCacheDisabledReason::TokenWithoutTeam => {
                        "Remote caching disabled (TURBO_TOKEN set without TURBO_TEAM)".to_string()
                    }
                    RemoteCacheDisabledReason::InConfig => {
                        "Remote caching disabled (in configuration)".to_string()
                    }
                    RemoteCacheDisabledReason::InEnvVar => {
                        "Remote caching disabled (by TURBO_REMOTE_CACHE_ENABLED)".to_string()
                    }
                    RemoteCacheDisabledReason::ByFlags => {
                        "Remote caching disabled (by flags)".to_string()
                    }
                    RemoteCacheDisabledReason::RequestedWithoutCredentials => {
                        "Remote caching disabled (remote cache requested \u{2014} set TURBO_TOKEN \
                         and TURBO_TEAM, or run \"turbo login\" and \"turbo link\")"
                            .to_string()
                    }
                };
                (msg, false)
            }
            RemoteCacheStatus::Unavailable(reason) => {
                let msg = match reason {
                    RemoteCacheUnavailableReason::CouldNotConnect => {
                        format!("Remote caching unavailable (Could not connect to \"{api_url}\")")
                    }
                    RemoteCacheUnavailableReason::AuthenticationFailed => {
                        "Remote caching unavailable (Authentication failed \u{2014} check \
                         TURBO_TOKEN or run \"turbo login\")"
                            .to_string()
                    }
                    RemoteCacheUnavailableReason::UsageLimitExceeded => {
                        "Remote caching unavailable (Usage limit exceeded)".to_string()
                    }
                    RemoteCacheUnavailableReason::SpendingPaused => {
                        "Remote caching unavailable (Spending paused)".to_string()
                    }
                    RemoteCacheUnavailableReason::DisabledForTeam => {
                        "Remote caching unavailable (Disabled for this team)".to_string()
                    }
                    RemoteCacheUnavailableReason::UnexpectedServerError => {
                        format!(
                            "Remote caching unavailable (Unexpected server error at \"{api_url}\")"
                        )
                    }
                };
                (msg, true)
            }
        };

        let cache_status = if self.opts.run_opts.is_shared_worktree_cache {
            format!("{pad}• {base_msg}, using shared worktree cache")
        } else {
            format!("{pad}• {base_msg}")
        };

        if is_warning {
            turborepo_log::warn(
                turborepo_log::Source::turbo(turborepo_log::Subsystem::Run),
                cache_status,
            )
            .emit();
        } else {
            turborepo_log::info(
                turborepo_log::Source::turbo(turborepo_log::Subsystem::Run),
                cache_status,
            )
            .emit();
        }
        turborepo_log::info(
            turborepo_log::Source::turbo(turborepo_log::Subsystem::Run),
            "",
        )
        .emit();
    }

    pub fn turbo_json_loader(&self) -> &UnifiedTurboJsonLoader {
        &self.turbo_json_loader
    }

    pub fn opts(&self) -> &Opts {
        &self.opts
    }

    pub fn repo_root(&self) -> &AbsoluteSystemPath {
        &self.repo_root
    }

    pub fn scm(&self) -> &SCM {
        &self.scm
    }

    pub fn root_turbo_json(&self) -> &TurboJson {
        &self.root_turbo_json
    }

    // Produces the transitive closure of the filtered packages,
    // i.e. the packages relevant for this run.
    #[instrument(skip(self), ret)]
    pub fn get_relevant_packages(&self) -> HashSet<PackageName> {
        let packages: Vec<_> = self
            .filtered_pkgs
            .iter()
            .map(|pkg| PackageNode::Workspace(pkg.clone()))
            .collect();
        self.pkg_dep_graph
            .transitive_closure(&packages)
            .into_iter()
            .filter_map(|node| match node {
                PackageNode::Root => None,
                PackageNode::Workspace(pkg) => Some(pkg.clone()),
            })
            .collect()
    }

    // Produces a map of tasks to the packages where they're defined.
    // Used to print a list of potential tasks to run. Obeys the `--filter` flag
    pub fn get_potential_tasks(&self) -> Result<BTreeMap<String, Vec<String>>, Error> {
        let mut tasks = BTreeMap::new();
        for (name, info) in self.pkg_dep_graph.packages() {
            if !self.filtered_pkgs.contains(name) {
                continue;
            }
            for task_name in info.package_json.scripts.keys() {
                tasks
                    .entry(task_name.clone())
                    .or_insert_with(Vec::new)
                    .push(name.to_string())
            }
        }

        Ok(tasks)
    }

    pub fn pkg_dep_graph(&self) -> &PackageGraph {
        &self.pkg_dep_graph
    }

    pub fn engine(&self) -> &Engine {
        &self.engine
    }

    pub fn filtered_pkgs(&self) -> &HashSet<PackageName> {
        &self.filtered_pkgs
    }

    pub fn color_config(&self) -> ColorConfig {
        self.color_config
    }

    pub fn has_tui(&self) -> bool {
        self.opts.run_opts.ui_mode.use_tui()
    }

    pub fn should_start_ui(&self) -> Result<bool, Error> {
        Ok(self.opts.run_opts.ui_mode.use_tui()
            && self.opts.run_opts.dry_run.is_none()
            && tui::terminal_big_enough()?)
    }

    pub fn start_ui(self: &Arc<Self>) -> UIResult<UISender> {
        match self.opts.run_opts.ui_mode {
            UIMode::Tui => self
                .start_terminal_ui()
                .map(|res| res.map(|(sender, handle)| (UISender::Tui(sender), handle))),
            UIMode::Stream | UIMode::StreamWithTimestamps => Ok(None),
            UIMode::Web => self
                .start_web_ui()
                .map(|res| res.map(|(sender, handle)| (UISender::Wui(sender), handle))),
        }
    }
    fn start_web_ui(self: &Arc<Self>) -> WuiResult {
        let Some(query_server) = self.query_server.clone() else {
            tracing::warn!("Web UI requires a query server implementation");
            return Ok(None);
        };
        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();

        let handle = tokio::spawn(ui::start_web_ui_server(rx, self.clone(), query_server));

        Ok(Some((WebUISender { tx }, handle)))
    }

    #[allow(clippy::type_complexity)]
    fn start_terminal_ui(&self) -> TuiResult {
        if !self.should_start_ui()? {
            return Ok(None);
        }

        let task_names = self.engine.tasks_with_command(&self.pkg_dep_graph);
        // If there aren't any tasks to run, then shouldn't start the UI
        if task_names.is_empty() {
            return Ok(None);
        }

        let (sender, receiver) = TuiSender::new();
        let color_config = self.color_config;
        let scrollback_len = self.opts.tui_opts.scrollback_length;
        let repo_root = self.repo_root.clone();
        let handle = tokio::task::spawn(async move {
            Ok(tui::run_app(
                task_names,
                receiver,
                color_config,
                &repo_root,
                scrollback_len,
            )
            .await?)
        });

        Ok(Some((sender, handle)))
    }

    /// Returns a handle that can be used to stop a run
    pub fn stopper(&self) -> RunStopper {
        RunStopper {
            manager: self.processes.clone(),
        }
    }

    async fn start_proxy_if_needed(
        &self,
    ) -> Result<
        Option<(
            tokio::sync::broadcast::Sender<()>,
            tokio::sync::oneshot::Receiver<()>,
        )>,
        Error,
    > {
        let Some(mfe_configs) = &self.micro_frontend_configs else {
            return Ok(None);
        };

        if !mfe_configs.should_use_turborepo_proxy()
            || !mfe_configs.has_dev_task(self.engine.task_ids())
        {
            return Ok(None);
        }

        info!("Starting Turborepo microfrontends proxy");

        let config = self.load_proxy_config(mfe_configs).await?;
        let (mut server, shutdown_handle) = self.start_proxy_server(config).await?;

        let signal_handler_complete_rx =
            self.setup_shutdown_handlers(&mut server, shutdown_handle.clone());

        tokio::spawn(async move {
            if let Err(e) = server.run().await {
                error!("Turborepo proxy error: {}", e);
            }
        });

        info!("Turborepo proxy started successfully");
        Ok(Some((shutdown_handle, signal_handler_complete_rx)))
    }

    async fn load_proxy_config(
        &self,
        mfe_configs: &MicrofrontendsConfigs,
    ) -> Result<turborepo_microfrontends::Config, Error> {
        let config_path = mfe_configs
            .configs()
            .sorted_by(|(a, _), (b, _)| a.cmp(b))
            .find_map(|(pkg, _tasks)| mfe_configs.config_filename(pkg));

        let Some(config_path) = config_path else {
            return Err(Error::Proxy(
                "No microfrontends config file found".to_string(),
            ));
        };

        let full_path = self.repo_root.join_unix_path(config_path);
        let contents = std::fs::read_to_string(&full_path).map_err(|e| {
            Error::Proxy(format!("Failed to read microfrontends config file: {}", e))
        })?;

        let config = turborepo_microfrontends::TurborepoMfeConfig::from_str(
            &contents,
            full_path.as_str(),
        )
        .map_err(|e| Error::Proxy(format!("Failed to parse microfrontends config: {}", e)))?;

        Ok(config.into_config())
    }

    async fn start_proxy_server(
        &self,
        config: turborepo_microfrontends::Config,
    ) -> Result<(ProxyServer, tokio::sync::broadcast::Sender<()>), Error> {
        let server = ProxyServer::new(config)
            .map_err(|e| Error::Proxy(format!("Failed to create Turborepo proxy: {}", e)))?;

        if !server.check_port_available().await {
            return Err(Error::Proxy("Port is not available.".to_string()));
        }

        let shutdown_handle = server.shutdown_handle();
        Ok((server, shutdown_handle))
    }

    fn setup_shutdown_handlers(
        &self,
        server: &mut ProxyServer,
        shutdown_handle: tokio::sync::broadcast::Sender<()>,
    ) -> tokio::sync::oneshot::Receiver<()> {
        let (proxy_shutdown_complete_tx, proxy_shutdown_complete_rx) =
            tokio::sync::oneshot::channel();
        let (cleanup_complete_tx, cleanup_complete_rx) = tokio::sync::oneshot::channel();
        let (signal_handler_complete_tx, signal_handler_complete_rx) =
            tokio::sync::oneshot::channel();

        server.set_shutdown_complete_tx(proxy_shutdown_complete_tx);

        tokio::spawn(async move {
            if proxy_shutdown_complete_rx.await.is_ok() {
                let _ = cleanup_complete_tx.send(());
                let _ = signal_handler_complete_tx.send(());
            }
        });

        self.register_proxy_signal_handler(shutdown_handle, signal_handler_complete_rx);

        cleanup_complete_rx
    }

    fn register_proxy_signal_handler(
        &self,
        shutdown_handle: tokio::sync::broadcast::Sender<()>,
        shutdown_complete_rx: tokio::sync::oneshot::Receiver<()>,
    ) {
        if let Some(subscriber) = self.signal_handler.subscribe() {
            let signal_handler = self.signal_handler.clone();
            let process_manager = self.processes.clone();
            let force_shutdown_timeout = Self::force_shutdown_timeout();
            let shutdown_started_emitted = self.shutdown_started_emitted.clone();
            tokio::spawn(async move {
                info!("Proxy signal handler registered and waiting");
                let Ok(_guard) = subscriber.listen().await else {
                    debug!("proxy signal handler exited before notifying subscriber");
                    return;
                };

                let shutdown_reason = signal_handler.shutdown_reason();

                if shutdown_reason != Some(ShutdownReason::Signal) {
                    let _ = shutdown_handle.send(());
                    let _ = tokio::time::timeout(Duration::from_millis(1000), shutdown_complete_rx)
                        .await;
                    process_manager.stop().await;
                    debug!("Child processes stopped");
                    return;
                }

                Self::emit_shutdown_started_once(
                    shutdown_started_emitted.as_ref(),
                    force_shutdown_timeout,
                );

                let graceful_process_manager = process_manager.clone();
                let graceful_shutdown = async move {
                    info!("Signal received! Shutting down proxy BEFORE process manager stops");
                    let _ = shutdown_handle.send(());
                    debug!(
                        "Proxy shutdown signal sent, waiting for shutdown completion notification"
                    );

                    match tokio::time::timeout(
                        tokio::time::Duration::from_millis(1000),
                        shutdown_complete_rx,
                    )
                    .await
                    {
                        Ok(Ok(())) => {
                            info!("Proxy websocket close complete, now stopping child processes");
                        }
                        Ok(Err(_)) => {
                            warn!("Proxy shutdown notification channel closed unexpectedly");
                        }
                        Err(_) => {
                            info!("Proxy shutdown notification timed out after 1000 milliseconds");
                        }
                    }

                    graceful_process_manager.shutdown(None).await;
                };
                Self::wait_for_process_manager_shutdown(
                    process_manager.clone(),
                    force_shutdown_timeout,
                    graceful_shutdown,
                )
                .await;

                debug!("Child processes stopped");
            });
        } else {
            warn!("Could not subscribe to signal handler for proxy shutdown");
        }
    }

    fn setup_cache_shutdown_handler(&self) {
        let skip_cache_writes = self.opts.cache_opts.cache.skip_writes();
        if skip_cache_writes {
            return;
        }

        let Some(subscriber) = self.signal_handler.subscribe() else {
            return;
        };

        let run_cache = self.run_cache.clone();
        let signal_handler = self.signal_handler.clone();
        let force_shutdown_timeout = Self::force_shutdown_timeout();
        let shutdown_started_emitted = self.shutdown_started_emitted.clone();
        let use_tui = self.opts.run_opts.ui_mode.use_tui();
        tokio::spawn(async move {
            let Ok(_guard) = subscriber.listen().await else {
                tracing::debug!(
                    "signal handler exited before cache shutdown subscriber was notified"
                );
                return;
            };
            let shutdown_reason = signal_handler.shutdown_reason();

            if shutdown_reason == Some(ShutdownReason::Signal) {
                Self::emit_shutdown_started_once(
                    shutdown_started_emitted.as_ref(),
                    force_shutdown_timeout,
                );
            }

            let spinner =
                (!use_tui).then(|| turborepo_ui::start_spinner("...Finishing writing to cache..."));
            if use_tui {
                turborepo_log::info(
                    turborepo_log::Source::turbo(turborepo_log::Subsystem::Cache),
                    "Finishing writing to cache...",
                )
                .emit();
            }

            if let Ok((status, closed)) = run_cache.shutdown_cache().await {
                let fut = async {
                    loop {
                        tokio::time::sleep(std::time::Duration::from_secs(1)).await;

                        let (bytes_per_second, bytes_uploaded, bytes_total) = {
                            let status = status
                                .lock()
                                .unwrap_or_else(|poisoned| poisoned.into_inner());
                            let total_bps: f64 =
                                status.values().filter_map(|task| task.average_bps()).sum();
                            let bytes_uploaded: usize =
                                status.values().filter_map(|task| task.bytes()).sum();
                            let bytes_total: usize = status
                                .iter()
                                .filter(|(_hash, task)| !task.done())
                                .filter_map(|(_hash, task)| task.size())
                                .sum();
                            (total_bps, bytes_uploaded, bytes_total)
                        };

                        if bytes_total == 0 {
                            continue;
                        }

                        let mut formatter = human_format::Formatter::new();
                        let formatter = formatter.with_decimals(2).with_separator("");
                        let bytes_per_second = formatter.with_units("B/s").format(bytes_per_second);
                        let bytes_remaining = formatter
                            .with_units("B")
                            .format(bytes_total.saturating_sub(bytes_uploaded) as f64);

                        if let Some(spinner) = spinner.as_ref() {
                            spinner.set_message(format!(
                                "...Finishing writing to cache... ({bytes_remaining} remaining, \
                                 {bytes_per_second})"
                            ));
                        }
                    }
                };

                if Self::wait_for_cache_shutdown(
                    shutdown_reason,
                    force_shutdown_timeout,
                    async {
                        let _ = closed.await;
                    },
                    fut,
                )
                .await
                    == CacheShutdownOutcome::ForcedShutdown
                {
                    tracing::debug!("received force shutdown while flushing cache, exiting");
                }
            } else {
                tracing::warn!("could not start shutdown, exiting");
            }
            if let Some(spinner) = spinner {
                spinner.finish_and_clear();
            }
        });
    }

    fn setup_process_manager_shutdown_handler(&self) {
        let Some(subscriber) = self.signal_handler.subscribe() else {
            return;
        };

        let signal_handler = self.signal_handler.clone();
        let process_manager = self.processes.clone();
        let force_shutdown_timeout = Self::force_shutdown_timeout();
        let shutdown_started_emitted = self.shutdown_started_emitted.clone();
        tokio::spawn(async move {
            let Ok(_guard) = subscriber.listen().await else {
                debug!("signal handler exited before process manager subscriber was notified");
                return;
            };
            let shutdown_reason = signal_handler.shutdown_reason();

            if shutdown_reason != Some(ShutdownReason::Signal) {
                process_manager.stop().await;
                debug!("Child processes stopped");
                return;
            }

            Self::emit_shutdown_started_once(
                shutdown_started_emitted.as_ref(),
                force_shutdown_timeout,
            );
            debug!("Signal received, stopping child processes");
            let graceful_process_manager = process_manager.clone();
            let graceful_shutdown = async move {
                graceful_process_manager.shutdown(None).await;
            };
            Self::wait_for_process_manager_shutdown(
                process_manager.clone(),
                force_shutdown_timeout,
                graceful_shutdown,
            )
            .await;

            debug!("Child processes stopped");
        });
    }

    async fn cleanup_proxy(
        &self,
        proxy_shutdown: Option<(
            tokio::sync::broadcast::Sender<()>,
            tokio::sync::oneshot::Receiver<()>,
        )>,
    ) {
        let Some((shutdown_tx, shutdown_complete_rx)) = proxy_shutdown else {
            return;
        };

        info!("Shutting down Turborepo proxy gracefully BEFORE stopping child processes");
        let _ = shutdown_tx.send(());
        debug!("Sent shutdown signal to proxy, waiting for completion signal");

        match tokio::time::timeout(tokio::time::Duration::from_secs(2), shutdown_complete_rx).await
        {
            Ok(Ok(())) => {
                info!("Proxy shutdown completed successfully");
            }
            Ok(Err(_)) => {
                warn!("Proxy shutdown channel closed unexpectedly");
            }
            Err(_) => {
                warn!("Proxy shutdown timed out after 2 seconds");
            }
        }

        info!("Proxy shutdown complete, proceeding with visitor cleanup");
    }

    #[instrument(skip_all)]
    async fn execute_visitor(
        &self,
        ui_sender: Option<UISender>,
        is_watch: bool,
        proxy_shutdown: Option<(
            tokio::sync::broadcast::Sender<()>,
            tokio::sync::oneshot::Receiver<()>,
        )>,
    ) -> Result<i32, Error> {
        let workspaces = self.pkg_dep_graph.packages().collect();
        let repo_index = self.repo_index.as_ref().as_ref();

        let root_workspace = self
            .pkg_dep_graph
            .package_info(&PackageName::Root)
            .ok_or(Error::MissingRootWorkspace)?;

        let is_monorepo = !self.opts.run_opts.single_package;

        // Run three expensive I/O-bound operations concurrently using rayon::scope:
        // 1. Package file hashing - walks every package's files and computes hashes
        // 2. Internal deps hashing - walks root internal dependency packages
        // 3. Global file hash inputs - globwalks global deps and hashes them
        //
        // These are completely independent and dominate the pre-execution phase.
        // Running them in parallel can significantly reduce wall-clock time.
        let internal_dep_paths = is_monorepo.then(|| {
            self.pkg_dep_graph
                .root_internal_package_dependencies_paths()
        });

        let env_mode = self.opts.run_opts.env_mode;

        let mut file_hash_result = None;
        let mut internal_deps_result = None;
        let mut global_file_result = None;

        let _hash_scope_span = tracing::info_span!("hash_scope").entered();
        crate::rayon_compat::block_in_place(|| {
            rayon::scope(|s| {
                s.spawn(|_| {
                    let _span = tracing::info_span!("calculate_file_hashes_task").entered();
                    let needs_expanded = self.opts.run_opts.dry_run.is_some()
                        || self.opts.run_opts.summarize
                        || self.observability_handle.is_some();
                    file_hash_result = Some(PackageInputsHashes::calculate_file_hashes(
                        &self.scm,
                        self.engine.tasks(),
                        workspaces,
                        self.engine.task_definitions(),
                        &self.repo_root,
                        &self.run_telemetry,
                        repo_index,
                        needs_expanded,
                    ));
                });
                s.spawn(|_| {
                    let _span = tracing::info_span!("get_internal_deps_hash_task").entered();
                    internal_deps_result = Some(
                        internal_dep_paths
                            .map(|dep_paths| {
                                get_internal_deps_hash(
                                    &self.scm,
                                    &self.repo_root,
                                    dep_paths,
                                    repo_index,
                                )
                            })
                            .transpose(),
                    );
                });
                s.spawn(|_| {
                    let _span =
                        tracing::info_span!("collect_global_file_hash_inputs_task").entered();
                    global_file_result = Some(collect_global_file_hash_inputs(
                        root_workspace,
                        &self.repo_root,
                        self.pkg_dep_graph.package_manager(),
                        self.pkg_dep_graph.lockfile(),
                        self.root_turbo_json.global_deps_for_hash(),
                        &self.env_at_execution_start,
                        &self.root_turbo_json.global_env,
                        &self.scm,
                    ));
                });
            });
        });

        drop(_hash_scope_span);

        let _setup_span = tracing::debug_span!("post_hashing_setup").entered();

        let package_inputs_hashes = file_hash_result.ok_or(Error::FileHashTaskIncomplete)??;
        let root_internal_dependencies_hash =
            internal_deps_result.ok_or(Error::InternalDepsTaskIncomplete)??;
        let global_file_inputs =
            global_file_result.ok_or(Error::GlobalFileHashTaskIncomplete)??;

        let root_external_dependencies_hash =
            is_monorepo.then(|| get_external_deps_hash(&root_workspace.transitive_dependencies));

        let pass_through_env = match env_mode {
            EnvMode::Loose => {
                // Remove the passthroughs from hash consideration if we're explicitly loose.
                None
            }
            EnvMode::Strict => self.root_turbo_json.global_pass_through_env.as_deref(),
        };

        let global_hash_inputs = GlobalHashableInputs {
            global_cache_key: GLOBAL_CACHE_KEY,
            global_file_hash_map: global_file_inputs.global_file_hash_map,
            root_external_dependencies_hash: root_external_dependencies_hash.as_deref(),
            root_internal_dependencies_hash: root_internal_dependencies_hash.as_deref(),
            engines: global_file_inputs.engines,
            env: &self.root_turbo_json.global_env,
            resolved_env_vars: Some(global_file_inputs.global_hashable_env_vars),
            pass_through_env,
            env_mode,
            framework_inference: self.opts.run_opts.framework_inference,
            env_at_execution_start: &self.env_at_execution_start,
            global_configuration: self.opts.future_flags.global_configuration,
        };
        let global_hash = global_hash_inputs.calculate_global_hash();

        let global_env = {
            let mut env = self
                .env_at_execution_start
                .from_wildcards(global_hash_inputs.pass_through_env.unwrap_or_default())
                .map_err(Error::Env)?;
            if let Some(resolved_global) = &global_hash_inputs.resolved_env_vars {
                env.union(&resolved_global.all);
            }
            env
        };

        let run_tracker = RunTracker::new(
            self.start_at,
            self.opts.synthesize_command(),
            self.version,
            Vendor::get_user(),
            self.observability_handle.clone(),
        );

        drop(_setup_span);

        let mut visitor = Visitor::new(
            self.pkg_dep_graph.clone(),
            self.run_cache.clone(),
            run_tracker,
            &self.task_access,
            &self.opts.run_opts,
            package_inputs_hashes,
            &self.env_at_execution_start,
            &global_hash,
            self.color_config,
            self.processes.clone(),
            &self.repo_root,
            global_env,
            &self.root_turbo_json.global_env,
            ui_sender,
            is_watch,
            self.micro_frontend_configs.as_ref(),
        )
        .await;

        if self.opts.run_opts.dry_run.is_some() {
            visitor.dry_run();
        }

        debug!("running visitor");

        let errors = visitor
            .visit(self.engine.clone(), &self.run_telemetry)
            .await?;

        debug!("visitor completed, calculating exit code");

        let exit_code = errors
            .iter()
            .filter_map(|err| err.exit_code())
            .max()
            .unwrap_or(if errors.is_empty() { 0 } else { 1 });

        for err in &errors {
            turborepo_log::error(
                turborepo_log::Source::turbo(turborepo_log::Subsystem::Run),
                err.to_string(),
            )
            .emit();
        }

        self.cleanup_proxy(proxy_shutdown).await;

        // When a proxy is present, the signal handler only stops processes on OS
        // signal. For normal completion without user interruption, we need an
        // explicit stop here.
        //
        // In watch mode, persistent tasks run as fire-and-forget background
        // processes that outlive the visit() call. The watch coordinator
        // manages their lifecycle via RunStopper, so we must not kill them here.
        if !is_watch {
            self.processes.stop().await;
        }

        visitor
            .finish(
                exit_code,
                &self.filtered_pkgs,
                global_hash_inputs,
                &self.engine,
                &self.env_at_execution_start,
                &self.scm,
                self.opts.scope_opts.pkg_inference_root.as_deref(),
            )
            .await?;

        debug!("visitor.finish() completed, run cleanup done");

        Ok(exit_code)
    }

    #[instrument(skip_all)]
    pub async fn run(&self, ui_sender: Option<UISender>, is_watch: bool) -> Result<i32, Error> {
        let proxy_shutdown = self.start_proxy_if_needed().await?;
        self.setup_cache_shutdown_handler();

        // If there's no proxy, we need a fallback signal handler for the process
        // manager When a proxy is present, register_proxy_signal_handler
        // handles process manager shutdown
        if proxy_shutdown.is_none() {
            self.setup_process_manager_shutdown_handler();
        }

        if let Some(graph_opts) = &self.opts.run_opts.graph {
            let spawner = SharedChildSpawner;
            let graphviz_warning: turborepo_engine::GraphvizWarningFn =
                Box::new(emit_graphviz_warning);
            turborepo_engine::write_graph(
                graph_opts,
                &self.engine,
                self.opts.run_opts.single_package,
                &self.repo_root,
                &spawner,
                Some(graphviz_warning),
                Some(&|filename: &AbsoluteSystemPath| {
                    turborepo_log::info(
                        turborepo_log::Source::turbo(turborepo_log::Subsystem::Run),
                        format!("\n✓ Generated task graph in {filename}"),
                    )
                    .emit();
                }),
            )?;
            return Ok(0);
        }

        self.execute_visitor(ui_sender, is_watch, proxy_shutdown)
            .await
    }
}

#[derive(Debug, Clone)]
pub struct RunStopper {
    manager: ProcessManager,
}

impl RunStopper {
    pub async fn stop(&self) {
        self.manager.stop().await;
    }

    pub async fn stop_tasks(&self, task_ids: &[turborepo_task_id::TaskId<'static>]) {
        self.manager.stop_tasks(task_ids).await;
    }
}

// Graph visualizer helper types and functions

/// Implementation of `ChildSpawner` for graph visualizer using `SharedChild`.
struct SharedChildSpawner;

impl turborepo_engine::ChildSpawner for SharedChildSpawner {
    type Child = SharedChildWrapper;

    fn spawn(&self, command: Command) -> Result<Self::Child, io::Error> {
        crate::spawn_child(command).map(SharedChildWrapper)
    }
}

/// Wrapper around `Arc<SharedChild>` to implement `ChildProcess` trait.
struct SharedChildWrapper(Arc<SharedChild>);

impl turborepo_engine::ChildProcess for SharedChildWrapper {
    fn take_stdin(&self) -> Option<Box<dyn Write + Send>> {
        self.0
            .take_stdin()
            .map(|s| Box::new(s) as Box<dyn Write + Send>)
    }

    fn wait(&self) -> Result<(), io::Error> {
        self.0.wait().map(|_| ())
    }
}

impl turborepo_query_api::QueryRun for Run {
    fn version(&self) -> &'static str {
        self.version
    }

    fn repo_root(&self) -> &turbopath::AbsoluteSystemPath {
        &self.repo_root
    }

    fn pkg_dep_graph(&self) -> &turborepo_repository::package_graph::PackageGraph {
        &self.pkg_dep_graph
    }

    fn engine(
        &self,
    ) -> &turborepo_engine::Engine<turborepo_engine::Built, turborepo_types::TaskDefinition> {
        &self.engine
    }

    fn scm(&self) -> &turborepo_scm::SCM {
        &self.scm
    }

    fn root_turbo_json(&self) -> &turborepo_turbo_json::TurboJson {
        &self.root_turbo_json
    }

    fn calculate_affected_packages(
        &self,
        base: Option<String>,
        head: Option<String>,
    ) -> Result<
        std::collections::HashMap<
            turborepo_repository::package_graph::PackageName,
            turborepo_repository::change_mapper::PackageInclusionReason,
        >,
        turborepo_query_api::AffectedPackagesError,
    > {
        let mut opts = self.opts.as_ref().clone();
        opts.scope_opts.affected_range = Some((base, head));
        builder::RunBuilder::calculate_filtered_packages(
            &self.repo_root,
            &opts,
            &self.pkg_dep_graph,
            &self.scm,
            &self.root_turbo_json,
        )
        .map_err(|e| turborepo_query_api::AffectedPackagesError::Other(Box::new(e)))
    }

    fn changed_files(
        &self,
        base: Option<&str>,
        head: Option<&str>,
    ) -> Result<
        std::collections::HashSet<turbopath::AnchoredSystemPathBuf>,
        turborepo_query_api::AffectedPackagesError,
    > {
        match self
            .scm
            .changed_files(&self.repo_root, base, head, true, true, true)
            .map_err(|e| turborepo_query_api::AffectedPackagesError::Other(Box::new(e)))?
        {
            Ok(files) => Ok(files),
            Err(_invalid_range) => {
                // If git range is invalid, return empty set — the affected packages
                // computation will handle reporting all-packages-changed separately.
                Ok(std::collections::HashSet::new())
            }
        }
    }

    fn check_boundaries(
        &self,
        show_progress: bool,
    ) -> std::pin::Pin<
        Box<
            dyn std::future::Future<
                    Output = Result<
                        turborepo_boundaries::BoundariesResult,
                        turborepo_boundaries::Error,
                    >,
                > + Send
                + '_,
        >,
    > {
        Box::pin(std::future::ready(self.check_boundaries(show_progress)))
    }
}

fn emit_graphviz_warning() -> Result<(), io::Error> {
    turborepo_log::warn(
        turborepo_log::Source::turbo(turborepo_log::Subsystem::Run),
        "`turbo` uses Graphviz to generate an image of your graph, but Graphviz isn't installed \
         on this machine.\n\nNote: --graph output to .png, .jpg, .pdf, and .json is deprecated \
         and will be removed in version 3.0. Use .svg, .html, .mermaid, or .dot instead.\n\nIn \
         the meantime, you can use this string output with an online Dot graph viewer.",
    )
    .emit();
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::{future::pending, time::Duration};

    use turborepo_signals::ShutdownReason;

    use super::{CacheShutdownOutcome, Run};

    #[tokio::test]
    async fn cache_shutdown_close_does_not_arm_force_timeout() {
        let result = tokio::time::timeout(
            Duration::from_millis(30),
            Run::wait_for_cache_shutdown(
                Some(ShutdownReason::Close),
                Some(Duration::from_millis(10)),
                pending::<()>(),
                pending::<()>(),
            ),
        )
        .await;

        assert!(
            result.is_err(),
            "close-driven cache shutdown should not be cut off by the force timeout"
        );
    }

    #[tokio::test]
    async fn cache_shutdown_signal_uses_force_timeout() {
        let result = Run::wait_for_cache_shutdown(
            Some(ShutdownReason::Signal),
            Some(Duration::from_millis(10)),
            pending::<()>(),
            pending::<()>(),
        )
        .await;

        assert_eq!(result, CacheShutdownOutcome::ForcedShutdown);
    }
}
