fn handle_issues(
    config_override: Option<&str>,
    args: IssuesArgs,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();
    let config = Config::load(config_override)?;
    let project = config.effective_project(args.project.as_deref());
    let asc = args.asc && !args.no_asc;
    let has_due = args.has_due && !args.no_has_due;
    let open = args.open && !args.no_open;
    let order = if asc { "asc" } else { "desc" };

    if let Some(iid) = args.iid {
        let result = run_show_issue(&config, iid, project)?;
        if robot_mode {
            print_show_issue_json(&result, start.elapsed().as_millis() as u64);
        } else {
            print_show_issue(&result);
        }
    } else {
        let state_normalized = args.state.as_deref().map(str::to_lowercase);
        let filters = ListFilters {
            limit: args.limit,
            project,
            state: state_normalized.as_deref(),
            author: args.author.as_deref(),
            assignee: args.assignee.as_deref(),
            labels: args.label.as_deref(),
            milestone: args.milestone.as_deref(),
            since: args.since.as_deref(),
            due_before: args.due_before.as_deref(),
            has_due_date: has_due,
            statuses: &args.status,
            sort: &args.sort,
            order,
        };

        let result = run_list_issues(&config, filters)?;

        if open {
            open_issue_in_browser(&result);
        } else if robot_mode {
            print_list_issues_json(
                &result,
                start.elapsed().as_millis() as u64,
                args.fields.as_deref(),
            );
        } else {
            print_list_issues(&result);
        }
    }

    Ok(())
}

fn handle_mrs(
    config_override: Option<&str>,
    args: MrsArgs,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();
    let config = Config::load(config_override)?;
    let project = config.effective_project(args.project.as_deref());
    let asc = args.asc && !args.no_asc;
    let open = args.open && !args.no_open;
    let order = if asc { "asc" } else { "desc" };

    if let Some(iid) = args.iid {
        let result = run_show_mr(&config, iid, project)?;
        if robot_mode {
            print_show_mr_json(&result, start.elapsed().as_millis() as u64);
        } else {
            print_show_mr(&result);
        }
    } else {
        let state_normalized = args.state.as_deref().map(str::to_lowercase);
        let filters = MrListFilters {
            limit: args.limit,
            project,
            state: state_normalized.as_deref(),
            author: args.author.as_deref(),
            assignee: args.assignee.as_deref(),
            reviewer: args.reviewer.as_deref(),
            labels: args.label.as_deref(),
            since: args.since.as_deref(),
            draft: args.draft,
            no_draft: args.no_draft,
            target_branch: args.target.as_deref(),
            source_branch: args.source.as_deref(),
            sort: &args.sort,
            order,
        };

        let result = run_list_mrs(&config, filters)?;

        if open {
            open_mr_in_browser(&result);
        } else if robot_mode {
            print_list_mrs_json(
                &result,
                start.elapsed().as_millis() as u64,
                args.fields.as_deref(),
            );
        } else {
            print_list_mrs(&result);
        }
    }

    Ok(())
}

fn handle_notes(
    config_override: Option<&str>,
    args: NotesArgs,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();
    let config = Config::load(config_override)?;
    let db_path = get_db_path(config.storage.db_path.as_deref());
    let conn = create_connection(&db_path)?;

    let order = if args.asc { "asc" } else { "desc" };
    let filters = NoteListFilters {
        limit: args.limit,
        project: args.project,
        author: args.author,
        note_type: args.note_type,
        include_system: args.include_system,
        for_issue_iid: args.for_issue,
        for_mr_iid: args.for_mr,
        note_id: args.note_id,
        gitlab_note_id: args.gitlab_note_id,
        discussion_id: args.discussion_id,
        since: args.since,
        until: args.until,
        path: args.path,
        contains: args.contains,
        resolution: args.resolution,
        sort: args.sort,
        order: order.to_string(),
    };

    let result = query_notes(&conn, &filters, &config)?;

    if robot_mode {
        print_list_notes_json(
            &result,
            start.elapsed().as_millis() as u64,
            args.fields.as_deref(),
        );
    } else {
        print_list_notes(&result);
    }

    Ok(())
}

async fn handle_ingest(
    config_override: Option<&str>,
    args: IngestArgs,
    robot_mode: bool,
    quiet: bool,
    metrics: &MetricsLayer,
    rt_handle: &asupersync::runtime::RuntimeHandle,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();
    let dry_run = args.dry_run && !args.no_dry_run;
    let config = Config::load(config_override)?;
    let project = config.effective_project(args.project.as_deref());

    let force = args.force && !args.no_force;
    let full = args.full && !args.no_full;

    // Handle dry run mode - show preview without making any changes
    if dry_run {
        match args.entity.as_deref() {
            Some(resource_type) => {
                let preview = run_ingest_dry_run(&config, resource_type, project, full)?;
                if robot_mode {
                    print_dry_run_preview_json(&preview);
                } else {
                    print_dry_run_preview(&preview);
                }
            }
            None => {
                let issues_preview = run_ingest_dry_run(&config, "issues", project, full)?;
                let mrs_preview = run_ingest_dry_run(&config, "mrs", project, full)?;
                if robot_mode {
                    print_combined_dry_run_json(&issues_preview, &mrs_preview);
                } else {
                    print_dry_run_preview(&issues_preview);
                    println!();
                    print_dry_run_preview(&mrs_preview);
                }
            }
        }
        return Ok(());
    }

    let display = if robot_mode || quiet {
        IngestDisplay::silent()
    } else {
        IngestDisplay::interactive()
    };

    let entity_label = args.entity.as_deref().unwrap_or("all");
    let command = format!("ingest:{entity_label}");
    let db_path = get_db_path(config.storage.db_path.as_deref());
    let recorder_conn = create_connection(&db_path)?;
    let run_id = uuid::Uuid::new_v4().simple().to_string();
    let run_id_short = &run_id[..8];
    let recorder = SyncRunRecorder::start(&recorder_conn, &command, run_id_short)?;

    let signal = ShutdownSignal::new();
    install_ctrl_c_handler(rt_handle, signal.clone());

    let ingest_result: std::result::Result<(), Box<dyn std::error::Error>> = async {
        match args.entity.as_deref() {
            Some(resource_type) => {
                let result = run_ingest(
                    &config,
                    resource_type,
                    project,
                    force,
                    full,
                    false,
                    display,
                    None,
                    &signal,
                )
                .await?;

                if robot_mode {
                    print_ingest_summary_json(&result, start.elapsed().as_millis() as u64);
                } else {
                    print_ingest_summary(&result);
                }
            }
            None => {
                if !robot_mode && !quiet {
                    println!(
                        "{}",
                        Theme::info().render("Ingesting all content (issues + merge requests)...")
                    );
                    println!();
                }

                let issues_result = run_ingest(
                    &config, "issues", project, force, full, false, display, None, &signal,
                )
                .await?;

                let mrs_result = run_ingest(
                    &config, "mrs", project, force, full, false, display, None, &signal,
                )
                .await?;

                if robot_mode {
                    print_combined_ingest_json(
                        &issues_result,
                        &mrs_result,
                        start.elapsed().as_millis() as u64,
                    );
                } else {
                    print_ingest_summary(&issues_result);
                    print_ingest_summary(&mrs_result);
                }
            }
        }
        Ok(())
    }
    .await;

    match ingest_result {
        Ok(()) if signal.is_cancelled() => {
            let stages = metrics.extract_timings();
            let _ = release_all_locked_jobs(&recorder_conn);
            let _ = recorder.fail(
                &recorder_conn,
                "Interrupted by user (Ctrl+C)",
                Some(&stages),
            );
            if !robot_mode {
                eprintln!(
                    "{}",
                    Theme::warning().render("Interrupted by Ctrl+C. Partial data has been saved.")
                );
            }
            Ok(())
        }
        Ok(()) => {
            let stages = metrics.extract_timings();
            let total_items: usize = stages.iter().map(|s| s.items_processed).sum();
            let total_errors: usize = stages.iter().map(|s| s.errors).sum();
            let _ = recorder.succeed(&recorder_conn, &stages, total_items, total_errors);
            if !robot_mode && !quiet {
                eprintln!(
                    "{}",
                    Theme::dim().render("Hint: Run 'lore generate-docs' to update searchable documents, then 'lore embed' for vectors.")
                );
            }
            Ok(())
        }
        Err(e) => {
            let stages = metrics.extract_timings();
            let _ = release_all_locked_jobs(&recorder_conn);
            let _ = recorder.fail(&recorder_conn, &e.to_string(), Some(&stages));
            Err(e)
        }
    }
}

