fn print_issue_project_summary(path: &str, result: &IngestProjectResult) {
    let labels_str = if result.labels_created > 0 {
        format!(", {} new labels", result.labels_created)
    } else {
        String::new()
    };

    println!(
        "  {}: {} issues fetched{}",
        Theme::info().render(path),
        result.issues_upserted,
        labels_str
    );

    if result.issues_synced_discussions > 0 {
        println!(
            "    {} issues -> {} discussions, {} notes",
            result.issues_synced_discussions, result.discussions_fetched, result.notes_upserted
        );
    }

    if result.issues_skipped_discussion_sync > 0 {
        println!(
            "    {} unchanged issues (discussion sync skipped)",
            Theme::dim().render(&result.issues_skipped_discussion_sync.to_string())
        );
    }
}

fn print_mr_project_summary(path: &str, result: &IngestMrProjectResult) {
    let labels_str = if result.labels_created > 0 {
        format!(", {} new labels", result.labels_created)
    } else {
        String::new()
    };

    let assignees_str = if result.assignees_linked > 0 || result.reviewers_linked > 0 {
        format!(
            ", {} assignees, {} reviewers",
            result.assignees_linked, result.reviewers_linked
        )
    } else {
        String::new()
    };

    println!(
        "  {}: {} MRs fetched{}{}",
        Theme::info().render(path),
        result.mrs_upserted,
        labels_str,
        assignees_str
    );

    if result.mrs_synced_discussions > 0 {
        let diffnotes_str = if result.diffnotes_count > 0 {
            format!(" ({} diff notes)", result.diffnotes_count)
        } else {
            String::new()
        };
        println!(
            "    {} MRs -> {} discussions, {} notes{}",
            result.mrs_synced_discussions,
            result.discussions_fetched,
            result.notes_upserted,
            diffnotes_str
        );
    }

    if result.mrs_skipped_discussion_sync > 0 {
        println!(
            "    {} unchanged MRs (discussion sync skipped)",
            Theme::dim().render(&result.mrs_skipped_discussion_sync.to_string())
        );
    }
}

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

#[derive(Serialize)]
struct IngestJsonData {
    resource_type: String,
    projects_synced: usize,
    #[serde(skip_serializing_if = "Option::is_none")]
    issues: Option<IngestIssueStats>,
    #[serde(skip_serializing_if = "Option::is_none")]
    merge_requests: Option<IngestMrStats>,
    labels_created: usize,
    discussions_fetched: usize,
    notes_upserted: usize,
    resource_events_fetched: usize,
    resource_events_failed: usize,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    status_enrichment: Vec<StatusEnrichmentJson>,
    status_enrichment_errors: usize,
}

#[derive(Serialize)]
struct StatusEnrichmentJson {
    mode: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    reason: Option<String>,
    seen: usize,
    enriched: usize,
    cleared: usize,
    without_widget: usize,
    partial_errors: usize,
    #[serde(skip_serializing_if = "Option::is_none")]
    first_partial_error: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<String>,
}

#[derive(Serialize)]
struct IngestIssueStats {
    fetched: usize,
    upserted: usize,
    synced_discussions: usize,
    skipped_discussion_sync: usize,
}

#[derive(Serialize)]
struct IngestMrStats {
    fetched: usize,
    upserted: usize,
    synced_discussions: usize,
    skipped_discussion_sync: usize,
    assignees_linked: usize,
    reviewers_linked: usize,
    diffnotes_count: usize,
}

