use clud::{
    args, auth, backend, backend_bootstrap, clud_settings, codex_auth, command, config,
    console_setup, console_title, cpu_banner, crash_report, ctrl_c_track, daemon, gc, graphics,
    grind, harness_picker, hook_health, job_orphan_reaper, large_file_guard, launch_log,
    launch_setup, log_event, loop_artifacts, loop_spec, optimize, orphan_reaper, provider_auth,
    runner, runtime_cache, settings_tui, soldr_activate, startup, symbols, test_runtime, tool_cli,
    tool_install, tools, trampoline, trash, ui, uv_run_hook_guard, verbose_log, wasm, webterm,
    worktrees,
};

use std::io::{self, IsTerminal, Read, Write};

fn main() {
    run(parse_args());
}

#[cfg(windows)]
fn parse_args() -> args::Args {
    // Clap builds the complete nested command tree while parsing argv. Keep
    // only that work off Windows' small default main-thread stack so adding a
    // maintenance command cannot make even `clud --help` overflow, while the
    // rest of clud remains on the process main thread for console/OLE APIs.
    std::thread::Builder::new()
        .name("clud-arg-parser".to_string())
        .stack_size(4 * 1024 * 1024)
        .spawn(args::Args::parse_with_passthrough)
        .expect("spawn clud argument parser")
        .join()
        .expect("clud argument parser panicked")
}

#[cfg(not(windows))]
fn parse_args() -> args::Args {
    args::Args::parse_with_passthrough()
}

