use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::Path;
use std::time::SystemTime;

use crate::state::{atomic_write_json, load_json_with_fallbacks, BACKUP_COUNT};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WindowState {
    pub width: i32,
    pub height: i32,
    pub x: Option<i32>,
    pub y: Option<i32>,
}

impl Default for WindowState {
    fn default() -> Self {
        Self {
            width: 1320,
            height: 860,
            x: None,
            y: None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Prefs {
    #[serde(default = "default_version")]
    pub version: u32,
    #[serde(default = "default_ui_zoom")]
    pub ui_zoom: f64,
    #[serde(default = "default_split_ratio")]
    pub split_ratio: f64,
    #[serde(default = "default_dock_ratio")]
    pub dock_ratio: f64,
    #[serde(default)]
    pub always_on_top: bool,
    #[serde(default)]
    pub window: WindowState,
    #[serde(default)]
    pub last_folder_path: Option<String>,
    #[serde(default)]
    pub last_library_id: Option<String>,
    #[serde(default)]
    pub updated_at: u64,
}

fn default_version() -> u32 {
    19
}
fn default_ui_zoom() -> f64 {
    1.0
}
fn default_split_ratio() -> f64 {
    0.62
}
fn default_dock_ratio() -> f64 {
    0.62
}

impl Default for Prefs {
    fn default() -> Self {
        Self {
            version: 19,
            ui_zoom: 1.0,
            split_ratio: 0.62,
            dock_ratio: 0.62,
            always_on_top: false,
            window: WindowState::default(),
            last_folder_path: None,
            last_library_id: None,
            updated_at: 0,
        }
    }
}

impl Prefs {
    /// Load preferences from `{state_dir}/prefs.json`, merging with defaults.
    ///
    /// Window fields are merged individually so that a saved file with only
    /// `{"window": {"width": 800}}` keeps the default height, x, and y.
    pub fn load(state_dir: &Path) -> Prefs {
        let path = state_dir.join("prefs.json");
        let loaded = load_json_with_fallbacks(&path, BACKUP_COUNT);

        let raw = match loaded {
            Some(v) => v,
            None => return Prefs::default(),
        };

        let raw_obj = match raw.as_object() {
            Some(obj) => obj,
            None => return Prefs::default(),
        };

        // Start with defaults
        let mut prefs = Prefs::default();

        // Merge window fields individually if present
        if let Some(Value::Object(win_obj)) = raw_obj.get("window") {
            if let Some(Value::Number(n)) = win_obj.get("width") {
                if let Some(v) = n.as_i64() {
                    prefs.window.width = v as i32;
                }
            }
            if let Some(Value::Number(n)) = win_obj.get("height") {
                if let Some(v) = n.as_i64() {
                    prefs.window.height = v as i32;
                }
            }
            if let Some(val) = win_obj.get("x") {
                prefs.window.x = val.as_i64().map(|v| v as i32);
            }
            if let Some(val) = win_obj.get("y") {
                prefs.window.y = val.as_i64().map(|v| v as i32);
            }
        }

        // Merge all other top-level keys
        if let Some(Value::Number(n)) = raw_obj.get("version") {
            if let Some(v) = n.as_u64() {
                prefs.version = v as u32;
            }
        }
        if let Some(Value::Number(n)) = raw_obj.get("ui_zoom") {
            if let Some(v) = n.as_f64() {
                prefs.ui_zoom = v;
            }
        }
        if let Some(Value::Number(n)) = raw_obj.get("split_ratio") {
            if let Some(v) = n.as_f64() {
                prefs.split_ratio = v;
            }
        }
        if let Some(Value::Number(n)) = raw_obj.get("dock_ratio") {
            if let Some(v) = n.as_f64() {
                prefs.dock_ratio = v;
            }
        }
        if let Some(Value::Bool(b)) = raw_obj.get("always_on_top") {
            prefs.always_on_top = *b;
        }
        if let Some(val) = raw_obj.get("last_folder_path") {
            prefs.last_folder_path = val.as_str().map(|s| s.to_string());
        }
        if let Some(val) = raw_obj.get("last_library_id") {
            prefs.last_library_id = val.as_str().map(|s| s.to_string());
        }
        if let Some(Value::Number(n)) = raw_obj.get("updated_at") {
            if let Some(v) = n.as_u64() {
                prefs.updated_at = v;
            }
        }

        prefs
    }

    /// Save preferences to `{state_dir}/prefs.json`.
    pub fn save(&self, state_dir: &Path) {
        let path = state_dir.join("prefs.json");
        let value = serde_json::to_value(self).expect("failed to serialize Prefs");
        atomic_write_json(&path, &value, BACKUP_COUNT);
    }

    /// Apply a partial JSON update, merge window fields individually, set
    /// `updated_at` to the current unix timestamp, and save.
    pub fn update(&mut self, patch: &Value, state_dir: &Path) {
        if let Some(obj) = patch.as_object() {
            // Merge window fields individually
            if let Some(Value::Object(win_obj)) = obj.get("window") {
                if let Some(Value::Number(n)) = win_obj.get("width") {
                    if let Some(v) = n.as_i64() {
                        self.window.width = v as i32;
                    }
                }
                if let Some(Value::Number(n)) = win_obj.get("height") {
                    if let Some(v) = n.as_i64() {
                        self.window.height = v as i32;
                    }
                }
                if let Some(val) = win_obj.get("x") {
                    self.window.x = val.as_i64().map(|v| v as i32);
                }
                if let Some(val) = win_obj.get("y") {
                    self.window.y = val.as_i64().map(|v| v as i32);
                }
            }

            // Apply other top-level keys
            if let Some(Value::Number(n)) = obj.get("version") {
                if let Some(v) = n.as_u64() {
                    self.version = v as u32;
                }
            }
            if let Some(Value::Number(n)) = obj.get("ui_zoom") {
                if let Some(v) = n.as_f64() {
                    self.ui_zoom = v;
                }
            }
            if let Some(Value::Number(n)) = obj.get("split_ratio") {
                if let Some(v) = n.as_f64() {
                    self.split_ratio = v;
                }
            }
            if let Some(Value::Number(n)) = obj.get("dock_ratio") {
                if let Some(v) = n.as_f64() {
                    self.dock_ratio = v;
                }
            }
            if let Some(Value::Bool(b)) = obj.get("always_on_top") {
                self.always_on_top = *b;
            }
            if let Some(val) = obj.get("last_folder_path") {
                self.last_folder_path = val.as_str().map(|s| s.to_string());
            }
            if let Some(val) = obj.get("last_library_id") {
                self.last_library_id = val.as_str().map(|s| s.to_string());
            }

            // Set updated_at to current unix timestamp
            self.updated_at = SystemTime::now()
                .duration_since(SystemTime::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs();

            self.save(state_dir);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;
    use tempfile::TempDir;

    #[test]
    fn test_default_prefs() {
        let prefs = Prefs::default();
        assert_eq!(prefs.version, 19);
        assert_eq!(prefs.ui_zoom, 1.0);
        assert_eq!(prefs.split_ratio, 0.62);
        assert_eq!(prefs.dock_ratio, 0.62);
        assert!(!prefs.always_on_top);
        assert_eq!(prefs.window.width, 1320);
        assert_eq!(prefs.window.height, 860);
        assert_eq!(prefs.window.x, None);
        assert_eq!(prefs.window.y, None);
        assert_eq!(prefs.last_folder_path, None);
        assert_eq!(prefs.last_library_id, None);
        assert_eq!(prefs.updated_at, 0);
    }

    #[test]
    fn test_default_window_state() {
        let ws = WindowState::default();
        assert_eq!(ws.width, 1320);
        assert_eq!(ws.height, 860);
        assert_eq!(ws.x, None);
        assert_eq!(ws.y, None);
    }

    #[test]
    fn test_save_and_load_round_trip() {
        let dir = TempDir::new().unwrap();
        let mut prefs = Prefs::default();
        prefs.ui_zoom = 1.5;
        prefs.split_ratio = 0.75;
        prefs.always_on_top = true;
        prefs.window.width = 1920;
        prefs.window.height = 1080;
        prefs.window.x = Some(100);
        prefs.window.y = Some(200);
        prefs.last_folder_path = Some("/my/folder".to_string());
        prefs.last_library_id = Some("lib-abc".to_string());
        prefs.updated_at = 1234567890;

        prefs.save(dir.path());

        let loaded = Prefs::load(dir.path());
        assert_eq!(loaded.version, 19);
        assert_eq!(loaded.ui_zoom, 1.5);
        assert_eq!(loaded.split_ratio, 0.75);
        assert_eq!(loaded.dock_ratio, 0.62);
        assert!(loaded.always_on_top);
        assert_eq!(loaded.window.width, 1920);
        assert_eq!(loaded.window.height, 1080);
        assert_eq!(loaded.window.x, Some(100));
        assert_eq!(loaded.window.y, Some(200));
        assert_eq!(loaded.last_folder_path, Some("/my/folder".to_string()));
        assert_eq!(loaded.last_library_id, Some("lib-abc".to_string()));
        assert_eq!(loaded.updated_at, 1234567890);
    }

    #[test]
    fn test_load_nonexistent_returns_defaults() {
        let dir = TempDir::new().unwrap();
        let prefs = Prefs::load(dir.path());
        assert_eq!(prefs.version, 19);
        assert_eq!(prefs.ui_zoom, 1.0);
        assert_eq!(prefs.window.width, 1320);
    }

    #[test]
    fn test_partial_update() {
        let dir = TempDir::new().unwrap();
        let mut prefs = Prefs::default();

        let patch = json!({
            "ui_zoom": 2.0,
            "always_on_top": true
        });
        prefs.update(&patch, dir.path());

        assert_eq!(prefs.ui_zoom, 2.0);
        assert!(prefs.always_on_top);
        // Unchanged fields stay at defaults
        assert_eq!(prefs.split_ratio, 0.62);
        assert_eq!(prefs.dock_ratio, 0.62);
        assert_eq!(prefs.window.width, 1320);
        // updated_at should have been set
        assert!(prefs.updated_at > 0);
    }

    #[test]
    fn test_update_saves_to_disk() {
        let dir = TempDir::new().unwrap();
        let mut prefs = Prefs::default();

        let patch = json!({"ui_zoom": 3.0});
        prefs.update(&patch, dir.path());

        let loaded = Prefs::load(dir.path());
        assert_eq!(loaded.ui_zoom, 3.0);
    }

    #[test]
    fn test_window_merge_on_load() {
        let dir = TempDir::new().unwrap();
        // Write a JSON file with only partial window data
        let partial = json!({
            "window": {"width": 800}
        });
        let path = dir.path().join("prefs.json");
        atomic_write_json(&path, &partial, BACKUP_COUNT);

        let prefs = Prefs::load(dir.path());
        // Width should be overridden
        assert_eq!(prefs.window.width, 800);
        // Height should remain at default
        assert_eq!(prefs.window.height, 860);
        // x, y should remain None (defaults)
        assert_eq!(prefs.window.x, None);
        assert_eq!(prefs.window.y, None);
    }

    #[test]
    fn test_window_merge_on_update() {
        let dir = TempDir::new().unwrap();
        let mut prefs = Prefs::default();
        prefs.window.width = 1000;
        prefs.window.height = 700;
        prefs.window.x = Some(50);
        prefs.window.y = Some(60);

        // Patch only width — height, x, y should remain
        let patch = json!({
            "window": {"width": 1600}
        });
        prefs.update(&patch, dir.path());

        assert_eq!(prefs.window.width, 1600);
        assert_eq!(prefs.window.height, 700);
        assert_eq!(prefs.window.x, Some(50));
        assert_eq!(prefs.window.y, Some(60));
    }

    #[test]
    fn test_window_merge_with_position() {
        let dir = TempDir::new().unwrap();
        // Saved file has window with x/y set
        let saved = json!({
            "version": 19,
            "window": {"width": 1320, "height": 860, "x": 150, "y": 250}
        });
        let path = dir.path().join("prefs.json");
        atomic_write_json(&path, &saved, BACKUP_COUNT);

        let prefs = Prefs::load(dir.path());
        assert_eq!(prefs.window.x, Some(150));
        assert_eq!(prefs.window.y, Some(250));
    }

    #[test]
    fn test_load_with_missing_fields_uses_defaults() {
        let dir = TempDir::new().unwrap();
        // Write a JSON with only version — everything else should be defaults
        let partial = json!({"version": 20});
        let path = dir.path().join("prefs.json");
        atomic_write_json(&path, &partial, BACKUP_COUNT);

        let prefs = Prefs::load(dir.path());
        assert_eq!(prefs.version, 20);
        assert_eq!(prefs.ui_zoom, 1.0);
        assert_eq!(prefs.split_ratio, 0.62);
        assert_eq!(prefs.window.width, 1320);
        assert_eq!(prefs.window.height, 860);
        assert_eq!(prefs.last_folder_path, None);
    }

    #[test]
    fn test_update_with_non_object_is_noop() {
        let dir = TempDir::new().unwrap();
        let mut prefs = Prefs::default();
        let original_zoom = prefs.ui_zoom;

        prefs.update(&json!("not an object"), dir.path());

        // Nothing should have changed
        assert_eq!(prefs.ui_zoom, original_zoom);
        assert_eq!(prefs.updated_at, 0);
    }

    #[test]
    fn test_window_null_xy_in_update() {
        let dir = TempDir::new().unwrap();
        let mut prefs = Prefs::default();
        prefs.window.x = Some(100);
        prefs.window.y = Some(200);

        // Setting x/y to null should clear them
        let patch = json!({
            "window": {"x": null, "y": null}
        });
        prefs.update(&patch, dir.path());

        assert_eq!(prefs.window.x, None);
        assert_eq!(prefs.window.y, None);
    }
}
