use axum::{Json, extract::State};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use std::sync::Mutex;

use crate::api::routes::AppState;

// ── Catalog themes (embedded in binary) ───────────────────────────────

/// Runtime set of installed catalog theme IDs (in-memory, resets on restart).
static INSTALLED_CATALOG_IDS: Mutex<Vec<String>> = Mutex::new(Vec::new());

/// A catalog entry extends `ThemeManifest` with marketplace metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct CatalogEntry {
    #[serde(flatten)]
    pub manifest: ThemeManifest,
    pub version: String,
    pub preview_swatch: String,
    pub installed: bool,
}

/// Returns the static catalog of themes available for installation.
fn catalog_themes() -> Vec<CatalogEntry> {
    let installed = INSTALLED_CATALOG_IDS
        .lock()
        .unwrap_or_else(|e| e.into_inner());

    let mut entries = vec![
        CatalogEntry {
            manifest: ThemeManifest {
                id: "parchment".into(),
                name: "Parchment".into(),
                description: "Warm parchment tones with elegant serif typography feel".into(),
                author: "Roboticus".into(),
                swatch: "#d4a574".into(),
                variables: HashMap::from([
                    ("--bg".into(), "#2a2118".into()),
                    ("--surface".into(), "#352a1f".into()),
                    ("--surface-2".into(), "#3f3326".into()),
                    ("--accent".into(), "#c17f3a".into()),
                    ("--text".into(), "#f5e6c8".into()),
                    ("--muted".into(), "#b8a080".into()),
                    ("--border".into(), "#6b5540".into()),
                    // Deep theme decorations
                    ("--theme-body-texture".into(), "repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(139,94,60,0.03) 3px, rgba(139,94,60,0.03) 4px)".into()),
                    ("--theme-separator".into(), "linear-gradient(90deg, transparent, #8b5e3c 20%, #c17f3a 50%, #8b5e3c 80%, transparent)".into()),
                    ("--theme-separator-height".into(), "2px".into()),
                    ("--theme-scrollbar".into(), "rgba(193,127,58,0.3)".into()),
                    ("--theme-card-border".into(), "linear-gradient(to bottom, #6b5540, #3f3326) 1".into()),
                ]),
                textures: HashMap::from([
                    ("body".into(), ThemeTexture {
                        kind: "css".into(),
                        value: "repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(139,94,60,0.03) 3px, rgba(139,94,60,0.03) 4px)".into(),
                    }),
                    ("surface".into(), ThemeTexture {
                        kind: "css".into(),
                        value: "radial-gradient(ellipse at 20% 50%, rgba(193,127,58,0.06) 0%, transparent 70%)".into(),
                    }),
                ]),
                fonts: vec![],
                thumbnail: String::new(),
                source: "catalog".into(),
            },
            version: "1.0.0".into(),
            preview_swatch: "linear-gradient(135deg, #f5e6c8, #d4a574)".into(),
            installed: false,
        },
        CatalogEntry {
            manifest: ThemeManifest {
                id: "midnight-ocean".into(),
                name: "Midnight Ocean".into(),
                description: "Deep navy depths with teal accents and wave-inspired separators".into(),
                author: "Roboticus".into(),
                swatch: "#0d9488".into(),
                variables: HashMap::from([
                    ("--bg".into(), "#0a1628".into()),
                    ("--surface".into(), "#0e1f3a".into()),
                    ("--surface-2".into(), "#132848".into()),
                    ("--accent".into(), "#0d9488".into()),
                    ("--text".into(), "#c8e1f5".into()),
                    ("--muted".into(), "#6b8ab0".into()),
                    ("--border".into(), "#1e3a5f".into()),
                    // Deep theme decorations
                    ("--theme-body-texture".into(), "radial-gradient(ellipse at 50% 0%, rgba(13,148,136,0.08) 0%, transparent 60%)".into()),
                    ("--theme-separator".into(), "linear-gradient(90deg, transparent, #0d9488 15%, #1e3a5f 50%, #0d9488 85%, transparent)".into()),
                    ("--theme-separator-height".into(), "2px".into()),
                    ("--theme-scrollbar".into(), "rgba(13,148,136,0.3)".into()),
                ]),
                textures: HashMap::from([
                    ("body".into(), ThemeTexture {
                        kind: "css".into(),
                        value: "radial-gradient(ellipse at 50% 0%, rgba(13,148,136,0.08) 0%, transparent 60%)".into(),
                    }),
                ]),
                fonts: vec![],
                thumbnail: String::new(),
                source: "catalog".into(),
            },
            version: "1.0.0".into(),
            preview_swatch: "linear-gradient(135deg, #0a1628, #0d9488)".into(),
            installed: false,
        },
        CatalogEntry {
            manifest: ThemeManifest {
                id: "solarized-dark".into(),
                name: "Solarized Dark".into(),
                description: "Ethan Schoonover's precision-engineered dark palette for low-fatigue reading".into(),
                author: "Roboticus".into(),
                swatch: "#268bd2".into(),
                variables: HashMap::from([
                    ("--bg".into(), "#002b36".into()),
                    ("--surface".into(), "#073642".into()),
                    ("--surface-2".into(), "#0a4050".into()),
                    ("--accent".into(), "#268bd2".into()),
                    ("--text".into(), "#93a1a1".into()),
                    ("--muted".into(), "#657b83".into()),
                    ("--border".into(), "#2aa198".into()),
                ]),
                textures: HashMap::new(),
                fonts: vec![],
                thumbnail: String::new(),
                source: "catalog".into(),
            },
            version: "1.0.0".into(),
            preview_swatch: "linear-gradient(135deg, #002b36, #268bd2)".into(),
            installed: false,
        },
        CatalogEntry {
            manifest: ThemeManifest {
                id: "dracula".into(),
                name: "Dracula".into(),
                description: "The beloved dark theme with purple, pink, and green highlights".into(),
                author: "Roboticus".into(),
                swatch: "#bd93f9".into(),
                variables: HashMap::from([
                    ("--bg".into(), "#282a36".into()),
                    ("--surface".into(), "#2d303e".into()),
                    ("--surface-2".into(), "#343746".into()),
                    ("--accent".into(), "#bd93f9".into()),
                    ("--text".into(), "#f8f8f2".into()),
                    ("--muted".into(), "#6272a4".into()),
                    ("--border".into(), "#44475a".into()),
                    ("--theme-scrollbar".into(), "rgba(189,147,249,0.25)".into()),
                ]),
                textures: HashMap::new(),
                fonts: vec![],
                thumbnail: String::new(),
                source: "catalog".into(),
            },
            version: "1.0.0".into(),
            preview_swatch: "linear-gradient(135deg, #282a36, #bd93f9, #ff79c6)".into(),
            installed: false,
        },
        CatalogEntry {
            manifest: ThemeManifest {
                id: "nord".into(),
                name: "Nord".into(),
                description: "Arctic blue-gray palette inspired by the cold beauty of the Nordic wilderness".into(),
                author: "Roboticus".into(),
                swatch: "#88c0d0".into(),
                variables: HashMap::from([
                    ("--bg".into(), "#2e3440".into()),
                    ("--surface".into(), "#3b4252".into()),
                    ("--surface-2".into(), "#434c5e".into()),
                    ("--accent".into(), "#88c0d0".into()),
                    ("--text".into(), "#eceff4".into()),
                    ("--muted".into(), "#81a1c1".into()),
                    ("--border".into(), "#4c566a".into()),
                ]),
                textures: HashMap::new(),
                fonts: vec![],
                thumbnail: String::new(),
                source: "catalog".into(),
            },
            version: "1.0.0".into(),
            preview_swatch: "linear-gradient(135deg, #2e3440, #88c0d0)".into(),
            installed: false,
        },
    ];

    // Mark installed entries
    for entry in &mut entries {
        if installed.contains(&entry.manifest.id) {
            entry.installed = true;
        }
    }

    entries
}

