//! Application configuration.
//!
//! Configuration can be loaded from:
//! 1. TOML file (config.toml)
//! 2. Environment variables (prefixed with IMMICH_)
//! 3. Programmatic overrides

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use crate::error::{Error, Result};

/// Default path to the config file, relative to the working directory.
pub const CONFIG_PATH: &str = "config/config.toml";

/// Main configuration struct.
///
/// This is the single source of truth for all configuration.
/// Pass this explicitly to functions that need it - no globals.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
    /// Immich API configuration.
    pub api: ApiConfig,

    /// Face processing parameters.
    pub processing: ProcessingConfig,

    /// Video output settings.
    pub video: VideoConfig,

    /// Output directory for processed images and video.
    pub output_dir: PathBuf,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            api: ApiConfig::default(),
            processing: ProcessingConfig::default(),
            video: VideoConfig::default(),
            output_dir: PathBuf::from("output"),
        }
    }
}

/// Immich API connection settings.
#[derive(Clone, Serialize, Deserialize)]
pub struct ApiConfig {
    /// API key for authentication.
    pub api_key: String,

    /// Base URL of the Immich instance (e.g., "http://192.168.1.94:2283/api").
    pub base_url: String,

    /// Request timeout in seconds.
    #[serde(default = "default_timeout")]
    pub timeout_secs: u64,
}

impl std::fmt::Debug for ApiConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ApiConfig")
            .field("api_key", &"[REDACTED]")
            .field("base_url", &self.base_url)
            .field("timeout_secs", &self.timeout_secs)
            .finish()
    }
}

fn default_timeout() -> u64 {
    30
}

impl Default for ApiConfig {
    fn default() -> Self {
        Self {
            api_key: String::new(),
            base_url: String::new(),
            timeout_secs: default_timeout(),
        }
    }
}

// ============================================================================
// Step Configurations
// ============================================================================

/// Face resolution validation configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FaceResolutionConfig {
    /// Whether face resolution validation is enabled.
    pub enabled: bool,

    /// Minimum face width/height in pixels.
    /// Faces smaller than this will be skipped.
    pub min_size: u32,
}

impl Default for FaceResolutionConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            min_size: 80,
        }
    }
}

impl FaceResolutionConfig {
    /// Validate the configuration values.
    pub fn validate(&self) -> Result<()> {
        if self.enabled && self.min_size == 0 {
            return Err(Error::Config(
                "Face resolution min_size must be greater than 0".to_string(),
            ));
        }
        Ok(())
    }
}

/// Brightness validation configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrightnessConfig {
    /// Whether brightness validation is enabled.
    pub enabled: bool,

    /// Minimum acceptable brightness (0.0-1.0).
    /// Images darker than this will be skipped.
    pub min_brightness: f32,

    /// Maximum acceptable brightness (0.0-1.0).
    /// Images brighter than this will be skipped.
    pub max_brightness: f32,
}

impl Default for BrightnessConfig {
    fn default() -> Self {
        Self {
            enabled: false,
            min_brightness: 0.1,
            max_brightness: 0.9,
        }
    }
}

impl BrightnessConfig {
    /// Validate the configuration values.
    pub fn validate(&self) -> Result<()> {
        if !self.enabled {
            return Ok(());
        }
        if self.min_brightness < 0.0 || self.min_brightness > 1.0 {
            return Err(Error::Config(
                "Brightness min_brightness must be between 0.0 and 1.0".to_string(),
            ));
        }
        if self.max_brightness < 0.0 || self.max_brightness > 1.0 {
            return Err(Error::Config(
                "Brightness max_brightness must be between 0.0 and 1.0".to_string(),
            ));
        }
        if self.min_brightness >= self.max_brightness {
            return Err(Error::Config(
                "Brightness min_brightness must be less than max_brightness".to_string(),
            ));
        }
        Ok(())
    }
}

/// Blur detection configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlurConfig {
    /// Whether blur detection is enabled.
    pub enabled: bool,

    /// Minimum Laplacian variance threshold.
    /// Images with Laplacian variance below this are considered blurry.
    /// Typical values: < 100 = blurry, 100–500 = borderline, > 500 = sharp.
    pub min_sharpness: f32,
}

impl Default for BlurConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            min_sharpness: 75.0,
        }
    }
}

impl BlurConfig {
    /// Validate the configuration values.
    pub fn validate(&self) -> Result<()> {
        if self.enabled && self.min_sharpness < 0.0 {
            return Err(Error::Config(
                "Blur min_sharpness must be non-negative".to_string(),
            ));
        }
        Ok(())
    }
}

