//! ccboard - Unified Claude Code Management Dashboard

mod cli;

use anyhow::{Context, Result};
use ccboard_core::DataStore;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use std::sync::Arc;

#[derive(Parser)]
#[command(
    name = "ccboard",
    version,
    about = "Unified Claude Code Management Dashboard",
    long_about = "A comprehensive TUI and web dashboard for managing Claude Code data.\n\
                  \n\
                  Visualizes sessions, statistics, configuration, hooks, agents, costs, and history\n\
                  from ~/.claude directories with real-time updates and file editing capabilities.\n\
                  \n\
                  Features:\n\
                    • 7 interactive tabs (Dashboard, Sessions, Config, Hooks, Agents, Costs, History)\n\
                    • File editing with $EDITOR integration (press 'e')\n\
                    • MCP server management and visualization\n\
                    • Real-time cost tracking and analytics\n\
                    • Session search and exploration\n\
                  \n\
                  Examples:\n\
                    ccboard                          # Run TUI (default)\n\
                    ccboard web                      # Run web (API + frontend if built)\n\
                    ccboard web --port 8080          # Custom port\n\
                    ccboard both                     # Run both TUI and web server\n\
                    ccboard stats                    # Print stats summary\n\
                    ccboard search \"query\"           # Search sessions\n\
                    ccboard recent 10                # Show 10 most recent sessions\n\
                    \n\
                  Web Frontend Workflow:\n\
                    # Option 1: Production (single command)\n\
                    trunk build --release            # Compile frontend once\n\
                    ccboard web                      # Serves API + static frontend\n\
                    \n\
                    # Option 2: Development (hot reload)\n\
                    ccboard web --port 8080          # Terminal 1: API server\n\
                    trunk serve --port 3333          # Terminal 2: Frontend dev server\n\
                  \n\
                  Environment Variables:\n\
                    CCBOARD_CLAUDE_HOME              # Override Claude home directory\n\
                    CCBOARD_NON_INTERACTIVE          # Disable interactive prompts (CI/CD)\n\
                    CCBOARD_FORMAT                   # Force output format: json|table\n\
                    CCBOARD_NO_COLOR                 # Disable ANSI colors (log-friendly)"
)]
struct Cli {
    #[command(subcommand)]
    mode: Option<Mode>,

    /// Path to Claude home directory (default: ~/.claude)
    #[arg(long, env = "CCBOARD_CLAUDE_HOME")]
    claude_home: Option<PathBuf>,

    /// Focus on specific project directory
    #[arg(long)]
    project: Option<PathBuf>,

    /// Disable interactive prompts (CI/CD mode)
    #[arg(long, env = "CCBOARD_NON_INTERACTIVE")]
    non_interactive: bool,

    /// Force output format (json|table)
    #[arg(long, env = "CCBOARD_FORMAT", value_parser = ["json", "table"])]
    format: Option<String>,

    /// Disable ANSI colors (log-friendly)
    #[arg(long, env = "CCBOARD_NO_COLOR")]
    no_color: bool,
}

