//! Ship manifest configuration parser
//!
//! Parses ship-manifest.toml with user-defined fleet groups, http, and modules.
//!
//! Supports base templates to avoid duplication:
//! ```toml
//! [base.ship]
//! env = { RAILS_ENV = "production" }
//! critical = true
//! tags = ["ruby"]
//! comment = "Default ship settings"
//!
//! [base.module]
//! phase = "request"
//! tags = ["security"]
//! ```

use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;

/// Root configuration from ship-manifest.toml
#[derive(Debug, Deserialize)]
pub struct Manifest {
    /// Mothership - command center / global config
    #[serde(default)]
    pub mothership: MothershipConfig,
    /// Base templates for ships and modules
    #[serde(default)]
    pub base: BaseTemplates,
    /// Fleet groups: fleet.web, fleet.workers, fleet.jobs, etc.
    #[serde(default, flatten)]
    fleet_raw: HashMap<String, toml::Value>,
    /// WASM modules (raw, before base merge)
    #[serde(default, rename = "modules")]
    modules_raw: Vec<ModuleRaw>,
    /// WASM modules (after base merge)
    #[serde(skip)]
    pub modules: Vec<Module>,
    /// Parsed fleet groups (populated after deserialization)
    #[serde(skip)]
    pub fleet: HashMap<String, Vec<ShipConfig>>,
}

/// Base templates for inheritance
#[derive(Debug, Clone, Default, Deserialize)]
pub struct BaseTemplates {
    /// Base template for all ships
    #[serde(default)]
    pub ship: BaseShip,
    /// Base template for all modules
    #[serde(default)]
    pub module: BaseModule,
}

/// Base ship template - fields that can be inherited
#[derive(Debug, Clone, Default, Deserialize)]
pub struct BaseShip {
    /// Default environment variables
    #[serde(default)]
    pub env: HashMap<String, String>,
    /// Default critical setting
    pub critical: Option<bool>,
    /// Default tags for categorization
    #[serde(default)]
    pub tags: Vec<String>,
    /// Comment/description
    pub comment: Option<String>,
}

/// Base module template - fields that can be inherited
#[derive(Debug, Clone, Default, Deserialize)]
pub struct BaseModule {
    /// Default phase
    pub phase: Option<Phase>,
    /// Default tags
    #[serde(default)]
    pub tags: Vec<String>,
    /// Comment/description
    pub comment: Option<String>,
    /// Default config
    #[serde(default)]
    pub config: HashMap<String, String>,
}

/// Mothership - command center configuration (global settings)
#[derive(Debug, Clone, Default, Deserialize)]
pub struct MothershipConfig {
    /// Named bind addresses for the fleet proxy (e.g., http, https, ws, admin)
    #[serde(default)]
    pub bind: HashMap<String, Bind>,
    /// Metrics port (always 127.0.0.1)
    pub metrics_port: Option<u16>,
    /// Global environment variables (inherited by all ships)
    #[serde(default)]
    pub env: HashMap<String, String>,
}

/// Ship configuration - unified for all ships
#[derive(Debug, Clone, Deserialize)]
pub struct ShipConfig {
    pub name: String,
    pub command: String,
    #[serde(default)]
    pub args: Vec<String>,
    /// Internal bind address (what the ship listens on)
    pub bind: Option<Bind>,
    pub healthcheck: Option<String>,
    #[serde(default)]
    pub depends_on: Vec<String>,
    /// Routes mapping mothership binds to this ship
    #[serde(default)]
    pub routes: Vec<RouteConfig>,
    #[serde(default)]
    pub env: HashMap<String, String>,
    /// Critical ship - crash kills fleet (default from base or true)
    #[serde(default, rename = "critical")]
    critical_override: Option<bool>,
    /// Resolved critical value (after base merge)
    #[serde(skip)]
    pub critical: bool,
    /// One-shot job - runs once then exits (default: false)
    #[serde(default)]
    pub oneshot: bool,
    /// Tags for categorization/filtering
    #[serde(default)]
    pub tags: Vec<String>,
    /// Comment/description
    pub comment: Option<String>,
}

impl ShipConfig {
    /// Merge with base template
    fn merge_base(&mut self, base: &BaseShip) {
        // Merge env: base first, then ship overrides
        let mut merged_env = base.env.clone();
        merged_env.extend(self.env.clone());
        self.env = merged_env;

        // Critical: ship override > base > default (true)
        self.critical = self.critical_override
            .or(base.critical)
            .unwrap_or(true);

        // Tags: combine base + ship (deduplicated)
        let mut merged_tags = base.tags.clone();
        for tag in &self.tags {
            if !merged_tags.contains(tag) {
                merged_tags.push(tag.clone());
            }
        }
        self.tags = merged_tags;

        // Comment: ship overrides base
        if self.comment.is_none() {
            self.comment = base.comment.clone();
        }
    }
}

