use std::fs::{File, create_dir_all};
use std::io::BufWriter;
use std::path::PathBuf;
use std::time::Instant;
use anyhow::{Context as _, Result, anyhow};
use bywind::fetch::{FetchProgress, FetchSpec, parse_yyyymmddhh, transcode_grib2_to_wcav};
use bywind::fetch_ensemble::{fetch_member_to_grib2, stride_sample_members};
use chrono::{DateTime, Utc};
use crate::error::AppError;
#[derive(clap::Args, Debug)]
pub struct FetchEnsembleArgs {
pub start: String,
pub end: String,
#[arg(long, short = 'o')]
pub out: PathBuf,
#[arg(long, value_name = "N", default_value_t = 3)]
pub interval_h: u32,
#[arg(long, value_name = "K", default_value_t = 10)]
pub members: usize,
}
pub fn run(args: &FetchEnsembleArgs) -> Result<(), AppError> {
let start = parse_yyyymmddhh(&args.start)
.map_err(|e| anyhow!(e))
.context("--start")?;
let end = parse_yyyymmddhh(&args.end)
.map_err(|e| anyhow!(e))
.context("--end")?;
let spec = FetchSpec {
start,
end,
interval_h: args.interval_h,
};
let members = stride_sample_members(args.members);
if members.is_empty() {
return Err(AppError::from(anyhow!(
"--members must be > 0 (got {})",
args.members
)));
}
create_dir_all(&args.out).with_context(|| format!("creating {}", args.out.display()))?;
eprintln!(
"fetch-ensemble: {} → {} (interval {} h), {} members → {}",
format_when(start),
format_when(end),
args.interval_h,
members.len(),
args.out.display(),
);
let total_start = Instant::now();
let mut total_fetched: u32 = 0;
let mut total_skipped: u32 = 0;
let mut total_bytes: u64 = 0;
let mut succeeded_members: usize = 0;
for (m_idx, member) in members.iter().enumerate() {
let prefix = member.filename_prefix();
eprintln!(
"\n[{}/{}] member {} — pulling...",
m_idx + 1,
members.len(),
prefix
);
let wcav_path = args.out.join(format!("{prefix}.wcav"));
let staging = wcav_path.with_extension("grib2.tmp");
let stats = {
let file = File::create(&staging)
.with_context(|| format!("creating {}", staging.display()))?;
let mut writer = BufWriter::new(file);
match fetch_member_to_grib2(&spec, *member, &mut writer, log_progress) {
Ok(s) => s,
Err(e) => {
eprintln!(" member {prefix} failed: {e}");
drop(std::fs::remove_file(&staging));
continue;
}
}
};
eprintln!(
" fetched {} frames ({} skipped, {} KB), encoding wind_av1...",
stats.fetched,
stats.skipped,
stats.total_bytes / 1024,
);
let t0 = Instant::now();
match transcode_grib2_to_wcav(&staging, &wcav_path) {
Ok(_) => {
eprintln!(
" encoded in {:.1}s → {}",
t0.elapsed().as_secs_f64(),
wcav_path.display()
);
total_fetched += stats.fetched;
total_skipped += stats.skipped;
total_bytes += stats.total_bytes;
succeeded_members += 1;
}
Err(e) => {
eprintln!(" encode failed: {e}");
}
}
if let Err(e) = std::fs::remove_file(&staging) {
eprintln!(
" note: failed to delete staging {}: {e}",
staging.display()
);
}
}
if succeeded_members == 0 {
return Err(AppError::no_result(anyhow!(
"no members fetched successfully across {} attempts",
members.len()
)));
}
eprintln!(
"\ndone: {succeeded_members}/{} members, {total_fetched} frames, \
{total_skipped} skipped, {} MB in {:.1}s",
members.len(),
total_bytes / (1024 * 1024),
total_start.elapsed().as_secs_f64(),
);
Ok(())
}
fn log_progress(ev: FetchProgress) -> std::ops::ControlFlow<()> {
match ev {
FetchProgress::Fetched {
idx,
total,
timestamp,
bytes,
} => eprintln!(
" [{idx:3}/{total:3}] {} ok ({} KB)",
format_when(timestamp),
bytes / 1024,
),
FetchProgress::Skipped {
idx,
total,
timestamp,
reason,
} => eprintln!(
" [{idx:3}/{total:3}] {} skipped: {reason}",
format_when(timestamp),
),
}
std::ops::ControlFlow::Continue(())
}
fn format_when(t: DateTime<Utc>) -> String {
t.format("%Y-%m-%d %H:%M UTC").to_string()
}