/// Crop padding filter configuration.
///
/// Skips images where too much of the crop region falls outside the original
/// image bounds (filled by edge-pixel replication).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CropConfig {
    /// Whether crop padding filtering is enabled.
    pub enabled: bool,

    /// Maximum allowed padding percentage (0.0-100.0).
    /// Images where the replicated-fill area exceeds this will be skipped.
    pub max_padding_percent: f32,
}

impl Default for CropConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            max_padding_percent: 30.0,
        }
    }
}

impl CropConfig {
    /// Validate the configuration values.
    pub fn validate(&self) -> Result<()> {
        if self.enabled && !(0.0..=100.0).contains(&self.max_padding_percent) {
            return Err(Error::Config(
                "Crop max_padding_percent must be between 0.0 and 100.0".to_string(),
            ));
        }
        Ok(())
    }
}

/// Output/resize configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
    /// Output image size (width and height in pixels).
    pub size: u32,

    /// Keep intermediate images (original, cropped) for inspection.
    pub keep_intermediates: bool,
}

impl Default for OutputConfig {
    fn default() -> Self {
        Self {
            size: 512,
            keep_intermediates: false,
        }
    }
}

impl OutputConfig {
    /// Validate the configuration values.
    pub fn validate(&self) -> Result<()> {
        if self.size < 64 {
            return Err(Error::Config(
                "Output size must be at least 64 pixels".to_string(),
            ));
        }
        if self.size > 4096 {
            return Err(Error::Config(
                "Output size must be at most 4096 pixels".to_string(),
            ));
        }
        Ok(())
    }
}

/// Head pose estimation configuration.
///
/// Uses DMHead ONNX model to estimate head pose angles and filter
/// non-front-facing faces.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeadPoseConfig {
    /// Whether head pose filtering is enabled.
    pub enabled: bool,

    /// Maximum allowed yaw angle (left/right turn) in degrees.
    pub max_yaw: f32,

    /// Maximum allowed pitch angle (up/down tilt) in degrees.
    pub max_pitch: f32,
}

impl Default for HeadPoseConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            max_yaw: 20.0,
            max_pitch: 20.0,
        }
    }
}

impl HeadPoseConfig {
    /// Validate the configuration values.
    pub fn validate(&self) -> Result<()> {
        if self.enabled {
            if self.max_yaw < 0.0 || self.max_yaw > 90.0 {
                return Err(Error::Config(
                    "Head pose max_yaw must be between 0 and 90 degrees".to_string(),
                ));
            }
            if self.max_pitch < 0.0 || self.max_pitch > 90.0 {
                return Err(Error::Config(
                    "Head pose max_pitch must be between 0 and 90 degrees".to_string(),
                ));
            }
        }
        Ok(())
    }
}

/// Eye filter configuration for blink detection.
///
/// Uses Eye Aspect Ratio (EAR) computed from facial landmarks to detect
/// closed eyes.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EyeFilterConfig {
    /// Whether eye filtering is enabled.
    pub enabled: bool,

    /// Minimum Eye Aspect Ratio (EAR) threshold.
    /// Eyes with EAR below this are considered closed.
    pub min_ear: f32,
}

impl Default for EyeFilterConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            min_ear: 0.12,
        }
    }
}

impl EyeFilterConfig {
    /// Validate the configuration values.
    pub fn validate(&self) -> Result<()> {
        if self.enabled && !(0.0..=0.5).contains(&self.min_ear) {
            return Err(Error::Config(
                "Eye filter min_ear must be between 0.0 and 0.5".to_string(),
            ));
        }
        Ok(())
    }
}

/// Face alignment configuration for landmark-based alignment.
///
/// Aligns faces based on eye positions detected from facial landmarks,
/// ensuring consistent eye placement across all images for smoother timelapses.
///
/// Uses a single `eye_distance` parameter to control framing. Eye positions are derived by
/// estimating the visual face center (see `FACE_CENTER_OFFSET_IN_IPD`) from standard face
/// proportions, then placing that point at the image center with a small downward offset to
/// show more neck.
///
/// Note: Face alignment is always enabled and is a core part of the pipeline.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlignmentConfig {
    /// Distance between left and right eye centers as a fraction of output
    /// image width (0.0-1.0). Default 0.15 means the eyes span 15% of the
    /// output width. Larger values zoom in (bigger face), smaller values
    /// zoom out (more background).
    pub eye_distance: f32,
}

impl Default for AlignmentConfig {
    fn default() -> Self {
        Self { eye_distance: 0.15 }
    }
}

