use std::collections::HashMap;
use std::sync::OnceLock;

use regex::Regex;
use serde::{Deserialize, Serialize};

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

/// Top-level config section; embed under `[channels]` in your TOML.
///
/// Credential fields (`webhook_url`, `bot_token`, etc.) support `${VAR}` and
/// `${VAR:-default}` interpolation.  Resolution is performed automatically by
/// [`ChannelRegistry::from_config`] / [`ChannelRegistry::from_map`] via
/// [`ChannelConfig::resolve_secrets`].  Plain literals pass through unchanged.
///
/// ```toml
/// [channels.team-slack]
/// type        = "slack"
/// webhook_url = "${SLACK_WEBHOOK_URL}"
///
/// [channels.ops-discord]
/// type        = "discord"
/// webhook_url = "${DISCORD_WEBHOOK_URL}"
///
/// [channels.alerts-telegram]
/// type      = "telegram"
/// bot_token = "${TELEGRAM_BOT_TOKEN}"
/// chat_id   = "${TELEGRAM_CHAT_ID:-100999}"
/// ```
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ChannelsConfig {
    #[serde(default)]
    pub channels: HashMap<String, ChannelConfig>,
}

impl ChannelsConfig {
    /// Resolve all `${VAR}` references in every channel entry.
    ///
    /// Consumes `self` and returns a new `ChannelsConfig` with literals. Fails
    /// on the first channel whose secrets cannot be resolved.
    pub fn resolve_secrets(self) -> Result<Self> {
        let channels = self
            .channels
            .into_iter()
            .map(|(name, cfg)| cfg.resolve_secrets().map(|c| (name, c)))
            .collect::<Result<_>>()?;
        Ok(Self { channels })
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ChannelConfig {
    Slack(SlackConfig),
    Discord(DiscordConfig),
    Telegram(TelegramConfig),
}

impl ChannelConfig {
    /// Resolve `${VAR}` / `${VAR:-default}` references in all credential
    /// fields. Plain string literals are returned unchanged.
    pub fn resolve_secrets(self) -> Result<Self> {
        match self {
            Self::Slack(c) => Ok(Self::Slack(c.resolve_secrets()?)),
            Self::Discord(c) => Ok(Self::Discord(c.resolve_secrets()?)),
            Self::Telegram(c) => Ok(Self::Telegram(c.resolve_secrets()?)),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlackConfig {
    pub webhook_url: String,
    /// Channel override (e.g. `#alerts`). The webhook already targets a
    /// channel; this overrides it for workspaces that allow it.
    pub channel: Option<String>,
    pub username: Option<String>,
}

impl SlackConfig {
    pub fn resolve_secrets(self) -> Result<Self> {
        Ok(Self {
            webhook_url: interpolate(&self.webhook_url)?,
            channel: self.channel.map(|s| interpolate(&s)).transpose()?,
            username: self.username.map(|s| interpolate(&s)).transpose()?,
        })
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscordConfig {
    pub webhook_url: String,
    pub username: Option<String>,
    pub avatar_url: Option<String>,
}

impl DiscordConfig {
    pub fn resolve_secrets(self) -> Result<Self> {
        Ok(Self {
            webhook_url: interpolate(&self.webhook_url)?,
            username: self.username.map(|s| interpolate(&s)).transpose()?,
            avatar_url: self.avatar_url.map(|s| interpolate(&s)).transpose()?,
        })
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelegramConfig {
    pub bot_token: String,
    /// Numeric chat ID (e.g. `-1001234567890` for a supergroup).
    pub chat_id: String,
    /// Override the Bot API base URL. Leave `None` for production.
    /// Useful for pointing at a local mock server during tests.
    pub api_base: Option<String>,
}

impl TelegramConfig {
    pub fn api_url(&self) -> String {
        let base = self
            .api_base
            .as_deref()
            .unwrap_or("https://api.telegram.org");
        format!("{}/bot{}/sendMessage", base, self.bot_token)
    }

    pub fn resolve_secrets(self) -> Result<Self> {
        Ok(Self {
            bot_token: interpolate(&self.bot_token)?,
            chat_id: interpolate(&self.chat_id)?,
            api_base: self.api_base.map(|s| interpolate(&s)).transpose()?,
        })
    }
}

/// Expand every `${VAR}` / `${VAR:-default}` reference found anywhere in `s`.
///
/// - `${FOO}` → value of `FOO`, error if unset
/// - `${FOO:-bar}` → value of `FOO` if set, `"bar"` otherwise
/// - Anything else → returned unchanged
fn interpolate(s: &str) -> Result<String> {
    static RE: OnceLock<Regex> = OnceLock::new();
    let re = RE.get_or_init(|| {
        // Matches ${VAR} and ${VAR:-default} anywhere in the string.
        Regex::new(r"\$\{([^}:]+)(?::-(.*?))?\}").expect("static regex is valid")
    });

    // Fast path: no placeholder in the string.
    if !s.contains("${") {
        return Ok(s.to_string());
    }

    let mut result = s.to_string();
    for cap in re.captures_iter(s) {
        let full = cap.get(0).unwrap().as_str();
        let var_name = cap.get(1).unwrap().as_str();
        let default = cap.get(2).map(|m| m.as_str());

        let value = match std::env::var(var_name) {
            Ok(v) => v,
            Err(_) => match default {
                Some(d) => d.to_string(),
                None => return Err(ChannelError::SecretNotFound(var_name.to_string())),
            },
        };
        result = result.replace(full, &value);
    }
    Ok(result)
}

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

    #[test]
    fn literal_passthrough() {
        let v = interpolate("https://hooks.slack.com/services/T/B/token").unwrap();
        assert_eq!(v, "https://hooks.slack.com/services/T/B/token");
    }

    #[test]
    fn env_var_resolved() {
        // SAFETY: single-threaded test process, no concurrent env access.
        unsafe { std::env::set_var("TEST_CHANNELS_WEBHOOK", "https://resolved.example.com") };
        let v = interpolate("${TEST_CHANNELS_WEBHOOK}").unwrap();
        assert_eq!(v, "https://resolved.example.com");
    }

    #[test]
    fn env_var_with_default_used_when_unset() {
        // SAFETY: single-threaded test process, no concurrent env access.
        unsafe { std::env::remove_var("TEST_CHANNELS_MISSING") };
        let v = interpolate("${TEST_CHANNELS_MISSING:-fallback-token}").unwrap();
        assert_eq!(v, "fallback-token");
    }

    #[test]
    fn env_var_missing_no_default_errors() {
        // SAFETY: single-threaded test process, no concurrent env access.
        unsafe { std::env::remove_var("TEST_CHANNELS_REQUIRED") };
        let err = interpolate("${TEST_CHANNELS_REQUIRED}").unwrap_err();
        assert!(
            matches!(err, ChannelError::SecretNotFound(ref v) if v == "TEST_CHANNELS_REQUIRED")
        );
    }

    #[test]
    fn partial_interpolation_in_url() {
        // SAFETY: single-threaded test process, no concurrent env access.
        unsafe { std::env::set_var("TEST_CHANNELS_PARTIAL_TOKEN", "abc123") };
        let v =
            interpolate("https://hooks.example.com/services/${TEST_CHANNELS_PARTIAL_TOKEN}/end")
                .unwrap();
        assert_eq!(v, "https://hooks.example.com/services/abc123/end");
    }

    #[test]
    fn slack_config_resolves_secrets() {
        // SAFETY: single-threaded test process, no concurrent env access.
        unsafe { std::env::set_var("TEST_SLACK_WEBHOOK", "https://hooks.slack.com/s/t/b/x") };
        let cfg = SlackConfig {
            webhook_url: "${TEST_SLACK_WEBHOOK}".to_string(),
            channel: Some("#alerts".to_string()),
            username: None,
        };
        let resolved = cfg.resolve_secrets().unwrap();
        assert_eq!(resolved.webhook_url, "https://hooks.slack.com/s/t/b/x");
        assert_eq!(resolved.channel.as_deref(), Some("#alerts"));
    }

    #[test]
    fn telegram_config_resolves_secrets() {
        // SAFETY: single-threaded test process, no concurrent env access.
        unsafe {
            std::env::set_var("TEST_TG_TOKEN", "999:TOKEN");
            std::env::set_var("TEST_TG_CHAT", "-100999");
        }
        let cfg = TelegramConfig {
            bot_token: "${TEST_TG_TOKEN}".to_string(),
            chat_id: "${TEST_TG_CHAT}".to_string(),
            api_base: None,
        };
        let resolved = cfg.resolve_secrets().unwrap();
        assert_eq!(resolved.bot_token, "999:TOKEN");
        assert_eq!(resolved.chat_id, "-100999");
    }
}
