use async_trait::async_trait;
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
use zeroclaw_api::channel::{Channel, ChannelMessage, SendMessage};

const DEFAULT_BASE_URL: &str = "https://ilinkai.weixin.qq.com";
const DEFAULT_LONG_POLL_TIMEOUT_MS: u64 = 35_000;
const SESSION_EXPIRED_ERRCODE: i64 = -14;
const MAX_CONSECUTIVE_FAILURES: u32 = 3;
const BACKOFF_DELAY_MS: u64 = 30_000;
const RETRY_DELAY_MS: u64 = 2_000;
const SESSION_PAUSE_DURATION_MS: u64 = 60 * 60 * 1000;
const TEXT_CHUNK_LIMIT: usize = 4000;

const MESSAGE_TYPE_BOT: i64 = 2;
const MESSAGE_ITEM_TYPE_TEXT: i64 = 1;
const MESSAGE_STATE_FINISH: i64 = 2;

#[derive(Debug, Clone, Serialize, Deserialize)]
struct BaseInfo {
    channel_version: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct GetUpdatesReq {
    get_updates_buf: String,
    base_info: BaseInfo,
}

#[derive(Debug, Clone, Deserialize)]
struct WeixinMessage {
    #[serde(default)]
    #[allow(dead_code)]
    seq: Option<i64>,
    #[serde(default)]
    #[allow(dead_code)]
    message_id: Option<i64>,
    #[serde(default)]
    from_user_id: Option<String>,
    #[serde(default)]
    #[allow(dead_code)]
    to_user_id: Option<String>,
    #[serde(default)]
    #[allow(dead_code)]
    client_id: Option<String>,
    #[serde(default)]
    #[allow(dead_code)]
    create_time_ms: Option<i64>,
    #[serde(default)]
    #[allow(dead_code)]
    message_type: Option<i64>,
    #[serde(default)]
    #[allow(dead_code)]
    message_state: Option<i64>,
    #[serde(default)]
    item_list: Option<Vec<MessageItem>>,
    #[serde(default)]
    context_token: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
struct MessageItem {
    #[serde(default)]
    r#type: Option<i64>,
    #[serde(default)]
    text_item: Option<TextItem>,
    #[serde(default)]
    voice_item: Option<VoiceItem>,
    #[serde(default)]
    ref_msg: Option<RefMessage>,
}

#[derive(Debug, Clone, Deserialize)]
struct TextItem {
    #[serde(default)]
    text: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
struct VoiceItem {
    #[serde(default)]
    text: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
struct RefMessage {
    #[serde(default)]
    title: Option<String>,
    #[serde(default)]
    message_item: Option<Box<MessageItem>>,
}

#[derive(Debug, Clone, Deserialize)]
struct GetUpdatesResp {
    #[serde(default)]
    ret: Option<i64>,
    #[serde(default)]
    errcode: Option<i64>,
    #[serde(default)]
    errmsg: Option<String>,
    #[serde(default)]
    msgs: Option<Vec<WeixinMessage>>,
    #[serde(default)]
    get_updates_buf: Option<String>,
    #[serde(default)]
    longpolling_timeout_ms: Option<u64>,
}

#[derive(Debug, Clone, Serialize)]
struct SendMessageReq {
    msg: SendMessageInner,
}

#[derive(Debug, Clone, Serialize)]
struct SendMessageInner {
    from_user_id: String,
    to_user_id: String,
    client_id: String,
    message_type: i64,
    message_state: i64,
    #[serde(skip_serializing_if = "Option::is_none")]
    item_list: Option<Vec<SendMessageItem>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    context_token: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
struct SendMessageItem {
    r#type: i64,
    text_item: SendTextItem,
}

#[derive(Debug, Clone, Serialize)]
struct SendTextItem {
    text: String,
}

#[derive(Debug, Clone, Deserialize)]
struct QrCodeResponse {
    #[serde(default)]
    #[allow(dead_code)]
    qrcode: Option<String>,
    #[serde(default)]
    qrcode_img_content: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
struct QrStatusResponse {
    #[serde(default)]
    status: Option<String>,
    #[serde(default)]
    #[allow(dead_code)]
    bot_token: Option<String>,
    #[serde(default)]
    #[allow(dead_code)]
    ilink_bot_id: Option<String>,
    #[serde(default)]
    #[allow(dead_code)]
    baseurl: Option<String>,
    #[serde(default)]
    #[allow(dead_code)]
    ilink_user_id: Option<String>,
    #[serde(default)]
    redirect_host: Option<String>,
}

#[derive(Debug, Clone)]
struct SessionPause {
    until: std::time::Instant,
}

fn now_secs() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs()
}

fn generate_client_id() -> String {
    let id = Uuid::new_v4();
    format!("wordbot-weixin-{}", id)
}

fn random_wechat_uin() -> String {
    let val: u32 = rand::random();
    BASE64.encode(val.to_string().as_bytes())
}

fn extract_text_from_items(items: &[MessageItem]) -> String {
    for item in items {
        if let Some(t) = item.r#type {
            if t == MESSAGE_ITEM_TYPE_TEXT
                && let Some(ref text_item) = item.text_item
                && let Some(ref text) = text_item.text
            {
                let text = text.trim().to_string();
                if text.is_empty() {
                    continue;
                }
                if let Some(ref ref_msg) = item.ref_msg {
                    let mut parts = Vec::new();
                    if let Some(ref title) = ref_msg.title
                        && !title.is_empty()
                    {
                        parts.push(title.clone());
                    }
                    if let Some(ref mi) = ref_msg.message_item {
                        let ref_body = extract_text_from_items(std::slice::from_ref(mi));
                        if !ref_body.is_empty() {
                            parts.push(ref_body);
                        }
                    }
                    if parts.is_empty() {
                        return text;
                    }
                    return format!("[引用: {}]\n{}", parts.join(" | "), text);
                }
                return text;
            }
            if t == 3
                && let Some(ref voice_item) = item.voice_item
                && let Some(ref text) = voice_item.text
            {
                let text = text.trim().to_string();
                if !text.is_empty() {
                    return text;
                }
            }
        }
    }
    String::new()
}

fn split_text_chunks(text: &str, limit: usize) -> Vec<&str> {
    if text.len() <= limit {
        return vec![text];
    }
    let mut chunks = Vec::new();
    let mut start = 0;
    while start < text.len() {
        let end = (start + limit).min(text.len());
        if end >= text.len() {
            chunks.push(&text[start..]);
            break;
        }
        let split_at = text[start..end]
            .rfind('\n')
            .map(|pos| start + pos + 1)
            .unwrap_or(end);
        let split_at = if text.is_char_boundary(split_at) {
            split_at
        } else {
            (start..split_at)
                .rev()
                .find(|&i| text.is_char_boundary(i))
                .unwrap_or(end)
        };
        if split_at <= start {
            let forced = (end..=text.len())
                .find(|&i| text.is_char_boundary(i))
                .unwrap_or(text.len());
            chunks.push(&text[start..forced]);
            start = forced;
        } else {
            chunks.push(&text[start..split_at]);
            start = split_at;
        }
    }
    chunks
}

pub struct WeixinChannel {
    base_url: String,
    token: String,
    allowed_users: Vec<String>,
    http_client: reqwest::Client,
    proxy_url: Option<String>,
    context_tokens: Arc<RwLock<HashMap<String, String>>>,
    session_pauses: Arc<RwLock<HashMap<String, SessionPause>>>,
    get_updates_buf: Arc<RwLock<String>>,
}

impl WeixinChannel {
    pub fn new(token: String, allowed_users: Vec<String>) -> Self {
        Self::new_with_base_url(DEFAULT_BASE_URL.to_string(), token, allowed_users)
    }