pub fn print_ingest_summary_json(result: &IngestResult, elapsed_ms: u64) {
    let (issues, merge_requests) = if result.resource_type == "issues" {
        (
            Some(IngestIssueStats {
                fetched: result.issues_fetched,
                upserted: result.issues_upserted,
                synced_discussions: result.issues_synced_discussions,
                skipped_discussion_sync: result.issues_skipped_discussion_sync,
            }),
            None,
        )
    } else {
        (
            None,
            Some(IngestMrStats {
                fetched: result.mrs_fetched,
                upserted: result.mrs_upserted,
                synced_discussions: result.mrs_synced_discussions,
                skipped_discussion_sync: result.mrs_skipped_discussion_sync,
                assignees_linked: result.assignees_linked,
                reviewers_linked: result.reviewers_linked,
                diffnotes_count: result.diffnotes_count,
            }),
        )
    };

    let status_enrichment: Vec<StatusEnrichmentJson> = result
        .status_enrichment_projects
        .iter()
        .map(|p| StatusEnrichmentJson {
            mode: p.mode.clone(),
            reason: p.reason.clone(),
            seen: p.seen,
            enriched: p.enriched,
            cleared: p.cleared,
            without_widget: p.without_widget,
            partial_errors: p.partial_errors,
            first_partial_error: p.first_partial_error.clone(),
            error: p.error.clone(),
        })
        .collect();

    let output = IngestJsonOutput {
        ok: true,
        data: IngestJsonData {
            resource_type: result.resource_type.clone(),
            projects_synced: result.projects_synced,
            issues,
            merge_requests,
            labels_created: result.labels_created,
            discussions_fetched: result.discussions_fetched,
            notes_upserted: result.notes_upserted,
            resource_events_fetched: result.resource_events_fetched,
            resource_events_failed: result.resource_events_failed,
            status_enrichment,
            status_enrichment_errors: result.status_enrichment_errors,
        },
        meta: RobotMeta::new(elapsed_ms),
    };

    match serde_json::to_string(&output) {
        Ok(json) => println!("{json}"),
        Err(e) => eprintln!("Error serializing to JSON: {e}"),
    }
}

pub fn print_ingest_summary(result: &IngestResult) {
    println!();

    if result.resource_type == "issues" {
        println!(
            "{}",
            Theme::success().render(&format!(
                "Total: {} issues, {} discussions, {} notes",
                result.issues_upserted, result.discussions_fetched, result.notes_upserted
            ))
        );

        if result.issues_skipped_discussion_sync > 0 {
            println!(
                "{}",
                Theme::dim().render(&format!(
                    "Skipped discussion sync for {} unchanged issues.",
                    result.issues_skipped_discussion_sync
                ))
            );
        }
    } else {
        let diffnotes_str = if result.diffnotes_count > 0 {
            format!(" ({} diff notes)", result.diffnotes_count)
        } else {
            String::new()
        };

        println!(
            "{}",
            Theme::success().render(&format!(
                "Total: {} MRs, {} discussions, {} notes{}",
                result.mrs_upserted,
                result.discussions_fetched,
                result.notes_upserted,
                diffnotes_str
            ))
        );

        if result.mrs_skipped_discussion_sync > 0 {
            println!(
                "{}",
                Theme::dim().render(&format!(
                    "Skipped discussion sync for {} unchanged MRs.",
                    result.mrs_skipped_discussion_sync
                ))
            );
        }
    }

    if result.resource_events_fetched > 0 || result.resource_events_failed > 0 {
        println!(
            "  Resource events: {} fetched{}",
            result.resource_events_fetched,
            if result.resource_events_failed > 0 {
                format!(", {} failed", result.resource_events_failed)
            } else {
                String::new()
            }
        );
    }
}

pub fn print_dry_run_preview(preview: &DryRunPreview) {
    println!(
        "{} {}",
        Theme::info().bold().render("Dry Run Preview"),
        Theme::warning().render("(no changes will be made)")
    );
    println!();

    let type_label = if preview.resource_type == "issues" {
        "issues"
    } else {
        "merge requests"
    };

    println!("  Resource type: {}", Theme::bold().render(type_label));
    println!(
        "  Sync mode: {}",
        if preview.sync_mode == "full" {
            Theme::warning().render("full (all data will be re-fetched)")
        } else {
            Theme::success().render("incremental (only changes since last sync)")
        }
    );
    println!("  Projects: {}", preview.projects.len());
    println!();

    println!("{}", Theme::info().bold().render("Projects to sync:"));
    for project in &preview.projects {
        let sync_status = if !project.has_cursor {
            Theme::warning().render("initial sync")
        } else {
            Theme::success().render("incremental")
        };

        println!(
            "  {} ({})",
            Theme::bold().render(&project.path),
            sync_status
        );
        println!("    Existing {}: {}", type_label, project.existing_count);

        if let Some(ref last_synced) = project.last_synced {
            println!("    Last synced: {}", last_synced);
        }
    }
}

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

pub fn print_dry_run_preview_json(preview: &DryRunPreview) {
    let output = DryRunJsonOutput {
        ok: true,
        dry_run: true,
        data: preview.clone(),
    };

    match serde_json::to_string(&output) {
        Ok(json) => println!("{json}"),
        Err(e) => eprintln!("Error serializing to JSON: {e}"),
    }
}
