//! Synor.json Configuration
//!
//! Parses and manages the synor.json configuration file that users
//! include in their deployed projects.

use crate::router::{RouteConfig, Redirect};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Synor.json configuration file
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SynorJson {
    /// Name of the deployment (optional, can be inferred)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,

    /// Build configuration
    #[serde(skip_serializing_if = "Option::is_none")]
    pub build: Option<BuildConfig>,

    /// Routes configuration
    #[serde(skip_serializing_if = "Option::is_none")]
    pub routes: Option<RoutesConfig>,

    /// Headers configuration
    #[serde(skip_serializing_if = "Option::is_none")]
    pub headers: Option<Vec<HeaderRule>>,

    /// Redirects configuration
    #[serde(skip_serializing_if = "Option::is_none")]
    pub redirects: Option<Vec<RedirectRule>>,

    /// Custom error pages
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error_pages: Option<HashMap<u16, String>>,

    /// Environment variables (non-secret, build-time)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub env: Option<HashMap<String, String>>,

    /// Functions/serverless configuration
    #[serde(skip_serializing_if = "Option::is_none")]
    pub functions: Option<FunctionsConfig>,
}

/// Build configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildConfig {
    /// Build command
    #[serde(skip_serializing_if = "Option::is_none")]
    pub command: Option<String>,

    /// Output directory (default: "dist" or "build")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub output: Option<String>,

    /// Install command (default: "npm install" or "pnpm install")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub install: Option<String>,

    /// Node.js version
    #[serde(skip_serializing_if = "Option::is_none")]
    pub node_version: Option<String>,
}

/// Routes configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutesConfig {
    /// Enable SPA mode (fallback to index.html)
    #[serde(default)]
    pub spa: bool,

    /// Custom cleanUrls (remove .html extensions)
    #[serde(default)]
    pub clean_urls: bool,

    /// Trailing slash behavior
    #[serde(skip_serializing_if = "Option::is_none")]
    pub trailing_slash: Option<bool>,

    /// Custom route rewrites
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rewrites: Option<Vec<RewriteRule>>,
}

/// Header rule
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeaderRule {
    /// Path pattern (glob)
    pub source: String,
    /// Headers to apply
    pub headers: Vec<HeaderKeyValue>,
}

/// Single header key-value pair
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeaderKeyValue {
    pub key: String,
    pub value: String,
}

/// Redirect rule (in synor.json format)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RedirectRule {
    /// Source path
    pub source: String,
    /// Destination path or URL
    pub destination: String,
    /// HTTP status (default: 308)
    #[serde(default = "default_redirect_status")]
    pub status: u16,
}

fn default_redirect_status() -> u16 {
    308
}

/// Rewrite rule
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RewriteRule {
    /// Source path pattern
    pub source: String,
    /// Destination path
    pub destination: String,
}

/// Serverless functions configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionsConfig {
    /// Functions directory
    #[serde(default = "default_functions_dir")]
    pub directory: String,

    /// Runtime (e.g., "nodejs20", "python3.11")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub runtime: Option<String>,

    /// Memory limit in MB
    #[serde(skip_serializing_if = "Option::is_none")]
    pub memory: Option<u32>,

    /// Timeout in seconds
    #[serde(skip_serializing_if = "Option::is_none")]
    pub timeout: Option<u32>,
}

fn default_functions_dir() -> String {
    "api".to_string()
}

