use std::path::{Path, PathBuf};

use outline::{MaskPipeline, Outline};

use crate::cli::{
    AlphaFromArg, CliMaskProcessingRequest, GlobalOptions, MaskExportSource, MaskProcessingArgs,
    MaskSourceArg,
};

fn resolve_model_path(global: &GlobalOptions) -> PathBuf {
    resolve_model_path_impl(global, default_cached_model_path().as_deref())
}

#[cfg(feature = "fetch-model")]
fn default_cached_model_path() -> Option<PathBuf> {
    Some(crate::model_fetch::default_model_cache_path())
}

#[cfg(not(feature = "fetch-model"))]
fn default_cached_model_path() -> Option<PathBuf> {
    None
}

fn resolve_model_path_impl(global: &GlobalOptions, cached: Option<&Path>) -> PathBuf {
    if let Some(model) = global.model.clone() {
        return model;
    }

    if let Some(cached) = cached.filter(|p| p.is_file()) {
        eprintln!("Using cached model at {}", cached.display());
        return cached.to_path_buf();
    }

    PathBuf::from(outline::DEFAULT_MODEL_PATH)
}

/// The convenience function to build an Outline instance with the input global options.
pub fn build_outline(global: &GlobalOptions) -> Outline {
    #[cfg(all(feature = "backend-rten", not(feature = "backend-ort")))]
    if global.intra_threads.is_some() {
        eprintln!(
            "Warning: --intra-threads is only supported by the ORT backend and will be ignored by the current backend."
        );
    }

    let outline = Outline::new(resolve_model_path(global))
        .with_input_resize_filter(global.input_resample_filter.into())
        .with_output_resize_filter(global.output_resample_filter.into())
        .with_intra_threads(global.intra_threads);

    if let Some(size) = global.model_input_size {
        outline.with_model_input_size(size.height(), size.width())
    } else {
        outline
    }
}

/// Derive a variant file path by appending a suffix before the extension.
pub fn derive_variant_path(input: &Path, suffix: &str, extension: &str) -> PathBuf {
    let mut derived = input.to_path_buf();
    let stem = input
        .file_stem()
        .map(|s| s.to_string_lossy().into_owned())
        .unwrap_or_else(|| suffix.to_string());
    let filename = format!("{}-{}.{}", stem, suffix, extension);
    derived.set_file_name(filename);
    derived
}

/// Resolve an export path from an optional double-Option field.
/// Returns Some(path) if export is requested, None otherwise.
pub fn resolve_export_path(
    opt: &Option<Option<PathBuf>>,
    input: &Path,
    suffix: &str,
) -> Option<PathBuf> {
    opt.as_ref().map(|inner| {
        inner
            .clone()
            .unwrap_or_else(|| derive_variant_path(input, suffix, "png"))
    })
}

/// Derive an SVG file path by changing the extension to "svg".
pub fn derive_svg_path(input: &Path) -> PathBuf {
    let mut path = input.to_path_buf();
    path.set_extension("svg");
    path
}

/// Determine if any mask processing is requested based on the provided arguments.
pub fn processing_requested(args: &MaskProcessingArgs) -> bool {
    !CliMaskProcessingRequest::from_args(args).is_empty()
}

/// Build the mask processing pipeline requested by CLI flags.
pub fn mask_pipeline_from_args(args: &MaskProcessingArgs) -> MaskPipeline {
    CliMaskProcessingRequest::from_args(args).to_pipeline()
}

/// Resolve alpha source with Auto behavior.
pub fn resolve_alpha_source(requested: AlphaFromArg, processing_requested: bool) -> AlphaFromArg {
    match requested {
        AlphaFromArg::Auto => {
            if processing_requested {
                AlphaFromArg::Processed
            } else {
                AlphaFromArg::Raw
            }
        }
        other => other,
    }
}

/// Resolve mask source arg with Auto behavior (trace command).
pub fn resolve_mask_source_arg(
    requested: MaskSourceArg,
    processing_requested: bool,
) -> MaskSourceArg {
    match requested {
        MaskSourceArg::Auto => {
            if processing_requested {
                MaskSourceArg::Processed
            } else {
                MaskSourceArg::Raw
            }
        }
        other => other,
    }
}