    pub fn new_with_base_url(base_url: String, token: String, allowed_users: Vec<String>) -> Self {
        Self {
            base_url,
            token,
            allowed_users,
            http_client: zeroclaw_config::schema::build_channel_proxy_client(
                "channel.weixin",
                None,
            ),
            proxy_url: None,
            context_tokens: Arc::new(RwLock::new(HashMap::new())),
            session_pauses: Arc::new(RwLock::new(HashMap::new())),
            get_updates_buf: Arc::new(RwLock::new(String::new())),
        }
    }

    pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
        self.http_client = zeroclaw_config::schema::build_channel_proxy_client(
            "channel.weixin",
            proxy_url.as_deref(),
        );
        self.proxy_url = proxy_url;
        self
    }

    fn is_user_allowed(&self, user_id: &str) -> bool {
        self.allowed_users.iter().any(|u| u == "*" || u == user_id)
    }

    fn build_base_info() -> BaseInfo {
        BaseInfo {
            channel_version: env!("CARGO_PKG_VERSION").to_string(),
        }
    }

    fn build_headers(&self, body_len: usize) -> HashMap<String, String> {
        let mut headers = HashMap::new();
        headers.insert("Content-Type".to_string(), "application/json".to_string());
        headers.insert(
            "AuthorizationType".to_string(),
            "ilink_bot_token".to_string(),
        );
        headers.insert(
            "Authorization".to_string(),
            format!("Bearer {}", self.token.trim()),
        );
        headers.insert("Content-Length".to_string(), body_len.to_string());
        headers.insert("X-WECHAT-UIN".to_string(), random_wechat_uin());
        headers
    }

    async fn api_post(&self, endpoint: &str, body: &str) -> anyhow::Result<String> {
        let url = format!("{}/{}", self.base_url.trim_end_matches('/'), endpoint);
        let headers = self.build_headers(body.len());

        let mut req = self.http_client.post(&url);
        for (k, v) in &headers {
            req = req.header(k.as_str(), v.as_str());
        }

        let resp = req.body(body.to_string()).send().await?;

        if !resp.status().is_success() {
            let status = resp.status();
            let err = resp.text().await.unwrap_or_default();
            anyhow::bail!("Weixin API POST {} failed ({}): {}", endpoint, status, err);
        }

        Ok(resp.text().await?)
    }

    async fn api_get(&self, endpoint: &str) -> anyhow::Result<String> {
        let url = format!("{}/{}", self.base_url.trim_end_matches('/'), endpoint);

        let resp = self.http_client.get(&url).send().await?;

        if !resp.status().is_success() {
            let status = resp.status();
            let err = resp.text().await.unwrap_or_default();
            anyhow::bail!("Weixin API GET {} failed ({}): {}", endpoint, status, err);
        }

        Ok(resp.text().await?)
    }

    async fn get_updates(
        &self,
        get_updates_buf: &str,
        timeout_ms: u64,
    ) -> anyhow::Result<GetUpdatesResp> {
        let req = GetUpdatesReq {
            get_updates_buf: get_updates_buf.to_string(),
            base_info: Self::build_base_info(),
        };
        let body = serde_json::to_string(&req)?;

        let client = self.http_client.clone();
        let url = format!(
            "{}/ilink/bot/getupdates",
            self.base_url.trim_end_matches('/')
        );
        let headers = self.build_headers(body.len());

        let resp = tokio::time::timeout(
            std::time::Duration::from_millis(timeout_ms + 10_000),
            async {
                let mut req_builder = client.post(&url);
                for (k, v) in &headers {
                    req_builder = req_builder.header(k.as_str(), v.as_str());
                }
                req_builder = req_builder.body(body.clone());
                let resp = req_builder.send().await?;
                let status = resp.status();
                let text = resp.text().await?;
                if !status.is_success() {
                    anyhow::bail!("getUpdates HTTP error ({}): {}", status, text);
                }
                let parsed: GetUpdatesResp = serde_json::from_str(&text)?;
                Ok(parsed)
            },
        )
        .await??;

        Ok(resp)
    }