/// GET /api/themes/catalog — returns the static catalog of installable themes.
pub(crate) async fn list_theme_catalog() -> Json<serde_json::Value> {
    let catalog = catalog_themes();
    Json(serde_json::json!({ "catalog": catalog }))
}

/// POST /api/themes/catalog/install — install a catalog theme by id.
#[derive(Debug, Deserialize)]
pub(crate) struct InstallRequest {
    pub id: String,
}

pub(crate) async fn install_catalog_theme(
    Json(req): Json<InstallRequest>,
) -> Json<serde_json::Value> {
    let catalog = catalog_themes();
    let Some(entry) = catalog.iter().find(|e| e.manifest.id == req.id) else {
        return Json(serde_json::json!({
            "ok": false,
            "error": format!("theme '{}' not found in catalog", req.id)
        }));
    };

    if entry.installed {
        return Json(serde_json::json!({
            "ok": true,
            "message": "already installed"
        }));
    }

    {
        let mut ids = INSTALLED_CATALOG_IDS
            .lock()
            .unwrap_or_else(|e| e.into_inner());
        if !ids.contains(&req.id) {
            ids.push(req.id.clone());
        }
    }

    tracing::info!(theme_id = %req.id, "catalog theme installed");

    Json(serde_json::json!({
        "ok": true,
        "theme": entry.manifest
    }))
}