fn run(mut args: args::Args) {
    args.normalize_explicit_run();
    if let Some(exit_code) = webterm::handle(&args) {
        std::process::exit(exit_code);
    }
    if let Some(exit_code) = webterm::handle(&args) {
        std::process::exit(exit_code);
    }
    // Fast tool path. Detect `clud tool ...` before
    // normal clud startup so hook/tool invocations do not connect to the
    // daemon, touch runtime-cache, start title keepers, or register as
    // foreground clud sessions.
    if let Some(args::Command::Tool { subcommand }) = &args.command {
        unsafe {
            std::env::set_var("UV_CACHE_DIR", tools::clud_uv_cache_dir());
            std::env::set_var(daemon::ENV_NO_DAEMON, "1");
        }
        std::process::exit(tool_cli::run(subcommand));
    }

    // Credential management is self-contained and must never resolve a backend,
    // start a daemon, or forward secrets to a harness.
    if let Some(args::Command::Auth { subcommand }) = &args.command {
        let interrupted = startup::install_ctrl_c_flag(args.verbose);
        std::process::exit(auth::run(subcommand.as_ref(), interrupted.as_ref()));
    }
    // Compatibility aliases remain through this major version. Keep their
    // provider implementation authoritative while giving callers the exact
    // action-first replacement.
    if let Some(args::Command::CodexAuth { subcommand }) = &args.command {
        eprintln!(
            "deprecated: use `{}`",
            auth::codex_alias_replacement(subcommand)
        );
        let interrupted = startup::install_ctrl_c_flag(args.verbose);
        std::process::exit(codex_auth::run(subcommand, interrupted.as_ref()));
    }
    if let Some(args::Command::DeepseekAuth { subcommand }) = &args.command {
        eprintln!(
            "deprecated: use `{}`",
            auth::deepseek_alias_replacement(subcommand)
        );
        std::process::exit(provider_auth::run(subcommand));
    }

    // Install the crash reporter first so a panic during the rest of startup
    // (arg parsing, runtime-cache hop, drop-target registration, ...) still
    // writes a JSON report under ~/.clud/state/crashes/. Idempotent; the
    // daemon and worker process entries re-call install_native() with their
    // own role to retag any future crash without reinstalling the hook.
    //
    // `install_native` covers SIGSEGV / SIGBUS / SIGILL / SIGFPE / SIGABRT on
    // Unix and structured exceptions on Windows in addition to Rust panics.
    // It explicitly does NOT attach a SIGINT / CTRL_C_EVENT handler — the
    // existing `ctrlc`-based path (`startup::install_ctrl_c_flag` below /
    // #372 forensic capture) remains the authoritative Ctrl-C handler.
    crash_report::install_native("foreground");

    // Issue #408 (Layer 3 of three-layer UV_CACHE_DIR enforcement): pin
    // every `uv` invocation spawned inside clud's process tree to
    // `~/.clud/cache/uv/`, so per-script venvs for bundled tools never
    // leak into the user's global `~/.cache/uv/`. The `clud tool run`
    // subcommand (Layer 1) re-affirms the same value; both layers read
    // from `tools::clud_uv_cache_dir()` so there is one source of truth.
    //
    // SAFETY: at this point we are still single-threaded (crash reporter
    // installs handlers but does not spawn threads). Setting env vars
    // before any other code runs is the standard cross-platform pattern
    // for this case.
    unsafe {
        std::env::set_var("UV_CACHE_DIR", tools::clud_uv_cache_dir());
    }

    verbose_log::init_launch_clock();

    // #333: the daemon and worker roles are exempt — on Windows the hop cannot
    // preserve the PID, and theirs is recorded by other processes.
    let subcommand_name = args.command.as_ref().and_then(args::Command::internal_name);
    if let Err(err) = runtime_cache::hop_to_runtime_cache_if_enabled(subcommand_name) {
        eprintln!("[clud] warning: runtime cache hop failed: {err}");
    }

    // Windows: rename ourselves so pip can always overwrite clud.exe.
    trampoline::unlock_exe();

    // Stamp the console title with `clud <cwd-name>` so the active
    // window is identifiable at a glance. Windows-only effective; a
    // no-op on POSIX (out of scope per the originating request).
    //
    // The one-shot stamp gets overwritten as soon as the backend (and
    // its tool subprocesses) emit OSC 0/2 sequences, so we also kick
    // off a background keeper that re-applies the title whenever it
    // drifts. PTY mode additionally strips the OSC sequences upstream
    // (see session.rs) so the keeper rarely fires and the title doesn't
    // visibly flicker. In subprocess mode (the default Claude path on
    // Windows) the child inherits stdio directly, so the keeper is the
    // only way to defend the title.
    console_title::set_for_current_cwd();
    console_title::keep_setting_in_background();

    if args.verbose {
        match verbose_log::enable_file_logging() {
            Ok(path) => {
                verbose_log::log(format_args!(
                    "[clud] verbose log: {}",
                    verbose_log::display_path(&path)
                ));
            }
            Err(err) => {
                verbose_log::log(format_args!("[clud] verbose log unavailable: {err}"));
            }
        }
        verbose_log::log(format_args!("[clud] pid {}", std::process::id()));
    }

    // Issue #233: standalone graphics smoke test. It must emit only the
    // Sixel payload plus status line, without backend or setup side effects.
    if args.demo_gfx_sixel {
        let terminal_cols = terminal_size::terminal_size().map(|(width, _height)| width.0);
        match graphics::render_demo_sixel_bytes(terminal_cols) {
            Ok(bytes) => {
                let mut out = io::stdout().lock();
                if let Err(err) = out.write_all(&bytes).and_then(|_| out.flush()) {
                    eprintln!("error: failed to write Sixel demo: {err}");
                    std::process::exit(1);
                }
                std::process::exit(0);
            }
            Err(err) => {
                eprintln!("error: failed to render Sixel demo: {err}");
                std::process::exit(1);
            }
        }
    }

    // Issue #110: `clud gc <subcommand>` is a self-contained
    // maintenance path that never launches a backend. Dispatch before
    // backend resolution and before any session registry / dnd work
    // so a registry-less host can still run `clud gc reconcile`.
    if let Some(args::Command::Gc { subcommand }) = &args.command {
        std::process::exit(gc::run(&args, subcommand.clone()));
    }

    // Issue #457: settings inspection/editing is self-contained. Dispatch
    // before backend resolution so `clud config show` never starts an agent.
    if let Some(args::Command::Config { subcommand }) = &args.command {
        std::process::exit(config::run(&args, subcommand.clone()));
    }

    // Issue #183: `clud ui` opens the local dashboard. Self-contained;
    // never launches a backend.
    if let Some(args::Command::Ui { json, no_open }) = &args.command {
        std::process::exit(ui::run(*json, *no_open));
    }

    // Issue #182: `clud trash` is self-contained maintenance. Dispatch
    // before backend resolution so quarantining a locked artifact never
    // launches an agent process.
    if let Some(args::Command::Trash {
        cross_volume,
        paths,
    }) = &args.command
    {
        std::process::exit(trash::run(&args, paths, *cross_volume));
    }

    // Issue #469: `clud log --cmd "..."` posts one telemetry event to
    // the always-on daemon's HTTP server. Discovers the daemon via
    // `$CLUD_DAEMON_HTTP_SERVER`. Self-contained; never launches an
    // agent backend.
    if let Some(args::Command::Log {
        cmd,
        fail_on_no_server,
    }) = &args.command
    {
        std::process::exit(log_event::run(cmd, *fail_on_no_server));
    }

    // #374 (PR 3): `clud symbols` inspects / verifies crash-report
    // symbolication against the running binary. Self-contained; never
    // launches a backend.
    if let Some(args::Command::Symbols { subcommand }) = &args.command {
        std::process::exit(symbols::run(&args, subcommand.clone()));
    }

    // #407: `clud test` records/reports per-bucket test runtimes. Self-
    // contained; never launches a backend. `run` returns the wrapped command's
    // exit code unchanged so prefixing it onto an existing invocation is safe.
    if let Some(args::Command::Test { subcommand }) = &args.command {
        let code = match subcommand {
            args::TestSubcommand::Run {
                bucket,
                target,
                command,
            } => test_runtime::cli::run(bucket, target.clone(), command),
            args::TestSubcommand::Stats { bucket, json } => {
                test_runtime::cli::stats(bucket.as_deref(), *json)
            }
        };
        std::process::exit(code);
    }

    // `clud settings` is an interactive global-settings TUI, self-contained;
    // never launches a backend.
    if let Some(args::Command::Settings { list }) = &args.command {
        std::process::exit(settings_tui::run(*list));
    }

    // `clud optimize` is machine/repo setup and never launches a backend.
    if let Some(args::Command::Optimize {
        target,
        global,
        repo,
        install_soldr,
        use_soldr_shims,
        soldr_version,
    }) = &args.command
    {
        std::process::exit(optimize::run(
            &args,
            *target,
            *global,
            *repo,
            *install_soldr,
            *use_soldr_shims,
            soldr_version,
        ));
    }

    // Issue #83: `--clean-worktrees` is a self-contained maintenance path.
    // It never launches a backend, so handle it before backend resolution.
    if args.clean_worktrees {
        let stale_after = match worktrees::parse_duration(&args.stale_after) {
            Ok(d) => d,
            Err(e) => {
                eprintln!("error: invalid --stale-after value: {e}");
                std::process::exit(2);
            }
        };
        let opts = worktrees::CleanOptions {
            stale_after,
            dry_run: args.dry_run,
            yes: args.yes,
            force: args.force,
        };
        std::process::exit(worktrees::run(&opts));
    }

    let auto_selected_harness = if harness_picker::should_select(
        &args,
        io::stdin().is_terminal(),
        io::stderr().is_terminal(),
    ) {
        let installed = harness_picker::discover_installed_with(|candidate| {
            backend_bootstrap::locate_installed_backend(candidate).is_some()
        });
        let saved = clud_settings::load_last_launcher_harness()
            .map_err(|error| {
                if args.verbose {
                    eprintln!("[clud] note: could not read last harness choice: {error}");
                }
            })
            .ok()
            .flatten();
        let selected = match harness_picker::selection_flow(&installed, saved) {
            harness_picker::SelectionFlow::NoneInstalled => None,
            harness_picker::SelectionFlow::Immediate(backend) => Some(backend),
            harness_picker::SelectionFlow::Prompt(default) => {
                let stderr = io::stderr();
                let mut out = stderr.lock();
                match harness_picker::prompt(&mut out, installed, default) {
                    Ok(harness_picker::PickerOutcome::Selected(backend)) => Some(backend),
                    Ok(harness_picker::PickerOutcome::Cancelled) => {
                        eprintln!("[clud] harness selection cancelled");
                        std::process::exit(130);
                    }
                    Err(error) => {
                        eprintln!("[clud] harness selection failed: {error}");
                        std::process::exit(1);
                    }
                }
            }
        };
        if let Some(selected) = selected {
            if let Err(error) = clud_settings::save_last_launcher_harness(selected) {
                eprintln!("[clud] note: could not remember harness choice: {error}");
            }
        }
        selected
    } else {
        None
    };

    // Routing is resolved from a read-only snapshot after the separate
    // launcher-history write above. No provider preference is mutated merely
    // because the user chose an installed harness from the launcher.
    let launch_preferences = match clud_settings::load_launch_preferences_read_only() {
        Ok(preferences) => preferences,
        Err(error) => {
            eprintln!("[clud] warning: failed to load launch preferences: {error}; using defaults");
            clud_settings::LaunchPreferencesSnapshot::default()
        }
    };
    let global_launch_preferences = launch_preferences.global;
    let cli_provider = args
        .explicit_model_provider()
        .or_else(|| auto_selected_harness.map(backend::Backend::as_model_provider));
    let cli_harness = args
        .harness
        .or_else(|| auto_selected_harness.map(backend::HarnessSelection::for_backend));
    let model_inferred_provider = args
        .model
        .as_deref()
        .and_then(clud::provider_catalog::infer_provider);
    let direct_provider = cli_provider
        .or(model_inferred_provider)
        .or(global_launch_preferences.model_provider)
        .unwrap_or(backend::ModelProvider::Claude);
    let explicit_provider_intent = cli_provider.is_some() || model_inferred_provider.is_some();
    let provider_profile = (args.routing_mode() == backend::RoutingMode::Direct)
        .then(|| launch_preferences.profile(direct_provider))
        .flatten();
    let profile_harness = (explicit_provider_intent && cli_harness.is_none())
        .then(|| provider_profile.and_then(|profile| profile.harness))
        .flatten();
    let mut launch_target = match backend::resolve_routed_launch_target(
        args.routing_mode(),
        cli_provider.or(model_inferred_provider),
        cli_harness,
        global_launch_preferences.model_provider,
        profile_harness.or(global_launch_preferences.harness),
    ) {
        Ok(target) => target,
        Err(error) => {
            eprintln!("{error}");
            std::process::exit(2);
        }
    };
    if profile_harness.is_some() {
        launch_target.harness_source = backend::PreferenceSource::ProviderSetting;
    }
    if let Err(error) = backend::validate_provider_options(launch_target, args.model.as_deref()) {
        eprintln!("{error}");
        std::process::exit(2);
    }
    if launch_target.effective_harness == backend::Backend::DeepSeek {
        if let Some(option) = args.unsupported_deepseek_harness_option() {
            eprintln!(
                "unsupported option for DeepSeek Harness: {option}; pass native dsh options after --"
            );
            std::process::exit(2);
        }
    }
    args.resolved_model_selection = match clud::provider_catalog::resolve_for_launch(
        launch_target.model_provider,
        args.model.as_deref(),
        args.effort.as_deref(),
        args.context_window.as_deref(),
        provider_profile.map(clud_settings::ProviderProfile::selection_defaults),
        launch_target.routing_mode == backend::RoutingMode::Direct,
    ) {
        Ok(selection) => selection,
        Err(error) => {
            eprintln!("{error}");
            std::process::exit(2);
        }
    };
    if launch_target.routing_mode == backend::RoutingMode::Unified {
        if let Some(selection) = args
            .resolved_model_selection
            .as_ref()
            .filter(|selection| selection.provider != backend::ModelProvider::Claude)
        {
            let has_discovery_route = selection
                .model
                .as_deref()
                .and_then(clud::provider_catalog::model_by_cli_id)
                .and_then(|model| model.discovery_id)
                .is_some();
            if !has_discovery_route {
                eprintln!(
                    "unified routing requires a registered Codex or DeepSeek model; '{}' has no gateway discovery route",
                    selection
                        .wire_model
                        .as_deref()
                        .or(selection.model.as_deref())
                        .unwrap_or("unknown")
                );
                std::process::exit(2);
            }
        }
    }

    if args.no_fix_hooks {
        if args.dry_run {
            println!("[clud] dry-run: would disable automatic hook-health repairs globally");
        } else if let Err(error) = clud_settings::save_auto_fix_hooks_enabled(false) {
            eprintln!("[clud] error: failed to persist --no-fix-hooks: {error}");
            std::process::exit(1);
        } else {
            eprintln!(
                "[clud] disabled automatic hook-health repairs globally; run `clud --fix-hooks` to re-enable"
            );
        }
    }
    let auto_fix_hooks = if args.no_fix_hooks {
        false
    } else {
        match clud_settings::load_auto_fix_hooks_enabled() {
            Ok(enabled) => enabled,
            Err(error) => {
                eprintln!(
                    "[clud] warning: failed to load hook-health settings: {error}; using default"
                );
                true
            }
        }
    };

    // Issue #112: explicit hook-parity remediation path. This flag resets the
    // sticky opt-out, applies deterministic repairs, and asks the selected
    // effective harness to migrate hook definitions when semantic translation
    // is needed.
    if args.fix_hooks {
        if !args.dry_run {
            if let Err(error) = clud_settings::save_auto_fix_hooks_enabled(true) {
                eprintln!("[clud] error: failed to persist --fix-hooks: {error}");
                std::process::exit(1);
            }
        }
        std::process::exit(hook_health::run_fix_hooks(&args, launch_target));
    }

    // Pipe mode: if stdin is not a terminal, read it as the prompt.
    if args.prompt.is_none()
        && args.message.is_none()
        && args.command.is_none()
        && !console_setup::atty_is_terminal()
    {
        let mut input = String::new();
        if io::stdin().read_to_string(&mut input).is_ok() && !input.trim().is_empty() {
            args.prompt = Some(input.trim().to_string());
        }
    }

    if let Some(args::Command::Wasm { module, invoke }) = &args.command {
        if args.dry_run {
            let json = serde_json::json!({
                "mode": "wasm",
                "module": module,
                "invoke": invoke,
            });
            println!("{}", serde_json::to_string_pretty(&json).unwrap());
            std::process::exit(0);
        }

        match wasm::run_file(module, invoke) {
            Ok(code) => std::process::exit(code),
            Err(error) => {
                eprintln!("error: {error}");
                std::process::exit(1);
            }
        }
    }

    if let Some(args::Command::Loop {
        repeat,
        done,
        no_done,
        ..
    }) = &args.command
    {
        if let Some(msg) =
            command::repeat_implies_no_done_warning(repeat.as_deref(), *no_done, done.as_deref())
        {
            eprintln!("{}", msg);
        }
    }

    // `clud grind` resolves the current repo's issues page from the `origin`
    // remote (GitHub `<repo>/issues`, GitLab `<repo>/-/issues`), prints a green
    // notice, then seeds a `/loop` contract that iterates issue-by-issue with
    // DONE/BLOCKED markers. An explicit URL argument is passed through verbatim.
    if let Some(args::Command::Grind { url }) = args.command.clone() {
        let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
        match grind::resolve_grind_target(&cwd, url.as_deref()) {
            Ok(issues_url) => {
                let color = std::io::IsTerminal::is_terminal(&io::stderr());
                eprintln!("{}", grind::grind_notice(&issues_url, color));
                args.command = Some(args::Command::Grind {
                    url: Some(issues_url),
                });
            }
            Err(error) => {
                eprintln!("[clud] error: {error}");
                std::process::exit(2);
            }
        }
    }

    // Provider credentials must exist before foreground or daemon-backed work
    // is accepted. DeepSeek Harness owns its own provider credentials, so only
    // clud-managed Claude/Codex routes use this preflight.
    // Anthropic-compat-provider (DeepSeek today) work is accepted. Dry-runs
    // intentionally remain vault-free -- `launch_preflight_target` returns
    // `None` for every dry run regardless of provider.
    if launch_target.effective_harness != backend::Backend::DeepSeek {
        if let Some(descriptor) =
            provider_auth::launch_preflight_target(launch_target.model_provider, args.dry_run)
        {
            let interactive = provider_auth::launch_is_interactive(
                &args,
                io::stdin().is_terminal(),
                io::stderr().is_terminal(),
            );
            if let Err(error) = provider_auth::preflight_native(descriptor, interactive) {
                eprintln!("{}: {}", descriptor.settings_id, error.describe(descriptor));
                std::process::exit(2);
            }
        }
    }

    let interrupted = startup::install_ctrl_c_flag(args.verbose);
    if let Some(exit_code) = daemon::handle_special_command(&args, interrupted.as_ref()) {
        flush_ctrl_c_exit_event(ctrl_c_track::InvocationKind::Attach, exit_code);
        std::process::exit(exit_code);
    }

    // zackees/clud#343: backend launches from repos with `.clud/settings.json`
    // and `rust.use_soldr = true` route cargo / rustc / rustfmt /
    // clippy-driver / rustdoc through soldr by prepending soldr's shim
    // dir to PATH in-process. Run after self-contained utility commands
    // have exited (`clud log`, `clud gc`, `clud config`, etc.) so those fast
    // paths don't block on toolchain probing, but before daemon/backend
    // startup so every launched agent subprocess inherits the shim PATH.
    // `--dry-run` intentionally skips this because it never launches the
    // backend process whose toolchain PATH we need to modify.
    if !args.dry_run {
        soldr_activate::activate_soldr_shims_if_requested();
    }

    // Bundled Python tools are embedded in this binary via BUNDLED_TOOLS.
    // Refresh managed copies during normal foreground startup so an
    // upgraded clud binary replaces stale `~/.clud/tools/...` commands even
    // when an older daemon is already running. `clud tool run` keeps its own
    // inline self-heal path for hook invocations; dry-run remains no-write.
    if !args.dry_run {
        tool_install::ensure_installed();
        clud::block_bad_cmd_rollout::run_startup_checks(auto_fix_hooks);
    }

    if hook_health::should_check_launch(&args, launch_target) {
        if auto_fix_hooks && !args.dry_run {
            if let Err(error) = hook_health::apply_default_repairs() {
                eprintln!("[clud] warning: failed to auto-repair hook health: {error}");
            }
        }
        if args.verbose {
            verbose_log::log("[clud] hooks: checking launch parity");
        }
        hook_health::emit_launch_warnings();
    }

    // Large-file guard runs only on actual backend launches (bare `clud`,
    // `clud --claude`, `clud --codex`, or piped/prompted variants). Skip
    // for every subcommand path: `clud tool run`, `clud loop`, `clud gc`,
    // `clud attach/kill/list/logs`, etc. — those are utility invocations
    // (including compatibility hook shims such as `clud tool run ...`)
    // where the warning would be noise, not signal. The subcommands that
    // already short-circuit above via `std::process::exit` never reached
    // this code anyway; this gate also covers `clud loop` and any future
    // subcommand that falls through.
    if args.command.is_none() && !args.clean_worktrees && !args.fix_hooks {
        let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
        let root = loop_spec::git_root_from(&cwd);
        if args.verbose {
            verbose_log::log("[clud] large-file guard: scanning project");
        }
        large_file_guard::run(&root);
    }

    // Issue #135: always-on clud daemon. One background process per user
    // hosts the GC registry (redb owner + worker thread) and is the
    // execution target for `--detach` / `--detachable` / repeat jobs /
    // `--experimental-daemon-centralized`. Foreground interactive
    // launches still use the direct runner by default (PR #152 reverted
    // the attach-pump default). Skip on `--no-daemon` /
    // `CLUD_NO_DAEMON=1` and on `--dry-run` so unit tests that copy
    // the binary into a tempdir don't leave the daemon's `.old` exe
    // locked when tempdir cleanup runs. Never blocks a launch on
    // spawn failure.
    let mut foreground_client_lease = None;
    if !args.no_daemon && !args.dry_run {
        if args.verbose {
            verbose_log::log("[clud] daemon: ensure running");
        }
        match daemon::default_state_dir() {
            Ok(state_dir) => {
                if let Err(e) = daemon::ensure_daemon(&state_dir) {
                    eprintln!("[clud] note: daemon unavailable: {}", e);
                    if args.verbose {
                        verbose_log::log(format_args!("[clud] daemon: unavailable: {e}"));
                    }
                } else {
                    if !daemon::experimental_enabled(&args) {
                        match daemon::acquire_foreground_client_lease(&state_dir) {
                            Ok(lease) => foreground_client_lease = Some(lease),
                            Err(error) => {
                                eprintln!("[clud] note: foreground lease unavailable: {error}");
                            }
                        }
                    }
                    // Issue #183: record one row in the `repo_visits` table
                    // per (repo_root, current launch). Errors are non-fatal:
                    // failing to record a visit must never block a launch.
                    record_repo_visit_best_effort(&state_dir, args.verbose);
                }
            }
            Err(e) => {
                eprintln!("[clud] note: cannot resolve state dir: {}", e);
            }
        }
    } else if args.verbose {
        verbose_log::log("[clud] daemon: skipped");
    }

    // #569: the persistent daemon deliberately starts before this foreground
    // process joins the Windows tracking job, so it can outlive the CLI.
    let job_orphan_reaper = job_orphan_reaper::ForegroundJobTracker::install();

    // After foreground startup has refreshed bundled tools, fire the
    // hook-config scanner against the cwd. Warns on bare
    // `uv run` in Pre/PostToolUse hooks of Python+Rust polyglot
    // repos — the failure mode that turns every hook fire into a
    // multi-minute Rust rebuild on maturin-backed projects (see the
    // tool's docstring for the setuptools-co-existing-with-Cargo
    // variant). Same gate as the large-file guard so the warning
    // only fires for backend launches, not subcommand utility calls
    // (`clud tool run`, `clud loop`, etc.).
    if args.command.is_none() && !args.clean_worktrees && !args.fix_hooks {
        let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
        let root = loop_spec::git_root_from(&cwd);
        if args.verbose {
            verbose_log::log("[clud] uv-run hook guard: scanning agent hooks");
        }
        uv_run_hook_guard::run(&root);
    }

    let backend = launch_target.effective_harness;
    let backend_path = {
        let mut bootstrap_host = backend_bootstrap::ProductionBackendBootstrapHost;
        let interactive = io::stdin().is_terminal() && io::stderr().is_terminal();
        let stdin = io::stdin();
        let stderr = io::stderr();
        let mut input = stdin.lock();
        let mut err = stderr.lock();
        match backend_bootstrap::resolve_backend_path(
            backend,
            args.dry_run,
            interactive,
            &mut input,
            &mut err,
            &mut bootstrap_host,
        ) {
            Ok(path) => path,
            Err(error) => {
                let _ = writeln!(err, "{error}");
                std::process::exit(error.exit_code());
            }
        }
    };
    if launch_target.routing_mode == backend::RoutingMode::Unified && !args.dry_run {
        if let Err(error) =
            backend_bootstrap::require_unified_claude_version(std::path::Path::new(&backend_path))
        {
            eprintln!("{error}");
            std::process::exit(2);
        }
    }

    // Issue #242: mutable harness setup is scoped per launch until the user
    // opts into a global selection. Dry-runs read provider/harness preferences
    // for faithful metadata but skip mutable setup. Interactive launches with
    // an explicit provider or harness can opt into global setup through the
    // inline selector and are prompted again when an explicit choice differs
    // from its stored default.
    let setup_interactive = io::stdin().is_terminal() && io::stderr().is_terminal();
    let configured_scope = if args.dry_run {
        None
    } else {
        match clud_settings::load_launch_setup_scope(backend) {
            Ok(scope) => scope,
            Err(error) => {
                if args.verbose {
                    eprintln!("[clud] note: could not read clud settings: {error}");
                }
                None
            }
        }
    };
    let mut persist_prompted_global_selection = false;
    let setup_scope = if let Some(scope) = launch_setup::scope_for_launch_selection(
        &args,
        setup_interactive,
        configured_scope,
        global_launch_preferences.model_provider,
        launch_target.model_provider,
        global_launch_preferences.harness,
        launch_target.requested_harness,
    ) {
        scope
    } else {
        let mut err = io::stderr().lock();
        match launch_setup::prompt_scope(&mut err) {
            Ok(scope) => {
                persist_prompted_global_selection =
                    launch_setup::should_persist_prompted_default_backend(&args, scope);
                scope
            }
            Err(error) if error.kind() == io::ErrorKind::Interrupted => {
                eprintln!("[clud] launch setup cancelled");
                std::process::exit(130);
            }
            Err(error) => {
                eprintln!(
                    "[clud] note: could not read launch setup scope ({error}); using session-only"
                );
                launch_setup::LaunchSetupScope::SessionOnly
            }
        }
    };
    if persist_prompted_global_selection {
        let explicit_provider = args
            .explicit_model_provider()
            .map(|_| launch_target.model_provider);
        if let Err(error) = clud_settings::save_global_launch_preferences(
            explicit_provider,
            args.harness,
            setup_scope,
        ) {
            eprintln!("[clud] note: could not save global setup preference: {error}");
        }
    }
    if matches!(setup_scope, launch_setup::LaunchSetupScope::Global) {
        let mut err = io::stderr().lock();
        if let Err(error) = launch_setup::run_setup(setup_scope, backend, args.verbose, &mut err) {
            eprintln!("[clud] note: global setup failed: {error}");
        }
    }
    if args.verbose {
        verbose_log::log(format_args!("[clud] setup scope: {}", setup_scope.as_str()));
    }

    if matches!(backend, backend::Backend::Codex) {
        match clud_settings::load_or_init_codex_config_overrides(!args.dry_run) {
            Ok(overrides) => {
                args.codex_config_overrides = overrides;
            }
            Err(error) => {
                eprintln!(
                    "[clud] warning: failed to load Codex settings: {error}; using default Codex config overrides"
                );
                args.codex_config_overrides = clud_settings::default_codex_config_overrides();
            }
        }
    }

    let plan = command::build_launch_plan_for_target(&args, launch_target, &backend_path);
    if args.verbose {
        verbose_log::log(format_args!(
            "[clud] plan: backend={} mode={} iterations={} stream_json={}",
            backend.executable_name(),
            plan.launch_mode.as_str(),
            plan.iterations,
            plan.stream_json_progress
        ));
    }

    if args.dry_run {
        let json = serde_json::json!({
            "command": plan.command,
            "iterations": plan.iterations,
            "backend": backend.executable_name(),
            "routing_mode": launch_target.routing_mode.as_str(),
            "model_provider": launch_target.model_provider.as_str(),
            "requested_harness": launch_target.requested_harness.as_str(),
            "effective_harness": launch_target.effective_harness.executable_name(),
            "provider_source": launch_target.provider_source.as_str(),
            "harness_source": launch_target.harness_source.as_str(),
            "launch_mode": plan.launch_mode.as_str(),
            "graphics": {
                "mode": plan.graphics.mode.to_string(),
                "image": plan.graphics.image_path.as_ref().map(|p| p.to_string_lossy().to_string()),
            },
            "repeat_interval_secs": plan.repeat_schedule.as_ref().map(|s| s.interval_secs),
            // The resolved Codex selection, expanded from whatever short form
            // was typed, so a dry run shows what will actually be billed.
            "codex_model": plan.codex_model,
            "model_selection": plan.model_selection,
            "transcript": args.transcript.as_ref().map(|p| p.to_string_lossy().to_string()),
            "loop_markers": plan.loop_markers.as_ref().map(|m| serde_json::json!({
                "done_path": m.done_path,
                "blocked_path": m.blocked_path,
            })),
        });
        println!("{}", serde_json::to_string_pretty(&json).unwrap());
        std::process::exit(0);
    }

    if let Some(notice) =
        backend::saved_harness_override_notice(launch_target, io::stderr().is_terminal(), false)
    {
        eprintln!("{notice}");
    }

    if let Some(notice) = command::plan_mode_suppression_notice(
        &args,
        launch_target,
        io::stderr().is_terminal(),
        false,
    ) {
        eprintln!("{notice}");
    }

    // Issue #79 / #65 / #66: register `clud` as the IDropTarget for
    // the console window so dropped files reach the backend. Held for
    // the lifetime of the launch; dropped on graceful exit so the
    // refresh worker thread joins and `RevokeDragDrop` runs. POSIX
    // skips this — terminals there already deliver drops as stdin
    // bytes that the #63 normalizer handles. `--no-dnd` opts out.
    //
    // PTY mode wires the registration *inside* `run_plan_pty` so the
    // injector can write into the live PTY via a channel. Subprocess
    // mode registers up-front because the `subprocess_console_injector`
    // operates on the shared console input buffer, no per-iteration
    // state required.
    let _dnd_subprocess_guard = if startup::should_register_drop_target(&args)
        && plan.launch_mode == backend::LaunchMode::Subprocess
    {
        if args.verbose {
            verbose_log::log("[clud] dnd: registering subprocess drop target");
        }
        startup::try_register_console_drop_target_subprocess()
    } else {
        if args.verbose {
            verbose_log::log("[clud] dnd: subprocess drop target skipped");
        }
        None
    };

    // Issue #73 / #138: enforce the live-session cap. Opens the redb file
    // inside a cross-process advisory lock, performs gc / cap-check /
    // register-self, and **closes redb before returning**. The returned
    // guard holds nothing but a "we registered" flag; on Drop it briefly
    // re-acquires the lock to remove our row. Holding redb for the
    // lifetime of `main` would race with concurrent `clud` launches and
    // print spurious `Database already open` warnings (issue #138).
    if args.verbose {
        verbose_log::log("[clud] session registry: enforcing cap");
    }
    let _session_guard = startup::enforce_session_cap();

    // Issues #545/#546: register conventional discovery roots with the
    // long-lived daemon. This is one best-effort IPC exchange; the daemon
    // deduplicates roots across foreground clients and owns all watching.
    if args.verbose {
        verbose_log::log("[clud] gc watch roots: registering with daemon");
    }
    let roots = gc::watch_roots_for_current_repo();
    if let Ok(state_dir) = daemon::default_state_dir() {
        let _ = daemon::try_register_gc_watch(&state_dir, &roots);
    }

    // Clear stale DONE/BLOCKED markers from a prior run so that loops don't
    // short-circuit on iteration 1. See loop_spec for semantics.
    if let Some(ref markers) = plan.loop_markers {
        if args.verbose {
            verbose_log::log("[clud] loop markers: clearing stale DONE/BLOCKED files");
        }
        loop_spec::clear_markers_at(&loop_spec::MarkerPaths {
            done: std::path::PathBuf::from(&markers.done_path),
            blocked: std::path::PathBuf::from(&markers.blocked_path),
        });
    }

    // Issue #96: durable `.clud/loop/` artifacts (info.json, log.txt,
    // motivation.md, working copy of LOOP.md / task file, .gitignore
    // auto-injection). Only active when the user actually ran
    // `clud loop`; other commands skip the bookkeeping entirely.
    let mut loop_session: Option<loop_artifacts::LoopSession> =
        if let Some(args::Command::Loop { task, .. }) = &args.command {
            if args.verbose {
                verbose_log::log("[clud] loop artifacts: starting session");
            }
            let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
            let git_root = loop_spec::git_root_from(&cwd);
            let _ = loop_spec::ensure_loop_dir(&git_root);
            loop_artifacts::ensure_loop_in_gitignore(&git_root);
            if let Some(t) = task {
                let spec = loop_spec::classify(t);
                let _ = loop_artifacts::materialize_working_copy(&git_root, &spec);
            }
            Some(loop_artifacts::LoopSession::start(
                &git_root,
                plan.iterations,
            ))
        } else {
            None
        };

    let centralized = daemon::experimental_enabled(&args);
    if args.verbose {
        verbose_log::log(if centralized {
            "[clud] launch: centralized daemon"
        } else {
            "[clud] launch: direct runner"
        });
    }
    let launch_log = if let Ok(state_dir) = daemon::default_state_dir() {
        let source = if centralized { "centralized" } else { "direct" };
        match launch_log::start_launch(&state_dir, &plan, source) {
            Ok(handle) => Some(handle),
            Err(err) => {
                eprintln!("[clud] warning: failed to record launch start: {err}");
                None
            }
        }
    } else {
        None
    };
    // Issue #466: build the CPU-burn banner cfg from CLI flags + settings.
    // Suppressed in non-interactive modes (`--dry-run`, `--detach`,
    // `--detachable`, `--repeat`), by `--no-cpu-banner`, and by the
    // `[foreground.cpu_banner] enabled = false` settings toggle. Builds an
    // inert cfg in any of those cases so `BannerWatcher::spawn` is a no-op.
    let cpu_banner_cfg = build_cpu_banner_cfg(&args, &plan);

    let exit_code = if centralized {
        daemon::run_centralized_session(&args, &plan, interrupted.as_ref())
    } else {
        match plan.launch_mode {
            backend::LaunchMode::Subprocess => runner::run_plan_subprocess(
                &plan,
                job_orphan_reaper.as_ref(),
                args.verbose,
                interrupted.as_ref(),
                loop_session.as_mut(),
                cpu_banner_cfg,
            ),
            backend::LaunchMode::Pty => runner::run_plan_pty(
                &plan,
                job_orphan_reaper.as_ref(),
                args.verbose,
                interrupted.as_ref(),
                startup::should_register_drop_target(&args),
                loop_session.as_mut(),
                cpu_banner_cfg,
            ),
        }
    };
    if let Some(handle) = &launch_log {
        handle.finish(exit_code);
    }
    if let Some(session) = loop_session.as_mut() {
        let (summary, err) = runner::summarize_loop_outcome(exit_code);
        session.on_loop_end(summary, err);
    }
    drop(_session_guard);
    drop(_dnd_subprocess_guard);
    // Issue #340: detect env-tagged orphans we are about to leave behind and
    // (unless --keep-orphans) reap them. Skip for detached / detachable
    // sessions — those descendants are intentionally outliving us and are
    // owned by the daemon now.
    // #594: the timeout victims on the Windows lanes finish their payload — the
    // backend's full report is already on stdout — and then miss the budget on
    // the way out, so the open question is which exit stage holds the process
    // open. Each stage below is `O(host processes)` or a daemon round trip, and
    // none of them is currently attributable from a timed-out run.
    //
    // Opt-in only: `verbose_log::log` writes to stderr, and an unconditional
    // line there would break every test that asserts clean stderr — the exact
    // failure mode #594 already documents. Default runs emit nothing.
    let exit_timing = args.verbose || std::env::var_os("CLUD_EXIT_TIMING").is_some();
    let mut exit_stages: Vec<(&'static str, u128)> = Vec::new();

    // Breadcrumbs are emitted as each stage starts and finishes, not batched
    // into the summary at the end of this block. The summary only prints on a
    // run that survives every stage -- and the case #594 needs attributed is
    // exactly the one that does not, because the harness kills the process at
    // the timeout. A `begin` with no matching `done` in the captured partial
    // stderr names the stage that held the process open; the summary alone
    // would say nothing at all about it.
    //
    // Two sinks, deliberately. stderr stays behind the opt-in above, because
    // an unconditional line there would break every test asserting clean
    // stderr. `CLUD_EXIT_TIMING_FILE` is the sink the integration harness
    // uses: it never touches stderr, so the harness can enable attribution on
    // *every* launch without perturbing a single assertion, and still recover
    // the trace from a process it had to kill.
    fn exit_stage_trace(enabled: bool, line: &str) {
        if enabled {
            verbose_log::log(format_args!("[clud] {line}"));
        }
        if let Some(path) = std::env::var_os("CLUD_EXIT_TIMING_FILE") {
            use std::io::Write as _;
            if let Ok(mut file) = std::fs::OpenOptions::new()
                .create(true)
                .append(true)
                .open(path)
            {
                let _ = writeln!(file, "{line}");
                let _ = file.flush();
            }
        }
    }
    fn exit_stage_begin(enabled: bool, name: &str) {
        exit_stage_trace(enabled, &format!("exit-stage begin {name}"));
    }
    fn exit_stage_done(
        enabled: bool,
        stages: &mut Vec<(&'static str, u128)>,
        name: &'static str,
        started: std::time::Instant,
    ) {
        let ms = started.elapsed().as_millis();
        stages.push((name, ms));
        exit_stage_trace(enabled, &format!("exit-stage done {name}={ms}ms"));
    }
    if !args.detach && !args.detachable {
        // #673 Phase 2d: exits the job tracker gave up on at runtime get one
        // last replan against a fresh process table. Runs before the
        // originator scan because it can reach descendants whose environment
        // was rebuilt and therefore carry no `CLUD:` tag for that scan to
        // find. Skipped with the rest of the exit cleanup on the detached
        // paths, where those descendants are outliving us on purpose.
        if let Some(tracker) = job_orphan_reaper.as_ref() {
            if !args.keep_orphans {
                exit_stage_begin(exit_timing, "sweep_abandoned_at_exit");
                let started = std::time::Instant::now();
                let swept = tracker.sweep_abandoned_at_exit();
                exit_stage_done(
                    exit_timing,
                    &mut exit_stages,
                    "sweep_abandoned_at_exit",
                    started,
                );
                if args.verbose && swept > 0 {
                    verbose_log::log(format_args!(
                        "[clud] reaper: re-planned {swept} abandoned tool-shell exit(s)"
                    ));
                }
            }
            // #673 Phase 5: reaping is destructive and was silent, which is
            // how #651 could be closed while the same symptom kept growing.
            // Suppressed entirely when nothing was tracked.
            exit_stage_begin(exit_timing, "finish_and_report");
            let started = std::time::Instant::now();
            let report_lines = tracker.finish_and_report(args.verbose);
            exit_stage_done(exit_timing, &mut exit_stages, "finish_and_report", started);
            for line in report_lines {
                if args.quiet_orphans {
                    verbose_log::log(format_args!("{line}"));
                } else {
                    eprintln!("{line}");
                }
            }
        }
        let opts = orphan_reaper::ReapOpts {
            keep: args.keep_orphans,
            quiet: args.quiet_orphans,
            explain: args.explain_orphans,
        };
        exit_stage_begin(exit_timing, "scan_and_report");
        let started = std::time::Instant::now();
        let outcome = orphan_reaper::scan_and_report(std::process::id(), &opts);
        exit_stage_done(exit_timing, &mut exit_stages, "scan_and_report", started);
        if args.verbose && outcome.found > 0 {
            verbose_log::log(format_args!(
                "[clud] orphan reaper: found={} reaped={}",
                outcome.found, outcome.reaped
            ));
        }

        // Have the daemon do a broader sweep on our behalf: any CLUD-tagged
        // process whose originator is gone (e.g., a sibling clud was
        // SIGKILL'd and never ran its own exit hook) gets reaped on the
        // daemon's background thread. Fire-and-forget with a tight
        // timeout; failure is silently absorbed — the daemon's periodic
        // heartbeat sweep will catch anything we miss, and the next
        // `clud slay` does the synchronous version.
        if !args.keep_orphans {
            if let Ok(state_dir) = daemon::default_state_dir() {
                exit_stage_begin(exit_timing, "request_orphan_reap");
                let started = std::time::Instant::now();
                let _ = daemon::try_request_orphan_reap(&state_dir);
                exit_stage_done(
                    exit_timing,
                    &mut exit_stages,
                    "request_orphan_reap",
                    started,
                );
            }
        }
    }

    // Dropping the tracker explicitly, rather than letting it fall off the end
    // of `main`, is what makes its `Drop` attributable. #594 names the
    // completion-port listener join as a suspect precisely because it can wait
    // on a queue that a churny CI host is still feeding -- and an unattributed
    // stall inside an implicit drop is invisible to every stage above. Line 786
    // is the last read of `job_orphan_reaper`, so this only makes the existing
    // drop point explicit; it does not move work.
    exit_stage_begin(exit_timing, "tracker_drop");
    let started = std::time::Instant::now();
    // `ForegroundJobTracker`'s `Drop` lives in `#[cfg(windows)] mod imp`, so on
    // every other platform this type has no destructor and clippy's
    // `drop_non_drop` correctly reports the call as a no-op. Suppressed rather
    // than obeyed: the join being timed is the Windows completion-port
    // listener, which is the only platform #594 fires on, and keeping the call
    // unconditional keeps the stage list identical across platforms.
    #[allow(clippy::drop_non_drop)]
    drop(job_orphan_reaper);
    exit_stage_done(exit_timing, &mut exit_stages, "tracker_drop", started);
    if exit_timing && !exit_stages.is_empty() {
        let detail = exit_stages
            .iter()
            .map(|(name, ms)| format!("{name}={ms}ms"))
            .collect::<Vec<_>>()
            .join(" ");
        let total: u128 = exit_stages.iter().map(|(_, ms)| *ms).sum();
        verbose_log::log(format_args!("[clud] exit timing: total={total}ms {detail}"));
    }
    if args.verbose {
        verbose_log::log(format_args!("[clud] exit: code {exit_code}"));
    }
    let kind = if centralized {
        ctrl_c_track::InvocationKind::Centralized
    } else {
        ctrl_c_track::InvocationKind::Direct
    };
    flush_ctrl_c_exit_event(kind, exit_code);
    if let Some(lease) = foreground_client_lease {
        lease.release();
    }
    std::process::exit(exit_code);
}

#[path = "main_helpers.rs"]
mod main_helpers;
use main_helpers::{build_cpu_banner_cfg, flush_ctrl_c_exit_event, record_repo_visit_best_effort};
