use std::path::PathBuf;

use clap::{Parser, ValueEnum};

#[derive(Debug, Parser)]
#[command(
    name = "kira-spatial",
    version,
    about = "Deterministic orchestrator: io -> field -> core -> 3d"
)]
pub(crate) struct Cli {
    #[command(subcommand)]
    pub(crate) cmd: Command,
}

#[derive(Debug, clap::Subcommand)]
pub(crate) enum Command {
    /// Run full deterministic pipeline from H5 to 3D exports.
    Run(RunArgs),
}

#[derive(Debug, Parser)]
pub(crate) struct RunArgs {
    /// Path to input .h5 file or directory containing feature_slice.h5 + spatial/
    #[arg(long)]
    pub(crate) h5: PathBuf,
    /// Comma-separated gene symbols to aggregate, for example: EPCAM,KRT19,MSLN.
    #[arg(long, required = true, value_delimiter = ',')]
    pub(crate) genes: Vec<String>,
    /// Output directory for all generated artifacts.
    #[arg(long)]
    pub(crate) out_dir: PathBuf,
    /// Derived scalar used for 3D projection.
    #[arg(long, value_enum, default_value = "grad-mag")]
    pub(crate) signal: SignalKind,
    /// Height interpretation mode before normalization.
    #[arg(long, value_enum, default_value = "abs")]
    pub(crate) height_mode: HeightModeArg,
    /// Normalization policy.
    #[arg(long, value_enum, default_value = "percentile")]
    pub(crate) normalization: NormalizationArg,
    /// Lower percentile for percentile normalization.
    #[arg(long, default_value_t = 5.0)]
    pub(crate) percentile_lo: f32,
    /// Upper percentile for percentile normalization.
    #[arg(long, default_value_t = 95.0)]
    pub(crate) percentile_hi: f32,
    /// Final z scale.
    #[arg(long, default_value_t = 20.0)]
    pub(crate) z_scale: f32,
    /// Final z offset.
    #[arg(long, default_value_t = 0.0)]
    pub(crate) z_offset: f32,
    /// Contour levels computed on normalized field (pre-affine):
    /// "auto" (default) or comma-separated list like "0.2,0.4,0.6,0.8".
    #[arg(long, default_value = "auto")]
    pub(crate) contour_levels: String,
    /// Quantization grid for deterministic contour stitching.
    #[arg(long, default_value_t = 0.01)]
    pub(crate) quantize_grid: f32,
    /// Float decimals for text exports.
    #[arg(long, default_value_t = 6)]
    pub(crate) float_decimals: usize,
    /// Force scalar backend (disable SIMD path).
    #[arg(long)]
    pub(crate) scalar: bool,
    /// Disable contour extraction even if levels are provided.
    #[arg(long)]
    pub(crate) no_contours: bool,
    /// Write heavy surface.obj and surface.ply artifacts.
    #[arg(long, default_value_t = false)]
    pub(crate) write_obj_ply: bool,
    /// Extended output mode: in addition to per-gene images, write mesh/contour/json artifacts.
    #[arg(long, default_value_t = false)]
    pub(crate) extended: bool,
    /// Laplacian visualization style for per-gene images.
    #[arg(long, value_enum, default_value = "diverging")]
    pub(crate) laplacian_viz: LaplacianVizMode,
    /// Optional Gaussian sigma for Laplacian image smoothing (LoG-like).
    #[arg(long, default_value_t = 0.0)]
    pub(crate) laplacian_sigma: f32,
    /// Per-bin expression normalization applied BEFORE any operator.
    /// `cpm-log1p` = library-size (CP10K-style) + log1p; `log1p` = log1p only;
    /// `none` = raw counts (legacy). CPM requires a full expression matrix; on
    /// feature-slice inputs (no library size) it falls back to log1p.
    #[arg(long, value_enum, default_value = "cpm-log1p")]
    pub(crate) normalize: NormalizeMode,
    /// Target sum for CPM-style library-size normalization (1e4 = CP10K, as in
    /// Seurat NormalizeData / scanpy default).
    #[arg(long, default_value_t = 10000.0)]
    pub(crate) target_sum: f32,
    /// How to aggregate a multi-gene panel into a single field. `module-score`
    /// (Seurat AddModuleScore: z-score each gene across bins, then average) is
    /// the biologically correct default; `sum` reproduces the legacy raw sum.
    #[arg(long, value_enum, default_value = "module-score")]
    pub(crate) panel: PanelMode,
    /// Gaussian pre-smoothing sigma (in cells) applied before noise-sensitive
    /// second-order operators (laplacian, hessian, curvature, divergence) on
    /// sparse counts. 0 disables pre-smoothing.
    #[arg(long, default_value_t = 1.0)]
    pub(crate) pre_smooth_sigma: f32,
    /// Local sliding-window radius (in cells) for `fractal-dimension`. 0 selects
    /// the legacy single global value broadcast to every bin.
    #[arg(long, default_value_t = 8)]
    pub(crate) fractal_window: u32,
    /// Compute spatial-autocorrelation statistics on the normalized field and
    /// emit `spatial_stats.json` + local Moran/Getis-Ord z-score & hotspot
    /// images (with permutation p-values and BH-FDR).
    #[arg(long, default_value_t = false)]
    pub(crate) spatial_stats: bool,
    /// Permutations for Moran's I significance (pseudo p-value resolution).
    #[arg(long, default_value_t = 999)]
    pub(crate) stats_permutations: u32,
    /// Deterministic seed for permutation inference.
    #[arg(long, default_value_t = 0x5EED_1234_ABCD_0001)]
    pub(crate) stats_seed: u64,
    /// Detect spatial domains (Louvain community detection over a queen-adjacency
    /// graph weighted by signal similarity); emits `domains.json` + label image.
    #[arg(long, default_value_t = false)]
    pub(crate) domains: bool,
    /// Modularity resolution for spatial-domain detection (higher → more, smaller domains).
    #[arg(long, default_value_t = 1.0)]
    pub(crate) domain_resolution: f32,
    /// Spatial co-localization between two genes via bivariate Moran's R
    /// (permutation p-value + BH-FDR). Format: `GENE_A,GENE_B`. Both genes must
    /// appear in `--genes`. Emits `colocalize.json` + local association images.
    /// Uses `--stats-permutations`/`--stats-seed`.
    #[arg(long)]
    pub(crate) colocalize: Option<String>,
    /// QC mask for spatial statistics: drop bins whose total library size (counts
    /// over all genes) is below this threshold, so spatial-stats / domains /
    /// colocalize ignore empty background bins. 0 disables QC. Applies only where
    /// a per-bin library size is available (dataset path; feature-slice fast path).
    #[arg(long, default_value_t = 0.0)]
    pub(crate) min_counts: f32,
    /// Coarsen the grid by this integer factor before signal/stats (8µm → N·8µm
    /// super-bins): per-gene fields are pooled, densifying sparse HD bins and
    /// lifting analysis to a compartment scale. 1 disables aggregation.
    #[arg(long, default_value_t = 1)]
    pub(crate) bin_aggregate: u32,
    /// Neighbourhood radius (in grid cells) for `--colocalize` bivariate Moran.
    /// 1 = immediate neighbours; larger integrates a wider Euclidean disk to
    /// capture compartment-scale co-localization (anti-MAUP).
    #[arg(long, default_value_t = 1)]
    pub(crate) neighbor_radius: u32,
}