/// Theme manifest matching the JSON format in theme files.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ThemeManifest {
    pub id: String,
    pub name: String,
    #[serde(default)]
    pub description: String,
    #[serde(default)]
    pub author: String,
    #[serde(default)]
    pub swatch: String,
    #[serde(default)]
    pub variables: HashMap<String, String>,
    #[serde(default)]
    pub textures: HashMap<String, ThemeTexture>,
    #[serde(default)]
    pub fonts: Vec<String>,
    /// Data URI or URL for a preview thumbnail (recommended 400x250).
    /// Can be a data:image/png;base64,... URI for self-contained themes,
    /// or an HTTPS URL for remotely hosted previews.
    #[serde(default)]
    pub thumbnail: String,
    /// Where this theme was loaded from.
    #[serde(skip_deserializing, default)]
    pub source: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ThemeTexture {
    /// "css" for CSS gradient/pattern, "url" for image URL.
    #[serde(rename = "type")]
    pub kind: String,
    pub value: String,
}

/// GET /api/themes — list all available themes (built-in + workspace).
pub(crate) async fn list_themes(State(state): State<AppState>) -> Json<Vec<ThemeManifest>> {
    let mut themes = Vec::new();

    // 1. Scan built-in themes directory next to the binary
    let builtin_dir = std::env::current_exe()
        .ok()
        .and_then(|p| p.parent().map(|d| d.join("themes")));
    if let Some(dir) = &builtin_dir {
        load_themes_from_dir(dir, "builtin", &mut themes);
    }

    // 2. Scan project-level themes directory
    let project_themes = Path::new("themes");
    if project_themes.is_dir() {
        load_themes_from_dir(project_themes, "project", &mut themes);
    }

    // 3. Scan workspace themes directory (from app packages)
    let config = state.config.read().await;
    let workspace = config.skills.skills_dir.parent().unwrap_or(Path::new("."));
    let workspace_themes = workspace.join("themes");
    drop(config);
    if workspace_themes.is_dir() {
        load_themes_from_dir(&workspace_themes, "workspace", &mut themes);
    }

    // 4. Scan the standard app-packages location
    if let Ok(home) = std::env::var("HOME") {
        let home_themes = std::path::PathBuf::from(home)
            .join(".roboticus")
            .join("workspace")
            .join("themes");
        if home_themes.is_dir() && home_themes != workspace_themes {
            load_themes_from_dir(&home_themes, "workspace", &mut themes);
        }
    }

    // 5. Include installed catalog themes so they survive page refresh.
    // The catalog is static — installed entries are tracked in-memory.
    {
        let installed = INSTALLED_CATALOG_IDS
            .lock()
            .unwrap_or_else(|e| e.into_inner());
        if !installed.is_empty() {
            let catalog = catalog_themes();
            for entry in &catalog {
                if installed.contains(&entry.manifest.id) {
                    themes.push(entry.manifest.clone());
                }
            }
        }
    }

    // Deduplicate by id — later sources (workspace/catalog) override earlier
    // ones (built-in/project). Reverse-retain keeps the last occurrence.
    let mut seen = std::collections::HashSet::new();
    themes.reverse();
    themes.retain(|t| seen.insert(t.id.clone()));
    themes.reverse();

    Json(themes)
}