impl AlignmentConfig {
    /// How far below the eye line the face center sits, in units of
    /// inter-pupillary distance (IPD). Value of 0.5 means the face center
    /// is 0.5 × IPD below the eyes.
    ///
    /// From face proportions: the vertical midpoint between hairline and chin
    /// is ~0.4× IPD below the eyes; adding a small neck bias brings it to
    /// 0.5× IPD. The alignment formula places this point at image center (y=0.5):
    ///
    ///   eye_y = 0.5 - eye_distance * FACE_CENTER_OFFSET_IN_IPD
    const FACE_CENTER_BELOW_EYES: f32 = 0.5;

    /// Validate the configuration values.
    pub fn validate(&self) -> Result<()> {
        if self.eye_distance <= 0.0 || self.eye_distance >= 1.0 {
            return Err(Error::Config(
                "Alignment eye_distance must be between 0.0 and 1.0 (exclusive)".to_string(),
            ));
        }
        Ok(())
    }

    /// Target X position for the left eye as a fraction of output width.
    /// Eyes are centered horizontally: `left_eye_x = (1 - eye_distance) / 2`.
    pub fn left_eye_x(&self) -> f32 {
        (1.0 - self.eye_distance) / 2.0
    }

    /// Target Y position for both eyes as a fraction of output height.
    /// Derived by placing the estimated face center at the image center:
    /// `eye_y = 0.5 - eye_distance * FACE_CENTER_BELOW_EYES`.
    pub fn left_eye_y(&self) -> f32 {
        0.5 - self.eye_distance * Self::FACE_CENTER_BELOW_EYES
    }
}

/// Position for timestamp text overlay.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum TextPosition {
    #[default]
    TopLeft,
    TopRight,
    BottomLeft,
    BottomRight,
}

/// Timestamp overlay configuration.
///
/// Overlays date information on the processed image.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimestampConfig {
    /// Whether timestamp overlay is enabled.
    pub enabled: bool,

    /// Position of the timestamp text.
    pub position: TextPosition,

    /// Whether to display the year.
    pub year: bool,

    /// Whether to display the month.
    pub month: bool,

    /// Whether to display the day.
    pub day: bool,
}

impl Default for TimestampConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            position: TextPosition::BottomLeft,
            year: true,
            month: true,
            day: true,
        }
    }
}

impl TimestampConfig {
    /// Validate the configuration values.
    pub fn validate(&self) -> Result<()> {
        // No validation needed for booleans and enum
        Ok(())
    }
}

/// Time range granularity for photo limiting.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum TimeRange {
    #[default]
    Day,
    Week,
    Month,
}

/// Time interval configuration.
///
/// Limits the number of successfully processed photos per time range
/// (day/week/month) to avoid over-representation of busy periods.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeIntervalConfig {
    /// Whether photo limiting is enabled.
    pub enabled: bool,

    /// Maximum number of photos per time range.
    pub max_photos: u32,

    /// Time range granularity.
    pub time_range: TimeRange,
}

impl Default for TimeIntervalConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            max_photos: 5,
            time_range: TimeRange::Day,
        }
    }
}

impl TimeIntervalConfig {
    /// Validate the configuration values.
    pub fn validate(&self) -> Result<()> {
        if self.enabled && self.max_photos == 0 {
            return Err(Error::Config(
                "Time interval max_photos must be greater than 0".to_string(),
            ));
        }
        if self.enabled && self.max_photos > 100 {
            return Err(Error::Config(
                "Time interval max_photos must be at most 100".to_string(),
            ));
        }
        Ok(())
    }
}

// ============================================================================
// Main Processing Configuration
// ============================================================================

/// Face processing parameters.
///
/// Each pipeline step has its own sub-configuration with an `enabled` flag.
/// This allows fine-grained control over which steps run and their parameters.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ProcessingConfig {
    /// Number of parallel workers for processing.
    pub max_workers: usize,

    /// Whether to download Immich preview images (1440p JPEG) instead of originals.
    /// Preview images are much faster to decode and sufficient for most use cases.
    /// Disable only if you need full original resolution (e.g., very small faces in high-res photos).
    #[serde(default = "default_true")]
    pub use_preview: bool,

    /// Face resolution validation settings.
    #[serde(default)]
    pub face_resolution: FaceResolutionConfig,

    /// Crop padding filter settings.
    #[serde(default)]
    pub crop: CropConfig,

    /// Blur detection settings.
    #[serde(default)]
    pub blur: BlurConfig,

    /// Brightness validation settings.
    #[serde(default)]
    pub brightness: BrightnessConfig,

    /// Head pose estimation settings.
    #[serde(default)]
    pub head_pose: HeadPoseConfig,

    /// Eye filter settings (blink detection).
    #[serde(default)]
    pub eye_filter: EyeFilterConfig,

    /// Output image settings.
    #[serde(default)]
    pub output: OutputConfig,

    /// Face alignment settings (landmark-based).
    #[serde(default)]
    pub alignment: AlignmentConfig,

    /// Timestamp overlay settings.
    #[serde(default)]
    pub timestamp: TimestampConfig,

    /// Time interval settings.
    #[serde(default)]
    pub time_interval: TimeIntervalConfig,
}

