mod grid;
mod load;
mod normalize;
mod render;
mod signal;
pub(crate) mod stats;

use std::fs::create_dir_all;
use std::time::Instant;

use crate::cli::RunArgs;
use crate::error::OrchestratorError;
use crate::image_render::write_per_gene_signal_webps;
use kira_spatial_io::{Dataset, LoadConfig};
use load::{
    compute_signal_dense_from_dataset, compute_signal_dense_from_feature_slice,
    is_feature_slice_matrix_missing,
};
use render::{render_outputs, resolve_contour_levels};

pub(crate) use grid::GridMapping;
pub(crate) use signal::{build_height_spec, percentile_from_sorted};

pub(crate) fn run_pipeline(args: RunArgs) -> Result<(), OrchestratorError> {
    let t_total = Instant::now();
    if args.genes.is_empty() {
        return Err(OrchestratorError::InvalidInput(
            "at least one gene is required in --genes",
        ));
    }

    if !args.quantize_grid.is_finite() || args.quantize_grid <= 0.0 {
        return Err(OrchestratorError::InvalidInput(
            "quantize_grid must be finite and > 0",
        ));
    }
    if !args.laplacian_sigma.is_finite() || args.laplacian_sigma < 0.0 {
        return Err(OrchestratorError::InvalidInput(
            "laplacian_sigma must be finite and >= 0",
        ));
    }

    create_dir_all(&args.out_dir)?;

    let t_load_signal = Instant::now();
    let loaded = if crate::feature_slice::detect_feature_slice_paths(&args.h5).is_some() {
        compute_signal_dense_from_feature_slice(&args)?
    } else {
        match Dataset::open_h5(&args.h5, LoadConfig::default()) {
            Ok(dataset) => compute_signal_dense_from_dataset(&args, &dataset)?,
            Err(err) if is_feature_slice_matrix_missing(&err) => {
                compute_signal_dense_from_feature_slice(&args)?
            }
            Err(err) => return Err(err.into()),
        }
    };
    let load::LoadedSignal {
        grid,
        signal_dense,
        per_gene_signal_dense,
        stats,
        domains,
        coloc,
    } = loaded;
    log_timing("load+signal", t_load_signal);

    let t_webp = Instant::now();
    write_per_gene_signal_webps(&args, &grid, &per_gene_signal_dense)?;
    log_timing("per_gene_webps", t_webp);

    if let Some(stats) = &stats {
        let t_stats = Instant::now();
        crate::image_render::write_spatial_stats_outputs(&args, stats)?;
        log_timing("spatial_stats_outputs", t_stats);
    }
    if let Some(domains) = &domains {
        let t_dom = Instant::now();
        crate::image_render::write_domains_outputs(&args, domains)?;
        log_timing("domains_outputs", t_dom);
    }
    if let Some(coloc) = &coloc {
        let t_col = Instant::now();
        crate::image_render::write_colocalize_outputs(&args, coloc)?;
        log_timing("colocalize_outputs", t_col);
    }

    if !args.extended {
        log_timing("run_pipeline_total", t_total);
        return Ok(());
    }

    let contour_levels = resolve_contour_levels(&args, &per_gene_signal_dense)?;

    let t_render = Instant::now();
    let out = render_outputs(&args, &grid, &signal_dense, &contour_levels);
    log_timing("render_outputs", t_render);
    log_timing("run_pipeline_total", t_total);
    out
}

pub(crate) fn log_timing(label: &str, start: Instant) {
    eprintln!("[timing] {} {:?}", label, start.elapsed());
}