    async fn send_message_api(
        &self,
        to: &str,
        text: &str,
        context_token: Option<&str>,
    ) -> anyhow::Result<String> {
        let client_id = generate_client_id();
        let items = if text.is_empty() {
            None
        } else {
            Some(vec![SendMessageItem {
                r#type: MESSAGE_ITEM_TYPE_TEXT,
                text_item: SendTextItem {
                    text: text.to_string(),
                },
            }])
        };

        let req = SendMessageReq {
            msg: SendMessageInner {
                from_user_id: String::new(),
                to_user_id: to.to_string(),
                client_id,
                message_type: MESSAGE_TYPE_BOT,
                message_state: MESSAGE_STATE_FINISH,
                item_list: items,
                context_token: context_token.map(|s| s.to_string()),
            },
        };

        let body = serde_json::to_string(&req)?;
        self.api_post("ilink/bot/sendmessage", &body).await
    }

    async fn is_session_paused(&self, account_id: &str) -> bool {
        let pauses = self.session_pauses.read().await;
        if let Some(pause) = pauses.get(account_id)
            && std::time::Instant::now() < pause.until
        {
            return true;
        }
        false
    }

    async fn pause_session(&self, account_id: &str) {
        let until =
            std::time::Instant::now() + std::time::Duration::from_millis(SESSION_PAUSE_DURATION_MS);
        let mut pauses = self.session_pauses.write().await;
        pauses.insert(account_id.to_string(), SessionPause { until });
    }

    async fn get_context_token(&self, user_id: &str) -> Option<String> {
        let tokens = self.context_tokens.read().await;
        tokens.get(user_id).cloned()
    }

    async fn set_context_token(&self, user_id: &str, token: &str) {
        let mut tokens = self.context_tokens.write().await;
        tokens.insert(user_id.to_string(), token.to_string());
    }

    pub async fn start_qr_login(&self) -> anyhow::Result<(String, String)> {
        let (key, url) = self.fetch_qr_code().await?;
        self.display_qr_code(&url).await;
        Ok((key, url))
    }

    pub async fn fetch_qr_code(&self) -> anyhow::Result<(String, String)> {
        let endpoint = "ilink/bot/get_bot_qrcode?bot_type=3";
        let raw = self.api_get(endpoint).await?;
        let resp: QrCodeResponse = serde_json::from_str(&raw)?;

        match (&resp.qrcode, &resp.qrcode_img_content) {
            (Some(key), Some(url)) => Ok((key.clone(), url.clone())),
            _ => {
                anyhow::bail!("Weixin: failed to get QR code from API")
            }
        }
    }

    pub fn generate_qr_png_base64(qr_url: &str) -> anyhow::Result<String> {
        let code = qrcode::QrCode::new(qr_url).map_err(|e| anyhow::anyhow!("QR encode: {e}"))?;
        let img = code.render::<image::Luma<u8>>().build();
        let mut buf = Vec::new();
        let mut cursor = std::io::Cursor::new(&mut buf);
        img.write_to(&mut cursor, image::ImageFormat::Png)
            .map_err(|e| anyhow::anyhow!("PNG encode: {e}"))?;
        Ok(format!(
            "data:image/png;base64,{}",
            BASE64.encode(&buf)
        ))
    }

    async fn display_qr_code(&self, data_url: &str) {
        if data_url.starts_with("http") {
            println!("\n┌─────────────────────────────────────┐");
            println!("│     Scanning QR Code with WeChat    │");
            println!("│                                     │");
            println!("│  QR code URL: {}", data_url);
            println!("│                                     │");
            println!("└─────────────────────────────────────┘\n");

            if let Err(e) = self.open_in_browser(data_url).await {
                eprintln!("Failed to open browser: {}", e);
                eprintln!("Please manually open the URL above in your browser\n");
            }
        } else {
            println!("\nQR code data: {}", data_url);
        }
    }

    #[cfg(target_os = "macos")]
    async fn open_in_browser(&self, url: &str) -> anyhow::Result<()> {
        let status = tokio::process::Command::new("open")
            .arg(url)
            .status()
            .await?;
        if !status.success() {
            anyhow::bail!("open command failed with status: {}", status);
        }
        Ok(())
    }

    #[cfg(target_os = "linux")]
    async fn open_in_browser(&self, url: &str) -> anyhow::Result<()> {
        let status = tokio::process::Command::new("xdg-open")
            .arg(url)
            .status()
            .await?;
        if !status.success() {
            anyhow::bail!("xdg-open command failed with status: {}", status);
        }
        Ok(())
    }

    #[cfg(target_os = "windows")]
    async fn open_in_browser(&self, url: &str) -> anyhow::Result<()> {
        let status = tokio::process::Command::new("cmd")
            .args(["/c", "start", "", url])
            .status()
            .await?;
        if !status.success() {
            anyhow::bail!("start command failed with status: {}", status);
        }
        Ok(())
    }

    pub async fn wait_for_qr_login(
        &self,
        qrcode_key: &str,
        timeout_ms: u64,
    ) -> anyhow::Result<QrLoginResult> {
        let deadline = std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms);
        let mut current_base_url = self.base_url.clone();

