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

use serde::{Deserialize, Serialize};

use crate::operations::*;
use crate::types::{ImageFormat, ImageSource};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessingJob {
    pub input_dir: PathBuf,
    pub output_dir: PathBuf,
    #[serde(skip)]
    pub sources: Vec<ImageSource>,
    pub resize: Option<ResizeConfig>,
    pub resize_algorithm: ResizeAlgorithm,
    pub rotation: Option<Rotation>,
    pub flip: Option<Flip>,
    pub adjustments: Option<AdjustmentsConfig>,
    pub convert: Option<ConvertConfig>,
    pub compress: Option<CompressConfig>,
    pub metadata: Option<MetadataConfig>,
    pub watermark: Option<WatermarkConfig>,
    pub rename: Option<RenameConfig>,
    pub overwrite_behavior: OverwriteAction,
    pub preserve_directory_structure: bool,
    pub progressive_jpeg: bool,
    pub avif_speed: u8,
    pub output_dpi: u32,
}

impl ProcessingJob {
    pub fn new(input_dir: impl AsRef<Path>, output_dir: impl AsRef<Path>) -> Self {
        Self {
            input_dir: input_dir.as_ref().to_path_buf(),
            output_dir: output_dir.as_ref().to_path_buf(),
            sources: Vec::new(),
            resize: None,
            resize_algorithm: ResizeAlgorithm::default(),
            rotation: None,
            flip: None,
            adjustments: None,
            convert: None,
            compress: None,
            metadata: None,
            watermark: None,
            rename: None,
            overwrite_behavior: OverwriteAction::default(),
            preserve_directory_structure: false,
            progressive_jpeg: false,
            avif_speed: 6,
            output_dpi: 0,
        }
    }

    pub fn add_source(&mut self, path: impl AsRef<Path>) {
        self.sources.push(ImageSource::from_path(path));
    }

    pub fn operation_count(&self) -> usize {
        let mut count = 0;
        if self.resize.is_some() { count += 1; }
        if self.rotation.is_some() { count += 1; }
        if self.flip.is_some() { count += 1; }
        if self.adjustments.as_ref().is_some_and(|a| !a.is_noop()) { count += 1; }
        if self.convert.is_some() { count += 1; }
        if self.compress.is_some() { count += 1; }
        if self.metadata.is_some() { count += 1; }
        if self.watermark.is_some() { count += 1; }
        if self.rename.is_some() { count += 1; }
        count
    }

    /// Returns true if the job requires decoding/encoding pixel data.
    /// When false, we can use a fast copy-and-rename path.
    pub fn needs_pixel_processing(&self) -> bool {
        self.resize.is_some()
            || matches!(self.rotation, Some(r) if !matches!(r, Rotation::None))
            || matches!(self.flip, Some(f) if !matches!(f, Flip::None))
            || self.adjustments.as_ref().is_some_and(|a| !a.is_noop())
            || self.convert.is_some()
            || self.compress.is_some()
            || self.watermark.is_some()
            || self.output_dpi > 0
    }

    pub fn output_path_for(
        &self,
        source: &ImageSource,
        output_format: Option<ImageFormat>,
    ) -> PathBuf {
        let stem = source
            .path
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("output");

        let ext = output_format
            .map(|f| f.extension())
            .or_else(|| {
                source
                    .path
                    .extension()
                    .and_then(|e| e.to_str())
            })
            .unwrap_or("bin");

        let filename = format!("{}.{}", stem, ext);

        if self.preserve_directory_structure {
            // Maintain relative path from input_dir
            if let Ok(rel) = source.path.strip_prefix(&self.input_dir) {
                if let Some(parent) = rel.parent() {
                    if parent.components().count() > 0 {
                        return self.output_dir.join(parent).join(filename);
                    }
                }
            }
        }

        self.output_dir.join(filename)
    }
}