/// Resolve mask export source with Auto behavior (mask command).
pub fn resolve_mask_export_source(
    requested: MaskExportSource,
    processing_requested: bool,
) -> MaskExportSource {
    match requested {
        MaskExportSource::Auto => {
            if processing_requested {
                MaskExportSource::Processed
            } else {
                MaskExportSource::Raw
            }
        }
        other => other,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    mod resolve_model_path {
        use super::*;
        use crate::cli::ResampleFilter;
        use std::fs;
        use tempfile::TempDir;

        fn temp_dir(prefix: &str) -> TempDir {
            tempfile::Builder::new()
                .prefix(prefix)
                .tempdir()
                .expect("failed to create temp dir")
        }

        fn make_global(model: Option<PathBuf>) -> GlobalOptions {
            GlobalOptions {
                model,
                intra_threads: None,
                model_input_size: None,
                input_resample_filter: ResampleFilter::Triangle,
                output_resample_filter: ResampleFilter::Lanczos3,
            }
        }

        #[test]
        fn explicit_model_is_used_as_is() {
            let global = make_global(Some(PathBuf::from("explicit.onnx")));
            assert_eq!(
                super::resolve_model_path_impl(&global, None),
                PathBuf::from("explicit.onnx")
            );
        }

        #[cfg(feature = "fetch-model")]
        #[test]
        fn cached_model_used_when_present() {
            let cache_dir = temp_dir("outline-model-resolve-cache");
            let cached = cache_dir.path().join("model.onnx");
            fs::write(&cached, b"cached").expect("failed to write cached model");

            let global = make_global(None);
            assert_eq!(
                super::resolve_model_path_impl(&global, Some(&cached)),
                cached
            );
        }

        #[test]
        fn default_returned_when_nothing_available() {
            let global = make_global(None);
            assert_eq!(
                super::resolve_model_path_impl(&global, None),
                PathBuf::from("model.onnx")
            );
        }

        #[cfg(feature = "fetch-model")]
        #[test]
        fn default_returned_when_cached_path_missing() {
            let cache_dir = temp_dir("outline-model-resolve-missing-cache");
            let missing_cached = cache_dir.path().join("model.onnx");

            let global = make_global(None);
            assert_eq!(
                super::resolve_model_path_impl(&global, Some(&missing_cached)),
                PathBuf::from("model.onnx")
            );
        }

        #[test]
        fn explicit_model_preferred_over_cache() {
            let cache_dir = temp_dir("outline-model-resolve-prefer-explicit");
            let cached = cache_dir.path().join("model.onnx");
            fs::write(&cached, b"cached").expect("failed to write cached model");

            let global = make_global(Some(PathBuf::from("my-model.onnx")));
            assert_eq!(
                super::resolve_model_path_impl(&global, Some(&cached)),
                PathBuf::from("my-model.onnx")
            );
        }

        #[cfg(feature = "fetch-model")]
        #[test]
        fn cached_preferred_over_default() {
            let cache_dir = temp_dir("outline-model-resolve-cached-over-default");
            let cached = cache_dir.path().join("model.onnx");
            fs::write(&cached, b"cached").expect("failed to write cached model");

            let global = make_global(None);
            let result = super::resolve_model_path_impl(&global, Some(&cached));
            // Should use cached, not fall through to DEFAULT_MODEL_PATH
            assert_eq!(result, cached);
            assert_ne!(result, PathBuf::from(outline::DEFAULT_MODEL_PATH));
        }
    }

    mod derive_variant_path {
        use super::*;

        #[test]
        fn basic() {
            let input = Path::new("/path/to/image.png");
            let result = derive_variant_path(input, "matte", "png");
            assert_eq!(result, PathBuf::from("/path/to/image-matte.png"));
        }

        #[test]
        fn different_extension() {
            let input = Path::new("/path/to/photo.jpg");
            let result = derive_variant_path(input, "mask", "png");
            assert_eq!(result, PathBuf::from("/path/to/photo-mask.png"));
        }

        #[test]
        fn no_extension() {
            let input = Path::new("/path/to/image");
            let result = derive_variant_path(input, "foreground", "png");
            assert_eq!(result, PathBuf::from("/path/to/image-foreground.png"));
        }

        #[test]
        fn relative_path() {
            let input = Path::new("image.png");
            let result = derive_variant_path(input, "composite", "png");
            assert_eq!(result, PathBuf::from("image-composite.png"));
        }

        #[test]
        fn multi_dot_name() {
            // file_stem() returns "archive.tar", not "archive"
            let input = Path::new("/path/to/archive.tar.gz");
            let result = derive_variant_path(input, "mask", "png");
            assert_eq!(result, PathBuf::from("/path/to/archive.tar-mask.png"));
        }
    }

    mod resolve_export_path {
        use super::*;

        #[test]
        fn none_returns_none() {
            let opt: Option<Option<PathBuf>> = None;
            let input = Path::new("/path/to/image.png");
            let result = resolve_export_path(&opt, input, "matte");
            assert_eq!(result, None);
        }

        #[test]
        fn some_none_uses_default() {
            let opt: Option<Option<PathBuf>> = Some(None);
            let input = Path::new("/path/to/image.png");
            let result = resolve_export_path(&opt, input, "matte");
            assert_eq!(result, Some(PathBuf::from("/path/to/image-matte.png")));
        }

        #[test]
        fn some_some_uses_custom() {
            let custom_path = PathBuf::from("/custom/output.png");
            let opt: Option<Option<PathBuf>> = Some(Some(custom_path.clone()));
            let input = Path::new("/path/to/image.png");
            let result = resolve_export_path(&opt, input, "matte");
            assert_eq!(result, Some(custom_path));
        }

        #[test]
        fn different_suffixes() {
            let opt: Option<Option<PathBuf>> = Some(None);
            let input = Path::new("photo.jpg");

            assert_eq!(
                resolve_export_path(&opt, input, "foreground"),
                Some(PathBuf::from("photo-foreground.png"))
            );
            assert_eq!(
                resolve_export_path(&opt, input, "mask"),
                Some(PathBuf::from("photo-mask.png"))
            );
            assert_eq!(
                resolve_export_path(&opt, input, "bg-layer"),
                Some(PathBuf::from("photo-bg-layer.png"))
            );
        }
    }

    mod derive_svg_path {
        use super::*;

        #[test]
        fn changes_extension() {
            let input = Path::new("/path/to/image.png");
            let result = derive_svg_path(input);
            assert_eq!(result, PathBuf::from("/path/to/image.svg"));
        }

        #[test]
        fn no_extension() {
            let input = Path::new("/path/to/image");
            let result = derive_svg_path(input);
            assert_eq!(result, PathBuf::from("/path/to/image.svg"));
        }
    }

    mod resolve_alpha_source {
        use super::*;

        #[test]
        fn auto_with_processing() {
            let result = resolve_alpha_source(AlphaFromArg::Auto, true);
            assert!(matches!(result, AlphaFromArg::Processed));
        }

        #[test]
        fn auto_without_processing() {
            let result = resolve_alpha_source(AlphaFromArg::Auto, false);
            assert!(matches!(result, AlphaFromArg::Raw));
        }

        #[test]
        fn explicit_raw() {
            let result = resolve_alpha_source(AlphaFromArg::Raw, true);
            assert!(matches!(result, AlphaFromArg::Raw));
        }

        #[test]
        fn explicit_processed() {
            let result = resolve_alpha_source(AlphaFromArg::Processed, false);
            assert!(matches!(result, AlphaFromArg::Processed));
        }
    }

    mod processing_requested {
        use crate::cli::{CliMaskProcessingStep, MaskProcessingArgs};

        fn default_args() -> MaskProcessingArgs {
            MaskProcessingArgs {
                blur: None,
                threshold: None,
                no_implicit_threshold: false,
                dilate: None,
                erode: None,
                erode_border: None,
                fill_holes: None,
                ordered_steps: vec![],
            }
        }

        #[test]
        fn false_for_defaults() {
            assert!(!super::processing_requested(&default_args()));
        }

        #[test]
        fn true_for_threshold_only() {
            let args = MaskProcessingArgs {
                ordered_steps: vec![CliMaskProcessingStep::Threshold(200)],
                ..default_args()
            };

            assert!(super::processing_requested(&args));
        }

        #[test]
        fn true_for_default_threshold() {
            let args = MaskProcessingArgs {
                ordered_steps: vec![CliMaskProcessingStep::Threshold(120)],
                ..default_args()
            };

            assert!(super::processing_requested(&args));
        }
    }

    mod resolve_mask_source_arg {
        use super::*;

        #[test]
        fn auto_with_processing() {
            let result = resolve_mask_source_arg(MaskSourceArg::Auto, true);
            assert!(matches!(result, MaskSourceArg::Processed));
        }

        #[test]
        fn auto_without_processing() {
            let result = resolve_mask_source_arg(MaskSourceArg::Auto, false);
            assert!(matches!(result, MaskSourceArg::Raw));
        }

        #[test]
        fn explicit_raw() {
            let result = resolve_mask_source_arg(MaskSourceArg::Raw, true);
            assert!(matches!(result, MaskSourceArg::Raw));
        }

        #[test]
        fn explicit_processed() {
            let result = resolve_mask_source_arg(MaskSourceArg::Processed, false);
            assert!(matches!(result, MaskSourceArg::Processed));
        }
    }

    mod resolve_mask_export_source {
        use super::*;

        #[test]
        fn auto_with_processing() {
            let result = resolve_mask_export_source(MaskExportSource::Auto, true);
            assert!(matches!(result, MaskExportSource::Processed));
        }

        #[test]
        fn auto_without_processing() {
            let result = resolve_mask_export_source(MaskExportSource::Auto, false);
            assert!(matches!(result, MaskExportSource::Raw));
        }

        #[test]
        fn explicit_raw() {
            let result = resolve_mask_export_source(MaskExportSource::Raw, true);
            assert!(matches!(result, MaskExportSource::Raw));
        }

        #[test]
        fn explicit_processed() {
            let result = resolve_mask_export_source(MaskExportSource::Processed, false);
            assert!(matches!(result, MaskExportSource::Processed));
        }
    }
}