        while std::time::Instant::now() < deadline {
            let endpoint = format!(
                "ilink/bot/get_qrcode_status?qrcode={}",
                urlencoding::encode(qrcode_key)
            );
            let url = format!("{}/{}", current_base_url.trim_end_matches('/'), endpoint);

            let result = tokio::time::timeout(
                std::time::Duration::from_millis(35_000),
                self.http_client.get(&url).send(),
            )
            .await;

            match result {
                Ok(Ok(resp)) if resp.status().is_success() => {
                    if let Ok(text) = resp.text().await
                        && let Ok(status) = serde_json::from_str::<QrStatusResponse>(&text)
                    {
                        match status.status.as_deref() {
                            Some("confirmed") => {
                                return Ok(QrLoginResult {
                                    connected: true,
                                    bot_token: status.bot_token,
                                    account_id: status.ilink_bot_id,
                                    base_url: status.baseurl,
                                    user_id: status.ilink_user_id,
                                    message: "登录成功".to_string(),
                                });
                            }
                            Some("expired") => {
                                return Ok(QrLoginResult {
                                    connected: false,
                                    bot_token: None,
                                    account_id: None,
                                    base_url: None,
                                    user_id: None,
                                    message: "二维码已过期，请重新登录".to_string(),
                                });
                            }
                            Some("scaned") => {
                                tracing::info!(
                                    "Weixin: QR code scanned, waiting for confirmation..."
                                );
                            }
                            Some("scaned_but_redirect") => {
                                if let Some(ref host) = status.redirect_host {
                                    current_base_url = format!("https://{}", host);
                                    tracing::info!("Weixin: IDC redirect to {}", host);
                                }
                            }
                            _ => {}
                        }
                    }
                }
                Ok(Ok(_)) => {}
                Ok(Err(e)) => {
                    tracing::warn!("Weixin: QR status poll error: {e}");
                }
                Err(_) => {
                    tracing::debug!("Weixin: QR status poll timeout, retrying...");
                }
            }

            tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        }

        Ok(QrLoginResult {
            connected: false,
            bot_token: None,
            account_id: None,
            base_url: None,
            user_id: None,
            message: "登录超时".to_string(),
        })
    }

    pub async fn qr_login_and_save(&self, config_path: &str) -> anyhow::Result<QrLoginResult> {
        let (qrcode_key, _qrcode_url) = self.start_qr_login().await?;

        tracing::info!("Waiting for WeChat scan confirmation...");

        let result = self.wait_for_qr_login(&qrcode_key, 300_000).await?;

        if result.connected
            && let Some(ref token) = result.bot_token
        {
            if let Some(ref account_id) = result.account_id {
                tracing::info!("Weixin: Login successful! Account ID: {}", account_id);
            }

            if let Err(e) = self.save_bot_token_to_config(config_path, token).await {
                tracing::error!("Weixin: Failed to save token to config: {}", e);
                return Err(anyhow::anyhow!("Failed to save token: {}", e));
            }

            tracing::info!("Weixin: Bot token saved to config file: {}", config_path);
        }

        Ok(result)
    }

    pub async fn save_bot_token_to_config(
        &self,
        config_path: &str,
        token: &str,
    ) -> anyhow::Result<()> {
        let content = tokio::fs::read_to_string(config_path).await?;
        let mut config: toml::Table = toml::from_str(&content)?;

        // Get or create [channels] section
        let channels = if let Some(ch) = config.get_mut("channels").and_then(|c| c.as_table_mut()) {
            ch
        } else {
            // Create [channels] section if it doesn't exist
            config.insert(
                "channels".to_string(),
                toml::Value::Table(toml::Table::new()),
            );
            config
                .get_mut("channels")
                .and_then(|c| c.as_table_mut())
                .expect("just inserted channels")
        };

        // Get or create [channels.weixin] section
        if let Some(channel) = channels.get_mut("weixin").and_then(|w| w.as_table_mut()) {
            // Update existing weixin config
            channel.insert(
                "bot_token".to_string(),
                toml::Value::String(token.to_string()),
            );
        } else {
            // Create new [channels.weixin] section
            let mut weixin_config = toml::Table::new();
            weixin_config.insert(
                "bot_token".to_string(),
                toml::Value::String(token.to_string()),
            );
            weixin_config.insert("enabled".to_string(), toml::Value::Boolean(true));
            weixin_config.insert(
                "allowed_users".to_string(),
                toml::Value::Array(vec![toml::Value::String("*".to_string())]),
            );
            channels.insert("weixin".to_string(), toml::Value::Table(weixin_config));
        }

        let new_content = toml::to_string_pretty(&config)?;
        tokio::fs::write(config_path, new_content).await?;

        Ok(())
    }
}

#[derive(Debug, Clone)]
pub struct QrLoginResult {
    pub connected: bool,
    pub bot_token: Option<String>,
    pub account_id: Option<String>,
    pub base_url: Option<String>,
    pub user_id: Option<String>,
    pub message: String,
}

#[derive(Debug, Clone)]
pub enum QrPollStatus {
    Waiting,
    Scaned,
    Confirmed(QrLoginResult),
    Expired,
    Redirect { host: String },
}

impl WeixinChannel {
    pub async fn poll_qr_status(&self, qrcode_key: &str) -> anyhow::Result<QrPollStatus> {
        let endpoint = format!(
            "ilink/bot/get_qrcode_status?qrcode={}",
            urlencoding::encode(qrcode_key)
        );
        let url = format!("{}/{}", self.base_url.trim_end_matches('/'), endpoint);

        let result = tokio::time::timeout(
            std::time::Duration::from_millis(35_000),
            self.http_client.get(&url).send(),
        )
        .await;

        match result {
            Ok(Ok(resp)) if resp.status().is_success() => {
                if let Ok(text) = resp.text().await
                    && let Ok(status) = serde_json::from_str::<QrStatusResponse>(&text)
                {
                    return match status.status.as_deref() {
                        Some("confirmed") => Ok(QrPollStatus::Confirmed(QrLoginResult {
                            connected: true,
                            bot_token: status.bot_token,
                            account_id: status.ilink_bot_id,
                            base_url: status.baseurl,
                            user_id: status.ilink_user_id,
                            message: "登录成功".to_string(),
                        })),
                        Some("expired") => Ok(QrPollStatus::Expired),
                        Some("scaned") => Ok(QrPollStatus::Scaned),
                        Some("scaned_but_redirect") => {
                            let host = status.redirect_host.unwrap_or_default();
                            Ok(QrPollStatus::Redirect { host })
                        }
                        _ => Ok(QrPollStatus::Waiting),
                    };
                }
                Ok(QrPollStatus::Waiting)
            }
            Ok(Ok(_)) => Ok(QrPollStatus::Waiting),
            Ok(Err(e)) => {
                tracing::warn!("Weixin: QR status poll error: {e}");
                Ok(QrPollStatus::Waiting)
            }
            Err(_) => {
                tracing::debug!("Weixin: QR status poll timeout, retrying...");
                Ok(QrPollStatus::Waiting)
            }
        }
    }
}

#[async_trait]
impl Channel for WeixinChannel {
    fn name(&self) -> &str {
        "weixin"
    }