#[derive(Subcommand)]
enum Mode {
    /// Run TUI interface (default)
    Tui,
    /// Run web interface
    Web {
        /// Port for web server
        #[arg(long, default_value = "3333")]
        port: u16,
    },
    /// Run both TUI and web interfaces
    Both {
        /// Port for web server
        #[arg(long, default_value = "3333")]
        port: u16,
    },
    /// Print stats to terminal and exit
    Stats,
    /// Clear session metadata cache and exit
    ClearCache,
    /// Search sessions by query
    Search {
        /// Query string (searches ID, project, message, branch)
        query: String,
        /// Date filter: 7d, 30d, 3m, 1y, YYYY-MM-DD
        #[arg(long, short = 'd')]
        since: Option<String>,
        /// Max results
        #[arg(long, short = 'n', default_value = "20")]
        limit: usize,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Show recent sessions
    Recent {
        /// Number of sessions
        #[arg(default_value = "10")]
        count: usize,
        /// Date filter: 7d, 30d, 3m, 1y, YYYY-MM-DD
        #[arg(long, short = 'd')]
        since: Option<String>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Show detailed session info
    Info {
        /// Session ID or prefix (min 8 chars)
        session_id: String,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Resume session in Claude CLI
    Resume {
        /// Session ID or prefix (min 8 chars)
        session_id: String,
    },
    /// Export data to files (sessions, stats, billing, or a single conversation)
    Export {
        #[command(subcommand)]
        command: ExportCommand,
    },
    /// Pricing management
    Pricing {
        #[command(subcommand)]
        command: PricingCommand,
    },
}

#[derive(Subcommand)]
enum ExportCommand {
    /// Export a single conversation to file (markdown, json, or html)
    Conversation {
        /// Session ID or prefix (min 8 chars)
        session_id: String,
        /// Output file path
        #[arg(short = 'o', long)]
        output: PathBuf,
        /// Export format: markdown, json, html
        #[arg(short = 'f', long, default_value = "markdown", value_parser = ["markdown", "json", "html"])]
        format: String,
    },
    /// Export sessions list to file (csv, json, or md)
    Sessions {
        /// Output file path
        #[arg(short = 'o', long)]
        output: PathBuf,
        /// Export format: csv, json, md
        #[arg(short = 'f', long, default_value = "csv", value_parser = ["csv", "json", "md"])]
        format: String,
        /// Date filter: 7d, 30d, 3m, 1y, YYYY-MM-DD
        #[arg(long, short = 'd')]
        since: Option<String>,
    },
    /// Export usage statistics to file (csv, json, or md)
    Stats {
        /// Output file path
        #[arg(short = 'o', long)]
        output: PathBuf,
        /// Export format: csv, json, md
        #[arg(short = 'f', long, default_value = "csv", value_parser = ["csv", "json", "md"])]
        format: String,
    },
    /// Export billing blocks to file (csv, json, or md)
    Billing {
        /// Output file path
        #[arg(short = 'o', long)]
        output: PathBuf,
        /// Export format: csv, json, md
        #[arg(short = 'f', long, default_value = "csv", value_parser = ["csv", "json", "md"])]
        format: String,
    },
}

#[derive(Subcommand)]
enum PricingCommand {
    /// Update pricing from LiteLLM API
    Update,
    /// Clear cached pricing data
    Clear,
}

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

    let claude_home = cli
        .claude_home
        .or_else(|| dirs::home_dir().map(|h: PathBuf| h.join(".claude")))
        .context("Could not determine Claude home directory")?;

    // Auto-detect project: if no --project specified, try current directory
    let project = cli.project.or_else(|| {
        let current_dir = std::env::current_dir().ok()?;
        // Check if current directory has a .claude/ subdirectory
        if current_dir.join(".claude").exists() {
            Some(current_dir)
        } else {
            None
        }
    });

    // Extract flags for command handlers
    let no_color = cli.no_color;

    match cli.mode.unwrap_or(Mode::Tui) {
        Mode::Tui => {
            run_tui(claude_home, project).await?;
        }
        Mode::Web { port } => {
            run_web(claude_home, project, port).await?;
        }
        Mode::Both { port } => {
            run_both(claude_home, project, port).await?;
        }
        Mode::Stats => {
            run_stats(claude_home, project).await?;
        }
        Mode::ClearCache => {
            run_clear_cache(claude_home).await?;
        }
        Mode::Search {
            query,
            since,
            limit,
            json,
        } => {
            run_search(claude_home, project, query, since, limit, json, no_color).await?;
        }
        Mode::Recent { count, since, json } => {
            run_recent(claude_home, project, count, since, json, no_color).await?;
        }
        Mode::Info { session_id, json } => {
            run_info(claude_home, project, session_id, json, no_color).await?;
        }
        Mode::Resume { session_id } => {
            run_resume(claude_home, project, session_id).await?;
        }
        Mode::Export { command } => match command {
            ExportCommand::Conversation {
                session_id,
                output,
                format,
            } => {
                run_export_conversation(claude_home, project, session_id, output, format, no_color)
                    .await?;
            }
            ExportCommand::Sessions {
                output,
                format,
                since,
            } => {
                run_export_sessions(claude_home, project, output, format, since, no_color).await?;
            }
            ExportCommand::Stats { output, format } => {
                run_export_stats(claude_home, project, output, format, no_color).await?;
            }
            ExportCommand::Billing { output, format } => {
                run_export_billing(claude_home, project, output, format, no_color).await?;
            }
        },
        Mode::Pricing { command } => match command {
            PricingCommand::Update => {
                run_pricing_update(no_color).await?;
            }
            PricingCommand::Clear => {
                run_pricing_clear(no_color).await?;
            }
        },
    }

    Ok(())
}

async fn run_tui(claude_home: PathBuf, project: Option<PathBuf>) -> Result<()> {
    // Initialize data store (without loading data yet - TUI will handle that)
    let store = Arc::new(DataStore::with_defaults(
        claude_home.clone(),
        project.clone(),
    ));

    // Start file watcher for live updates
    let _watcher = ccboard_core::FileWatcher::start(
        claude_home.clone(),
        project.clone(),
        Arc::clone(&store),
        Default::default(),
    )
    .await
    .context("Failed to start file watcher")?;

    // Run TUI (will show loading spinner and load data in background)
    ccboard_tui::run(store, claude_home, project).await
}

async fn run_web(claude_home: PathBuf, project: Option<PathBuf>, port: u16) -> Result<()> {
    use indicatif::{ProgressBar, ProgressStyle};
    use std::time::Instant;

    let start = Instant::now();

    // Create spinner
    let spinner = ProgressBar::new_spinner();
    spinner.set_style(
        ProgressStyle::default_spinner()
            .template("{spinner:.cyan} {msg}")
            .unwrap()
            .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"),
    );
    spinner.enable_steady_tick(std::time::Duration::from_millis(80));

    // Initialize data store
    spinner.set_message("Initializing data store...");
    let store = Arc::new(DataStore::with_defaults(
        claude_home.clone(),
        project.clone(),
    ));

    // Load initial data
    spinner.set_message("Loading sessions and statistics...");
    let report = store.initial_load().await;

    if report.has_fatal_errors() {
        spinner.finish_and_clear();
        eprintln!("Fatal errors during data load:");
        for error in report.errors.iter() {
            eprintln!("  - {}: {}", error.source, error.message);
        }
        return Ok(());
    }

    // Compute invocation statistics and billing blocks in background (can take minutes for 1000+ sessions)
    spinner.set_message("Starting background analytics computation...");
    let store_clone = Arc::clone(&store);
    tokio::spawn(async move {
        store_clone.compute_invocations().await;
        store_clone.compute_billing_blocks().await;
    });

    // Start file watcher for live updates
    spinner.set_message("Starting file watcher...");
    let _watcher = ccboard_core::FileWatcher::start(
        claude_home.clone(),
        project.clone(),
        Arc::clone(&store),
        Default::default(),
    )
    .await
    .context("Failed to start file watcher")?;

    let elapsed = start.elapsed();
    spinner.finish_with_message(format!(
        "✓ Ready in {:.2}s ({} sessions loaded)",
        elapsed.as_secs_f64(),
        report.sessions_scanned
    ));

    // Check if frontend dist/ exists
    let dist_path = std::path::Path::new("crates/ccboard-web/dist");
    if dist_path.exists() && dist_path.join("index.html").exists() {
        println!("\n🌐 Backend API + Frontend: http://localhost:{}", port);
        println!("   API endpoints:          http://localhost:{}/api/*", port);
    } else {
        println!(
            "\n🌐 Backend API only:       http://localhost:{}/api/*",
            port
        );
        println!("   💡 Run 'trunk build' to compile frontend");
    }

    ccboard_web::run(store, port).await
}

async fn run_both(claude_home: PathBuf, project: Option<PathBuf>, port: u16) -> Result<()> {
    use indicatif::{ProgressBar, ProgressStyle};
    use std::time::Instant;

    let start = Instant::now();

    // Create spinner
    let spinner = ProgressBar::new_spinner();
    spinner.set_style(
        ProgressStyle::default_spinner()
            .template("{spinner:.cyan} {msg}")
            .unwrap()
            .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"),
    );
    spinner.enable_steady_tick(std::time::Duration::from_millis(80));

    // Initialize data store
    spinner.set_message("Initializing data store...");
    let store = Arc::new(DataStore::with_defaults(
        claude_home.clone(),
        project.clone(),
    ));

    // Load initial data
    spinner.set_message("Loading sessions and statistics...");
    let report = store.initial_load().await;

    if report.has_fatal_errors() {
        spinner.finish_and_clear();
        eprintln!("Fatal errors during data load:");
        for error in report.errors.iter() {
            eprintln!("  - {}: {}", error.source, error.message);
        }
        return Ok(());
    }

    // Compute invocation statistics (agents/commands/skills usage)
    spinner.set_message("Computing invocation statistics...");
    store.compute_invocations().await;

    // Compute billing blocks (5h usage tracking)
    spinner.set_message("Computing billing blocks...");
    store.compute_billing_blocks().await;

    // Start file watcher for live updates (shared by TUI and web)
    spinner.set_message("Starting file watcher...");
    let _watcher = ccboard_core::FileWatcher::start(
        claude_home.clone(),
        project.clone(),
        Arc::clone(&store),
        Default::default(),
    )
    .await
    .context("Failed to start file watcher")?;

    let elapsed = start.elapsed();
    spinner.finish_with_message(format!(
        "✓ Ready in {:.2}s ({} sessions loaded)",
        elapsed.as_secs_f64(),
        report.sessions_scanned
    ));

    // Check if frontend dist/ exists
    let dist_path = std::path::Path::new("crates/ccboard-web/dist");
    if dist_path.exists() && dist_path.join("index.html").exists() {
        println!("🌐 Backend API + Frontend: http://localhost:{}", port);
    } else {
        println!("🌐 Backend API only:       http://localhost:{}/api/*", port);
        println!("   💡 Run 'trunk build' to compile frontend");
    }

    // Start web server in background
    let web_store = Arc::clone(&store);
    let web_handle = tokio::spawn(async move {
        if let Err(e) = ccboard_web::run(web_store, port).await {
            eprintln!("Web server error: {}", e);
        }
    });

    // Run TUI in foreground
    let tui_result = ccboard_tui::run(store, claude_home, project).await;

    // Clean up web server
    web_handle.abort();

    tui_result
}

async fn run_stats(claude_home: PathBuf, project: Option<PathBuf>) -> Result<()> {
    // Initialize data store
    let store = DataStore::with_defaults(claude_home, project);

    // Load initial data
    let report = store.initial_load().await;

    // Print stats summary
    println!("ccboard - Claude Code Statistics");
    println!("================================");
    println!();

    if let Some(stats) = store.stats() {
        println!("Total Tokens:     {}", format_number(stats.total_tokens()));
        println!(
            "  Input:          {}",
            format_number(stats.total_input_tokens())
        );
        println!(
            "  Output:         {}",
            format_number(stats.total_output_tokens())
        );
        println!(
            "  Cache Read:     {}",
            format_number(stats.total_cache_read_tokens())
        );
        println!(
            "  Cache Write:    {}",
            format_number(stats.total_cache_write_tokens())
        );
        println!();
        println!("Sessions:         {}", stats.session_count());
        println!("Messages:         {}", stats.message_count());
        println!("Cache Hit Ratio:  {:.1}%", stats.cache_ratio() * 100.0);
        println!();

        if !stats.model_usage.is_empty() {
            println!("Models:");
            for (name, usage) in stats.top_models(5) {
                println!(
                    "  {}: {} tokens (in: {}, out: {})",
                    name,
                    format_number(usage.total_tokens()),
                    format_number(usage.input_tokens),
                    format_number(usage.output_tokens)
                );
            }
        }
    } else {
        println!("No stats available");
    }

    println!();
    println!("Sessions indexed: {}", store.session_count());

    if report.has_errors() {
        println!();
        println!("Warnings:");
        for error in report.warnings() {
            println!("  - {}: {}", error.source, error.message);
        }
    }

    Ok(())
}

async fn run_clear_cache(claude_home: PathBuf) -> Result<()> {
    let cache_dir = claude_home.join("cache");
    let cache_path = cache_dir.join("session-metadata.db");

    if !cache_path.exists() {
        println!("❌ Cache not found at: {}", cache_path.display());
        println!("   Nothing to clear.");
        return Ok(());
    }

    // Get file size before deletion
    let size_bytes = std::fs::metadata(&cache_path)
        .with_context(|| format!("Failed to read cache metadata: {}", cache_path.display()))?
        .len();

    // Delete cache file
    std::fs::remove_file(&cache_path)
        .with_context(|| format!("Failed to delete cache: {}", cache_path.display()))?;

    // Delete WAL files if they exist
    let wal_path = cache_dir.join("session-metadata.db-wal");
    let shm_path = cache_dir.join("session-metadata.db-shm");

    if wal_path.exists() {
        let _ = std::fs::remove_file(&wal_path);
    }
    if shm_path.exists() {
        let _ = std::fs::remove_file(&shm_path);
    }

    println!("✅ Cache cleared successfully");
    println!("   Location: {}", cache_path.display());
    println!("   Freed: {}", format_size(size_bytes));
    println!();
    println!("💡 Next run will rebuild cache with fresh metadata.");

    Ok(())
}

fn format_size(bytes: u64) -> String {
    if bytes >= 1_048_576 {
        format!("{:.1}MB", bytes as f64 / 1_048_576.0)
    } else if bytes >= 1_024 {
        format!("{:.1}KB", bytes as f64 / 1_024.0)
    } else {
        format!("{}B", bytes)
    }
}

fn format_number(n: u64) -> String {
    if n >= 1_000_000_000 {
        format!("{:.2}B", n as f64 / 1_000_000_000.0)
    } else if n >= 1_000_000 {
        format!("{:.2}M", n as f64 / 1_000_000.0)
    } else if n >= 1_000 {
        format!("{:.2}K", n as f64 / 1_000.0)
    } else {
        n.to_string()
    }
}

// ============================================================================
// CLI Command Handlers
// ============================================================================

async fn run_search(
    claude_home: PathBuf,
    project: Option<PathBuf>,
    query: String,
    since: Option<String>,
    limit: usize,
    json: bool,
    no_color: bool,
) -> Result<()> {
    let store = DataStore::with_defaults(claude_home, project);

    // Show progress
    if !json {
        eprint!("Scanning sessions... ");
    }

    let report = store.initial_load().await;

    if !json && report.sessions_scanned > 0 {
        eprintln!("✓ {} sessions", report.sessions_scanned);
    }

    // Parse date filter
    let date_filter = if let Some(ref s) = since {
        Some(cli::DateFilter::parse(s).context("Invalid date filter")?)
    } else {
        None
    };

    // Search
    let all = store.recent_sessions(usize::MAX);
    let results = cli::search_sessions(&all, &query, date_filter.as_ref(), limit);

    if results.is_empty() {
        return Err(cli::CliError::NoResults {
            query,
            scanned: all.len(),
        }
        .into());
    }

    println!("{}", cli::format_session_table(&results, json, no_color));

    if !json {
        eprintln!("\n{} results from {} sessions", results.len(), all.len());
    }

    Ok(())
}

async fn run_recent(
    claude_home: PathBuf,
    project: Option<PathBuf>,
    count: usize,
    since: Option<String>,
    json: bool,
    no_color: bool,
) -> Result<()> {
    let store = DataStore::with_defaults(claude_home, project);

    if !json {
        eprint!("Loading sessions... ");
    }

    let report = store.initial_load().await;

    if !json && report.sessions_scanned > 0 {
        eprintln!("✓ {} sessions", report.sessions_scanned);
    }

    // Parse date filter
    let date_filter = if let Some(ref s) = since {
        Some(cli::DateFilter::parse(s).context("Invalid date filter")?)
    } else {
        None
    };

    // Get recent sessions
    let mut all = store.recent_sessions(usize::MAX);

    // Apply date filter if specified
    if let Some(filter) = date_filter {
        all.retain(|s| {
            s.first_timestamp
                .map(|ts| filter.matches(&ts))
                .unwrap_or(false)
        });
    }

    let results: Vec<_> = all.into_iter().take(count).collect();

    if results.is_empty() {
        if !json {
            println!("No sessions found.");
        }
        return Ok(());
    }

    println!("{}", cli::format_session_table(&results, json, no_color));

    if !json {
        eprintln!(
            "\nShowing {} of {} sessions",
            results.len(),
            report.sessions_scanned
        );
    }

    Ok(())
}

async fn run_info(
    claude_home: PathBuf,
    project: Option<PathBuf>,
    session_id: String,
    json: bool,
    _no_color: bool,
) -> Result<()> {
    let store = DataStore::with_defaults(claude_home, project);

    if !json {
        eprint!("Loading sessions... ");
    }

    store.initial_load().await;

    if !json {
        eprintln!("✓");
    }

    let all = store.recent_sessions(usize::MAX);
    let session = cli::find_by_id_or_prefix(&all, &session_id)?;

    println!("{}", cli::format_session_info(&session, json));

    Ok(())
}

async fn run_resume(
    claude_home: PathBuf,
    project: Option<PathBuf>,
    session_id: String,
) -> Result<()> {
    let store = DataStore::with_defaults(claude_home, project);

    eprint!("Loading sessions... ");
    store.initial_load().await;
    eprintln!("✓");

    let all = store.recent_sessions(usize::MAX);
    let session = cli::find_by_id_or_prefix(&all, &session_id)?;

    eprintln!(
        "Resuming session {} in {}",
        &session.id[..8],
        session.project_path
    );

    // Unix: use exec() to replace process (no need to wait)
    #[cfg(unix)]
    {
        use std::os::unix::process::CommandExt;
        let err = std::process::Command::new("claude")
            .args(["--resume", &session.id])
            .exec();
        anyhow::bail!("Failed to exec claude: {}", err);
    }

    // Windows: spawn and exit with same code
    #[cfg(not(unix))]
    {
        let status = std::process::Command::new("claude")
            .args(["--resume", &session.id])
            .status()
            .context("Failed to spawn claude (is 'claude' in PATH?)")?;
        std::process::exit(status.code().unwrap_or(1));
    }
}

async fn run_export_conversation(
    claude_home: PathBuf,
    project: Option<PathBuf>,
    session_id: String,
    output: PathBuf,
    format: String,
    no_color: bool,
) -> Result<()> {
    use ccboard_core::export::{
        export_conversation_to_html, export_conversation_to_json, export_conversation_to_markdown,
    };

    let store = Arc::new(DataStore::with_defaults(claude_home, project));

    // Show progress
    if !no_color {
        eprint!("Loading sessions... ");
    }

    store.initial_load().await;

    if !no_color {
        eprintln!("✓");
    }

    // Find session
    let all = store.recent_sessions(usize::MAX);
    let session = cli::find_by_id_or_prefix(&all, &session_id)?;

    // Load conversation content
    if !no_color {
        eprint!("Loading conversation... ");
    }

    let messages = store
        .load_session_content(&session.id)
        .await
        .context("Failed to load session content")?;

    if !no_color {
        eprintln!("✓ {} messages", messages.len());
    }

    // Export based on format
    if !no_color {
        eprint!("Exporting to {}... ", output.display());
    }

    match format.as_str() {
        "markdown" | "md" => {
            export_conversation_to_markdown(&messages, &session, &output)
                .context("Failed to export to Markdown")?;
        }
        "json" => {
            export_conversation_to_json(&messages, &session, &output)
                .context("Failed to export to JSON")?;
        }
        "html" => {
            export_conversation_to_html(&messages, &session, &output)
                .context("Failed to export to HTML")?;
        }
        _ => {
            anyhow::bail!("Invalid format: {}. Use markdown, json, or html", format);
        }
    }

    if !no_color {
        eprintln!("✓");
        println!("✅ Exported to {}", output.display());
        println!("   Session: {}", session.id);
        println!("   Messages: {}", messages.len());
        println!("   Format: {}", format);
    } else {
        println!("{}", output.display());
    }

    Ok(())
}

async fn run_export_sessions(
    claude_home: PathBuf,
    project: Option<PathBuf>,
    output: PathBuf,
    format: String,
    since: Option<String>,
    no_color: bool,
) -> Result<()> {
    use ccboard_core::{
        export_sessions_to_csv, export_sessions_to_json, export_sessions_to_markdown,
    };

    let store = DataStore::with_defaults(claude_home, project);

    if !no_color {
        eprint!("Loading sessions... ");
    }

    let report = store.initial_load().await;

    if !no_color {
        eprintln!("✓ {} sessions", report.sessions_scanned);
    }

    // Parse and apply date filter
    let date_filter = if let Some(ref s) = since {
        Some(cli::DateFilter::parse(s).context("Invalid date filter")?)
    } else {
        None
    };

    let mut sessions = store.recent_sessions(usize::MAX);
    if let Some(filter) = date_filter {
        sessions.retain(|s| {
            s.first_timestamp
                .map(|ts| filter.matches(&ts))
                .unwrap_or(false)
        });
    }

    if !no_color {
        eprint!(
            "Exporting {} sessions to {}... ",
            sessions.len(),
            output.display()
        );
    }

    match format.as_str() {
        "csv" => {
            export_sessions_to_csv(&sessions, &output)
                .context("Failed to export sessions to CSV")?;
        }
        "json" => {
            export_sessions_to_json(&sessions, &output)
                .context("Failed to export sessions to JSON")?;
        }
        "md" | "markdown" => {
            export_sessions_to_markdown(&sessions, &output)
                .context("Failed to export sessions to Markdown")?;
        }
        _ => {
            anyhow::bail!("Invalid format: {}. Use csv, json, or md", format);
        }
    }

    if !no_color {
        eprintln!("✓");
        println!("✅ Exported to {}", output.display());
        println!("   Sessions: {}", sessions.len());
        println!("   Format: {}", format);
    } else {
        println!("{}", output.display());
    }

    Ok(())
}

async fn run_export_stats(
    claude_home: PathBuf,
    project: Option<PathBuf>,
    output: PathBuf,
    format: String,
    no_color: bool,
) -> Result<()> {
    use ccboard_core::{export_stats_to_csv, export_stats_to_json, export_stats_to_markdown};

    let store = DataStore::with_defaults(claude_home, project);

    if !no_color {
        eprint!("Loading statistics... ");
    }

    store.initial_load().await;

    if !no_color {
        eprintln!("✓");
    }

    let stats = store
        .stats()
        .ok_or_else(|| anyhow::anyhow!("No stats available (stats-cache.json not found)"))?;

    if !no_color {
        eprint!("Exporting stats to {}... ", output.display());
    }

    match format.as_str() {
        "csv" => {
            export_stats_to_csv(&stats, &output).context("Failed to export stats to CSV")?;
        }
        "json" => {
            export_stats_to_json(&stats, &output).context("Failed to export stats to JSON")?;
        }
        "md" | "markdown" => {
            export_stats_to_markdown(&stats, &output)
                .context("Failed to export stats to Markdown")?;
        }
        _ => {
            anyhow::bail!("Invalid format: {}. Use csv, json, or md", format);
        }
    }

    if !no_color {
        eprintln!("✓");
        println!("✅ Exported to {}", output.display());
        println!("   Sessions: {}", stats.total_sessions);
        println!("   Messages: {}", stats.total_messages);
        println!("   Format: {}", format);
    } else {
        println!("{}", output.display());
    }

    Ok(())
}

async fn run_export_billing(
    claude_home: PathBuf,
    project: Option<PathBuf>,
    output: PathBuf,
    format: String,
    no_color: bool,
) -> Result<()> {
    use ccboard_core::{
        export_billing_blocks_to_csv, export_billing_blocks_to_json,
        export_billing_blocks_to_markdown,
    };

    let store = DataStore::with_defaults(claude_home, project);

    if !no_color {
        eprint!("Loading billing data... ");
    }

    store.initial_load().await;
    store.compute_billing_blocks().await;

    if !no_color {
        eprintln!("✓");
    }

    let manager = store.billing_blocks();
    let block_count = manager.get_all_blocks().len();

    if !no_color {
        eprint!(
            "Exporting {} blocks to {}... ",
            block_count,
            output.display()
        );
    }

    match format.as_str() {
        "csv" => {
            export_billing_blocks_to_csv(&manager, &output)
                .context("Failed to export billing to CSV")?;
        }
        "json" => {
            export_billing_blocks_to_json(&manager, &output)
                .context("Failed to export billing to JSON")?;
        }
        "md" | "markdown" => {
            export_billing_blocks_to_markdown(&manager, &output)
                .context("Failed to export billing to Markdown")?;
        }
        _ => {
            anyhow::bail!("Invalid format: {}. Use csv, json, or md", format);
        }
    }

    if !no_color {
        eprintln!("✓");
        println!("✅ Exported to {}", output.display());
        println!("   Blocks: {}", block_count);
        println!("   Format: {}", format);
    } else {
        println!("{}", output.display());
    }

    Ok(())
}

async fn run_pricing_update(_no_color: bool) -> Result<()> {
    use indicatif::{ProgressBar, ProgressStyle};

    let spinner = ProgressBar::new_spinner();
    spinner.set_style(
        ProgressStyle::default_spinner()
            .template("{spinner:.cyan} {msg}")
            .unwrap()
            .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"),
    );
    spinner.enable_steady_tick(std::time::Duration::from_millis(80));

    spinner.set_message("Fetching pricing from LiteLLM...");

    match ccboard_core::pricing::update_pricing_from_litellm().await {
        Ok(count) => {
            spinner.finish_and_clear();
            println!("✓ Updated {} model prices from LiteLLM", count);
            println!("  Cache: ~/.cache/ccboard/pricing.json (TTL: 7 days)");
            Ok(())
        }
        Err(e) => {
            spinner.finish_and_clear();
            eprintln!("✗ Failed to update pricing: {}", e);
            eprintln!("  Using embedded pricing as fallback");
            Ok(())
        }
    }
}

async fn run_pricing_clear(_no_color: bool) -> Result<()> {
    match ccboard_core::pricing::clear_cache() {
        Ok(()) => {
            println!("✓ Cleared pricing cache");
            println!("  File: ~/.cache/ccboard/pricing.json");
            Ok(())
        }
        Err(e) => {
            eprintln!("✗ Failed to clear cache: {}", e);
            Err(e)
        }
    }
}