impl RunArgs {
    /// Parsed `--colocalize` gene pair, if provided and well-formed.
    pub(crate) fn colocalize_pair(&self) -> Option<(String, String)> {
        let raw = self.colocalize.as_ref()?;
        let parts: Vec<&str> = raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
        if parts.len() == 2 {
            Some((parts[0].to_string(), parts[1].to_string()))
        } else {
            None
        }
    }
}

#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq)]
pub(crate) enum NormalizeMode {
    None,
    Log1p,
    CpmLog1p,
}

#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq)]
pub(crate) enum PanelMode {
    Sum,
    ModuleScore,
}

#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq)]
pub(crate) enum SignalKind {
    Raw,
    Gradient,
    GradMag,
    Laplacian,
    HessianRidge,
    HessianValley,
    StructureTensor,
    Divergence,
    Curl,
    DistanceTransform,
    Curvature,
    FractalDimension,
    Skeletonization,
    Diffusion,
    MultiscaleLog,
    CrossGradient,
}

#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq)]
pub(crate) enum LaplacianVizMode {
    Diverging,
    ZeroCrossings,
    Both,
}

#[derive(Clone, Copy, Debug, ValueEnum)]
pub(crate) enum HeightModeArg {
    Raw,
    Abs,
    Signed,
}

#[derive(Clone, Copy, Debug, ValueEnum)]
pub(crate) enum NormalizationArg {
    None,
    Minmax,
    Robustz,
    Percentile,
}