fn load_themes_from_dir(dir: &Path, source: &str, out: &mut Vec<ThemeManifest>) {
    let Ok(entries) = std::fs::read_dir(dir) else {
        return;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.extension().and_then(|e| e.to_str()) != Some("json") {
            continue;
        }
        match std::fs::read_to_string(&path) {
            Ok(content) => match serde_json::from_str::<ThemeManifest>(&content) {
                Ok(mut manifest) => {
                    manifest.source = source.to_string();
                    tracing::debug!(
                        theme_id = %manifest.id,
                        source,
                        path = %path.display(),
                        "loaded theme manifest"
                    );
                    out.push(manifest);
                }
                Err(e) => {
                    tracing::warn!(
                        path = %path.display(),
                        error = %e,
                        "failed to parse theme manifest"
                    );
                }
            },
            Err(e) => {
                tracing::warn!(
                    path = %path.display(),
                    error = %e,
                    "failed to read theme file"
                );
            }
        }
    }
}

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

    #[test]
    fn parse_theme_with_textures() {
        let json = r##"{
            "id": "parchment",
            "name": "Parchment & Ink",
            "description": "Warm tones",
            "author": "Test",
            "swatch": "#8b4513",
            "variables": { "--bg": "#1a1410", "--accent": "#c17f3a" },
            "textures": {
                "body": { "type": "css", "value": "repeating-linear-gradient(0deg, transparent, transparent 2px)" }
            },
            "fonts": ["https://fonts.googleapis.com/css2?family=Cinzel"]
        }"##;
        let manifest: ThemeManifest = serde_json::from_str(json).unwrap();
        assert_eq!(manifest.id, "parchment");
        assert_eq!(manifest.name, "Parchment & Ink");
        assert!(!manifest.variables.is_empty());
        assert!(manifest.textures.contains_key("body"));
        assert!(!manifest.fonts.is_empty());
    }

    #[test]
    fn theme_without_textures_parses() {
        let json = r##"{
            "id": "minimal",
            "name": "Minimal",
            "variables": { "--accent": "#ff0000" }
        }"##;
        let manifest: ThemeManifest = serde_json::from_str(json).unwrap();
        assert_eq!(manifest.id, "minimal");
        assert!(manifest.textures.is_empty());
        assert!(manifest.fonts.is_empty());
    }

    #[test]
    fn parse_theme_minimal_fields_only() {
        let json = r#"{"id": "bare", "name": "Bare"}"#;
        let manifest: ThemeManifest = serde_json::from_str(json).unwrap();
        assert_eq!(manifest.id, "bare");
        assert_eq!(manifest.name, "Bare");
        assert!(manifest.description.is_empty());
        assert!(manifest.author.is_empty());
        assert!(manifest.swatch.is_empty());
        assert!(manifest.variables.is_empty());
        assert!(manifest.textures.is_empty());
        assert!(manifest.fonts.is_empty());
        assert!(manifest.thumbnail.is_empty());
    }

    #[test]
    fn parse_theme_with_thumbnail() {
        let json = r#"{
            "id": "preview",
            "name": "Preview Theme",
            "thumbnail": "data:image/png;base64,iVBOR"
        }"#;
        let manifest: ThemeManifest = serde_json::from_str(json).unwrap();
        assert_eq!(manifest.id, "preview");
        assert!(manifest.thumbnail.starts_with("data:image/png"));
    }

    #[test]
    fn source_field_not_deserialized() {
        // `source` has `skip_deserializing` — even if present in JSON it should be empty
        let json = r#"{
            "id": "src-test",
            "name": "Source Test",
            "source": "should-be-ignored"
        }"#;
        let manifest: ThemeManifest = serde_json::from_str(json).unwrap();
        assert!(manifest.source.is_empty());
    }

    #[test]
    fn source_field_serializes() {
        let mut manifest = ThemeManifest {
            id: "s".into(),
            name: "S".into(),
            description: String::new(),
            author: String::new(),
            swatch: String::new(),
            variables: HashMap::new(),
            textures: HashMap::new(),
            fonts: vec![],
            thumbnail: String::new(),
            source: "builtin".into(),
        };
        let json = serde_json::to_string(&manifest).unwrap();
        assert!(json.contains(r#""source":"builtin""#));
        manifest.source = "workspace".into();
        let json = serde_json::to_string(&manifest).unwrap();
        assert!(json.contains(r#""source":"workspace""#));
    }

    #[test]
    fn texture_kind_rename() {
        let json = r#"{"type": "url", "value": "https://example.com/bg.png"}"#;
        let tex: ThemeTexture = serde_json::from_str(json).unwrap();
        assert_eq!(tex.kind, "url");
        assert_eq!(tex.value, "https://example.com/bg.png");
    }

    #[test]
    fn load_themes_from_dir_picks_up_json_files() {
        let dir = TempDir::new().unwrap();
        let theme_json = r#"{"id": "loaded", "name": "Loaded Theme"}"#;
        fs::write(dir.path().join("cool.json"), theme_json).unwrap();
        // Non-JSON file should be skipped
        fs::write(dir.path().join("readme.txt"), "not a theme").unwrap();

        let mut themes = Vec::new();
        load_themes_from_dir(dir.path(), "test-source", &mut themes);

        assert_eq!(themes.len(), 1);
        assert_eq!(themes[0].id, "loaded");
        assert_eq!(themes[0].name, "Loaded Theme");
        assert_eq!(themes[0].source, "test-source");
    }

    #[test]
    fn load_themes_from_dir_skips_invalid_json() {
        let dir = TempDir::new().unwrap();
        fs::write(dir.path().join("bad.json"), "{ not valid json }").unwrap();
        fs::write(
            dir.path().join("good.json"),
            r#"{"id": "ok", "name": "OK"}"#,
        )
        .unwrap();

        let mut themes = Vec::new();
        load_themes_from_dir(dir.path(), "test", &mut themes);

        assert_eq!(themes.len(), 1);
        assert_eq!(themes[0].id, "ok");
    }

    #[test]
    fn load_themes_from_dir_handles_missing_dir() {
        let mut themes = Vec::new();
        load_themes_from_dir(Path::new("/nonexistent/themes/dir"), "ghost", &mut themes);
        assert!(themes.is_empty());
    }

    #[test]
    fn load_themes_from_dir_empty_directory() {
        let dir = TempDir::new().unwrap();
        let mut themes = Vec::new();
        load_themes_from_dir(dir.path(), "empty", &mut themes);
        assert!(themes.is_empty());
    }

    #[test]
    fn load_themes_from_dir_multiple_themes() {
        let dir = TempDir::new().unwrap();
        for i in 0..3 {
            let json = format!(r#"{{"id": "theme-{i}", "name": "Theme {i}"}}"#);
            fs::write(dir.path().join(format!("theme-{i}.json")), json).unwrap();
        }

        let mut themes = Vec::new();
        load_themes_from_dir(dir.path(), "multi", &mut themes);

        assert_eq!(themes.len(), 3);
        let ids: Vec<&str> = themes.iter().map(|t| t.id.as_str()).collect();
        for i in 0..3 {
            assert!(ids.contains(&format!("theme-{i}").as_str()));
        }
        assert!(themes.iter().all(|t| t.source == "multi"));
    }

    #[test]
    fn load_themes_from_dir_skips_missing_required_fields() {
        let dir = TempDir::new().unwrap();
        // Missing "name" — serde should fail since `name` has no default
        fs::write(dir.path().join("no-name.json"), r#"{"id": "orphan"}"#).unwrap();
        // Valid one
        fs::write(
            dir.path().join("valid.json"),
            r#"{"id": "v", "name": "Valid"}"#,
        )
        .unwrap();

        let mut themes = Vec::new();
        load_themes_from_dir(dir.path(), "test", &mut themes);

        assert_eq!(themes.len(), 1);
        assert_eq!(themes[0].id, "v");
    }

    #[test]
    fn theme_manifest_clone_and_debug() {
        let manifest = ThemeManifest {
            id: "clone-test".into(),
            name: "Clone".into(),
            description: String::new(),
            author: String::new(),
            swatch: String::new(),
            variables: HashMap::new(),
            textures: HashMap::new(),
            fonts: vec![],
            thumbnail: String::new(),
            source: String::new(),
        };
        let cloned = manifest.clone();
        assert_eq!(cloned.id, manifest.id);
        // Debug impl exists
        let debug = format!("{:?}", manifest);
        assert!(debug.contains("clone-test"));
    }

    #[test]
    fn catalog_themes_returns_five_entries() {
        let catalog = catalog_themes();
        assert_eq!(catalog.len(), 5);
        let ids: Vec<&str> = catalog.iter().map(|e| e.manifest.id.as_str()).collect();
        assert!(ids.contains(&"parchment"));
        assert!(ids.contains(&"midnight-ocean"));
        assert!(ids.contains(&"solarized-dark"));
        assert!(ids.contains(&"dracula"));
        assert!(ids.contains(&"nord"));
    }

    #[test]
    fn catalog_themes_have_required_variables() {
        let catalog = catalog_themes();
        for entry in &catalog {
            let v = &entry.manifest.variables;
            assert!(v.contains_key("--bg"), "{} missing --bg", entry.manifest.id);
            assert!(
                v.contains_key("--accent"),
                "{} missing --accent",
                entry.manifest.id
            );
            assert!(
                v.contains_key("--text"),
                "{} missing --text",
                entry.manifest.id
            );
            assert!(
                !entry.version.is_empty(),
                "{} missing version",
                entry.manifest.id
            );
            assert!(
                !entry.preview_swatch.is_empty(),
                "{} missing swatch",
                entry.manifest.id
            );
            assert_eq!(entry.manifest.source, "catalog");
        }
    }

    #[test]
    fn catalog_entry_serializes_with_flatten() {
        let catalog = catalog_themes();
        let json = serde_json::to_value(&catalog[0]).unwrap();
        // Flattened manifest fields should appear at top level
        assert!(json.get("id").is_some());
        assert!(json.get("name").is_some());
        assert!(json.get("variables").is_some());
        assert!(json.get("version").is_some());
        assert!(json.get("preview_swatch").is_some());
        assert!(json.get("installed").is_some());
    }

    #[test]
    fn parchment_has_deep_theme_decorations() {
        let catalog = catalog_themes();
        let parchment = catalog
            .iter()
            .find(|e| e.manifest.id == "parchment")
            .unwrap();
        let v = &parchment.manifest.variables;
        assert!(v.contains_key("--theme-body-texture"));
        assert!(v.contains_key("--theme-separator"));
        assert!(v.contains_key("--theme-separator-height"));
        assert!(v.contains_key("--theme-scrollbar"));
        assert!(v.contains_key("--theme-card-border"));
    }

    #[test]
    fn midnight_ocean_has_deep_theme_decorations() {
        let catalog = catalog_themes();
        let ocean = catalog
            .iter()
            .find(|e| e.manifest.id == "midnight-ocean")
            .unwrap();
        let v = &ocean.manifest.variables;
        assert!(v.contains_key("--theme-body-texture"));
        assert!(v.contains_key("--theme-separator"));
        assert!(v.contains_key("--theme-scrollbar"));
    }
}
