oc-session 0.1.0

Global OpenCode session browser and resume tool
use crate::{cli, config::Config, db, resume, search, session::Session, tui};
use std::path::Path;

pub mod doctor;
pub mod skill;

pub fn run(cli: cli::Cli, cfg: Config) -> anyhow::Result<()> {
    let options = db::Options {
        include_children: cli.include_children,
    };
    if let Some(query) = cli.search.as_deref() {
        return search_query(&cfg, options, query, cfg.limit, false);
    }
    match cli.command {
        None => open(&cfg, options, ""),
        Some(cli::Command::Search(args)) => search_cmd(&cfg, options, args),
        Some(cli::Command::Recent(args)) => recent(&cfg, options, args.limit, args.json),
        Some(cli::Command::Last(args)) => last(&cfg, options, args.print),
        Some(cli::Command::Resume(args)) => resume_cmd(&cfg, options, &args.id),
        Some(cli::Command::Print(args)) => print_cmd(&cfg, options, &args.id),
        Some(cli::Command::Copy(args)) => copy_cmd(&cfg, options, &args.id),
        Some(cli::Command::Scan(args)) => scan(&args.include),
        Some(cli::Command::Doctor) => doctor::run(&cfg, options),
        Some(cli::Command::Skill(args)) => skill::run(args),
    }
}

fn open(cfg: &Config, options: db::Options, query: &str) -> anyhow::Result<()> {
    let paths = db::discover(cfg);
    let list = db::load(&paths, cfg.limit, options);
    if let Some(session) = tui::pick(cfg, &list, query)? {
        resume::run(cfg, &session)?;
    }
    Ok(())
}

fn search_cmd(cfg: &Config, options: db::Options, args: cli::SearchArgs) -> anyhow::Result<()> {
    let query = args.query.join(" ");
    search_query(cfg, options, &query, args.limit, args.no_tui)
}

fn search_query(
    cfg: &Config,
    options: db::Options,
    query: &str,
    limit: usize,
    no_tui: bool,
) -> anyhow::Result<()> {
    let paths = db::discover(cfg);
    let list = db::load(&paths, cfg.limit.max(limit), options);
    let matches = search::filter(&list, query, limit);
    if no_tui {
        print_list(cfg, &matches);
        return Ok(());
    }
    if let Some(session) = tui::pick(cfg, &matches, query)? {
        resume::run(cfg, &session)?;
    }
    Ok(())
}

fn recent(cfg: &Config, options: db::Options, limit: usize, json: bool) -> anyhow::Result<()> {
    let list = db::load(&db::discover(cfg), limit, options);
    if json {
        println!("{}", serde_json::to_string_pretty(&list)?);
    } else {
        print_list(cfg, &list);
    }
    Ok(())
}

fn last(cfg: &Config, options: db::Options, print: bool) -> anyhow::Result<()> {
    let Some(session) = db::load(&db::discover(cfg), 1, options).into_iter().next() else {
        anyhow::bail!("no sessions found");
    };
    if print {
        println!("{}", resume::command(cfg, &session));
        return Ok(());
    }
    resume::run(cfg, &session)
}

fn resume_cmd(cfg: &Config, options: db::Options, id: &str) -> anyhow::Result<()> {
    resume::run(cfg, &db::load_one(&db::discover(cfg), id, options)?)
}

fn print_cmd(cfg: &Config, options: db::Options, id: &str) -> anyhow::Result<()> {
    println!(
        "{}",
        resume::command(cfg, &db::load_one(&db::discover(cfg), id, options)?)
    );
    Ok(())
}

fn copy_cmd(cfg: &Config, options: db::Options, id: &str) -> anyhow::Result<()> {
    let session = db::load_one(&db::discover(cfg), id, options)?;
    tui::copy_text(&resume::command(cfg, &session))?;
    println!("copied resume command for {}", session.id);
    Ok(())
}

fn scan(include: &[std::path::PathBuf]) -> anyhow::Result<()> {
    for path in db::scan(include) {
        println!("{}", path.display());
    }
    Ok(())
}

fn print_list(cfg: &Config, list: &[Session]) {
    if list.is_empty() {
        println!("No sessions found.");
        return;
    }

    for (index, session) in list.iter().enumerate() {
        if index > 0 {
            println!();
        }
        println!(
            "{}  {}",
            session.title,
            format_time(session.updated).unwrap_or_else(|| "unknown".to_string())
        );
        println!("  id:      {}", session.id);
        println!("  path:    {}", pretty_path(&session.directory));
        if let Some(agent) = session.agent.as_deref().filter(|agent| !agent.is_empty()) {
            println!("  agent:   {agent}");
        }
        println!("  command: {}", resume::command(cfg, session));
    }
}

fn format_time(ms: i64) -> Option<String> {
    let ms = u64::try_from(ms).ok()?;
    let time = std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_millis(ms))?;
    Some(
        chrono::DateTime::<chrono::Local>::from(time)
            .format("%Y-%m-%d %I:%M %p")
            .to_string(),
    )
}

fn pretty_path(path: &Path) -> String {
    let Some(home) = dirs::home_dir() else {
        return path.display().to_string();
    };
    let Ok(rest) = path.strip_prefix(home) else {
        return path.display().to_string();
    };
    if rest.as_os_str().is_empty() {
        return "~".to_string();
    }
    format!("~/{}", rest.display())
}