#[derive(Serialize)]
struct CombinedIngestOutput {
    ok: bool,
    data: CombinedIngestData,
    meta: RobotMeta,
}

#[derive(Serialize)]
struct CombinedIngestData {
    resource_type: String,
    issues: CombinedIngestEntityStats,
    merge_requests: CombinedIngestEntityStats,
}

#[derive(Serialize)]
struct CombinedIngestEntityStats {
    projects_synced: usize,
    fetched: usize,
    upserted: usize,
    labels_created: usize,
    discussions_fetched: usize,
    notes_upserted: usize,
}

fn print_combined_ingest_json(
    issues: &lore::cli::commands::ingest::IngestResult,
    mrs: &lore::cli::commands::ingest::IngestResult,
    elapsed_ms: u64,
) {
    let output = CombinedIngestOutput {
        ok: true,
        data: CombinedIngestData {
            resource_type: "all".to_string(),
            issues: CombinedIngestEntityStats {
                projects_synced: issues.projects_synced,
                fetched: issues.issues_fetched,
                upserted: issues.issues_upserted,
                labels_created: issues.labels_created,
                discussions_fetched: issues.discussions_fetched,
                notes_upserted: issues.notes_upserted,
            },
            merge_requests: CombinedIngestEntityStats {
                projects_synced: mrs.projects_synced,
                fetched: mrs.mrs_fetched,
                upserted: mrs.mrs_upserted,
                labels_created: mrs.labels_created,
                discussions_fetched: mrs.discussions_fetched,
                notes_upserted: mrs.notes_upserted,
            },
        },
        meta: RobotMeta::new(elapsed_ms),
    };

    println!(
        "{}",
        serde_json::to_string(&output).unwrap_or_else(|e| {
            format!(r#"{{"ok":false,"error":{{"code":"INTERNAL_ERROR","message":"JSON serialization failed: {e}"}}}}"#)
        })
    );
}

#[derive(Serialize)]
struct CombinedDryRunOutput {
    ok: bool,
    dry_run: bool,
    data: CombinedDryRunData,
}

#[derive(Serialize)]
struct CombinedDryRunData {
    issues: lore::cli::commands::DryRunPreview,
    merge_requests: lore::cli::commands::DryRunPreview,
}

fn print_combined_dry_run_json(
    issues: &lore::cli::commands::DryRunPreview,
    mrs: &lore::cli::commands::DryRunPreview,
) {
    let output = CombinedDryRunOutput {
        ok: true,
        dry_run: true,
        data: CombinedDryRunData {
            issues: issues.clone(),
            merge_requests: mrs.clone(),
        },
    };

    println!(
        "{}",
        serde_json::to_string(&output).unwrap_or_else(|e| {
            format!(r#"{{"ok":false,"error":{{"code":"INTERNAL_ERROR","message":"JSON serialization failed: {e}"}}}}"#)
        })
    );
}

async fn handle_count(
    config_override: Option<&str>,
    args: CountArgs,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();
    let config = Config::load(config_override)?;

    if args.entity == "events" {
        let counts = run_count_events(&config)?;
        if robot_mode {
            print_event_count_json(&counts, start.elapsed().as_millis() as u64);
        } else {
            print_event_count(&counts);
        }
        return Ok(());
    }

    let result = run_count(&config, &args.entity, args.for_entity.as_deref())?;
    if robot_mode {
        print_count_json(&result, start.elapsed().as_millis() as u64);
    } else {
        print_count(&result);
    }
    Ok(())
}

async fn handle_sync_status_cmd(
    config_override: Option<&str>,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();
    let config = Config::load(config_override)?;

    let result = run_sync_status(&config)?;
    if robot_mode {
        print_sync_status_json(&result, start.elapsed().as_millis() as u64);
    } else {
        print_sync_status(&result);
    }
    Ok(())
}

#[derive(Serialize)]
struct InitOutput {
    ok: bool,
    data: InitOutputData,
}

#[derive(Serialize)]
struct InitOutputData {
    config_path: String,
    data_dir: String,
    user: InitOutputUser,
    projects: Vec<InitOutputProject>,
    #[serde(skip_serializing_if = "Option::is_none")]
    default_project: Option<String>,
}

#[derive(Serialize)]
struct InitOutputUser {
    username: String,
    name: String,
}

#[derive(Serialize)]
struct InitOutputProject {
    path: String,
    name: String,
}

fn print_init_json(result: &InitResult) {
    let output = InitOutput {
        ok: true,
        data: InitOutputData {
            config_path: result.config_path.clone(),
            data_dir: result.data_dir.clone(),
            user: InitOutputUser {
                username: result.user.username.clone(),
                name: result.user.name.clone(),
            },
            projects: result
                .projects
                .iter()
                .map(|p| InitOutputProject {
                    path: p.path.clone(),
                    name: p.name.clone(),
                })
                .collect(),
            default_project: result.default_project.clone(),
        },
    };
    println!(
        "{}",
        serde_json::to_string(&output).unwrap_or_else(|e| {
            format!(r#"{{"ok":false,"error":{{"code":"INTERNAL_ERROR","message":"JSON serialization failed: {e}"}}}}"#)
        })
    );
}

// ── Refresh JSON types ──

#[derive(Serialize)]
struct RefreshOutput {
    ok: bool,
    data: RefreshOutputData,
}

#[derive(Serialize)]
struct RefreshOutputData {
    mode: &'static str,
    user: InitOutputUser,
    projects_registered: Vec<InitOutputProject>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    projects_failed: Vec<RefreshOutputFailure>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    orphans_found: Vec<String>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    orphans_deleted: Vec<String>,
}

#[derive(Serialize)]
struct RefreshOutputFailure {
    path: String,
    error: String,
}

fn print_refresh_json(result: &RefreshResult) {
    let output = RefreshOutput {
        ok: true,
        data: RefreshOutputData {
            mode: "refresh",
            user: InitOutputUser {
                username: result.user.username.clone(),
                name: result.user.name.clone(),
            },
            projects_registered: result
                .projects_registered
                .iter()
                .map(|p| InitOutputProject {
                    path: p.path.clone(),
                    name: p.name.clone(),
                })
                .collect(),
            projects_failed: result
                .projects_failed
                .iter()
                .map(|p| RefreshOutputFailure {
                    path: p.path.clone(),
                    error: p.error.clone(),
                })
                .collect(),
            orphans_found: result.orphans_found.clone(),
            orphans_deleted: result.orphans_deleted.clone(),
        },
    };
    println!(
        "{}",
        serde_json::to_string(&output).unwrap_or_else(|e| {
            format!(r#"{{"ok":false,"error":{{"code":"INTERNAL_ERROR","message":"JSON serialization failed: {e}"}}}}"#)
        })
    );
}

async fn handle_init_refresh(
    config_override: Option<&str>,
    non_interactive: bool,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut result = run_init_refresh(RefreshOptions {
        config_path: config_override.map(String::from),
        non_interactive,
    })
    .await?;

    // Handle orphan deletion prompt (interactive only)
    let mut orphans_deleted: Vec<String> = Vec::new();
    if !result.orphans_found.is_empty() && !robot_mode && !non_interactive {
        println!(
            "\n{}",
            Theme::warning().render(&format!(
                "Found {} orphan project{} in database (not in config):",
                result.orphans_found.len(),
                if result.orphans_found.len() == 1 {
                    ""
                } else {
                    "s"
                }
            ))
        );
        for orphan in &result.orphans_found {
            println!("    {}", Theme::muted().render(&format!("• {orphan}")));
        }
        println!();

        let confirm = Confirm::new()
            .with_prompt(format!(
                "Delete {} orphan project{} from database?",
                result.orphans_found.len(),
                if result.orphans_found.len() == 1 {
                    ""
                } else {
                    "s"
                }
            ))
            .default(false)
            .interact()?;

        if confirm {
            let deleted = delete_orphan_projects(config_override, &result.orphans_found)?;
            orphans_deleted = result.orphans_found.clone();
            println!(
                "{}",
                Theme::success().render(&format!("  Deleted {deleted} orphan project(s)"))
            );
        }
    }
    result.orphans_deleted = orphans_deleted;

    if robot_mode {
        print_refresh_json(&result);
        return Ok(());
    }

    // Human output
    println!(
        "\n{}",
        Theme::success().render(&format!(
            "\u{2713} Authenticated as @{} ({})",
            result.user.username, result.user.name
        ))
    );

    if !result.projects_registered.is_empty() {
        println!("\n  {}", Theme::bold().render("Projects"));
        for project in &result.projects_registered {
            println!(
                "    {} {:<40} registered",
                Theme::success().render("\u{2713}"),
                project.path
            );
        }
    }

    if !result.projects_failed.is_empty() {
        for failure in &result.projects_failed {
            println!(
                "    {} {:<40} {}",
                Theme::error().render("\u{2717}"),
                failure.path,
                failure.error
            );
        }
    }

    // Summary
    let registered = result.projects_registered.len();
    let failed = result.projects_failed.len();
    let orphans_kept = result.orphans_found.len() - result.orphans_deleted.len();

    let mut summary_parts: Vec<String> = Vec::new();
    summary_parts.push(format!(
        "{} project{} registered",
        registered,
        if registered == 1 { "" } else { "s" }
    ));
    if failed > 0 {
        summary_parts.push(format!("{failed} failed"));
    }
    if !result.orphans_deleted.is_empty() {
        summary_parts.push(format!(
            "{} orphan(s) deleted",
            result.orphans_deleted.len()
        ));
    }
    if orphans_kept > 0 {
        summary_parts.push(format!("{orphans_kept} orphan(s) kept"));
    }

    println!(
        "\n{}",
        Theme::info().render(&format!("  {}", summary_parts.join(", ")))
    );

    Ok(())
}

#[allow(clippy::too_many_arguments)]
async fn handle_init(
    config_override: Option<&str>,
    refresh: bool,
    force: bool,
    non_interactive: bool,
    robot_mode: bool,
    gitlab_url_flag: Option<String>,
    token_env_var_flag: Option<String>,
    projects_flag: Option<String>,
    default_project_flag: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
    // ── Handle --refresh mode ──
    if refresh {
        return handle_init_refresh(config_override, non_interactive, robot_mode).await;
    }

    if robot_mode {
        let missing: Vec<&str> = [
            gitlab_url_flag.is_none().then_some("--gitlab-url"),
            token_env_var_flag.is_none().then_some("--token-env-var"),
            projects_flag.is_none().then_some("--projects"),
        ]
        .into_iter()
        .flatten()
        .collect();

        if !missing.is_empty() {
            let output = RobotErrorWithSuggestion {
                error: RobotErrorSuggestionData {
                    code: "MISSING_FLAGS".to_string(),
                    message: format!("Robot mode requires flags: {}", missing.join(", ")),
                    suggestion: "lore --robot init --gitlab-url https://gitlab.com --token-env-var GITLAB_TOKEN --projects group/project".to_string(),
                    correction: None,
                    valid_values: None,
                },
            };
            eprintln!("{}", serde_json::to_string(&output)?);
            std::process::exit(2);
        }

        let project_paths: Vec<String> = projects_flag
            .expect("validated: checked for None at lines 714-721")
            .split(',')
            .map(|p| p.trim().to_string())
            .filter(|p| !p.is_empty())
            .collect();

        let result = run_init(
            InitInputs {
                gitlab_url: gitlab_url_flag
                    .expect("validated: checked for None at lines 714-721"),
                token_env_var: token_env_var_flag
                    .expect("validated: checked for None at lines 714-721"),
                project_paths,
                default_project: default_project_flag.clone(),
            },
            InitOptions {
                config_path: config_override.map(String::from),
                force: true,
                non_interactive: true,
            },
        )
        .await?;

        print_init_json(&result);
        return Ok(());
    }

    let config_path = get_config_path(config_override);
    let mut confirmed_overwrite = force;

    if config_path.exists() && !force {
        if non_interactive {
            eprintln!(
                "{}",
                Theme::error().render(&format!(
                    "Config already exists at {}",
                    config_path.display()
                ))
            );
            eprintln!(
                "{}",
                Theme::info().render("  • Use --refresh to register new projects from config")
            );
            eprintln!(
                "{}",
                Theme::info().render("  • Use --force to overwrite the config file")
            );
            std::process::exit(2);
        }

        println!(
            "{}",
            Theme::warning().render(&format!(
                "Config already exists at {}",
                config_path.display()
            ))
        );
        println!(
            "{}",
            Theme::info().render("  • Use --refresh to register new projects from config")
        );
        println!();

        let confirm = Confirm::new()
            .with_prompt("Overwrite existing config?")
            .default(false)
            .interact()?;

        if !confirm {
            println!("{}", Theme::warning().render("Cancelled."));
            std::process::exit(2);
        }
        confirmed_overwrite = true;
    }

    let gitlab_url: String = if let Some(url) = gitlab_url_flag {
        url
    } else {
        Input::new()
            .with_prompt("GitLab URL")
            .default("https://gitlab.com".to_string())
            .validate_with(|input: &String| -> Result<(), &str> {
                if url::Url::parse(input).is_ok() {
                    Ok(())
                } else {
                    Err("Please enter a valid URL")
                }
            })
            .interact_text()?
    };

    let token_env_var: String = if let Some(var) = token_env_var_flag {
        var
    } else {
        Input::new()
            .with_prompt("Token environment variable name")
            .default("GITLAB_TOKEN".to_string())
            .interact_text()?
    };

    let project_paths: Vec<String> = if let Some(projects) = projects_flag {
        projects
            .split(',')
            .map(|p| p.trim().to_string())
            .filter(|p| !p.is_empty())
            .collect()
    } else {
        let project_paths_input: String = Input::new()
            .with_prompt("Project paths (comma-separated, e.g., group/project)")
            .validate_with(|input: &String| -> Result<(), &str> {
                if input.trim().is_empty() {
                    Err("Please enter at least one project path")
                } else {
                    Ok(())
                }
            })
            .interact_text()?;

        project_paths_input
            .split(',')
            .map(|p| p.trim().to_string())
            .filter(|p| !p.is_empty())
            .collect()
    };

    // Resolve default project: CLI flag, interactive prompt, or None
    let default_project = if default_project_flag.is_some() {
        default_project_flag
    } else if project_paths.len() > 1 && !non_interactive {
        let set_default = Confirm::new()
            .with_prompt("Set a default project? (used when -p is omitted)")
            .default(true)
            .interact()?;

        if set_default {
            let selection = dialoguer::Select::new()
                .with_prompt("Default project")
                .items(&project_paths)
                .default(0)
                .interact()?;
            Some(project_paths[selection].clone())
        } else {
            None
        }
    } else {
        None
    };

    println!("{}", Theme::info().render("Validating configuration..."));

    let result = run_init(
        InitInputs {
            gitlab_url,
            token_env_var,
            project_paths,
            default_project,
        },
        InitOptions {
            config_path: config_override.map(String::from),
            force: confirmed_overwrite,
            non_interactive,
        },
    )
    .await?;

    println!(
        "{}",
        Theme::success().render(&format!(
            "\n\u{2713} Authenticated as @{} ({})",
            result.user.username, result.user.name
        ))
    );

    for project in &result.projects {
        println!(
            "{}",
            Theme::success().render(&format!("\u{2713} {} ({})", project.path, project.name))
        );
    }

    if let Some(ref dp) = result.default_project {
        println!(
            "{}",
            Theme::success().render(&format!("\u{2713} Default project: {dp}"))
        );
    }

    println!(
        "{}",
        Theme::success().render(&format!(
            "\n\u{2713} Config written to {}",
            result.config_path
        ))
    );
    println!(
        "{}",
        Theme::success().render(&format!(
            "\u{2713} Database initialized at {}",
            result.data_dir
        ))
    );
    println!(
        "{}",
        Theme::info().render("\nSetup complete! Run 'lore doctor' to verify.")
    );

    Ok(())
}

#[derive(Serialize)]
struct AuthTestOutput {
    ok: bool,
    data: AuthTestData,
    meta: RobotMeta,
}

#[derive(Serialize)]
struct AuthTestData {
    authenticated: bool,
    username: String,
    name: String,
    gitlab_url: String,
}

async fn handle_auth_test(
    config_override: Option<&str>,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();
    match run_auth_test(config_override).await {
        Ok(result) => {
            if robot_mode {
                let output = AuthTestOutput {
                    ok: true,
                    data: AuthTestData {
                        authenticated: true,
                        username: result.username.clone(),
                        name: result.name.clone(),
                        gitlab_url: result.base_url.clone(),
                    },
                    meta: RobotMeta::new(start.elapsed().as_millis() as u64),
                };
                println!("{}", serde_json::to_string(&output)?);
            } else {
                println!("Authenticated as @{} ({})", result.username, result.name);
                println!("GitLab: {}", result.base_url);
            }
            Ok(())
        }
        Err(e) => {
            if robot_mode {
                let output = RobotErrorOutput::from(&e);
                eprintln!(
                    "{}",
                    serde_json::to_string(&output).unwrap_or_else(|_| {
                        let msg = e.to_string().replace('\\', "\\\\").replace('"', "\\\"");
                        format!(
                            r#"{{"error":{{"code":"{}","message":"{}"}}}}"#,
                            e.code(),
                            msg
                        )
                    })
                );
            } else {
                eprintln!("{} {}", Theme::error().render("Error:"), e);
                if let Some(suggestion) = e.suggestion() {
                    eprintln!("{} {}", Theme::warning().render("Hint:"), suggestion);
                }
            }
            std::process::exit(e.exit_code());
        }
    }
}

#[derive(Serialize)]
struct DoctorOutput {
    ok: bool,
    data: DoctorData,
    meta: RobotMeta,
}

#[derive(Serialize)]
struct DoctorData {
    success: bool,
    checks: lore::cli::commands::DoctorChecks,
}

async fn handle_doctor(
    config_override: Option<&str>,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();
    let result = run_doctor(config_override).await;

    if robot_mode {
        let output = DoctorOutput {
            ok: true,
            data: DoctorData {
                success: result.success,
                checks: result.checks,
            },
            meta: RobotMeta::new(start.elapsed().as_millis() as u64),
        };
        println!("{}", serde_json::to_string(&output)?);
    } else {
        print_doctor_results(&result);
    }

    if !result.success {
        std::process::exit(1);
    }

    Ok(())
}

#[derive(Serialize)]
struct VersionOutput {
    ok: bool,
    data: VersionData,
    meta: RobotMeta,
}

#[derive(Serialize)]
struct VersionData {
    name: &'static str,
    version: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    git_hash: Option<String>,
}

fn handle_version(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();
    let version = env!("CARGO_PKG_VERSION").to_string();
    let git_hash = env!("GIT_HASH").to_string();
    if robot_mode {
        let output = VersionOutput {
            ok: true,
            data: VersionData {
                name: "lore",
                version,
                git_hash: if git_hash.is_empty() {
                    None
                } else {
                    Some(git_hash)
                },
            },
            meta: RobotMeta::new(start.elapsed().as_millis() as u64),
        };
        println!("{}", serde_json::to_string(&output)?);
    } else if git_hash.is_empty() {
        println!("lore version {}", version);
    } else {
        println!("lore version {} ({})", version, git_hash);
    }
    Ok(())
}

fn handle_completions(shell: &str) -> Result<(), Box<dyn std::error::Error>> {
    use clap::CommandFactory;
    use clap_complete::{Shell, generate};

    let shell = match shell {
        "bash" => Shell::Bash,
        "zsh" => Shell::Zsh,
        "fish" => Shell::Fish,
        "powershell" => Shell::PowerShell,
        other => {
            return Err(format!("Unsupported shell: {other}").into());
        }
    };

    let mut cmd = Cli::command();
    generate(shell, &mut cmd, "lore", &mut std::io::stdout());
    Ok(())
}

fn handle_backup(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
    if robot_mode {
        let output = RobotErrorWithSuggestion {
            error: RobotErrorSuggestionData {
                code: "NOT_IMPLEMENTED".to_string(),
                message: "The 'backup' command is not yet implemented.".to_string(),
                suggestion: "Use manual database backup: cp ~/.local/share/lore/lore.db ~/.local/share/lore/lore.db.bak".to_string(),
                correction: None,
                valid_values: None,
            },
        };
        eprintln!("{}", serde_json::to_string(&output)?);
    } else {
        eprintln!(
            "{} The 'backup' command is not yet implemented.",
            Theme::error().render("Error:")
        );
    }
    std::process::exit(1);
}

fn handle_reset(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
    if robot_mode {
        let output = RobotErrorWithSuggestion {
            error: RobotErrorSuggestionData {
                code: "NOT_IMPLEMENTED".to_string(),
                message: "The 'reset' command is not yet implemented.".to_string(),
                suggestion: "Manually delete the database: rm ~/.local/share/lore/lore.db"
                    .to_string(),
                correction: None,
                valid_values: None,
            },
        };
        eprintln!("{}", serde_json::to_string(&output)?);
    } else {
        eprintln!(
            "{} The 'reset' command is not yet implemented.",
            Theme::error().render("Error:")
        );
    }
    std::process::exit(1);
}

#[derive(Serialize)]
struct MigrateOutput {
    ok: bool,
    data: MigrateData,
    meta: RobotMeta,
}

#[derive(Serialize)]
struct MigrateData {
    before_version: i32,
    after_version: i32,
    migrated: bool,
}

#[derive(Serialize)]
struct RobotErrorWithSuggestion {
    error: RobotErrorSuggestionData,
}

#[derive(Serialize)]
struct RobotErrorSuggestionData {
    code: String,
    message: String,
    suggestion: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    correction: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    valid_values: Option<Vec<String>>,
}

async fn handle_migrate(
    config_override: Option<&str>,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();
    let config = Config::load(config_override)?;
    let db_path = get_db_path(config.storage.db_path.as_deref());

    if !db_path.exists() {
        if robot_mode {
            let output = RobotErrorWithSuggestion {
                error: RobotErrorSuggestionData {
                    code: "DB_ERROR".to_string(),
                    message: format!("Database not found at {}", db_path.display()),
                    suggestion: "Run 'lore init' first".to_string(),
                    correction: None,
                    valid_values: None,
                },
            };
            eprintln!("{}", serde_json::to_string(&output)?);
        } else {
            eprintln!(
                "{}",
                Theme::error().render(&format!("Database not found at {}", db_path.display()))
            );
            eprintln!(
                "{}",
                Theme::warning().render("Run 'lore init' first to create the database.")
            );
        }
        std::process::exit(10);
    }

    let conn = create_connection(&db_path)?;
    let before_version = get_schema_version(&conn);

    if !robot_mode {
        println!(
            "{}",
            Theme::info().render(&format!("Current schema version: {}", before_version))
        );
    }

    run_migrations(&conn)?;

    let after_version = get_schema_version(&conn);

    if robot_mode {
        let output = MigrateOutput {
            ok: true,
            data: MigrateData {
                before_version,
                after_version,
                migrated: after_version > before_version,
            },
            meta: RobotMeta::new(start.elapsed().as_millis() as u64),
        };
        println!("{}", serde_json::to_string(&output)?);
    } else if after_version > before_version {
        println!(
            "{}",
            Theme::success().render(&format!(
                "Migrations applied: {} -> {}",
                before_version, after_version
            ))
        );
    } else {
        println!(
            "{}",
            Theme::success().render("Database is already up to date.")
        );
    }

    Ok(())
}

async fn handle_stats(
    config_override: Option<&str>,
    args: StatsArgs,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();
    let dry_run = args.dry_run && !args.no_dry_run;
    let config = Config::load(config_override)?;
    let check = (args.check && !args.no_check) || args.repair;
    let result = run_stats(&config, check, args.repair, dry_run)?;
    if robot_mode {
        print_stats_json(&result, start.elapsed().as_millis() as u64);
    } else {
        print_stats(&result);
    }
    Ok(())
}

fn handle_file_history(
    config_override: Option<&str>,
    args: FileHistoryArgs,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();
    let config = Config::load(config_override)?;

    let project = config
        .effective_project(args.project.as_deref())
        .map(String::from);

    let normalized = normalize_repo_path(&args.path);

    // Resolve bare filenames before querying (same path resolution as trace/who)
    let db_path_tmp = get_db_path(config.storage.db_path.as_deref());
    let conn_tmp = create_connection(&db_path_tmp)?;
    let project_id_tmp = project
        .as_deref()
        .map(|p| resolve_project(&conn_tmp, p))
        .transpose()?;
    let pq = build_path_query(&conn_tmp, &normalized, project_id_tmp)?;
    let resolved_path = if pq.is_prefix {
        // Directory prefix — file-history is file-oriented, pass the raw path.
        // Don't use pq.value which contains LIKE-escaped metacharacters.
        normalized.trim_end_matches('/').to_string()
    } else {
        pq.value
    };

    let result = run_file_history(
        &config,
        &resolved_path,
        project.as_deref(),
        args.no_follow_renames,
        args.merged,
        args.discussions,
        args.limit,
    )?;

    if robot_mode {
        let elapsed_ms = start.elapsed().as_millis() as u64;
        print_file_history_json(&result, elapsed_ms)?;
    } else {
        print_file_history(&result);
    }
    Ok(())
}

fn handle_trace(
    config_override: Option<&str>,
    args: TraceArgs,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();
    let config = Config::load(config_override)?;

    let (raw_path, line_requested) = parse_trace_path(&args.path);
    let normalized = normalize_repo_path(&raw_path);

    if line_requested.is_some() && !robot_mode {
        eprintln!(
            "Note: Line-level tracing requires Tier 2 (git blame). Showing file-level results."
        );
    }

    let project = config
        .effective_project(args.project.as_deref())
        .map(String::from);

    let db_path = get_db_path(config.storage.db_path.as_deref());
    let conn = create_connection(&db_path)?;
    let project_id = project
        .as_deref()
        .map(|p| resolve_project(&conn, p))
        .transpose()?;

    // Resolve bare filenames (e.g. "operators.ts" -> "src/utils/operators.ts")
    let pq = build_path_query(&conn, &normalized, project_id)?;
    let path = if pq.is_prefix {
        // Directory prefix — trace is file-oriented, pass the raw path.
        // Don't use pq.value which contains LIKE-escaped metacharacters.
        normalized.trim_end_matches('/').to_string()
    } else {
        pq.value
    };

    let result = run_trace(
        &conn,
        project_id,
        &path,
        !args.no_follow_renames,
        args.discussions,
        args.limit,
    )?;

    if robot_mode {
        let elapsed_ms = start.elapsed().as_millis() as u64;
        print_trace_json(&result, elapsed_ms, line_requested)?;
    } else {
        print_trace(&result);
    }
    Ok(())
}

async fn handle_timeline(
    config_override: Option<&str>,
    args: TimelineArgs,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let config = Config::load(config_override)?;

    let params = TimelineParams {
        query: args.query,
        project: config
            .effective_project(args.project.as_deref())
            .map(String::from),
        since: args.since,
        depth: args.depth,
        no_mentions: args.no_mentions,
        limit: args.limit,
        max_seeds: args.max_seeds,
        max_entities: args.max_entities,
        max_evidence: args.max_evidence,
        robot_mode,
    };

    let result = run_timeline(&config, &params).await?;

    if robot_mode {
        print_timeline_json_with_meta(
            &result,
            result.total_filtered_events,
            params.depth,
            !params.no_mentions,
            args.fields.as_deref(),
        );
    } else {
        print_timeline(&result);
    }
    Ok(())
}

async fn handle_search(
    config_override: Option<&str>,
    args: SearchArgs,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let config = Config::load(config_override)?;
    let explain = args.explain && !args.no_explain;

    let fts_mode = match args.fts_mode.as_str() {
        "raw" => lore::search::FtsQueryMode::Raw,
        _ => lore::search::FtsQueryMode::Safe,
    };

    let cli_filters = SearchCliFilters {
        source_type: args.source_type,
        author: args.author,
        project: config
            .effective_project(args.project.as_deref())
            .map(String::from),
        labels: args.label,
        path: args.path,
        since: args.since,
        updated_since: args.updated_since,
        limit: args.limit,
    };

    let spinner = lore::cli::progress::stage_spinner_v2(
        lore::cli::render::Icons::search(),
        "Search",
        &format!("Searching ({})...", args.mode),
        robot_mode,
    );
    let start = std::time::Instant::now();
    let response = run_search(
        &config,
        &args.query,
        cli_filters,
        fts_mode,
        &args.mode,
        explain,
    )
    .await?;
    let elapsed_ms = start.elapsed().as_millis() as u64;
    spinner.finish_and_clear();

    if robot_mode {
        print_search_results_json(&response, elapsed_ms, args.fields.as_deref());
    } else {
        print_search_results(&response, explain);
    }
    Ok(())
}

async fn handle_generate_docs(
    config_override: Option<&str>,
    args: GenerateDocsArgs,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();
    let config = Config::load(config_override)?;

    let project = config.effective_project(args.project.as_deref());
    let result = run_generate_docs(&config, args.full, project, None)?;
    let elapsed = start.elapsed();
    if robot_mode {
        print_generate_docs_json(&result, elapsed.as_millis() as u64);
    } else {
        print_generate_docs(&result);
        if elapsed.as_secs() >= 1 {
            eprintln!(
                "{}",
                Theme::dim().render(&format!("  Done in {:.1}s", elapsed.as_secs_f64()))
            );
        }
        if result.regenerated > 0 {
            eprintln!(
                "{}",
                Theme::dim().render(
                    "Hint: Run 'lore embed' to update vector embeddings for changed documents."
                )
            );
        }
    }
    Ok(())
}

async fn handle_embed(
    config_override: Option<&str>,
    args: EmbedArgs,
    robot_mode: bool,
    rt_handle: &asupersync::runtime::RuntimeHandle,
) -> Result<(), Box<dyn std::error::Error>> {
    use std::sync::Arc;
    use std::sync::atomic::{AtomicBool, Ordering};

    let start = std::time::Instant::now();
    let config = Config::load(config_override)?;
    let full = args.full && !args.no_full;
    let retry_failed = args.retry_failed && !args.no_retry_failed;

    let signal = ShutdownSignal::new();
    install_ctrl_c_handler(rt_handle, signal.clone());

    let embed_bar = lore::cli::progress::nested_progress("Embedding", 0, robot_mode);
    let bar_clone = embed_bar.clone();
    let tick_started = Arc::new(AtomicBool::new(false));
    let tick_clone = Arc::clone(&tick_started);
    let progress_cb: Box<dyn Fn(usize, usize)> = Box::new(move |processed, total| {
        if total > 0 {
            if !tick_clone.swap(true, Ordering::Relaxed) {
                bar_clone.enable_steady_tick(std::time::Duration::from_millis(100));
            }
            bar_clone.set_length(total as u64);
            bar_clone.set_position(processed as u64);
        }
    });

    let result = run_embed(&config, full, retry_failed, Some(progress_cb), &signal).await?;
    embed_bar.finish_and_clear();

    let elapsed = start.elapsed();
    if robot_mode {
        print_embed_json(&result, elapsed.as_millis() as u64);
    } else {
        print_embed(&result);
        if elapsed.as_secs() >= 1 {
            eprintln!(
                "{}",
                Theme::dim().render(&format!("  Done in {:.1}s", elapsed.as_secs_f64()))
            );
        }
    }
    Ok(())
}

async fn handle_sync_cmd(
    config_override: Option<&str>,
    args: SyncArgs,
    robot_mode: bool,
    metrics: &MetricsLayer,
    rt_handle: &asupersync::runtime::RuntimeHandle,
) -> Result<(), Box<dyn std::error::Error>> {
    let dry_run = args.dry_run && !args.no_dry_run;

    // Dedup and sort IIDs
    let mut issue_iids = args.issue;
    let mut mr_iids = args.mr;
    issue_iids.sort_unstable();
    issue_iids.dedup();
    mr_iids.sort_unstable();
    mr_iids.dedup();

    let mut config = Config::load(config_override)?;
    if args.no_events {
        config.sync.fetch_resource_events = false;
    }
    if args.no_file_changes {
        config.sync.fetch_mr_file_changes = false;
    }
    if args.no_status {
        config.sync.fetch_work_item_status = false;
    }
    let options = SyncOptions {
        full: args.full && !args.no_full,
        force: args.force && !args.no_force,
        no_embed: args.no_embed,
        no_docs: args.no_docs,
        no_events: args.no_events,
        robot_mode,
        dry_run,
        issue_iids,
        mr_iids,
        project: args.project,
        preflight_only: args.preflight_only,
    };

    // Validation: preflight_only requires surgical mode
    if options.preflight_only && !options.is_surgical() {
        return Err("--preflight-only requires --issue or --mr".into());
    }

    // Validation: full + surgical are incompatible
    if options.full && options.is_surgical() {
        return Err("--full and --issue/--mr are incompatible".into());
    }

    // Validation: surgical mode requires a project (via -p or config defaultProject)
    if options.is_surgical()
        && config
            .effective_project(options.project.as_deref())
            .is_none()
    {
        return Err("--issue/--mr requires -p/--project (or set defaultProject in config)".into());
    }

    // Validation: hard cap on total surgical targets
    let total_targets = options.issue_iids.len() + options.mr_iids.len();
    if total_targets > SyncOptions::MAX_SURGICAL_TARGETS {
        return Err(format!(
            "Too many surgical targets ({total_targets}); maximum is {}",
            SyncOptions::MAX_SURGICAL_TARGETS
        )
        .into());
    }

    // Surgical + dry-run → treat as preflight-only
    let mut options = options;
    if dry_run && options.is_surgical() {
        options.preflight_only = true;
    }

    // Resolve effective project for surgical mode: when -p is not passed but
    // defaultProject is set in config, populate options.project so the surgical
    // orchestrator receives the resolved project path.
    if options.is_surgical() && options.project.is_none() {
        options.project = config.default_project.clone();
    }

    // For non-surgical dry run, skip recording and just show the preview
    if dry_run && !options.is_surgical() {
        let signal = ShutdownSignal::new();
        run_sync(&config, options, None, &signal).await?;
        return Ok(());
    }

    // Acquire file lock if --lock was passed (used by cron to skip overlapping runs)
    let _sync_lock = if args.lock {
        match lore::core::cron::acquire_sync_lock() {
            Ok(Some(guard)) => Some(guard),
            Ok(None) => {
                // Another sync is running — silently exit (expected for cron)
                tracing::debug!("--lock: another sync is running, skipping");
                return Ok(());
            }
            Err(e) => {
                tracing::warn!(error = %e, "--lock: failed to acquire file lock, skipping sync");
                return Ok(());
            }
        }
    } else {
        None
    };

    // In cron mode (--lock), ensure Ollama is running for embeddings
    if args.lock {
        let result = lore::core::ollama_mgmt::ensure_ollama(&config.embedding.base_url);
        if !result.installed {
            tracing::warn!(
                "Ollama is not installed — embeddings will be skipped. {}",
                result.install_hint.as_deref().unwrap_or("")
            );
        } else if result.started && result.running {
            tracing::info!("Started ollama serve (was not running)");
        } else if result.started {
            tracing::info!("Spawned ollama serve (cold start in progress, should be ready by embed stage)");
        } else if !result.running {
            tracing::warn!(
                "Failed to start Ollama: {}",
                result.error.as_deref().unwrap_or("unknown error")
            );
        }
    }

    // Surgical mode: run_sync_surgical manages its own recorder, signal, and recording.
    // Skip the normal recorder setup and let the dispatch handle everything.
    if options.is_surgical() {
        let signal = ShutdownSignal::new();
        install_ctrl_c_handler(rt_handle, signal.clone());

        let start = std::time::Instant::now();
        match run_sync(&config, options, None, &signal).await {
            Ok(result) => {
                let elapsed = start.elapsed();
                if robot_mode {
                    print_sync_json(&result, elapsed.as_millis() as u64, Some(metrics));
                } else {
                    print_sync(&result, elapsed, Some(metrics), args.timings);
                }
                return Ok(());
            }
            Err(e) => return Err(e.into()),
        }
    }

    let db_path = get_db_path(config.storage.db_path.as_deref());
    let recorder_conn = create_connection(&db_path)?;
    let run_id = uuid::Uuid::new_v4().simple().to_string();
    let run_id_short = &run_id[..8];
    let recorder = SyncRunRecorder::start(&recorder_conn, "sync", run_id_short)?;

    let signal = ShutdownSignal::new();
    install_ctrl_c_handler(rt_handle, signal.clone());

    let start = std::time::Instant::now();
    match run_sync(&config, options, Some(run_id_short), &signal).await {
        Ok(result) if signal.is_cancelled() => {
            let elapsed = start.elapsed();
            let stages = metrics.extract_timings();
            let released = release_all_locked_jobs(&recorder_conn).unwrap_or(0);
            let _ = recorder.fail(
                &recorder_conn,
                "Interrupted by user (Ctrl+C)",
                Some(&stages),
            );

            if robot_mode {
                print_sync_json(&result, elapsed.as_millis() as u64, Some(metrics));
            } else {
                eprintln!();
                eprintln!(
                    "{}",
                    Theme::warning().render("Interrupted by Ctrl+C. Partial results:")
                );
                print_sync(&result, elapsed, Some(metrics), args.timings);
                if released > 0 {
                    eprintln!(
                        "{}",
                        Theme::dim().render(&format!("Released {released} locked jobs"))
                    );
                }
            }
            Ok(())
        }
        Ok(result) => {
            let elapsed = start.elapsed();
            let stages = metrics.extract_timings();
            let total_items = result.issues_updated
                + result.mrs_updated
                + result.documents_regenerated
                + result.documents_embedded;
            let total_errors = result.resource_events_failed;
            let _ = recorder.succeed(&recorder_conn, &stages, total_items, total_errors);

            if robot_mode {
                print_sync_json(&result, elapsed.as_millis() as u64, Some(metrics));
            } else {
                print_sync(&result, elapsed, Some(metrics), args.timings);
            }
            Ok(())
        }
        Err(e) => {
            let stages = metrics.extract_timings();
            let _ = release_all_locked_jobs(&recorder_conn);
            let _ = recorder.fail(&recorder_conn, &e.to_string(), Some(&stages));
            Err(e.into())
        }
    }
}

fn handle_cron(
    config_override: Option<&str>,
    args: CronArgs,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();

    match args.action {
        CronAction::Install { interval } => {
            let result = run_cron_install(interval)?;
            let elapsed_ms = start.elapsed().as_millis() as u64;
            if robot_mode {
                print_cron_install_json(&result, elapsed_ms);
            } else {
                print_cron_install(&result);
            }
            // Warn if no stored token — cron runs in a minimal shell with no env vars
            if let Ok(config) = Config::load(config_override)
                && config
                    .gitlab
                    .token
                    .as_ref()
                    .is_none_or(|t| t.trim().is_empty())
            {
                if robot_mode {
                    eprintln!(
                        "{{\"warning\":\"No stored token found. Cron sync requires a stored token. Run: lore token set\"}}"
                    );
                } else {
                    eprintln!();
                    eprintln!(
                        "  {} No stored token found. Cron sync requires a stored token.",
                        lore::cli::render::Theme::warning()
                            .render(lore::cli::render::Icons::warning()),
                    );
                    eprintln!("  Run: lore token set");
                    eprintln!();
                }
            }
        }
        CronAction::Uninstall => {
            let result = run_cron_uninstall()?;
            let elapsed_ms = start.elapsed().as_millis() as u64;
            if robot_mode {
                print_cron_uninstall_json(&result, elapsed_ms);
            } else {
                print_cron_uninstall(&result);
            }
        }
        CronAction::Status => {
            let config = Config::load(config_override)?;
            let info = run_cron_status(&config)?;
            let elapsed_ms = start.elapsed().as_millis() as u64;
            if robot_mode {
                print_cron_status_json(&info, elapsed_ms);
            } else {
                print_cron_status(&info);
            }
        }
    }

    Ok(())
}

async fn handle_token(
    config_override: Option<&str>,
    args: TokenArgs,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();

    match args.action {
        TokenAction::Set { token } => {
            let result = run_token_set(config_override, token).await?;
            let elapsed_ms = start.elapsed().as_millis() as u64;
            if robot_mode {
                let output = serde_json::json!({
                    "ok": true,
                    "data": {
                        "action": "set",
                        "username": result.username,
                        "config_path": result.config_path,
                    },
                    "meta": { "elapsed_ms": elapsed_ms },
                });
                println!("{}", serde_json::to_string(&output)?);
            } else {
                println!(
                    "  {} Token stored and validated (authenticated as @{})",
                    lore::cli::render::Theme::success().render(lore::cli::render::Icons::success()),
                    result.username
                );
                println!(
                    "  {} {}",
                    lore::cli::render::Theme::dim().render("config:"),
                    result.config_path
                );
                println!();
            }
        }
        TokenAction::Show { unmask } => {
            let result = run_token_show(config_override, unmask)?;
            let elapsed_ms = start.elapsed().as_millis() as u64;
            if robot_mode {
                let output = serde_json::json!({
                    "ok": true,
                    "data": {
                        "token": result.token,
                        "source": result.source,
                    },
                    "meta": { "elapsed_ms": elapsed_ms },
                });
                println!("{}", serde_json::to_string(&output)?);
            } else {
                println!(
                    "  {} {}",
                    lore::cli::render::Theme::dim().render("token:"),
                    result.token
                );
                println!(
                    "  {} {}",
                    lore::cli::render::Theme::dim().render("source:"),
                    result.source
                );
                println!();
            }
        }
    }

    Ok(())
}

#[derive(Serialize)]
struct HealthOutput {
    ok: bool,
    data: HealthData,
    meta: RobotMeta,
}

#[derive(Serialize)]
struct HealthData {
    healthy: bool,
    config_found: bool,
    db_found: bool,
    schema_current: bool,
    schema_version: i32,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    actions: Vec<String>,
}

async fn handle_health(
    config_override: Option<&str>,
    robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();
    let config_path = get_config_path(config_override);
    let config_found = config_path.exists();

    let (db_found, schema_version, schema_current) = if config_found {
        match Config::load(config_override) {
            Ok(config) => {
                let db_path = get_db_path(config.storage.db_path.as_deref());
                if db_path.exists() {
                    match create_connection(&db_path) {
                        Ok(conn) => {
                            let version = get_schema_version(&conn);
                            (true, version, version >= LATEST_SCHEMA_VERSION)
                        }
                        Err(_) => (true, 0, false),
                    }
                } else {
                    (false, 0, false)
                }
            }
            Err(_) => (false, 0, false),
        }
    } else {
        (false, 0, false)
    };

    let healthy = config_found && db_found && schema_current;

    let mut actions = Vec::new();
    if !config_found {
        actions.push("lore init".to_string());
    }
    if !db_found && config_found {
        actions.push("lore sync".to_string());
    }
    if db_found && !schema_current {
        actions.push("lore migrate".to_string());
    }

    if robot_mode {
        let output = HealthOutput {
            ok: true,
            data: HealthData {
                healthy,
                config_found,
                db_found,
                schema_current,
                schema_version,
                actions,
            },
            meta: RobotMeta::new(start.elapsed().as_millis() as u64),
        };
        println!("{}", serde_json::to_string(&output)?);
    } else {
        let status = |ok: bool| {
            if ok {
                Theme::success().render("pass")
            } else {
                Theme::error().render("FAIL")
            }
        };
        println!(
            "Config:  {} ({})",
            status(config_found),
            config_path.display()
        );
        println!("DB:      {}", status(db_found));
        println!("Schema:  {} (v{})", status(schema_current), schema_version);
        println!();
        if healthy {
            println!("{}", Theme::success().bold().render("Healthy"));
        } else {
            println!(
                "{}",
                Theme::error()
                    .bold()
                    .render("Unhealthy - run 'lore doctor' for details")
            );
        }
    }

    if !healthy {
        std::process::exit(19);
    }

    Ok(())
}