    async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {
        let context_token = self.get_context_token(&message.recipient).await;

        let chunks = split_text_chunks(&message.content, TEXT_CHUNK_LIMIT);
        for chunk in &chunks {
            self.send_message_api(&message.recipient, chunk, context_token.as_deref())
                .await?;
        }

        Ok(())
    }

    async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> anyhow::Result<()> {
        tracing::info!("Weixin: starting long-poll monitor ({})...", self.base_url);

        let mut next_timeout_ms = DEFAULT_LONG_POLL_TIMEOUT_MS;
        let mut consecutive_failures: u32 = 0;
        let account_id = "default";

        loop {
            if self.is_session_paused(account_id).await {
                let pause = self.session_pauses.read().await;
                let remaining = pause
                    .get(account_id)
                    .map(|p| p.until.duration_since(std::time::Instant::now()).as_secs() / 60)
                    .unwrap_or(60);
                tracing::warn!("Weixin: session paused, waiting {} min...", remaining);
                drop(pause);
                tokio::time::sleep(std::time::Duration::from_secs(60)).await;
                continue;
            }

            let buf = self.get_updates_buf.read().await.clone();

            match self.get_updates(&buf, next_timeout_ms).await {
                Ok(resp) => {
                    if let Some(timeout) = resp.longpolling_timeout_ms
                        && timeout > 0
                    {
                        next_timeout_ms = timeout;
                    }

                    let is_error = resp.ret.unwrap_or(0) != 0 || resp.errcode.unwrap_or(0) != 0;

                    if is_error {
                        let errcode = resp.errcode.unwrap_or(0);
                        let ret = resp.ret.unwrap_or(0);

                        if errcode == SESSION_EXPIRED_ERRCODE || ret == SESSION_EXPIRED_ERRCODE {
                            self.pause_session(account_id).await;
                            tracing::error!(
                                "Weixin: session expired (errcode={}), pausing for 1 hour",
                                errcode
                            );
                            consecutive_failures = 0;
                            continue;
                        }

                        consecutive_failures += 1;
                        tracing::error!(
                            "Weixin: getUpdates error ret={} errcode={} msg={}",
                            ret,
                            errcode,
                            resp.errmsg.as_deref().unwrap_or("")
                        );

                        if consecutive_failures >= MAX_CONSECUTIVE_FAILURES {
                            tracing::error!(
                                "Weixin: {} consecutive failures, backing off 30s",
                                consecutive_failures
                            );
                            consecutive_failures = 0;
                            tokio::time::sleep(std::time::Duration::from_millis(BACKOFF_DELAY_MS))
                                .await;
                        } else {
                            tokio::time::sleep(std::time::Duration::from_millis(RETRY_DELAY_MS))
                                .await;
                        }
                        continue;
                    }

                    consecutive_failures = 0;

                    if let Some(ref new_buf) = resp.get_updates_buf
                        && !new_buf.is_empty()
                    {
                        let mut buf_guard = self.get_updates_buf.write().await;
                        *buf_guard = new_buf.clone();
                    }

                    let msgs = resp.msgs.unwrap_or_default();
                    for msg in &msgs {
                        let from_user_id = msg.from_user_id.as_deref().unwrap_or("");

                        if !self.is_user_allowed(from_user_id) {
                            tracing::warn!(
                                "Weixin: ignoring message from unauthorized user: {}",
                                from_user_id
                            );
                            continue;
                        }

                        if let Some(ref context_token) = msg.context_token
                            && !context_token.is_empty()
                        {
                            self.set_context_token(from_user_id, context_token).await;
                        }

                        let items = msg.item_list.as_deref().unwrap_or(&[]);
                        let text = extract_text_from_items(items);
                        if text.is_empty() {
                            continue;
                        }

                        let channel_msg = ChannelMessage {
                            id: Uuid::new_v4().to_string(),
                            sender: from_user_id.to_string(),
                            reply_target: from_user_id.to_string(),
                            content: text,
                            channel: "weixin".to_string(),
                            timestamp: now_secs(),
                            thread_ts: None,
                            interruption_scope_id: None,
                            attachments: vec![],
                        };

                        if tx.send(channel_msg).await.is_err() {
                            tracing::warn!("Weixin: message channel closed");
                            anyhow::bail!("Weixin: message channel closed");
                        }
                    }
                }
                Err(e) => {
                    consecutive_failures += 1;
                    tracing::error!(
                        "Weixin: getUpdates error ({}/{}): {}",
                        consecutive_failures,
                        MAX_CONSECUTIVE_FAILURES,
                        e
                    );

                    if consecutive_failures >= MAX_CONSECUTIVE_FAILURES {
                        tracing::error!("Weixin: backing off 30s");
                        consecutive_failures = 0;
                        tokio::time::sleep(std::time::Duration::from_millis(BACKOFF_DELAY_MS))
                            .await;
                    } else {
                        tokio::time::sleep(std::time::Duration::from_millis(RETRY_DELAY_MS)).await;
                    }
                }
            }
        }
    }

    async fn health_check(&self) -> bool {
        let buf = self.get_updates_buf.read().await;
        match self.get_updates(&buf, 5_000).await {
            Ok(resp) => resp.ret.unwrap_or(0) == 0 && resp.errcode.unwrap_or(0) == 0,
            Err(_) => false,
        }
    }
}

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