impl Default for ProcessingConfig {
    fn default() -> Self {
        Self {
            max_workers: 1,
            use_preview: true,
            face_resolution: FaceResolutionConfig::default(),
            crop: CropConfig::default(),
            blur: BlurConfig::default(),
            brightness: BrightnessConfig::default(),
            head_pose: HeadPoseConfig::default(),
            eye_filter: EyeFilterConfig::default(),
            output: OutputConfig::default(),
            alignment: AlignmentConfig::default(),
            timestamp: TimestampConfig::default(),
            time_interval: TimeIntervalConfig::default(),
        }
    }
}

impl ProcessingConfig {
    /// Validate all step configurations.
    pub fn validate(&self) -> Result<()> {
        if self.max_workers == 0 {
            return Err(Error::Config(
                "max_workers must be greater than 0".to_string(),
            ));
        }
        self.face_resolution.validate()?;
        self.crop.validate()?;
        self.blur.validate()?;
        self.brightness.validate()?;
        self.head_pose.validate()?;
        self.eye_filter.validate()?;
        self.output.validate()?;
        self.alignment.validate()?;
        self.timestamp.validate()?;
        self.time_interval.validate()?;
        Ok(())
    }
}

fn default_true() -> bool {
    true
}

// ============================================================================
// Video Configuration
// ============================================================================

/// Video compilation settings.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct VideoConfig {
    /// Whether to compile video after processing.
    pub enabled: bool,

    /// Output video framerate.
    pub framerate: u32,

    /// Video codec (e.g., "libx264").
    pub codec: String,

    /// Constant Rate Factor for quality (0-51, lower = better, 18-28 recommended).
    pub crf: u32,
}

impl Default for VideoConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            framerate: 20,
            codec: "libx264".to_string(),
            crf: 23,
        }
    }
}

/// Valid video codecs that can be used with FFmpeg.
const VALID_CODECS: &[&str] = &["libx264", "libx265", "libvpx", "libvpx-vp9", "libaom-av1"];

impl VideoConfig {
    /// Validate the configuration values.
    pub fn validate(&self) -> Result<()> {
        if self.framerate == 0 {
            return Err(Error::Config(
                "Video framerate must be greater than 0".to_string(),
            ));
        }
        if self.framerate > 120 {
            return Err(Error::Config(
                "Video framerate must be at most 120".to_string(),
            ));
        }
        if self.crf > 51 {
            return Err(Error::Config(
                "Video CRF must be between 0 and 51".to_string(),
            ));
        }
        if !VALID_CODECS.contains(&self.codec.as_str()) {
            return Err(Error::Config(format!(
                "Video codec must be one of: {}, got '{}'",
                VALID_CODECS.join(", "),
                self.codec
            )));
        }
        Ok(())
    }
}

// ============================================================================
// Persistence
// ============================================================================

/// Persistable configuration (excludes sensitive API credentials).
///
/// This struct contains only the settings that can safely be written to disk.
/// API credentials should always come from environment variables.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersistableConfig {
    pub processing: ProcessingConfig,
    pub video: VideoConfig,
}

impl From<&Config> for PersistableConfig {
    fn from(config: &Config) -> Self {
        Self {
            processing: config.processing.clone(),
            video: config.video.clone(),
        }
    }
}

// ============================================================================
// Config Loading and Validation
// ============================================================================

impl Config {
    /// Load configuration from a TOML file.
    pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self> {
        let content = std::fs::read_to_string(path)?;
        let config: Config = toml::from_str(&content).map_err(|e| Error::Config(e.to_string()))?;
        Ok(config)
    }

    /// Load configuration from environment variables.
    ///
    /// Recognized variables:
    /// - IMMICH_API_KEY - API key for Immich
    /// - IMMICH_BASE_URL - Base URL of Immich instance
    pub fn from_env() -> Self {
        Config::default().with_env()
    }

    /// Merge environment variables into an existing config.
    pub fn with_env(mut self) -> Self {
        if let Ok(key) = std::env::var("IMMICH_API_KEY") {
            self.api.api_key = key;
        }
        if let Ok(url) = std::env::var("IMMICH_BASE_URL") {
            self.api.base_url = url;
        }
        self
    }

