//! Channel command handling and health checks.

use super::dingtalk::DingTalkChannel;
use super::discord::DiscordChannel;
use super::email_channel::EmailChannel;
use super::imessage::IMessageChannel;
use super::irc;
use super::irc::IrcChannel;
use super::lark::LarkChannel;
use super::linq::LinqChannel;
#[cfg(feature = "channel-matrix")]
use super::matrix::MatrixChannel;
use super::qq::QQChannel;
use super::signal::SignalChannel;
use super::slack::SlackChannel;
use super::telegram::TelegramChannel;
use super::whatsapp::WhatsAppChannel;
#[cfg(feature = "whatsapp-web")]
use super::whatsapp_web::WhatsAppWebChannel;
use super::Channel;
use crate::openhuman::config::Config;
use anyhow::Result;
use std::sync::Arc;
use std::time::Duration;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ChannelHealthState {
    Healthy,
    Unhealthy,
    Timeout,
}

pub(crate) fn classify_health_result(
    result: &std::result::Result<bool, tokio::time::error::Elapsed>,
) -> ChannelHealthState {
    match result {
        Ok(true) => ChannelHealthState::Healthy,
        Ok(false) => ChannelHealthState::Unhealthy,
        Err(_) => ChannelHealthState::Timeout,
    }
}