/// Route configuration - maps a mothership bind to a pattern
#[derive(Debug, Clone)]
pub struct RouteConfig {
    /// Name of the mothership bind (e.g., "http", "ws")
    pub bind: String,
    /// URL pattern to match (regex)
    pub pattern: String,
}

impl std::fmt::Display for RouteConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}:{}", self.bind, self.pattern)
    }
}

impl<'de> Deserialize<'de> for RouteConfig {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        use serde::de::{self, MapAccess, Visitor};

        struct RouteConfigVisitor;

        impl<'de> Visitor<'de> for RouteConfigVisitor {
            type Value = RouteConfig;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("a route config object or a string pattern")
            }

            // Handle { bind = "http", pattern = "/.*" }
            fn visit_map<M>(self, mut map: M) -> Result<RouteConfig, M::Error>
            where
                M: MapAccess<'de>,
            {
                let mut bind = None;
                let mut pattern = None;

                while let Some(key) = map.next_key::<String>()? {
                    match key.as_str() {
                        "bind" => bind = Some(map.next_value()?),
                        "pattern" => pattern = Some(map.next_value()?),
                        _ => {
                            let _: toml::Value = map.next_value()?;
                        }
                    }
                }

                Ok(RouteConfig {
                    bind: bind.ok_or_else(|| de::Error::missing_field("bind"))?,
                    pattern: pattern.ok_or_else(|| de::Error::missing_field("pattern"))?,
                })
            }

            // Handle "http:/.*" shorthand
            fn visit_str<E>(self, s: &str) -> Result<RouteConfig, E>
            where
                E: de::Error,
            {
                if let Some((bind, pattern)) = s.split_once(':') {
                    Ok(RouteConfig {
                        bind: bind.to_string(),
                        pattern: pattern.to_string(),
                    })
                } else {
                    // Legacy: just a pattern, assume "http" bind
                    Ok(RouteConfig {
                        bind: "http".to_string(),
                        pattern: s.to_string(),
                    })
                }
            }
        }

        deserializer.deserialize_any(RouteConfigVisitor)
    }
}

/// Raw WASM module configuration (before base merge)
#[derive(Debug, Clone, Deserialize)]
pub struct ModuleRaw {
    pub name: String,
    pub wasm: String,
    #[serde(default)]
    pub routes: Vec<String>,
    pub phase: Option<Phase>,
    #[serde(default)]
    pub config: HashMap<String, String>,
    #[serde(default)]
    pub tags: Vec<String>,
    pub comment: Option<String>,
}

/// WASM module configuration (after base merge)
#[derive(Debug, Clone)]
pub struct Module {
    pub name: String,
    pub wasm: String,
    pub routes: Vec<String>,
    pub phase: Phase,
    pub config: HashMap<String, String>,
    pub tags: Vec<String>,
    pub comment: Option<String>,
}

impl Module {
    /// Create from raw with base template merge
    fn from_raw(raw: ModuleRaw, base: &BaseModule) -> Self {
        // Merge config: base first, then module overrides
        let mut merged_config = base.config.clone();
        merged_config.extend(raw.config);

        // Tags: combine base + module (deduplicated)
        let mut merged_tags = base.tags.clone();
        for tag in &raw.tags {
            if !merged_tags.contains(tag) {
                merged_tags.push(tag.clone());
            }
        }

        Self {
            name: raw.name,
            wasm: raw.wasm,
            routes: raw.routes,
            phase: raw.phase.or(base.phase).unwrap_or_default(),
            config: merged_config,
            tags: merged_tags,
            comment: raw.comment.or_else(|| base.comment.clone()),
        }
    }
}

/// Module execution phase
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Phase {
    #[default]
    Request,
    Response,
}


/// Bind address - parsed from string
#[derive(Debug, Clone)]
pub enum Bind {
    Tcp { host: String, port: u16 },
    Unix { path: String },
}

impl<'de> Deserialize<'de> for Bind {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        Bind::parse(&s).map_err(serde::de::Error::custom)
    }
}