impl SynorJson {
    /// Parse from JSON string
    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
        serde_json::from_str(json)
    }

    /// Parse from JSON bytes
    pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
        serde_json::from_slice(bytes)
    }

    /// Serialize to JSON string
    pub fn to_json(&self) -> Result<String, serde_json::Error> {
        serde_json::to_string_pretty(self)
    }

    /// Convert to RouteConfig for the router
    pub fn to_route_config(&self) -> RouteConfig {
        let mut config = RouteConfig::default();

        // Process routes
        if let Some(routes) = &self.routes {
            if routes.spa {
                // SPA mode: all non-file paths go to index.html
                config.routes.insert("/*".to_string(), "/index.html".to_string());
            }

            // Process rewrites
            if let Some(rewrites) = &routes.rewrites {
                for rewrite in rewrites {
                    config.routes.insert(rewrite.source.clone(), rewrite.destination.clone());
                }
            }
        }

        // Process headers
        if let Some(headers) = &self.headers {
            for rule in headers {
                let mut header_map = HashMap::new();
                for kv in &rule.headers {
                    header_map.insert(kv.key.clone(), kv.value.clone());
                }
                config.headers.insert(rule.source.clone(), header_map);
            }
        }

        // Process redirects
        if let Some(redirects) = &self.redirects {
            for rule in redirects {
                config.redirects.push(Redirect {
                    from: rule.source.clone(),
                    to: rule.destination.clone(),
                    status: rule.status,
                });
            }
        }

        // Process error pages
        if let Some(error_pages) = &self.error_pages {
            config.error_pages = error_pages.clone();
        }

        config
    }
}

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

    #[test]
    fn test_parse_minimal() {
        let json = r#"{}"#;
        let config = SynorJson::from_json(json).unwrap();
        assert!(config.name.is_none());
        assert!(config.build.is_none());
    }

    #[test]
    fn test_parse_spa_config() {
        let json = r#"{
            "name": "myapp",
            "routes": {
                "spa": true,
                "clean_urls": true
            }
        }"#;

        let config = SynorJson::from_json(json).unwrap();
        assert_eq!(config.name, Some("myapp".to_string()));
        assert!(config.routes.as_ref().unwrap().spa);
        assert!(config.routes.as_ref().unwrap().clean_urls);
    }

    #[test]
    fn test_parse_full_config() {
        let json = r#"{
            "name": "my-blog",
            "build": {
                "command": "npm run build",
                "output": "dist"
            },
            "routes": {
                "spa": true
            },
            "headers": [
                {
                    "source": "/**",
                    "headers": [
                        { "key": "X-Frame-Options", "value": "DENY" }
                    ]
                }
            ],
            "redirects": [
                {
                    "source": "/old-page",
                    "destination": "/new-page",
                    "status": 301
                }
            ],
            "error_pages": {
                "404": "/404.html"
            }
        }"#;

        let config = SynorJson::from_json(json).unwrap();
        assert_eq!(config.name, Some("my-blog".to_string()));
        assert_eq!(config.build.as_ref().unwrap().command, Some("npm run build".to_string()));
        assert!(config.routes.as_ref().unwrap().spa);
        assert_eq!(config.headers.as_ref().unwrap().len(), 1);
        assert_eq!(config.redirects.as_ref().unwrap().len(), 1);
        assert_eq!(config.error_pages.as_ref().unwrap().get(&404), Some(&"/404.html".to_string()));
    }

    #[test]
    fn test_to_route_config() {
        let json = r#"{
            "routes": {
                "spa": true
            },
            "redirects": [
                {
                    "source": "/blog",
                    "destination": "/posts",
                    "status": 301
                }
            ]
        }"#;

        let synor_json = SynorJson::from_json(json).unwrap();
        let route_config = synor_json.to_route_config();

        // SPA mode should add /* -> /index.html
        assert_eq!(route_config.routes.get("/*"), Some(&"/index.html".to_string()));

        // Redirect should be converted
        assert_eq!(route_config.redirects.len(), 1);
        assert_eq!(route_config.redirects[0].from, "/blog");
        assert_eq!(route_config.redirects[0].to, "/posts");
        assert_eq!(route_config.redirects[0].status, 301);
    }

    #[test]
    fn test_serialize() {
        let config = SynorJson {
            name: Some("test".to_string()),
            routes: Some(RoutesConfig {
                spa: true,
                clean_urls: false,
                trailing_slash: None,
                rewrites: None,
            }),
            ..Default::default()
        };

        let json = config.to_json().unwrap();
        assert!(json.contains("\"name\": \"test\""));
        assert!(json.contains("\"spa\": true"));
    }
}