/// Run health checks for configured channels.
pub async fn doctor_channels(config: Config) -> Result<()> {
    let mut channels: Vec<(&'static str, Arc<dyn Channel>)> = Vec::new();

    if let Some(ref tg) = config.channels_config.telegram {
        channels.push((
            "Telegram",
            Arc::new(
                TelegramChannel::new(
                    tg.bot_token.clone(),
                    tg.allowed_users.clone(),
                    tg.mention_only,
                )
                .with_streaming(
                    tg.stream_mode,
                    tg.draft_update_interval_ms,
                    tg.silent_streaming,
                ),
            ),
        ));
    }

    if let Some(ref dc) = config.channels_config.discord {
        channels.push((
            "Discord",
            Arc::new(DiscordChannel::new(
                dc.bot_token.clone(),
                dc.guild_id.clone(),
                dc.channel_id.clone(),
                dc.allowed_users.clone(),
                dc.listen_to_bots,
                dc.mention_only,
            )),
        ));
    }

    if let Some(ref sl) = config.channels_config.slack {
        channels.push((
            "Slack",
            Arc::new(SlackChannel::new(
                sl.bot_token.clone(),
                sl.channel_id.clone(),
                sl.allowed_users.clone(),
            )),
        ));
    }

    if let Some(ref im) = config.channels_config.imessage {
        channels.push((
            "iMessage",
            Arc::new(IMessageChannel::new(im.allowed_contacts.clone())),
        ));
    }

    #[cfg(feature = "channel-matrix")]
    if let Some(ref mx) = config.channels_config.matrix {
        channels.push((
            "Matrix",
            Arc::new(MatrixChannel::new_with_session_hint(
                mx.homeserver.clone(),
                mx.access_token.clone(),
                mx.room_id.clone(),
                mx.allowed_users.clone(),
                mx.user_id.clone(),
                mx.device_id.clone(),
            )),
        ));
    }

    #[cfg(not(feature = "channel-matrix"))]
    if config.channels_config.matrix.is_some() {
        tracing::warn!(
            "Matrix channel is configured but this build was compiled without `channel-matrix`; skipping Matrix health check."
        );
    }

    if let Some(ref sig) = config.channels_config.signal {
        channels.push((
            "Signal",
            Arc::new(SignalChannel::new(
                sig.http_url.clone(),
                sig.account.clone(),
                sig.group_id.clone(),
                sig.allowed_from.clone(),
                sig.ignore_attachments,
                sig.ignore_stories,
            )),
        ));
    }

    if let Some(ref wa) = config.channels_config.whatsapp {
        // Runtime negotiation: detect backend type from config
        match wa.backend_type() {
            "cloud" => {
                // Cloud API mode: requires phone_number_id, access_token, verify_token
                if wa.is_cloud_config() {
                    channels.push((
                        "WhatsApp",
                        Arc::new(WhatsAppChannel::new(
                            wa.access_token.clone().unwrap_or_default(),
                            wa.phone_number_id.clone().unwrap_or_default(),
                            wa.verify_token.clone().unwrap_or_default(),
                            wa.allowed_numbers.clone(),
                        )),
                    ));
                } else {
                    tracing::warn!("WhatsApp Cloud API configured but missing required fields (phone_number_id, access_token, verify_token)");
                }
            }
            "web" => {
                // Web mode: requires session_path
                #[cfg(feature = "whatsapp-web")]
                if wa.is_web_config() {
                    channels.push((
                        "WhatsApp",
                        Arc::new(WhatsAppWebChannel::new(
                            wa.session_path.clone().unwrap_or_default(),
                            wa.pair_phone.clone(),
                            wa.pair_code.clone(),
                            wa.allowed_numbers.clone(),
                        )),
                    ));
                } else {
                    tracing::warn!("WhatsApp Web configured but session_path not set");
                }
                #[cfg(not(feature = "whatsapp-web"))]
                {
                    tracing::warn!("WhatsApp Web backend requires 'whatsapp-web' feature. Enable with: cargo build --features whatsapp-web");
                }
            }
            _ => {
                tracing::warn!("WhatsApp config invalid: neither phone_number_id (Cloud API) nor session_path (Web) is set");
            }
        }
    }

    if let Some(ref lq) = config.channels_config.linq {
        channels.push((
            "Linq",
            Arc::new(LinqChannel::new(
                lq.api_token.clone(),
                lq.from_phone.clone(),
                lq.allowed_senders.clone(),
            )),
        ));
    }

    if let Some(ref email_cfg) = config.channels_config.email {
        channels.push(("Email", Arc::new(EmailChannel::new(email_cfg.clone()))));
    }

    if let Some(ref irc) = config.channels_config.irc {
        channels.push((
            "IRC",
            Arc::new(IrcChannel::new(irc::IrcChannelConfig {
                server: irc.server.clone(),
                port: irc.port,
                nickname: irc.nickname.clone(),
                username: irc.username.clone(),
                channels: irc.channels.clone(),
                allowed_users: irc.allowed_users.clone(),
                server_password: irc.server_password.clone(),
                nickserv_password: irc.nickserv_password.clone(),
                sasl_password: irc.sasl_password.clone(),
                verify_tls: irc.verify_tls.unwrap_or(true),
            })),
        ));
    }

    if let Some(ref lk) = config.channels_config.lark {
        channels.push(("Lark", Arc::new(LarkChannel::from_config(lk))));
    }

    if let Some(ref dt) = config.channels_config.dingtalk {
        channels.push((
            "DingTalk",
            Arc::new(DingTalkChannel::new(
                dt.client_id.clone(),
                dt.client_secret.clone(),
                dt.allowed_users.clone(),
            )),
        ));
    }

    if let Some(ref qq) = config.channels_config.qq {
        channels.push((
            "QQ",
            Arc::new(QQChannel::new(
                qq.app_id.clone(),
                qq.app_secret.clone(),
                qq.allowed_users.clone(),
            )),
        ));
    }

    if channels.is_empty() {
        println!("No real-time channels configured. Configure channels in the web UI.");
        return Ok(());
    }

    println!("🩺 OpenHuman Channel Doctor");
    println!();

    let mut healthy = 0_u32;
    let mut unhealthy = 0_u32;
    let mut timeout = 0_u32;

    for (name, channel) in channels {
        let result = tokio::time::timeout(Duration::from_secs(10), channel.health_check()).await;
        let state = classify_health_result(&result);

        match state {
            ChannelHealthState::Healthy => {
                healthy += 1;
                println!("  ✅ {name:<9} healthy");
            }
            ChannelHealthState::Unhealthy => {
                unhealthy += 1;
                println!("  ❌ {name:<9} unhealthy (auth/config/network)");
            }
            ChannelHealthState::Timeout => {
                timeout += 1;
                println!("  ⏱️  {name:<9} timed out (>10s)");
            }
        }
    }

    if config.channels_config.webhook.is_some() {
        println!("  ℹ️  Webhook   ensure your webhook endpoint is reachable");
    }

    println!();
    println!("Summary: {healthy} healthy, {unhealthy} unhealthy, {timeout} timed out");
    Ok(())
}

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

    #[test]
    fn classify_health_result_maps_all_outcomes() {
        assert_eq!(
            classify_health_result(&Ok(true)),
            ChannelHealthState::Healthy
        );
        assert_eq!(
            classify_health_result(&Ok(false)),
            ChannelHealthState::Unhealthy
        );
    }

    #[tokio::test]
    async fn classify_health_result_maps_timeout() {
        let elapsed = tokio::time::timeout(
            std::time::Duration::from_millis(1),
            std::future::pending::<()>(),
        )
        .await
        .unwrap_err();
        assert_eq!(
            classify_health_result(&Err(elapsed)),
            ChannelHealthState::Timeout
        );
    }

    #[tokio::test]
    async fn doctor_channels_returns_ok_when_no_channels_are_configured() {
        let mut config = Config::default();
        config.channels_config = crate::openhuman::config::ChannelsConfig::default();
        doctor_channels(config).await.unwrap();
    }

    #[tokio::test]
    async fn doctor_channels_runs_with_telegram_config() {
        use crate::openhuman::config::{StreamMode, TelegramConfig};
        let mut config = Config::default();
        config.channels_config = crate::openhuman::config::ChannelsConfig::default();
        config.channels_config.telegram = Some(TelegramConfig {
            bot_token: "fake:token".into(),
            allowed_users: vec!["user1".into()],
            stream_mode: StreamMode::default(),
            draft_update_interval_ms: 2000,
            silent_streaming: true,
            mention_only: false,
        });
        let _ = doctor_channels(config).await;
    }

    #[tokio::test]
    async fn doctor_channels_runs_with_discord_config() {
        use crate::openhuman::config::DiscordConfig;
        let mut config = Config::default();
        config.channels_config = crate::openhuman::config::ChannelsConfig::default();
        config.channels_config.discord = Some(DiscordConfig {
            bot_token: "fake".into(),
            guild_id: Some("123".into()),
            channel_id: Some("456".into()),
            allowed_users: vec![],
            listen_to_bots: false,
            mention_only: true,
        });
        let _ = doctor_channels(config).await;
    }

    #[tokio::test]
    async fn doctor_channels_runs_with_slack_config() {
        use crate::openhuman::config::SlackConfig;
        let mut config = Config::default();
        config.channels_config = crate::openhuman::config::ChannelsConfig::default();
        config.channels_config.slack = Some(SlackConfig {
            bot_token: "fake".into(),
            app_token: None,
            channel_id: Some("C123".into()),
            allowed_users: vec![],
        });
        let _ = doctor_channels(config).await;
    }

    #[tokio::test]
    async fn doctor_channels_runs_with_imessage_config() {
        use crate::openhuman::config::IMessageConfig;
        let mut config = Config::default();
        config.channels_config = crate::openhuman::config::ChannelsConfig::default();
        config.channels_config.imessage = Some(IMessageConfig {
            allowed_contacts: vec!["a@b.com".into()],
        });
        let _ = doctor_channels(config).await;
    }

    #[tokio::test]
    async fn doctor_channels_runs_with_multiple_channels() {
        use crate::openhuman::config::{DiscordConfig, SlackConfig, StreamMode, TelegramConfig};
        let mut config = Config::default();
        config.channels_config = crate::openhuman::config::ChannelsConfig::default();
        config.channels_config.telegram = Some(TelegramConfig {
            bot_token: "fake".into(),
            allowed_users: vec![],
            stream_mode: StreamMode::default(),
            draft_update_interval_ms: 2000,
            silent_streaming: true,
            mention_only: false,
        });
        config.channels_config.discord = Some(DiscordConfig {
            bot_token: "fake".into(),
            guild_id: Some("123".into()),
            channel_id: Some("456".into()),
            allowed_users: vec![],
            listen_to_bots: false,
            mention_only: false,
        });
        config.channels_config.slack = Some(SlackConfig {
            bot_token: "fake".into(),
            app_token: None,
            channel_id: Some("C123".into()),
            allowed_users: vec![],
        });
        let _ = doctor_channels(config).await;
    }
}