    /// Validate the entire configuration.
    pub fn validate(&self) -> Result<()> {
        // API validation
        if self.api.api_key.is_empty() {
            return Err(Error::Config("API key is required".to_string()));
        }
        if self.api.base_url.is_empty() {
            return Err(Error::Config("Base URL is required".to_string()));
        }

        // Delegate to sub-config validation
        self.processing.validate()?;
        self.video.validate()?;

        Ok(())
    }

    /// Save processing and video configuration to a TOML file.
    ///
    /// Only saves `processing` and `video` sections - API credentials are
    /// intentionally excluded as they should come from environment variables.
    pub fn save_to_file(&self, path: impl AsRef<std::path::Path>) -> Result<()> {
        let path = path.as_ref();
        let persistable = PersistableConfig::from(self);
        let content = toml::to_string_pretty(&persistable)
            .map_err(|e| Error::Config(format!("Failed to serialize config: {}", e)))?;

        // Write to a temp file then atomically rename to avoid partial writes
        let tmp_path = path.with_extension("toml.tmp");
        std::fs::write(&tmp_path, &content).map_err(|e| {
            if e.kind() == std::io::ErrorKind::PermissionDenied {
                Error::Config(format!(
                    "Permission denied writing config to '{}'. \
                    If running in Docker, mount a writable volume at the config directory \
                    (e.g. -v /host/config:/app/config).",
                    tmp_path.display()
                ))
            } else {
                Error::Io(e)
            }
        })?;
        std::fs::rename(&tmp_path, path).map_err(|e| {
            if e.kind() == std::io::ErrorKind::PermissionDenied {
                Error::Config(format!(
                    "Permission denied saving config to '{}'. \
                    If running in Docker, mount a writable volume at the config directory \
                    (e.g. -v /host/config:/app/config).",
                    path.display()
                ))
            } else {
                Error::Io(e)
            }
        })?;
        Ok(())
    }
}

// ============================================================================
// Tests
// ============================================================================

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

    #[test]
    fn test_default_config() {
        let config = Config::default();
        assert_eq!(config.processing.output.size, 512);
        assert_eq!(config.processing.face_resolution.min_size, 80);
        assert_eq!(config.video.framerate, 20);
    }

    #[test]
    fn test_validation_missing_api_key() {
        let config = Config::default();
        let result = config.validate();
        assert!(result.is_err());
    }

    #[test]
    fn test_brightness_validation() {
        let mut config = BrightnessConfig {
            enabled: true,
            min_brightness: 0.1,
            max_brightness: 0.9,
        };

        // Valid config
        assert!(config.validate().is_ok());

        // Invalid: min >= max
        config.min_brightness = 0.9;
        config.max_brightness = 0.1;
        assert!(config.validate().is_err());

        // Invalid: out of range
        config.min_brightness = -0.1;
        config.max_brightness = 0.9;
        assert!(config.validate().is_err());

        // Disabled config skips validation
        config.enabled = false;
        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_video_validation() {
        let mut config = VideoConfig::default();

        // Valid
        assert!(config.validate().is_ok());

        // Invalid CRF
        config.crf = 52;
        assert!(config.validate().is_err());

        // Invalid framerate
        config.crf = 23;
        config.framerate = 0;
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_output_validation() {
        let mut config = OutputConfig::default();

        // Valid
        assert!(config.validate().is_ok());

        // Too small
        config.size = 32;
        assert!(config.validate().is_err());

        // Too large
        config.size = 8192;
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_save_and_load_config() {
        // Create a config with custom values
        let mut config = Config::default();
        config.processing.output.size = 256;
        config.processing.face_resolution.min_size = 100;
        config.video.framerate = 30;
        config.video.crf = 18;

        // Save to a temp file
        let temp_dir = std::env::temp_dir();
        let temp_path = temp_dir.join("test_config_new.toml");

        config
            .save_to_file(&temp_path)
            .expect("Failed to save config");

        // Verify file was created and contains expected content
        let content = std::fs::read_to_string(&temp_path).expect("Failed to read config");
        assert!(content.contains("size = 256"));
        assert!(content.contains("min_size = 100"));
        assert!(content.contains("framerate = 30"));
        assert!(content.contains("crf = 18"));

        // Verify API credentials are NOT in the file
        assert!(!content.contains("api_key"));
        assert!(!content.contains("base_url"));

        // Clean up
        let _ = std::fs::remove_file(&temp_path);
    }
}