    #[test]
    fn test_name() {
        let ch = WeixinChannel::new("test_token".into(), vec![]);
        assert_eq!(ch.name(), "weixin");
    }

    #[test]
    fn test_user_allowed_wildcard() {
        let ch = WeixinChannel::new("token".into(), vec!["*".into()]);
        assert!(ch.is_user_allowed("anyone"));
    }

    #[test]
    fn test_user_allowed_specific() {
        let ch = WeixinChannel::new("token".into(), vec!["user123".into()]);
        assert!(ch.is_user_allowed("user123"));
        assert!(!ch.is_user_allowed("other"));
    }

    #[test]
    fn test_user_denied_empty() {
        let ch = WeixinChannel::new("token".into(), vec![]);
        assert!(!ch.is_user_allowed("anyone"));
    }

    #[test]
    fn test_extract_text_from_items() {
        let items = vec![MessageItem {
            r#type: Some(1),
            text_item: Some(TextItem {
                text: Some("hello world".to_string()),
            }),
            voice_item: None,
            ref_msg: None,
        }];
        assert_eq!(extract_text_from_items(&items), "hello world");
    }

    #[test]
    fn test_extract_text_from_items_empty() {
        let items = vec![MessageItem {
            r#type: Some(1),
            text_item: Some(TextItem {
                text: Some("".to_string()),
            }),
            voice_item: None,
            ref_msg: None,
        }];
        assert_eq!(extract_text_from_items(&items), "");
    }

    #[test]
    fn test_extract_text_from_voice() {
        let items = vec![MessageItem {
            r#type: Some(3),
            text_item: None,
            voice_item: Some(VoiceItem {
                text: Some("语音转文字".to_string()),
            }),
            ref_msg: None,
        }];
        assert_eq!(extract_text_from_items(&items), "语音转文字");
    }

    #[test]
    fn test_extract_text_with_quote() {
        let items = vec![MessageItem {
            r#type: Some(1),
            text_item: Some(TextItem {
                text: Some("回复内容".to_string()),
            }),
            voice_item: None,
            ref_msg: Some(RefMessage {
                title: Some("原始消息".to_string()),
                message_item: None,
            }),
        }];
        let result = extract_text_from_items(&items);
        assert!(result.contains("引用"));
        assert!(result.contains("回复内容"));
    }

    #[test]
    fn test_extract_text_with_nested_quote() {
        let items = vec![MessageItem {
            r#type: Some(1),
            text_item: Some(TextItem {
                text: Some("回复内容".to_string()),
            }),
            voice_item: None,
            ref_msg: Some(RefMessage {
                title: Some("标题".to_string()),
                message_item: Some(Box::new(MessageItem {
                    r#type: Some(1),
                    text_item: Some(TextItem {
                        text: Some("引用的正文".to_string()),
                    }),
                    voice_item: None,
                    ref_msg: None,
                })),
            }),
        }];
        let result = extract_text_from_items(&items);
        assert!(result.contains("引用"));
        assert!(result.contains("标题"));
        assert!(result.contains("引用的正文"));
        assert!(result.contains("回复内容"));
    }

    #[test]
    fn test_extract_text_no_matching_type() {
        let items = vec![MessageItem {
            r#type: Some(99),
            text_item: None,
            voice_item: None,
            ref_msg: None,
        }];
        assert_eq!(extract_text_from_items(&items), "");
    }

    #[test]
    fn test_extract_text_empty_items() {
        let items: Vec<MessageItem> = vec![];
        assert_eq!(extract_text_from_items(&items), "");
    }

    #[test]
    fn test_split_text_chunks_small() {
        let chunks = split_text_chunks("hello", 100);
        assert_eq!(chunks, vec!["hello"]);
    }

    #[test]
    fn test_split_text_chunks_large() {
        let text = "line1\nline2\nline3\nline4\nline5";
        let chunks = split_text_chunks(text, 12);
        assert!(chunks.len() > 1);
        let reconstructed: String = chunks.join("");
        assert_eq!(reconstructed, text);
    }

    #[test]
    fn test_split_text_chunks_preserves_content() {
        let text = "a".repeat(5000);
        let chunks = split_text_chunks(&text, 4000);
        let reconstructed: String = chunks.join("");
        assert_eq!(reconstructed, text);
    }

    #[test]
    fn test_split_text_chunks_at_newline() {
        let text = "short\nlonger line here that exceeds limit\nfinal";
        let chunks = split_text_chunks(text, 20);
        let reconstructed: String = chunks.join("");
        assert_eq!(reconstructed, text);
    }

    #[test]
    fn test_generate_client_id() {
        let id = generate_client_id();
        assert!(id.starts_with("wordbot-weixin-"));
        assert!(id.len() > "wordbot-weixin-".len());
    }

    #[test]
    fn test_random_wechat_uin() {
        let uin = random_wechat_uin();
        assert!(!uin.is_empty());
        let decoded = BASE64.decode(&uin).unwrap();
        let original: String = String::from_utf8(decoded).unwrap();
        original.parse::<u32>().unwrap();
    }

    #[test]
    fn test_build_base_info() {
        let info = WeixinChannel::build_base_info();
        assert!(!info.channel_version.is_empty());
    }

    #[test]
    fn test_build_headers() {
        let ch = WeixinChannel::new("my-token".into(), vec![]);
        let headers = ch.build_headers(42);
        assert_eq!(headers.get("Content-Type").unwrap(), "application/json");
        assert_eq!(headers.get("Authorization").unwrap(), "Bearer my-token");
        assert_eq!(headers.get("AuthorizationType").unwrap(), "ilink_bot_token");
        assert_eq!(headers.get("Content-Length").unwrap(), "42");
        assert!(headers.contains_key("X-WECHAT-UIN"));
    }