impl Bind {
    /// Parse bind string: tcp://host:port, unix:///path, or just port
    pub fn parse(s: &str) -> Result<Self, String> {
        if let Some(path) = s.strip_prefix("unix://") {
            Ok(Bind::Unix {
                path: path.to_string(),
            })
        } else if let Some(addr) = s.strip_prefix("tcp://") {
            Self::parse_tcp(addr)
        } else if let Ok(port) = s.parse::<u16>() {
            Ok(Bind::Tcp {
                host: "127.0.0.1".to_string(),
                port,
            })
        } else if s.contains(':') {
            Self::parse_tcp(s)
        } else {
            Err(format!("Invalid bind format: {s}"))
        }
    }

    fn parse_tcp(addr: &str) -> Result<Self, String> {
        let (host, port_str) = addr
            .rsplit_once(':')
            .ok_or_else(|| format!("Missing port in: {addr}"))?;
        let port = port_str
            .parse::<u16>()
            .map_err(|_| format!("Invalid port: {port_str}"))?;
        Ok(Bind::Tcp {
            host: host.to_string(),
            port,
        })
    }

    /// Get the address for health checks
    pub fn healthcheck_base(&self) -> String {
        match self {
            Bind::Tcp { host, port } => {
                let h = if host == "0.0.0.0" { "127.0.0.1" } else { host };
                format!("http://{h}:{port}")
            }
            Bind::Unix { path } => format!("unix://{path}"),
        }
    }
}

impl Manifest {
    /// Load manifest from file
    pub fn load<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
        let content = std::fs::read_to_string(path.as_ref())?;
        Self::parse(&content)
    }

    /// Parse manifest from TOML string
    pub fn parse(content: &str) -> anyhow::Result<Self> {
        let mut manifest: Manifest = toml::from_str(content)?;
        manifest.parse_fleet_groups()?;
        manifest.merge_modules();
        manifest.expand_env_vars()?;
        Ok(manifest)
    }

    /// Parse fleet.* groups from raw TOML values and merge base template
    fn parse_fleet_groups(&mut self) -> anyhow::Result<()> {
        // Look for "fleet" key which contains all groups
        if let Some(fleet_value) = self.fleet_raw.remove("fleet") {
            if let toml::Value::Table(groups) = fleet_value {
                for (group_name, ships_value) in groups {
                    if let toml::Value::Array(ships_array) = ships_value {
                        let mut ships = Vec::new();
                        for ship_value in ships_array {
                            let mut ship: ShipConfig = ship_value.try_into().map_err(|e| {
                                anyhow::anyhow!(
                                    "Failed to parse ship in fleet.{}: {}",
                                    group_name,
                                    e
                                )
                            })?;
                            // Merge base template
                            ship.merge_base(&self.base.ship);
                            ships.push(ship);
                        }
                        self.fleet.insert(group_name, ships);
                    }
                }
            }
        }
        Ok(())
    }

    /// Merge raw modules with base template
    fn merge_modules(&mut self) {
        self.modules = std::mem::take(&mut self.modules_raw)
            .into_iter()
            .map(|raw| Module::from_raw(raw, &self.base.module))
            .collect();
    }

    /// Get all ships flattened from all groups
    pub fn all_ships(&self) -> Vec<(&str, &ShipConfig)> {
        self.fleet
            .iter()
            .flat_map(|(group, ships)| {
                ships.iter().map(move |ship| (group.as_str(), ship))
            })
            .collect()
    }

    /// Get total ship count
    pub fn ship_count(&self) -> usize {
        self.fleet.values().map(|v| v.len()).sum()
    }

    /// Filter ships by tags using the provided filter function
    pub fn filter_ships<F>(&mut self, filter: &F)
    where
        F: Fn(&[String]) -> bool,
    {
        for ships in self.fleet.values_mut() {
            ships.retain(|ship| filter(&ship.tags));
        }
        // Remove empty groups
        self.fleet.retain(|_, ships| !ships.is_empty());
    }

    /// Expand shell variables in env sections
    fn expand_env_vars(&mut self) -> anyhow::Result<()> {
        for ships in self.fleet.values_mut() {
            for ship in ships {
                expand_env_map(&mut ship.env)?;
            }
        }
        for module in &mut self.modules {
            expand_env_map(&mut module.config)?;
        }
        Ok(())
    }
}