    #[tokio::test]
    async fn test_context_token_store() {
        let ch = WeixinChannel::new("token".into(), vec![]);
        assert!(ch.get_context_token("user1").await.is_none());
        ch.set_context_token("user1", "tok123").await;
        assert_eq!(
            ch.get_context_token("user1").await,
            Some("tok123".to_string())
        );
    }

    #[tokio::test]
    async fn test_context_token_overwrite() {
        let ch = WeixinChannel::new("token".into(), vec![]);
        ch.set_context_token("user1", "tok1").await;
        ch.set_context_token("user1", "tok2").await;
        assert_eq!(
            ch.get_context_token("user1").await,
            Some("tok2".to_string())
        );
    }

    #[tokio::test]
    async fn test_session_pause_and_check() {
        let ch = WeixinChannel::new("token".into(), vec![]);
        assert!(!ch.is_session_paused("default").await);
        ch.pause_session("default").await;
        assert!(ch.is_session_paused("default").await);
        assert!(!ch.is_session_paused("other").await);
    }

    #[test]
    fn test_weixin_message_deserialize() {
        let json = r#"{
            "seq": 1,
            "message_id": 100,
            "from_user_id": "user1",
            "to_user_id": "bot1",
            "client_id": "client-abc",
            "create_time_ms": 1700000000000,
            "message_type": 1,
            "message_state": 2,
            "item_list": [{"type": 1, "text_item": {"text": "hello"}}],
            "context_token": "ctx-123"
        }"#;
        let msg: WeixinMessage = serde_json::from_str(json).unwrap();
        assert_eq!(msg.seq, Some(1));
        assert_eq!(msg.from_user_id, Some("user1".to_string()));
        assert_eq!(msg.context_token, Some("ctx-123".to_string()));
        let items = msg.item_list.unwrap();
        assert_eq!(items.len(), 1);
        assert_eq!(items[0].r#type, Some(1));
        assert_eq!(
            items[0].text_item.as_ref().unwrap().text,
            Some("hello".to_string())
        );
    }

    #[test]
    fn test_weixin_message_deserialize_empty() {
        let json = r#"{}"#;
        let msg: WeixinMessage = serde_json::from_str(json).unwrap();
        assert!(msg.seq.is_none());
        assert!(msg.from_user_id.is_none());
        assert!(msg.item_list.is_none());
    }

    #[test]
    fn test_get_updates_resp_deserialize() {
        let json = r#"{
            "ret": 0,
            "errcode": 0,
            "errmsg": "ok",
            "msgs": [],
            "get_updates_buf": "buf123",
            "longpolling_timeout_ms": 35000
        }"#;
        let resp: GetUpdatesResp = serde_json::from_str(json).unwrap();
        assert_eq!(resp.ret, Some(0));
        assert_eq!(resp.errcode, Some(0));
        assert_eq!(resp.get_updates_buf, Some("buf123".to_string()));
        assert_eq!(resp.longpolling_timeout_ms, Some(35000));
    }

    #[test]
    fn test_send_message_req_serialize() {
        let req = SendMessageReq {
            msg: SendMessageInner {
                from_user_id: String::new(),
                to_user_id: "user1".to_string(),
                client_id: "client-1".to_string(),
                message_type: MESSAGE_TYPE_BOT,
                message_state: MESSAGE_STATE_FINISH,
                item_list: Some(vec![SendMessageItem {
                    r#type: MESSAGE_ITEM_TYPE_TEXT,
                    text_item: SendTextItem {
                        text: "hello".to_string(),
                    },
                }]),
                context_token: Some("ctx-1".to_string()),
            },
        };
        let json = serde_json::to_string(&req).unwrap();
        assert!(json.contains("\"to_user_id\":\"user1\""));
        assert!(json.contains("\"message_type\":2"));
        assert!(json.contains("\"message_state\":2"));
        assert!(json.contains("\"context_token\":\"ctx-1\""));
    }

    #[test]
    fn test_send_message_req_skip_none() {
        let req = SendMessageReq {
            msg: SendMessageInner {
                from_user_id: String::new(),
                to_user_id: "user1".to_string(),
                client_id: "client-1".to_string(),
                message_type: MESSAGE_TYPE_BOT,
                message_state: MESSAGE_STATE_FINISH,
                item_list: None,
                context_token: None,
            },
        };
        let json = serde_json::to_string(&req).unwrap();
        assert!(!json.contains("item_list"));
        assert!(!json.contains("context_token"));
    }

    #[test]
    fn test_qr_login_result_debug() {
        let result = QrLoginResult {
            connected: true,
            bot_token: Some("tok".to_string()),
            account_id: Some("acc".to_string()),
            base_url: Some("https://example.com".to_string()),
            user_id: Some("uid".to_string()),
            message: "登录成功".to_string(),
        };
        let debug_str = format!("{:?}", result);
        assert!(debug_str.contains("connected: true"));
        assert!(debug_str.contains("登录成功"));
    }

    #[test]
    fn test_new_with_custom_base_url() {
        let ch = WeixinChannel::new_with_base_url(
            "https://custom.example.com".to_string(),
            "token".into(),
            vec!["*".into()],
        );
        assert_eq!(ch.name(), "weixin");
        assert_eq!(ch.base_url, "https://custom.example.com");
    }

    #[test]
    fn test_with_proxy_url() {
        let ch = WeixinChannel::new("token".into(), vec!["*".into()])
            .with_proxy_url(Some("socks5://127.0.0.1:1080".to_string()));
        assert_eq!(ch.name(), "weixin");
        assert_eq!(ch.proxy_url, Some("socks5://127.0.0.1:1080".to_string()));
    }

    #[test]
    fn test_config_serde() {
        let toml_str = r#"
bot_token = "my-bot-token"
allowed_users = ["user1", "*"]
"#;
        let config: zeroclaw_config::schema::WeixinConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(config.bot_token, "my-bot-token");
        assert_eq!(config.allowed_users, vec!["user1", "*"]);
    }