/// Expand ${...} shell expressions in environment map
fn expand_env_vars(value: &str) -> anyhow::Result<String> {
    let mut result = value.to_string();

    while let Some(start) = result.find("${") {
        let end = result[start..]
            .find('}')
            .map(|i| start + i)
            .ok_or_else(|| anyhow::anyhow!("Unclosed ${{ in: {value}"))?;

        let expr = &result[start + 2..end];
        let expanded = if expr.contains(' ') || expr.contains('|') {
            let output = std::process::Command::new("sh")
                .arg("-c")
                .arg(expr)
                .output()?;
            String::from_utf8_lossy(&output.stdout).trim().to_string()
        } else {
            std::env::var(expr).unwrap_or_default()
        };

        result.replace_range(start..=end, &expanded);
    }

    Ok(result)
}

fn expand_env_map(map: &mut HashMap<String, String>) -> anyhow::Result<()> {
    for value in map.values_mut() {
        *value = expand_env_vars(value)?;
    }
    Ok(())
}

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

    #[test]
    fn test_bind_parse_tcp() {
        let bind = Bind::parse("tcp://0.0.0.0:3000").unwrap();
        match bind {
            Bind::Tcp { host, port } => {
                assert_eq!(host, "0.0.0.0");
                assert_eq!(port, 3000);
            }
            _ => panic!("Expected TCP bind"),
        }
    }

    #[test]
    fn test_bind_parse_unix() {
        let bind = Bind::parse("unix:///tmp/app.sock").unwrap();
        match bind {
            Bind::Unix { path } => assert_eq!(path, "/tmp/app.sock"),
            _ => panic!("Expected Unix bind"),
        }
    }

    #[test]
    fn test_bind_parse_port_only() {
        let bind = Bind::parse("3000").unwrap();
        match bind {
            Bind::Tcp { host, port } => {
                assert_eq!(host, "127.0.0.1");
                assert_eq!(port, 3000);
            }
            _ => panic!("Expected TCP bind"),
        }
    }

    #[test]
    fn test_manifest_parse_named_binds() {
        let toml = r#"
[mothership]
metrics_port = 9090

[mothership.bind]
http = "0.0.0.0:80"
https = "0.0.0.0:443"
ws = "0.0.0.0:8080"

[[fleet.web]]
name = "app"
command = "ruby"
args = ["app.rb"]
bind = "tcp://127.0.0.1:3000"
healthcheck = "/health"
routes = [
  { bind = "http", pattern = "/.*" },
  { bind = "https", pattern = "/.*" },
]

[[fleet.web]]
name = "cable"
command = "anycable-go"
bind = "tcp://127.0.0.1:8081"
routes = [
  { bind = "ws", pattern = "/cable" },
]

[[fleet.workers]]
name = "sidekiq"
command = "bundle"
args = ["exec", "sidekiq"]
critical = false

[[fleet.jobs]]
name = "migrate"
command = "bundle"
args = ["exec", "rails", "db:migrate"]
oneshot = true
"#;
        let manifest = Manifest::parse(toml).unwrap();

        // Check mothership named binds
        assert_eq!(manifest.mothership.bind.len(), 3);
        assert!(manifest.mothership.bind.contains_key("http"));
        assert!(manifest.mothership.bind.contains_key("https"));
        assert!(manifest.mothership.bind.contains_key("ws"));

        // Check HTTP bind
        match &manifest.mothership.bind["http"] {
            Bind::Tcp { host, port } => {
                assert_eq!(host, "0.0.0.0");
                assert_eq!(*port, 80);
            }
            _ => panic!("Expected TCP bind"),
        }

        assert_eq!(manifest.mothership.metrics_port, Some(9090));

        // Check groups
        assert_eq!(manifest.fleet.len(), 3);

        // Check routes
        let web = &manifest.fleet["web"];
        assert_eq!(web[0].name, "app");
        assert_eq!(web[0].routes.len(), 2);
        assert_eq!(web[0].routes[0].bind, "http");
        assert_eq!(web[0].routes[0].pattern, "/.*");

        let workers = &manifest.fleet["workers"];
        assert!(!workers[0].critical);

        let jobs = &manifest.fleet["jobs"];
        assert!(jobs[0].oneshot);
    }

    #[test]
    fn test_route_shorthand_parsing() {
        let toml = r#"
[[fleet.web]]
name = "app"
command = "ruby"
routes = ["http:/.*", "https:/api/.*"]
"#;
        let manifest = Manifest::parse(toml).unwrap();

        let web = &manifest.fleet["web"];
        assert_eq!(web[0].routes.len(), 2);
        assert_eq!(web[0].routes[0].bind, "http");
        assert_eq!(web[0].routes[0].pattern, "/.*");
        assert_eq!(web[0].routes[1].bind, "https");
        assert_eq!(web[0].routes[1].pattern, "/api/.*");
    }

    #[test]
    fn test_route_legacy_fallback() {
        // Legacy format: just pattern, defaults to "http" bind
        let toml = r#"
[[fleet.web]]
name = "app"
command = "ruby"
routes = ["/.*", "/api/.*"]
"#;
        let manifest = Manifest::parse(toml).unwrap();

        let web = &manifest.fleet["web"];
        assert_eq!(web[0].routes.len(), 2);
        assert_eq!(web[0].routes[0].bind, "http");
        assert_eq!(web[0].routes[0].pattern, "/.*");
        assert_eq!(web[0].routes[1].bind, "http");
        assert_eq!(web[0].routes[1].pattern, "/api/.*");
    }

    #[test]
    fn test_manifest_all_ships() {
        let toml = r#"
[[fleet.web]]
name = "app"
command = "ruby"

[[fleet.workers]]
name = "sidekiq"
command = "bundle"
"#;
        let manifest = Manifest::parse(toml).unwrap();
        let all = manifest.all_ships();
        assert_eq!(all.len(), 2);
    }

    #[test]
    fn test_manifest_empty_binds() {
        let toml = r#"
[mothership]
metrics_port = 9090
"#;
        let manifest = Manifest::parse(toml).unwrap();
        assert_eq!(manifest.ship_count(), 0);
        assert!(manifest.mothership.bind.is_empty());
    }

    #[test]
    fn test_manifest_minimal() {
        // Completely empty manifest is valid
        let toml = "";
        let manifest = Manifest::parse(toml).unwrap();
        assert_eq!(manifest.ship_count(), 0);
        assert!(manifest.mothership.bind.is_empty());
    }

    #[test]
    fn test_base_ship_inheritance() {
        let toml = r#"
[base.ship]
env = { RAILS_ENV = "production", LOG_LEVEL = "info" }
critical = false
tags = ["ruby", "rails"]
comment = "Default Rails ship"

[[fleet.web]]
name = "app"
command = "ruby"
env = { PORT = "3000" }  # Override/extend env
tags = ["web"]  # Additional tag

[[fleet.web]]
name = "api"
command = "ruby"
critical = true  # Override critical
comment = "API server"  # Override comment
"#;
        let manifest = Manifest::parse(toml).unwrap();
        let web = &manifest.fleet["web"];

        // First ship inherits from base
        let app = &web[0];
        assert_eq!(app.env.get("RAILS_ENV"), Some(&"production".to_string()));
        assert_eq!(app.env.get("LOG_LEVEL"), Some(&"info".to_string()));
        assert_eq!(app.env.get("PORT"), Some(&"3000".to_string()));
        assert!(!app.critical); // From base
        assert!(app.tags.contains(&"ruby".to_string()));
        assert!(app.tags.contains(&"rails".to_string()));
        assert!(app.tags.contains(&"web".to_string()));
        assert_eq!(app.comment, Some("Default Rails ship".to_string()));

        // Second ship overrides some base values
        let api = &web[1];
        assert!(api.critical); // Overridden
        assert_eq!(api.comment, Some("API server".to_string())); // Overridden
        assert!(api.tags.contains(&"ruby".to_string())); // Still inherited
    }

    #[test]
    fn test_base_module_inheritance() {
        let toml = r#"
[base.module]
phase = "request"
tags = ["security"]
config = { timeout = "30" }
comment = "Default security module"

[[modules]]
name = "auth"
wasm = "auth.wasm"
config = { secret = "xyz" }

[[modules]]
name = "rate_limit"
wasm = "rate_limit.wasm"
phase = "response"  # Override
tags = ["performance"]
"#;
        let manifest = Manifest::parse(toml).unwrap();

        // First module inherits from base
        let auth = &manifest.modules[0];
        assert_eq!(auth.phase, Phase::Request);
        assert_eq!(auth.config.get("timeout"), Some(&"30".to_string()));
        assert_eq!(auth.config.get("secret"), Some(&"xyz".to_string()));
        assert!(auth.tags.contains(&"security".to_string()));
        assert_eq!(auth.comment, Some("Default security module".to_string()));

        // Second module overrides phase and adds tags
        let rate_limit = &manifest.modules[1];
        assert_eq!(rate_limit.phase, Phase::Response); // Overridden
        assert!(rate_limit.tags.contains(&"security".to_string())); // Inherited
        assert!(rate_limit.tags.contains(&"performance".to_string())); // Added
    }
}