    #[test]
    fn test_config_serde_defaults() {
        let toml_str = r#"
bot_token = "token"
"#;
        let config: zeroclaw_config::schema::WeixinConfig = toml::from_str(toml_str).unwrap();
        assert!(config.allowed_users.is_empty());
        assert!(config.base_url.is_none());
        assert!(config.proxy_url.is_none());
    }

    #[test]
    fn test_config_serde_with_base_url() {
        let toml_str = r#"
bot_token = "token"
base_url = "https://custom.weixin.example.com"
"#;
        let config: zeroclaw_config::schema::WeixinConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(
            config.base_url,
            Some("https://custom.weixin.example.com".to_string())
        );
    }

    #[test]
    fn test_config_serde_with_proxy_url() {
        let toml_str = r#"
bot_token = "token"
proxy_url = "socks5://127.0.0.1:1080"
"#;
        let config: zeroclaw_config::schema::WeixinConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(
            config.proxy_url,
            Some("socks5://127.0.0.1:1080".to_string())
        );
    }

    #[test]
    fn test_qr_code_response_deserialize() {
        let json = r#"{"qrcode": "key123", "qrcode_img_content": "data:image/png;base64,abc"}"#;
        let resp: QrCodeResponse = serde_json::from_str(json).unwrap();
        assert_eq!(resp.qrcode, Some("key123".to_string()));
        assert_eq!(
            resp.qrcode_img_content,
            Some("data:image/png;base64,abc".to_string())
        );
    }

    #[test]
    fn test_qr_status_response_deserialize() {
        let json = r#"{"status": "confirmed", "bot_token": "tok", "ilink_bot_id": "bid", "baseurl": "https://api.weixin.qq.com", "ilink_user_id": "uid", "redirect_host": "host.example.com"}"#;
        let resp: QrStatusResponse = serde_json::from_str(json).unwrap();
        assert_eq!(resp.status, Some("confirmed".to_string()));
        assert_eq!(resp.bot_token, Some("tok".to_string()));
        assert_eq!(resp.ilink_bot_id, Some("bid".to_string()));
        assert_eq!(resp.baseurl, Some("https://api.weixin.qq.com".to_string()));
        assert_eq!(resp.ilink_user_id, Some("uid".to_string()));
        assert_eq!(resp.redirect_host, Some("host.example.com".to_string()));
    }

    #[test]
    fn test_qr_status_response_deserialize_empty() {
        let json = r#"{}"#;
        let resp: QrStatusResponse = serde_json::from_str(json).unwrap();
        assert!(resp.status.is_none());
        assert!(resp.bot_token.is_none());
    }

    #[tokio::test]
    async fn test_save_bot_token_to_config_new_token() {
        let ch = WeixinChannel::new("old-token".into(), vec!["*".into()]);
        let temp_dir = tempfile::tempdir().unwrap();
        let config_path = temp_dir.path().join("zeroclaw.toml");

        let config_content = r#"
[channels.weixin]
enabled = true
bot_token = "old-token"
allowed_users = ["*"]
"#;
        tokio::fs::write(&config_path, config_content)
            .await
            .unwrap();

        ch.save_bot_token_to_config(config_path.to_str().unwrap(), "new-token-123")
            .await
            .unwrap();

        let saved = tokio::fs::read_to_string(&config_path).await.unwrap();
        let parsed: toml::Table = toml::from_str(&saved).unwrap();
        let saved_token = parsed
            .get("channels")
            .unwrap()
            .get("weixin")
            .unwrap()
            .get("bot_token")
            .unwrap()
            .as_str()
            .unwrap();

        assert_eq!(saved_token, "new-token-123");
    }

    #[tokio::test]
    async fn test_save_bot_token_to_config_creates_weixin_section() {
        let ch = WeixinChannel::new("token".into(), vec![]);
        let temp_dir = tempfile::tempdir().unwrap();
        let config_path = temp_dir.path().join("zeroclaw.toml");

        let config_content = r#"
[channels]
"#;
        tokio::fs::write(&config_path, config_content)
            .await
            .unwrap();

        ch.save_bot_token_to_config(config_path.to_str().unwrap(), "token-abc")
            .await
            .unwrap();

        let saved = tokio::fs::read_to_string(&config_path).await.unwrap();
        let parsed: toml::Table = toml::from_str(&saved).unwrap();
        let saved_token = parsed
            .get("channels")
            .unwrap()
            .get("weixin")
            .unwrap()
            .get("bot_token")
            .unwrap()
            .as_str()
            .unwrap();
        assert_eq!(saved_token, "token-abc");
    }

    #[tokio::test]
    async fn test_save_bot_token_to_config_missing_channels_section() {
        let ch = WeixinChannel::new("token".into(), vec![]);
        let temp_dir = tempfile::tempdir().unwrap();
        let config_path = temp_dir.path().join("zeroclaw.toml");

        // Config without [channels] section - should be auto-created
        let config_content = r#"
[other]
key = "value"
"#;
        tokio::fs::write(&config_path, config_content)
            .await
            .unwrap();

        // Should succeed now - creates [channels] and [channels.weixin] automatically
        ch.save_bot_token_to_config(config_path.to_str().unwrap(), "token-xyz")
            .await
            .unwrap();

        let saved = tokio::fs::read_to_string(&config_path).await.unwrap();
        let parsed: toml::Table = toml::from_str(&saved).unwrap();

        // Verify [channels.weixin] was created
        let saved_token = parsed
            .get("channels")
            .unwrap()
            .get("weixin")
            .unwrap()
            .get("bot_token")
            .unwrap()
            .as_str()
            .unwrap();
        assert_eq!(saved_token, "token-xyz");

        // Verify original [other] section is preserved
        let other_value = parsed
            .get("other")
            .unwrap()
            .get("key")
            .unwrap()
            .as_str()
            .unwrap();
        assert_eq!(other_value, "value");
    }
}
