use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, Instant};

use async_trait::async_trait;
use futures_util::{SinkExt, StreamExt};
use prost::{Message as ProstMessage, bytes::Bytes};
use serde::Deserialize;
use tokio::sync::{Mutex, RwLock};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;

use crate::bus::{MediaItem, MessageBus, OutboundMessage};
use crate::channels::base::{Channel, ChannelError, LivePolicy, TurnSink, TurnTarget};
use crate::config::FeishuChannelConfig;
use crate::session::{ToolStatus, TurnBlock, TurnSnapshot, TurnStatus};

const FEISHU_API_BASE: &str = "https://open.feishu.cn/open-apis";
const FEISHU_WS_BASE: &str = "https://open.feishu.cn";

/// Heartbeat timeout for WS connection — must be larger than ping_interval (default 120 s).
/// If no binary frame (pong or event) is received within this window, reconnect.
const WS_HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(300);
const CHANNEL_STOP_GRACE: Duration = if cfg!(test) {
    Duration::from_millis(100)
} else {
    Duration::from_secs(5)
};
/// Refresh tenant token this many seconds before the announced expiry.
const TOKEN_REFRESH_SKEW: Duration = Duration::from_secs(120);
/// Default tenant token TTL when `expire`/`expires_in` is absent.
const DEFAULT_TOKEN_TTL: Duration = Duration::from_secs(7200);
/// Dedup cache TTL (30 minutes).
const DEDUP_CACHE_TTL: Duration = Duration::from_secs(30 * 60);
const WS_EVENT_QUEUE_CAPACITY: usize = 32;
const WS_ACK_TIMEOUT: Duration = Duration::from_secs(2);
const WS_FRAGMENT_TTL: Duration = Duration::from_secs(5 * 60);
const MAX_WS_FRAGMENTS: usize = 256;
const MAX_WS_EVENT_BYTES: usize = 8 * 1024 * 1024;
const STABLE_CONNECTION_WINDOW: Duration = Duration::from_secs(30);
const FEISHU_API_ATTEMPTS: usize = 3;
const FEISHU_INVALID_TOKEN_CODE: i32 = 99_991_663;

// ─────────────────────────────────────────────────────────────────────────────
// Protobuf types for Feishu WebSocket protocol (pbbp2.proto)
// ─────────────────────────────────────────────────────────────────────────────

#[derive(Clone, PartialEq, prost::Message)]
struct PbHeader {
    #[prost(string, tag = "1")]
    pub key: String,
    #[prost(string, tag = "2")]
    pub value: String,
}

/// Feishu WS frame.
/// method=0 → CONTROL (ping/pong)  method=1 → DATA (events)
#[derive(Clone, PartialEq, prost::Message)]
struct PbFrame {
    #[prost(uint64, tag = "1")]
    pub seq_id: u64,
    #[prost(uint64, tag = "2")]
    pub log_id: u64,
    #[prost(int32, tag = "3")]
    pub service: i32,
    #[prost(int32, tag = "4")]
    pub method: i32,
    #[prost(message, repeated, tag = "5")]
    pub headers: Vec<PbHeader>,
    #[prost(bytes = "vec", optional, tag = "8")]
    pub payload: Option<Vec<u8>>,
}

impl PbFrame {
    fn header_value(&self, key: &str) -> Option<&str> {
        self.headers
            .iter()
            .find(|header| header.key == key)
            .map(|header| header.value.as_str())
    }
}

struct FragmentAssembly {
    parts: Vec<Option<Vec<u8>>>,
    total_bytes: usize,
    created_at: Instant,
}

/// POST /callback/ws/endpoint response
#[derive(Deserialize)]
struct WsEndpointResp {
    code: i32,
    msg: Option<String>,
    data: Option<WsEndpoint>,
}

#[derive(Deserialize)]
struct WsEndpoint {
    #[serde(rename = "URL")]
    url: String,
    #[serde(default)]
    client_config: Option<WsClientConfig>,
}

#[derive(Deserialize, Default)]
struct WsClientConfig {
    #[serde(rename = "PingInterval")]
    ping_interval: Option<u64>,
}

/// Lark event envelope (method=1 / type=event payload)
#[derive(Deserialize)]
struct LarkEvent {
    header: LarkEventHeader,
    event: serde_json::Value,
}

#[derive(Deserialize)]
struct LarkEventHeader {
    event_type: String,
    event_id: String,
}

impl std::fmt::Debug for LarkEventHeader {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("LarkEventHeader")
            .field("event_type", &self.event_type)
            .field("event_id", &self.event_id)
            .finish()
    }
}

#[derive(Deserialize)]
struct MsgReceivePayload {
    sender: LarkSender,
    message: LarkMessage,
}

#[derive(Deserialize)]
struct LarkSender {
    sender_id: LarkSenderId,
    #[serde(default)]
    sender_type: String,
}

#[derive(Deserialize, Default)]
struct LarkSenderId {
    open_id: Option<String>,
}

#[derive(Deserialize)]
struct LarkMessage {
    message_id: String,
    chat_id: String,
    #[serde(default)]
    chat_type: String,
    message_type: String,
    #[serde(default)]
    content: String,
    #[serde(default)]
    parent_id: Option<String>,
    #[serde(default)]
    root_id: Option<String>,
    #[serde(default)]
    thread_id: Option<String>,
    #[serde(default)]
    mentions: Vec<serde_json::Value>,
}

// ─────────────────────────────────────────────────────────────────────────────

/// Cached tenant token with proactive refresh metadata.
#[derive(Clone)]
struct CachedTenantToken {
    value: String,
    refresh_after: Instant,
}

#[derive(Clone)]
pub struct FeishuChannel {
    config: FeishuChannelConfig,
    http_client: reqwest::Client,
    running: Arc<RwLock<bool>>,
    shutdown: Arc<RwLock<Option<CancellationToken>>>,
    run_task: Arc<Mutex<Option<JoinHandle<()>>>>,
    connected: Arc<RwLock<bool>>,
    /// Cached tenant access token with proactive refresh.
    tenant_token: Arc<RwLock<Option<CachedTenantToken>>>,
    /// Dedup cache: WS message_ids seen in the last ~30 min.
    seen_message_ids: Arc<RwLock<HashMap<String, Instant>>>,
    /// Bot identity used to enforce group-chat @mention admission.
    bot_open_id: Arc<RwLock<Option<String>>>,
}

/// Parsed message data from a Feishu frame
struct ParsedMessage {
    message_id: String,
    open_id: String,
    chat_id: String,
    content: String,
    media: Vec<MediaItem>,
    /// ID of the message this message is replying to (if any).
    /// Used to fetch quoted message content for display.
    parent_id: Option<String>,
    root_id: Option<String>,
    thread_id: Option<String>,
    chat_type: String,
}

#[derive(Clone, Debug, PartialEq, Eq)]
struct FeishuSendTarget {
    receive_id: String,
    receive_id_type: &'static str,
    reply_to: Option<String>,
    reply_in_thread: bool,
}

#[derive(Deserialize)]
struct FeishuMessageResponse {
    code: i32,
    msg: String,
    data: Option<FeishuMessageData>,
}

#[derive(Deserialize)]
struct FeishuMessageData {
    message_id: String,
}

impl FeishuSendTarget {
    fn from_message(
        chat_id: String,
        reply_to: Option<String>,
        metadata: &HashMap<String, String>,
    ) -> Self {
        let thread_id = metadata
            .get("feishu.thread_id")
            .filter(|value| !value.is_empty());
        if reply_to.is_none()
            && let Some(thread_id) = thread_id
        {
            return Self {
                receive_id: thread_id.clone(),
                receive_id_type: "thread_id",
                reply_to: None,
                reply_in_thread: false,
            };
        }
        let receive_id_type = if chat_id.starts_with("oc_") {
            "chat_id"
        } else {
            "open_id"
        };
        Self {
            receive_id: chat_id,
            receive_id_type,
            reply_to,
            reply_in_thread: thread_id.is_some(),
        }
    }

    fn request(&self, msg_type: &str, content: &str) -> (String, serde_json::Value) {
        let uuid = uuid::Uuid::new_v4().to_string();
        if let Some(message_id) = &self.reply_to {
            return (
                format!("{}/im/v1/messages/{message_id}/reply", FEISHU_API_BASE),
                serde_json::json!({
                    "msg_type": msg_type,
                    "content": content,
                    "reply_in_thread": self.reply_in_thread,
                    "uuid": uuid
                }),
            );
        }
        (
            format!(
                "{}/im/v1/messages?receive_id_type={}",
                FEISHU_API_BASE, self.receive_id_type
            ),
            serde_json::json!({
                "receive_id": self.receive_id,
                "msg_type": msg_type,
                "content": content,
                "uuid": uuid
            }),
        )
    }
}

impl FeishuChannel {
    pub fn new(
        mut config: FeishuChannelConfig,
        workspace_dir: &Path,
    ) -> Result<Self, ChannelError> {
        // Override media_dir to use workspace_dir/media/feishu
        let media_dir = workspace_dir.join("media").join("feishu");
        config.media_dir = media_dir.to_string_lossy().to_string();

        let request_timeout = Duration::from_secs(config.request_timeout_secs.clamp(5, 120));
        let http_client = reqwest::Client::builder()
            .timeout(request_timeout)
            .build()
            .map_err(|error| {
                ChannelError::ConfigError(format!("Failed to build Feishu HTTP client: {error}"))
            })?;

        Ok(Self {
            config,
            http_client,
            running: Arc::new(RwLock::new(false)),
            shutdown: Arc::new(RwLock::new(None)),
            run_task: Arc::new(Mutex::new(None)),
            connected: Arc::new(RwLock::new(false)),
            tenant_token: Arc::new(RwLock::new(None)),
            seen_message_ids: Arc::new(RwLock::new(HashMap::new())),
            bot_open_id: Arc::new(RwLock::new(None)),
        })
    }

    async fn invalidate_tenant_token(&self) {
        *self.tenant_token.write().await = None;
    }

    fn request_timeout(&self) -> Duration {
        Duration::from_secs(self.config.request_timeout_secs.clamp(5, 120))
    }

    async fn send_authenticated<F>(
        &self,
        operation: &str,
        build: F,
    ) -> Result<reqwest::Response, ChannelError>
    where
        F: Fn(&reqwest::Client, &str) -> reqwest::RequestBuilder,
    {
        for attempt in 0..FEISHU_API_ATTEMPTS {
            let token = self.get_tenant_access_token().await?;
            match build(&self.http_client, &token).send().await {
                Ok(response) => {
                    let status = response.status();
                    if status == reqwest::StatusCode::UNAUTHORIZED {
                        self.invalidate_tenant_token().await;
                    }
                    if status == reqwest::StatusCode::TOO_MANY_REQUESTS
                        || status.is_server_error()
                        || status == reqwest::StatusCode::UNAUTHORIZED
                    {
                        if attempt + 1 < FEISHU_API_ATTEMPTS {
                            let delay = retry_after_delay(response.headers(), attempt);
                            tokio::time::sleep(delay).await;
                            continue;
                        }
                        return Err(ChannelError::ConnectionError(format!(
                            "{operation} failed after {} attempts: HTTP {status}",
                            attempt + 1
                        )));
                    }
                    return Ok(response);
                }
                Err(error) => {
                    if attempt + 1 == FEISHU_API_ATTEMPTS {
                        return Err(ChannelError::ConnectionError(format!(
                            "{operation} failed after {} attempts: {error}",
                            attempt + 1
                        )));
                    }
                    tokio::time::sleep(api_retry_delay(attempt)).await;
                }
            }
        }
        unreachable!("Feishu API attempt loop always returns")
    }

    async fn refresh_bot_open_id(&self) -> Result<String, ChannelError> {
        let response = self
            .send_authenticated("fetch Feishu bot info", |client, token| {
                client
                    .get(format!("{}/bot/v3/info", FEISHU_API_BASE))
                    .bearer_auth(token)
            })
            .await?;
        let status = response.status();
        let body: serde_json::Value = response.json().await.map_err(|error| {
            ChannelError::Other(format!("Failed to parse bot info response: {error}"))
        })?;
        if !status.is_success() || body.get("code").and_then(|value| value.as_i64()) != Some(0) {
            return Err(ChannelError::Other(format!(
                "Bot info request failed: status={status}, code={}",
                body.get("code")
                    .and_then(|value| value.as_i64())
                    .unwrap_or(-1)
            )));
        }
        let open_id = body
            .pointer("/bot/open_id")
            .or_else(|| body.pointer("/data/bot/open_id"))
            .and_then(|value| value.as_str())
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .ok_or_else(|| ChannelError::Other("Bot info response has no open_id".to_string()))?
            .to_string();
        *self.bot_open_id.write().await = Some(open_id.clone());
        Ok(open_id)
    }

    /// Get WebSocket endpoint URL from Feishu API
    async fn get_ws_endpoint(
        &self,
        client: &reqwest::Client,
    ) -> Result<(String, WsClientConfig), ChannelError> {
        let resp = client
            .post(format!("{}/callback/ws/endpoint", FEISHU_WS_BASE))
            .header("locale", "zh")
            .json(&serde_json::json!({
                "AppID": self.config.app_id,
                "AppSecret": self.config.app_secret,
            }))
            .send()
            .await
            .map_err(|e| ChannelError::ConnectionError(format!("HTTP error: {}", e)))?;

        let endpoint_resp: WsEndpointResp = resp.json().await.map_err(|e| {
            ChannelError::ConnectionError(format!("Failed to parse endpoint response: {}", e))
        })?;

        if endpoint_resp.code != 0 {
            return Err(ChannelError::ConnectionError(format!(
                "WS endpoint failed: code={} msg={}",
                endpoint_resp.code,
                endpoint_resp.msg.as_deref().unwrap_or("unknown")
            )));
        }

        let ep = endpoint_resp
            .data
            .ok_or_else(|| ChannelError::ConnectionError("Empty endpoint data".to_string()))?;

        let client_config = ep.client_config.unwrap_or_default();
        Ok((ep.url, client_config))
    }

    /// Get tenant access token (cached with proactive refresh).
    async fn get_tenant_access_token(&self) -> Result<String, ChannelError> {
        // 1. Check cache
        {
            let cached = self.tenant_token.read().await;
            if let Some(ref token) = *cached
                && Instant::now() < token.refresh_after
            {
                return Ok(token.value.clone());
            }
        }

        // 2. Fetch new token
        let (token, ttl) = self.fetch_new_token().await?;

        // 3. Cache with proactive refresh time (提前 120 秒)
        let refresh_after = Instant::now() + ttl.saturating_sub(TOKEN_REFRESH_SKEW);
        {
            let mut cached = self.tenant_token.write().await;
            *cached = Some(CachedTenantToken {
                value: token.clone(),
                refresh_after,
            });
        }

        Ok(token)
    }

    /// Fetch a new tenant access token from Feishu.
    async fn fetch_new_token(&self) -> Result<(String, Duration), ChannelError> {
        let resp = self
            .http_client
            .post(format!(
                "{}/auth/v3/tenant_access_token/internal",
                FEISHU_API_BASE
            ))
            .header("Content-Type", "application/json")
            .json(&serde_json::json!({
                "app_id": self.config.app_id,
                "app_secret": self.config.app_secret,
            }))
            .send()
            .await
            .map_err(|e| ChannelError::ConnectionError(format!("HTTP error: {}", e)))?;

        #[derive(Deserialize)]
        struct TokenResponse {
            code: i32,
            tenant_access_token: Option<String>,
            expire: Option<i64>,
        }

        let token_resp: TokenResponse = resp
            .json()
            .await
            .map_err(|e| ChannelError::Other(format!("Failed to parse token response: {}", e)))?;

        if token_resp.code != 0 {
            return Err(ChannelError::Other("Auth failed".to_string()));
        }

        let token = token_resp
            .tenant_access_token
            .ok_or_else(|| ChannelError::Other("No token in response".to_string()))?;

        let ttl = token_resp
            .expire
            .and_then(|v| u64::try_from(v).ok())
            .map(Duration::from_secs)
            .unwrap_or(DEFAULT_TOKEN_TTL);

        Ok((token, ttl))
    }

    /// Check if message_id has been seen (dedup), and mark it as seen if not.
    /// Returns true if the message was already processed.
    /// Note: GC of stale entries is handled in the heartbeat timeout_check loop.
    async fn is_message_seen(&self, message_id: &str) -> bool {
        let mut seen = self.seen_message_ids.write().await;
        let now = Instant::now();

        if seen.contains_key(message_id) {
            true
        } else {
            seen.insert(message_id.to_string(), now);
            false
        }
    }

    /// Download media and save locally, return (description, media_item)
    async fn download_media(
        &self,
        msg_type: &str,
        content_json: &serde_json::Value,
        message_id: &str,
    ) -> Result<(String, Option<MediaItem>), ChannelError> {
        let media_dir = Path::new(&self.config.media_dir);
        tokio::fs::create_dir_all(media_dir)
            .await
            .map_err(|e| ChannelError::Other(format!("Failed to create media dir: {}", e)))?;

        match msg_type {
            "image" => {
                self.download_image(content_json, message_id, media_dir)
                    .await
            }
            "audio" | "file" | "media" => {
                self.download_file(content_json, message_id, media_dir, msg_type)
                    .await
            }
            _ => Ok((format!("[unsupported media type: {}]", msg_type), None)),
        }
    }

    /// Download image from Feishu
    async fn download_image(
        &self,
        content_json: &serde_json::Value,
        message_id: &str,
        media_dir: &Path,
    ) -> Result<(String, Option<MediaItem>), ChannelError> {
        let image_key = content_json
            .get("image_key")
            .and_then(|v| v.as_str())
            .ok_or_else(|| ChannelError::Other("No image_key in message".to_string()))?;

        // Use message resource API for downloading message images
        let url = format!(
            "{}/im/v1/messages/{}/resources/{}?type=image",
            FEISHU_API_BASE, message_id, image_key
        );

        #[cfg(debug_assertions)]
        tracing::debug!(url = %url, image_key = %image_key, message_id = %message_id, "Downloading image from Feishu via message resource API");

        let resp = self
            .send_authenticated("download Feishu image", |client, token| {
                client.get(&url).bearer_auth(token)
            })
            .await?;

        let status = resp.status();
        #[cfg(debug_assertions)]
        tracing::debug!(status = %status, "Image download response status");

        if !status.is_success() {
            let error_body =
                read_response_limited(resp, 64 * 1024, self.request_timeout(), "error response")
                    .await
                    .unwrap_or_default();
            return Err(ChannelError::Other(format!(
                "Image download failed {}: {}",
                status,
                String::from_utf8_lossy(&error_body)
            )));
        }

        let content_type = resp
            .headers()
            .get("content-type")
            .and_then(|v| v.to_str().ok())
            .unwrap_or("image/jpeg")
            .to_string();

        let ext = resolve_image_ext(&content_type);

        let data = read_response_limited(
            resp,
            self.config.max_image_bytes,
            self.request_timeout(),
            "image",
        )
        .await?;

        #[cfg(debug_assertions)]
        tracing::debug!(data_len = %data.len(), content_type = %content_type, "Downloaded image data");

        let filename = format!(
            "{}_{}.{}",
            message_id,
            &image_key[..8.min(image_key.len())],
            ext
        );
        let file_path = resolve_unique_path(media_dir, &filename).await;

        ensure_media_capacity(
            media_dir,
            data.len() as u64,
            self.config.media_dir_max_bytes,
        )
        .await?;

        tokio::fs::write(&file_path, &data)
            .await
            .map_err(|e| ChannelError::Other(format!("Failed to write image: {}", e)))?;

        let media_item = MediaItem::new(file_path.to_string_lossy().to_string(), "image");

        tracing::info!(message_id = %message_id, filename = %filename, "Downloaded image");

        Ok((String::new(), Some(media_item)))
    }

    /// Download file/audio from Feishu
    async fn download_file(
        &self,
        content_json: &serde_json::Value,
        message_id: &str,
        media_dir: &Path,
        file_type: &str,
    ) -> Result<(String, Option<MediaItem>), ChannelError> {
        let file_key = content_json
            .get("file_key")
            .and_then(|v| v.as_str())
            .ok_or_else(|| ChannelError::Other("No file_key in message".to_string()))?;

        // Use message resource API for downloading message files
        let url = format!(
            "{}/im/v1/messages/{}/resources/{}?type=file",
            FEISHU_API_BASE, message_id, file_key
        );

        #[cfg(debug_assertions)]
        tracing::debug!(url = %url, file_key = %file_key, message_id = %message_id, "Downloading file from Feishu via message resource API");

        let resp = self
            .send_authenticated("download Feishu file", |client, token| {
                client.get(&url).bearer_auth(token)
            })
            .await?;

        let status = resp.status();
        if !status.is_success() {
            let error_body =
                read_response_limited(resp, 64 * 1024, self.request_timeout(), "error response")
                    .await
                    .unwrap_or_default();
            return Err(ChannelError::Other(format!(
                "File download failed {}: {}",
                status,
                String::from_utf8_lossy(&error_body)
            )));
        }

        let data = read_response_limited(
            resp,
            self.config.max_file_bytes,
            self.request_timeout(),
            file_type,
        )
        .await?;

        let filename = content_json
            .get("file_name")
            .and_then(|v| v.as_str())
            .map(sanitize_filename)
            .unwrap_or_else(|| {
                let ext = resolve_file_ext(content_json);
                if ext.is_empty() {
                    format!("{}_{}", message_id, &file_key[..8.min(file_key.len())])
                } else {
                    format!(
                        "{}_{}.{}",
                        message_id,
                        &file_key[..8.min(file_key.len())],
                        ext
                    )
                }
            });
        let file_path = resolve_unique_path(media_dir, &filename).await;

        ensure_media_capacity(
            media_dir,
            data.len() as u64,
            self.config.media_dir_max_bytes,
        )
        .await?;

        tokio::fs::write(&file_path, &data)
            .await
            .map_err(|e| ChannelError::Other(format!("Failed to write file: {}", e)))?;

        let media_item = MediaItem::new(file_path.to_string_lossy().to_string(), file_type);

        tracing::info!(message_id = %message_id, filename = %filename, file_type = %file_type, "Downloaded file");

        Ok((String::new(), Some(media_item)))
    }

    /// Upload image to Feishu and return the image_key
    async fn upload_image(&self, file_path: &str) -> Result<String, ChannelError> {
        ensure_local_media_size(file_path, self.config.max_image_bytes, "image").await?;
        let token = self.get_tenant_access_token().await?;

        let mime = mime_guess::from_path(file_path)
            .first_or_octet_stream()
            .to_string();

        let file_name = std::path::Path::new(file_path)
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("image.jpg");

        let file_data = tokio::fs::read(file_path)
            .await
            .map_err(|e| ChannelError::Other(format!("Failed to read file: {}", e)))?;

        let part = reqwest::multipart::Part::bytes(file_data)
            .file_name(file_name.to_string())
            .mime_str(&mime)
            .map_err(|e| ChannelError::Other(format!("Invalid mime type: {}", e)))?;

        let form = reqwest::multipart::Form::new()
            .text("image_type", "message".to_string())
            .part("image", part);

        let resp = self
            .http_client
            .post(format!("{}/im/v1/images", FEISHU_API_BASE))
            .header("Authorization", format!("Bearer {}", token))
            .multipart(form)
            .send()
            .await
            .map_err(|e| {
                ChannelError::ConnectionError(format!("Upload image HTTP error: {}", e))
            })?;

        let status = resp.status();
        let body_text = resp
            .text()
            .await
            .map_err(|e| ChannelError::Other(format!("Failed to read upload response: {}", e)))?;
        tracing::debug!(status = %status, body = %body_text, "Feishu upload image");

        #[derive(Deserialize)]
        struct UploadResp {
            code: i32,
            msg: Option<String>,
            data: Option<UploadData>,
        }
        #[derive(Deserialize)]
        struct UploadData {
            image_key: String,
        }

        let result: UploadResp = serde_json::from_str(&body_text).map_err(|e| {
            ChannelError::Other(format!(
                "Parse upload response error: {} | body: {}",
                e, body_text
            ))
        })?;

        if result.code != 0 {
            return Err(ChannelError::Other(format!(
                "Upload image failed: code={} msg={}",
                result.code,
                result.msg.as_deref().unwrap_or("unknown")
            )));
        }

        result
            .data
            .map(|d| d.image_key)
            .ok_or_else(|| ChannelError::Other("No image_key in response".to_string()))
    }

    /// Upload file to Feishu and return the file_key
    async fn upload_file(&self, file_path: &str) -> Result<String, ChannelError> {
        ensure_local_media_size(file_path, self.config.max_file_bytes, "file").await?;
        let token = self.get_tenant_access_token().await?;

        let file_name = std::path::Path::new(file_path)
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("file.bin");

        let extension = std::path::Path::new(file_path)
            .extension()
            .and_then(|e| e.to_str())
            .unwrap_or("")
            .to_lowercase();

        let file_type = match extension.as_str() {
            "opus" => "opus",
            "mp4" | "mov" | "avi" | "mkv" => "mp4",
            "pdf" => "pdf",
            "doc" | "docx" => "doc",
            "xls" | "xlsx" => "xls",
            "ppt" | "pptx" => "ppt",
            _ => "stream",
        };

        let file_data = tokio::fs::read(file_path)
            .await
            .map_err(|e| ChannelError::Other(format!("Failed to read file: {}", e)))?;

        let part = reqwest::multipart::Part::bytes(file_data)
            .file_name(file_name.to_string())
            .mime_str("application/octet-stream")
            .map_err(|e| ChannelError::Other(format!("Invalid mime type: {}", e)))?;

        let form = reqwest::multipart::Form::new()
            .text("file_type", file_type.to_string())
            .text("file_name", file_name.to_string())
            .part("file", part);

        let resp = self
            .http_client
            .post(format!("{}/im/v1/files", FEISHU_API_BASE))
            .header("Authorization", format!("Bearer {}", token))
            .multipart(form)
            .send()
            .await
            .map_err(|e| ChannelError::ConnectionError(format!("Upload file HTTP error: {}", e)))?;

        let status = resp.status();
        let body_text = resp
            .text()
            .await
            .map_err(|e| ChannelError::Other(format!("Failed to read upload response: {}", e)))?;
        tracing::debug!(status = %status, body = %body_text, "Feishu upload file");

        #[derive(Deserialize)]
        struct UploadResp {
            code: i32,
            msg: Option<String>,
            data: Option<UploadData>,
        }
        #[derive(Deserialize)]
        struct UploadData {
            file_key: String,
        }

        let result: UploadResp = serde_json::from_str(&body_text).map_err(|e| {
            ChannelError::Other(format!(
                "Parse upload response error: {} | body: {}",
                e, body_text
            ))
        })?;

        if result.code != 0 {
            return Err(ChannelError::Other(format!(
                "Upload file failed: code={} msg={}",
                result.code,
                result.msg.as_deref().unwrap_or("unknown")
            )));
        }

        result
            .data
            .map(|d| d.file_key)
            .ok_or_else(|| ChannelError::Other("No file_key in response".to_string()))
    }

    /// Add a reaction emoji to a message and store the reaction_id for later removal.
    /// Returns the reaction_id if successful, None otherwise.
    async fn add_reaction(&self, message_id: &str) -> Result<Option<String>, ChannelError> {
        let emoji = self.config.reaction_emoji.trim();
        if emoji.is_empty() {
            return Ok(None);
        }
        let resp = self
            .send_authenticated("add Feishu reaction", |client, token| {
                client
                    .post(format!(
                        "{}/im/v1/messages/{}/reactions",
                        FEISHU_API_BASE, message_id
                    ))
                    .bearer_auth(token)
                    .json(&serde_json::json!({
                        "reaction_type": { "emoji_type": emoji }
                    }))
            })
            .await?;

        #[derive(Deserialize)]
        struct ReactionResp {
            code: i32,
            msg: Option<String>,
            data: Option<ReactionData>,
        }
        #[derive(Deserialize)]
        struct ReactionData {
            reaction_id: Option<String>,
        }

        let result: ReactionResp = resp
            .json()
            .await
            .map_err(|e| ChannelError::Other(format!("Parse reaction response error: {}", e)))?;

        if result.code != 0 {
            tracing::warn!(
                "Failed to add reaction to message {}: code={} msg={}",
                message_id,
                result.code,
                result.msg.as_deref().unwrap_or("unknown")
            );
            return Ok(None);
        }

        let reaction_id = result.data.and_then(|d| d.reaction_id);
        Ok(reaction_id)
    }

    /// Remove reaction using feishu metadata propagated through OutboundMessage.
    /// Reads feishu.message_id and feishu.reaction_id from metadata.
    async fn remove_reaction_from_metadata(
        &self,
        metadata: &std::collections::HashMap<String, String>,
    ) {
        let (message_id, reaction_id) = match (
            metadata.get("feishu.message_id"),
            metadata.get("feishu.reaction_id"),
        ) {
            (Some(msg_id), Some(rid)) => (msg_id.clone(), rid.clone()),
            _ => return,
        };
        if let Err(e) = self.remove_reaction(&message_id, &reaction_id).await {
            tracing::debug!(error = %e, message_id = %message_id, "Failed to remove reaction");
        }
    }

    /// Remove a reaction emoji from a message.
    async fn remove_reaction(
        &self,
        message_id: &str,
        reaction_id: &str,
    ) -> Result<(), ChannelError> {
        let resp = self
            .send_authenticated("remove Feishu reaction", |client, token| {
                client
                    .delete(format!(
                        "{}/im/v1/messages/{}/reactions/{}",
                        FEISHU_API_BASE, message_id, reaction_id
                    ))
                    .bearer_auth(token)
            })
            .await?;

        #[derive(Deserialize)]
        struct ReactionResp {
            code: i32,
            msg: Option<String>,
        }

        let result: ReactionResp = resp.json().await.map_err(|e| {
            ChannelError::Other(format!("Parse remove reaction response error: {}", e))
        })?;

        if result.code != 0 {
            tracing::debug!(
                "Failed to remove reaction {} from message {}: code={} msg={}",
                reaction_id,
                message_id,
                result.code,
                result.msg.as_deref().unwrap_or("unknown")
            );
        }
        Ok(())
    }

    const REPLY_CONTEXT_MAX_LEN: usize = 500;

    /// Fetch the text content of a Feishu message by ID.
    /// Returns a "[Reply to: ...]" context string, or None on failure.
    async fn get_message_content(&self, message_id: &str) -> Option<String> {
        let resp = self
            .send_authenticated("fetch Feishu parent message", |client, token| {
                client
                    .get(format!("{}/im/v1/messages/{}", FEISHU_API_BASE, message_id))
                    .bearer_auth(token)
            })
            .await
            .ok()?;

        #[derive(Deserialize)]
        struct MessageResp {
            code: i32,
            msg: Option<String>,
            data: Option<MessageData>,
        }
        #[derive(Deserialize)]
        struct MessageData {
            items: Option<Vec<MessageItem>>,
        }
        #[derive(Deserialize)]
        struct MessageItem {
            msg_type: String,
            body: Option<MessageBody>,
        }
        #[derive(Deserialize)]
        struct MessageBody {
            content: Option<String>,
        }

        let result: MessageResp = match resp.json().await {
            Ok(r) => r,
            Err(e) => {
                tracing::debug!(error = %e, message_id = %message_id, "Feishu: failed to parse parent message response");
                return None;
            }
        };

        if result.code != 0 {
            tracing::debug!(
                message_id = %message_id,
                code = %result.code,
                msg = ?result.msg,
                "Feishu: failed to fetch parent message"
            );
            return None;
        }

        let items = result.data?.items?;
        let msg_obj = items.first()?;

        let raw_content = msg_obj.body.as_ref()?.content.as_ref()?;
        let msg_type = msg_obj.msg_type.as_str();

        let text = match msg_type {
            "text" => serde_json::from_str::<serde_json::Value>(raw_content)
                .ok()?
                .get("text")?
                .as_str()?
                .to_string(),
            "post" => parse_post_content(raw_content),
            _ => String::new(),
        };

        if text.is_empty() {
            return None;
        }

        let text = if text.len() > Self::REPLY_CONTEXT_MAX_LEN {
            format!(
                "{}...",
                &text[..text.floor_char_boundary(Self::REPLY_CONTEXT_MAX_LEN)]
            )
        } else {
            text
        };

        Some(format!("[Reply to: {}]", text))
    }

    /// Deliver one native Feishu message using create or reply semantics.
    async fn send_raw_message(
        &self,
        target: &FeishuSendTarget,
        msg_type: &str,
        content: &str,
    ) -> Result<String, ChannelError> {
        let mut active_target = target.clone();
        let mut token_refreshes = 0;
        loop {
            let (url, body) = active_target.request(msg_type, content);
            let response = self
                .send_authenticated("send Feishu message", |client, token| {
                    client.post(url.clone()).bearer_auth(token).json(&body)
                })
                .await?;
            let result: FeishuMessageResponse = response.json().await.map_err(|error| {
                ChannelError::Other(format!("Parse send response error: {error}"))
            })?;

            if result.code == FEISHU_INVALID_TOKEN_CODE && token_refreshes == 0 {
                token_refreshes += 1;
                self.invalidate_tenant_token().await;
                continue;
            }
            if matches!(result.code, 230011 | 231003)
                && active_target.reply_to.is_some()
                && !active_target.reply_in_thread
            {
                tracing::warn!(
                    code = result.code,
                    "Feishu reply target is unavailable; falling back to a new chat message"
                );
                active_target.reply_to = None;
                continue;
            }
            if result.code != 0 {
                return Err(ChannelError::Other(format!(
                    "Send message failed: code={} msg={}",
                    result.code, result.msg
                )));
            }
            return Ok(result.data.map_or_else(String::new, |data| data.message_id));
        }
    }

    /// Extract service_id from WebSocket URL query params
    fn extract_service_id(url: &str) -> i32 {
        url.split('?')
            .nth(1)
            .and_then(|qs| {
                qs.split('&')
                    .find(|kv| kv.starts_with("service_id="))
                    .and_then(|kv| kv.split('=').nth(1))
                    .and_then(|v| v.parse::<i32>().ok())
            })
            .unwrap_or(0)
    }

    /// Parse one complete event frame. Transport ACK and fragment handling happen earlier.
    async fn handle_frame(&self, frame: &PbFrame) -> Result<Option<ParsedMessage>, ChannelError> {
        // method 0 = CONTROL (ping/pong)
        if frame.method == 0 {
            return Ok(None);
        }

        // method 1 = DATA (events)
        if frame.method != 1 {
            return Ok(None);
        }

        let payload = frame
            .payload
            .as_deref()
            .ok_or_else(|| ChannelError::Other("No payload in frame".to_string()))?;

        #[cfg(debug_assertions)]
        tracing::debug!(payload_len = %payload.len(), "Received frame payload");

        let event: LarkEvent = serde_json::from_slice(payload)
            .map_err(|e| ChannelError::Other(format!("Parse event error: {}", e)))?;

        let event_type = event.header.event_type.as_str();
        #[cfg(debug_assertions)]
        tracing::debug!(event_type = %event_type, "Received event type");
        if event_type != "im.message.receive_v1" {
            return Ok(None);
        }

        let payload_data: MsgReceivePayload = serde_json::from_value(event.event.clone())
            .map_err(|e| ChannelError::Other(format!("Parse payload error: {}", e)))?;

        // Never let bot/app traffic trigger another model turn.
        if matches!(payload_data.sender.sender_type.as_str(), "bot" | "app") {
            return Ok(None);
        }

        let open_id = payload_data
            .sender
            .sender_id
            .open_id
            .ok_or_else(|| ChannelError::Other("No open_id".to_string()))?;

        if !self.is_allowed(&open_id) {
            tracing::warn!(sender = %open_id, "Rejected unauthorized Feishu sender");
            return Ok(None);
        }

        let message_id = payload_data.message.message_id.clone();
        let msg = payload_data.message;
        if msg.chat_type == "group" && self.config.require_mention {
            let bot_open_id = self.bot_open_id.read().await.clone();
            if !message_mentions_bot(&msg, bot_open_id.as_deref()) {
                #[cfg(debug_assertions)]
                tracing::debug!(message_id = %message_id, "Ignoring group message without bot mention");
                return Ok(None);
            }
        }

        // Deduplicate only after admission so rejected traffic does not consume cache capacity.
        if self.is_message_seen(&message_id).await {
            #[cfg(debug_assertions)]
            tracing::debug!(message_id = %message_id, "Duplicate message, skipping");
            return Ok(None);
        }

        #[cfg(debug_assertions)]
        tracing::debug!(message_id = %message_id, "Received Feishu message");

        let chat_id = msg.chat_id.clone();
        let msg_type = msg.message_type.as_str();
        let raw_content = msg.content.clone();
        let parent_id = msg.parent_id.clone();
        let root_id = msg.root_id.clone();
        let thread_id = msg.thread_id.clone().or_else(|| root_id.clone());
        let chat_type = msg.chat_type.clone();

        #[cfg(debug_assertions)]
        tracing::debug!(msg_type = %msg_type, chat_id = %chat_id, open_id = %open_id, "Parsing message content");

        let (mut content, media) = self
            .parse_and_download_message(msg_type, &raw_content, &message_id)
            .await?;
        content = normalize_mentions(
            &content,
            &msg.mentions,
            self.bot_open_id.read().await.as_deref(),
        );

        // Fetch and prepend quoted message content if this is a reply
        if let Some(ref pid) = parent_id
            && let Some(reply_ctx) = self.get_message_content(pid).await
        {
            content = format!("{}\n{}", reply_ctx, content);
        }

        #[cfg(debug_assertions)]
        for m in &media {
            tracing::debug!(media_type = %m.media_type, media_path = %m.path, "Media downloaded successfully");
        }

        Ok(Some(ParsedMessage {
            message_id,
            open_id,
            chat_id,
            content,
            media,
            parent_id,
            root_id,
            thread_id,
            chat_type,
        }))
    }

    /// Parse message content and download media if needed
    async fn parse_and_download_message(
        &self,
        msg_type: &str,
        content: &str,
        message_id: &str,
    ) -> Result<(String, Vec<MediaItem>), ChannelError> {
        let (text, media) = match msg_type {
            "text" => {
                let text = if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(content) {
                    parsed
                        .get("text")
                        .and_then(|v| v.as_str())
                        .unwrap_or(content)
                        .to_string()
                } else {
                    content.to_string()
                };
                (text, Vec::new())
            }
            "post" => {
                let text = parse_post_content(content);
                let mut media = Vec::new();

                for image_key in collect_post_image_keys(content) {
                    let content_json = serde_json::json!({ "image_key": image_key });
                    match self
                        .download_media("image", &content_json, message_id)
                        .await
                    {
                        Ok((_text, Some(item))) => media.push(item),
                        Ok((_text, None)) => {}
                        Err(e) => {
                            tracing::warn!(error = %e, "Failed to download image from Feishu post message");
                        }
                    }
                }

                (text, media)
            }
            "image" | "audio" | "file" | "media" => {
                if let Ok(content_json) = serde_json::from_str::<serde_json::Value>(content) {
                    match self
                        .download_media(msg_type, &content_json, message_id)
                        .await
                    {
                        Ok((text, Some(media))) => (text, vec![media]),
                        Ok((text, None)) => (text, Vec::new()),
                        Err(_) => (format!("[{}: content unavailable]", msg_type), Vec::new()),
                    }
                } else {
                    (format!("[{}: content unavailable]", msg_type), Vec::new())
                }
            }
            "share_chat" => {
                // Shared chat/cannel messages
                if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(content) {
                    let chat_id = parsed
                        .get("chat_id")
                        .and_then(|v| v.as_str())
                        .unwrap_or("unknown");
                    (format!("[shared chat: {}]", chat_id), Vec::new())
                } else {
                    ("[shared chat]".to_string(), Vec::new())
                }
            }
            "share_user" => {
                // Shared user messages
                if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(content) {
                    let user_id = parsed
                        .get("user_id")
                        .and_then(|v| v.as_str())
                        .unwrap_or("unknown");
                    (format!("[shared user: {}]", user_id), Vec::new())
                } else {
                    ("[shared user]".to_string(), Vec::new())
                }
            }
            "interactive" => {
                // Interactive card messages - extract text content
                match extract_interactive_content(content) {
                    Ok((text, Some(media))) => (text, vec![media]),
                    Ok((text, None)) => (text, Vec::new()),
                    Err(e) => {
                        tracing::warn!(error = %e, "Failed to extract interactive content");
                        (content.to_string(), Vec::new())
                    }
                }
            }
            "list" => {
                // List/bullet messages
                match parse_list_content(content) {
                    Ok((text, Some(media))) => (text, vec![media]),
                    Ok((text, None)) => (text, Vec::new()),
                    Err(_) => (content.to_string(), Vec::new()),
                }
            }
            "merge_forward" => ("[merged forward messages]".to_string(), Vec::new()),
            "share_calendar_event" => {
                if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(content) {
                    let event_key = parsed
                        .get("event_key")
                        .and_then(|v| v.as_str())
                        .unwrap_or("unknown");
                    (
                        format!("[shared calendar event: {}]", event_key),
                        Vec::new(),
                    )
                } else {
                    ("[shared calendar event]".to_string(), Vec::new())
                }
            }
            "system" => ("[system message]".to_string(), Vec::new()),
            _ => (content.to_string(), Vec::new()),
        };

        Ok((text, media))
    }

    async fn process_event_frame(&self, bus: &Arc<MessageBus>, frame: &PbFrame) {
        let parsed = match self.handle_frame(frame).await {
            Ok(Some(parsed)) => parsed,
            Ok(None) => return,
            Err(error) => {
                tracing::warn!(error = %error, "Failed to parse Feishu frame");
                return;
            }
        };

        let message_id = parsed.message_id.clone();
        let reaction_id = match self.add_reaction(&message_id).await {
            Ok(Some(reaction_id)) => Some(reaction_id),
            Ok(None) => None,
            Err(error) => {
                tracing::debug!(error = %error, message_id = %message_id, "Failed to add reaction");
                None
            }
        };

        let mut private_context = HashMap::new();
        private_context.insert("feishu.message_id".to_string(), message_id.clone());
        private_context.insert("feishu.chat_type".to_string(), parsed.chat_type);
        if let Some(parent_id) = parsed.parent_id.as_ref() {
            private_context.insert("feishu.parent_id".to_string(), parent_id.clone());
        }
        if let Some(root_id) = parsed.root_id {
            private_context.insert("feishu.root_id".to_string(), root_id);
        }
        if let Some(thread_id) = parsed.thread_id {
            private_context.insert("feishu.thread_id".to_string(), thread_id);
        }
        if let Some(reaction_id) = reaction_id {
            private_context.insert("feishu.reaction_id".to_string(), reaction_id);
        }

        // Durable context only carries values safe to reuse across Turns:
        // thread/root identity.  Message and reaction ids are one-shot and
        // stay in `private`/`reply_to`.
        let mut durable_context = HashMap::new();
        for key in ["feishu.chat_type", "feishu.thread_id", "feishu.root_id"] {
            if let Some(value) = private_context.get(key) {
                durable_context.insert(key.to_string(), value.clone());
            }
        }

        let msg = crate::bus::InboundMessage {
            channel: "feishu".to_string(),
            sender_id: parsed.open_id.clone(),
            chat_id: parsed.chat_id.clone(),
            client_message_id: None,
            content: parsed.content,
            received_at: crate::bus::message::current_timestamp(),
            media: parsed.media,
            channel_context: crate::bus::ChannelContext {
                reply_to: Some(message_id),
                private: private_context,
                durable_private: durable_context,
            },
        };
        if let Err(error) = self.handle_and_publish(bus, &msg).await {
            tracing::error!(
                error = %error,
                open_id = %parsed.open_id,
                chat_id = %parsed.chat_id,
                "Failed to publish Feishu message to bus"
            );
        }
    }

    /// Send acknowledgment for a message
    async fn send_ack(
        frame: &PbFrame,
        write: &mut futures_util::stream::SplitSink<
            tokio_tungstenite::WebSocketStream<
                tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
            >,
            tokio_tungstenite::tungstenite::Message,
        >,
    ) -> Result<(), ChannelError> {
        let mut ack = frame.clone();
        ack.payload = Some(br#"{"code":200,"headers":{},"data":[]}"#.to_vec());
        ack.headers.push(PbHeader {
            key: "biz_rt".into(),
            value: "0".into(),
        });
        write
            .send(tokio_tungstenite::tungstenite::Message::Binary(
                ack.encode_to_vec().into(),
            ))
            .await
            .map_err(|e| ChannelError::Other(format!("Failed to send ack: {}", e)))?;
        Ok(())
    }

    async fn run_ws_loop(
        &self,
        bus: Arc<MessageBus>,
        shutdown: CancellationToken,
    ) -> Result<Duration, ChannelError> {
        let (wss_url, client_config) = tokio::select! {
            result = self.get_ws_endpoint(&self.http_client) => result?,
            _ = shutdown.cancelled() => return Ok(Duration::ZERO),
        };

        let service_id = Self::extract_service_id(&wss_url);
        tracing::info!(service_id, "Connecting to Feishu WebSocket");

        let (ws_stream, _) = tokio::select! {
            result = tokio_tungstenite::connect_async(&wss_url) => result.map_err(|e| {
                ChannelError::ConnectionError(format!("WebSocket connection failed: {}", e))
            })?,
            _ = shutdown.cancelled() => return Ok(Duration::ZERO),
        };
        let connected_at = Instant::now();

        *self.connected.write().await = true;
        tracing::info!("Feishu WebSocket connected");

        let (mut write, mut read) = ws_stream.split();

        // Send initial ping
        let ping_frame = PbFrame {
            seq_id: 1,
            log_id: 0,
            service: service_id,
            method: 0,
            headers: vec![PbHeader {
                key: "type".into(),
                value: "ping".into(),
            }],
            payload: None,
        };
        tokio::select! {
            result = write.send(tokio_tungstenite::tungstenite::Message::Binary(
                ping_frame.encode_to_vec().into(),
            )) => result.map_err(|e| {
                ChannelError::ConnectionError(format!("Failed to send initial ping: {}", e))
            })?,
            _ = shutdown.cancelled() => return Ok(connected_at.elapsed()),
        };

        let (event_tx, mut event_rx) = tokio::sync::mpsc::channel(WS_EVENT_QUEUE_CAPACITY);
        let worker_channel = self.clone();
        let worker_bus = bus.clone();
        let mut event_worker = tokio::spawn(async move {
            while let Some(frame) = event_rx.recv().await {
                worker_channel
                    .process_event_frame(&worker_bus, &frame)
                    .await;
            }
        });

        let ping_interval = client_config.ping_interval.unwrap_or(120).max(10);
        let mut ping_interval_tok =
            tokio::time::interval(tokio::time::Duration::from_secs(ping_interval));
        let mut timeout_check = tokio::time::interval(tokio::time::Duration::from_secs(10));
        let mut seq: u64 = 1;
        let mut last_recv = Instant::now();
        let mut fragment_cache: HashMap<String, FragmentAssembly> = HashMap::new();

        // Consume the immediate tick
        ping_interval_tok.tick().await;
        timeout_check.tick().await;

        loop {
            tokio::select! {
                msg = read.next() => {
                    match msg {
                        Some(Ok(tokio_tungstenite::tungstenite::Message::Binary(data))) => {
                            last_recv = Instant::now();
                            let bytes: Bytes = data;
                            if let Ok(frame) = PbFrame::decode(bytes.as_ref()) {
                                if frame.method != 1 {
                                    continue;
                                }
                                match tokio::time::timeout(
                                    WS_ACK_TIMEOUT,
                                    Self::send_ack(&frame, &mut write),
                                )
                                .await
                                {
                                    Ok(Ok(())) => {}
                                    Ok(Err(error)) => {
                                        tracing::warn!(error = %error, "Failed to ACK Feishu DATA frame");
                                        break;
                                    }
                                    Err(_) => {
                                        tracing::warn!("Timed out ACKing Feishu DATA frame");
                                        break;
                                    }
                                }

                                match reassemble_data_frame(&frame, &mut fragment_cache) {
                                    Ok(Some(complete_frame)) => {
                                        match event_tx.try_send(complete_frame) {
                                            Ok(()) => {}
                                            Err(tokio::sync::mpsc::error::TrySendError::Full(frame)) => {
                                                tracing::warn!(
                                                    capacity = WS_EVENT_QUEUE_CAPACITY,
                                                    "Feishu event queue is full; processing one event on the connection task"
                                                );
                                                self.process_event_frame(&bus, &frame).await;
                                            }
                                            Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
                                                tracing::warn!("Feishu event worker stopped unexpectedly");
                                                break;
                                            }
                                        }
                                    }
                                    Ok(None) => {}
                                    Err(error) => {
                                        tracing::warn!(error = %error, "Rejected invalid Feishu fragment sequence");
                                    }
                                }
                            }
                        }
                        Some(Ok(tokio_tungstenite::tungstenite::Message::Ping(data))) => {
                            last_recv = Instant::now();
                            let pong = PbFrame {
                                seq_id: seq.wrapping_add(1),
                                log_id: 0,
                                service: service_id,
                                method: 0,
                                headers: vec![PbHeader {
                                    key: "type".into(),
                                    value: "pong".into(),
                                }],
                                payload: Some(data.to_vec()),
                            };
                            let _ = write.send(tokio_tungstenite::tungstenite::Message::Binary(pong.encode_to_vec().into())).await;
                        }
                        Some(Ok(tokio_tungstenite::tungstenite::Message::Pong(_))) => {
                            last_recv = Instant::now();
                        }
                        Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_))) | None => {
                            #[cfg(debug_assertions)]
                            tracing::debug!("Feishu WebSocket closed");
                            break;
                        }
                        Some(Err(e)) => {
                            tracing::warn!(error = %e, "Feishu WebSocket error");
                            break;
                        }
                        _ => {}
                    }
                }
                _ = ping_interval_tok.tick() => {
                    seq = seq.wrapping_add(1);
                    let ping = PbFrame {
                        seq_id: seq,
                        log_id: 0,
                        service: service_id,
                        method: 0,
                        headers: vec![PbHeader {
                            key: "type".into(),
                            value: "ping".into(),
                        }],
                        payload: None,
                    };
                    if write.send(tokio_tungstenite::tungstenite::Message::Binary(ping.encode_to_vec().into())).await.is_err() {
                        tracing::warn!("Feishu ping failed, reconnecting");
                        break;
                    }
                }
                _ = timeout_check.tick() => {
                    if last_recv.elapsed() > WS_HEARTBEAT_TIMEOUT {
                        tracing::warn!("Feishu WebSocket heartbeat timeout, reconnecting");
                        break;
                    }
                    // GC dedup cache: remove entries older than TTL (matches zeroclaw pattern)
                    let now = Instant::now();
                    let mut seen = self.seen_message_ids.write().await;
                    seen.retain(|_, ts| now.duration_since(*ts) < DEDUP_CACHE_TTL);
                    fragment_cache.retain(|_, assembly| {
                        now.duration_since(assembly.created_at) < WS_FRAGMENT_TTL
                    });
                }
                _ = shutdown.cancelled() => {
                    tracing::info!("Feishu channel shutdown signal received");
                    break;
                }
            }
        }

        *self.connected.write().await = false;
        drop(event_tx);
        if tokio::time::timeout(CHANNEL_STOP_GRACE, &mut event_worker)
            .await
            .is_err()
        {
            event_worker.abort();
            let _ = event_worker.await;
        }
        Ok(connected_at.elapsed())
    }
}

fn reassemble_data_frame(
    frame: &PbFrame,
    cache: &mut HashMap<String, FragmentAssembly>,
) -> Result<Option<PbFrame>, ChannelError> {
    let sum = frame
        .header_value("sum")
        .and_then(|value| value.parse::<usize>().ok())
        .unwrap_or(1)
        .max(1);
    if sum == 1 {
        if frame
            .payload
            .as_ref()
            .is_some_and(|payload| payload.len() > MAX_WS_EVENT_BYTES)
        {
            return Err(ChannelError::Other(format!(
                "Feishu event exceeds {MAX_WS_EVENT_BYTES} byte limit"
            )));
        }
        return Ok(Some(frame.clone()));
    }
    if sum > MAX_WS_FRAGMENTS {
        return Err(ChannelError::Other(format!(
            "Feishu event declares too many fragments: {sum}"
        )));
    }

    let message_id = frame.header_value("message_id").unwrap_or_default();
    if message_id.is_empty() {
        return Err(ChannelError::Other(
            "Fragmented Feishu event has no message_id".to_string(),
        ));
    }
    let seq = frame
        .header_value("seq")
        .and_then(|value| value.parse::<usize>().ok())
        .unwrap_or(0);
    if seq >= sum {
        return Err(ChannelError::Other(format!(
            "Feishu fragment index {seq} is outside declared count {sum}"
        )));
    }

    let assembly = cache
        .entry(message_id.to_string())
        .or_insert_with(|| FragmentAssembly {
            parts: vec![None; sum],
            total_bytes: 0,
            created_at: Instant::now(),
        });
    if assembly.parts.len() != sum {
        *assembly = FragmentAssembly {
            parts: vec![None; sum],
            total_bytes: 0,
            created_at: Instant::now(),
        };
    }
    let payload = frame.payload.clone().unwrap_or_default();
    let previous_len = assembly.parts[seq].as_ref().map_or(0, Vec::len);
    assembly.total_bytes = assembly.total_bytes - previous_len + payload.len();
    if assembly.total_bytes > MAX_WS_EVENT_BYTES {
        cache.remove(message_id);
        return Err(ChannelError::Other(format!(
            "Feishu fragmented event exceeds {MAX_WS_EVENT_BYTES} byte limit"
        )));
    }
    assembly.parts[seq] = Some(payload);
    if assembly.parts.iter().any(Option::is_none) {
        return Ok(None);
    }

    let complete_payload = assembly
        .parts
        .iter()
        .flat_map(|part| part.as_deref().unwrap_or_default())
        .copied()
        .collect();
    cache.remove(message_id);
    let mut complete = frame.clone();
    complete.payload = Some(complete_payload);
    Ok(Some(complete))
}

fn reconnect_delay(attempt: u32) -> Duration {
    let exponent = attempt.saturating_sub(1).min(6);
    Duration::from_secs(1_u64 << exponent)
}

fn api_retry_delay(attempt: usize) -> Duration {
    Duration::from_millis(250_u64.saturating_mul(1_u64 << attempt.min(4)))
}

fn retry_after_delay(headers: &reqwest::header::HeaderMap, attempt: usize) -> Duration {
    headers
        .get(reqwest::header::RETRY_AFTER)
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.parse::<u64>().ok())
        .map(|seconds| Duration::from_secs(seconds.min(5)))
        .unwrap_or_else(|| api_retry_delay(attempt))
}

async fn read_response_limited(
    mut response: reqwest::Response,
    max_bytes: u64,
    timeout: Duration,
    media_type: &str,
) -> Result<Vec<u8>, ChannelError> {
    if response
        .content_length()
        .is_some_and(|length| length > max_bytes)
    {
        return Err(ChannelError::Other(format!(
            "Feishu {media_type} exceeds {max_bytes} byte limit"
        )));
    }
    tokio::time::timeout(timeout, async move {
        let mut data = Vec::new();
        while let Some(chunk) = response.chunk().await.map_err(|error| {
            ChannelError::ConnectionError(format!("Failed to read {media_type} data: {error}"))
        })? {
            if (data.len() as u64).saturating_add(chunk.len() as u64) > max_bytes {
                return Err(ChannelError::Other(format!(
                    "Feishu {media_type} exceeds {max_bytes} byte limit"
                )));
            }
            data.extend_from_slice(&chunk);
        }
        Ok(data)
    })
    .await
    .map_err(|_| ChannelError::ConnectionError(format!("Feishu {media_type} download timed out")))?
}

async fn ensure_media_capacity(
    media_dir: &Path,
    incoming_bytes: u64,
    max_bytes: u64,
) -> Result<(), ChannelError> {
    let mut total = 0_u64;
    let mut entries = tokio::fs::read_dir(media_dir)
        .await
        .map_err(|error| ChannelError::Other(format!("Failed to inspect media dir: {error}")))?;
    while let Some(entry) = entries
        .next_entry()
        .await
        .map_err(|error| ChannelError::Other(format!("Failed to inspect media dir: {error}")))?
    {
        if let Ok(metadata) = entry.metadata().await
            && metadata.is_file()
        {
            total = total.saturating_add(metadata.len());
        }
    }
    if total.saturating_add(incoming_bytes) > max_bytes {
        return Err(ChannelError::Other(format!(
            "Feishu media directory would exceed {max_bytes} byte limit"
        )));
    }
    Ok(())
}

async fn ensure_local_media_size(
    file_path: &str,
    max_bytes: u64,
    media_type: &str,
) -> Result<(), ChannelError> {
    let size = tokio::fs::metadata(file_path)
        .await
        .map_err(|error| ChannelError::Other(format!("Failed to inspect {media_type}: {error}")))?
        .len();
    if size > max_bytes {
        return Err(ChannelError::Other(format!(
            "Outbound {media_type} is {size} bytes, exceeding {max_bytes} byte limit"
        )));
    }
    Ok(())
}

fn sanitize_filename(filename: &str) -> String {
    let basename = Path::new(filename)
        .file_name()
        .and_then(|value| value.to_str())
        .unwrap_or("file.bin");
    let sanitized: String = basename
        .chars()
        .map(|character| {
            if character.is_control() || matches!(character, '/' | '\\' | ':' | '\0') {
                '_'
            } else {
                character
            }
        })
        .collect();
    let sanitized = sanitized.trim_matches(['.', ' ']);
    if sanitized.is_empty() {
        "file.bin".to_string()
    } else {
        let boundary = sanitized.floor_char_boundary(sanitized.len().min(255));
        sanitized[..boundary].to_string()
    }
}

fn parse_post_content(content: &str) -> String {
    /// Extract text from a single post element (text, link, at-mention).
    fn extract_element(el: &serde_json::Value, out: &mut Vec<String>) {
        match el.get("tag").and_then(|t| t.as_str()).unwrap_or("") {
            "text" => {
                if let Some(text) = el.get("text").and_then(|t| t.as_str()) {
                    out.push(text.to_string());
                }
            }
            "a" => {
                let link_text = el
                    .get("text")
                    .and_then(|t| t.as_str())
                    .filter(|s| !s.is_empty())
                    .or_else(|| el.get("href").and_then(|h| h.as_str()))
                    .unwrap_or("");
                out.push(link_text.to_string());
            }
            "at" => {
                let name = el
                    .get("user_name")
                    .and_then(|n| n.as_str())
                    .or_else(|| el.get("user_id").and_then(|i| i.as_str()))
                    .unwrap_or("user");
                out.push(format!("@{}", name));
            }
            "img" => {
                out.push("[image]".to_string());
            }
            "code_block" => {
                let lang = el.get("language").and_then(|l| l.as_str()).unwrap_or("");
                let code_text = el.get("text").and_then(|t| t.as_str()).unwrap_or("");
                out.push(format!("\n```{}\n{}\n```\n", lang, code_text));
            }
            _ => {
                if let Some(text) = el.get("text").and_then(|t| t.as_str()) {
                    out.push(text.to_string());
                }
            }
        }
    }

    /// Parse a single block {title, content: [[...]]} and append text to out.
    fn parse_block(block: &serde_json::Value, out: &mut Vec<String>) {
        let title = block
            .get("title")
            .and_then(|t| t.as_str())
            .filter(|s| !s.is_empty());
        if let Some(t) = title {
            out.push(t.to_string());
            out.push("\n\n".to_string());
        }

        if let Some(content_arr) = block.get("content").and_then(|c| c.as_array()) {
            for row in content_arr {
                if let Some(row_arr) = row.as_array() {
                    for el in row_arr {
                        extract_element(el, out);
                    }
                    out.push("\n".to_string());
                }
            }
        }
    }

    let Ok(parsed) = serde_json::from_str::<serde_json::Value>(content) else {
        return content.to_string();
    };

    let mut texts = Vec::new();

    // Unwrap optional {"post": ...} envelope (nanobot: root = root["post"])
    let root = if parsed.get("post").and_then(|p| p.as_object()).is_some() {
        parsed.get("post").unwrap()
    } else {
        &parsed
    };

    // Try direct format: {"title": ..., "content": [[...]]}
    if root.get("content").and_then(|c| c.as_array()).is_some() {
        parse_block(root, &mut texts);
        let result = texts.join("");
        if !result.trim().is_empty() {
            return result.trim().to_string();
        }
        texts.clear();
    }

    // Try localized: {"zh_cn": {"title": ..., "content": [...]}}
    for key in ["zh_cn", "en_us", "ja_jp"] {
        if let Some(locale_data) = root.get(key).and_then(|l| l.as_object()) {
            parse_block(&serde_json::json!(locale_data), &mut texts);
            let result = texts.join("");
            if !result.trim().is_empty() {
                return result.trim().to_string();
            }
            texts.clear();
        }
    }

    // Fall back: try any dict child
    if let Some(root_obj) = root.as_object() {
        for (_key, val) in root_obj {
            if let Some(obj) = val.as_object()
                && obj.get("content").and_then(|c| c.as_array()).is_some()
            {
                parse_block(val, &mut texts);
                let result = texts.join("");
                if !result.trim().is_empty() {
                    return result.trim().to_string();
                }
                texts.clear();
            }
        }
    }

    content.to_string()
}

fn collect_post_image_keys(content: &str) -> Vec<String> {
    fn visit(value: &serde_json::Value, keys: &mut Vec<String>) {
        match value {
            serde_json::Value::Object(map) => {
                if let Some(image_key) = map.get("image_key").and_then(|v| v.as_str())
                    && !keys.iter().any(|k| k == image_key)
                {
                    keys.push(image_key.to_string());
                }

                for child in map.values() {
                    visit(child, keys);
                }
            }
            serde_json::Value::Array(items) => {
                for item in items {
                    visit(item, keys);
                }
            }
            _ => {}
        }
    }

    let Ok(parsed) = serde_json::from_str::<serde_json::Value>(content) else {
        return Vec::new();
    };

    let mut keys = Vec::new();
    visit(&parsed, &mut keys);
    keys
}

/// Extract text content from interactive card messages
fn extract_interactive_content(content: &str) -> Result<(String, Option<MediaItem>), ChannelError> {
    let parsed = match serde_json::from_str::<serde_json::Value>(content) {
        Ok(p) => p,
        Err(_) => return Ok((content.to_string(), None)),
    };

    let mut texts = Vec::new();

    // Extract from elements array
    if let Some(elements) = parsed.get("elements").and_then(|e| e.as_array()) {
        for el in elements {
            extract_element_content(el, &mut texts);
        }
    }

    // Extract from card object
    if let Some(card) = parsed.get("card").and_then(|c| c.as_object())
        && let Some(elements) = card.get("elements").and_then(|e| e.as_array())
    {
        for el in elements {
            extract_element_content(el, &mut texts);
        }
    }

    // Extract from header
    if let Some(header) = parsed.get("header").and_then(|h| h.as_object())
        && let Some(title) = header.get("title").and_then(|t| t.as_object())
        && let Some(text) = title.get("content").and_then(|c| c.as_str())
    {
        texts.push(format!("title: {}\n", text));
    }

    let result = texts.join("").trim().to_string();
    if result.is_empty() {
        Ok((content.to_string(), None))
    } else {
        Ok((result, None))
    }
}

/// Extract content from a single card element
fn extract_element_content(element: &serde_json::Value, texts: &mut Vec<String>) {
    let tag = element.get("tag").and_then(|t| t.as_str()).unwrap_or("");

    match tag {
        "markdown" | "lark_md" => {
            if let Some(content) = element.get("content").and_then(|c| c.as_str()) {
                texts.push(content.to_string());
                texts.push("\n".to_string());
            }
        }
        "div" => {
            if let Some(text_obj) = element.get("text").and_then(|t| t.as_object()) {
                let content = text_obj
                    .get("content")
                    .and_then(|c| c.as_str())
                    .unwrap_or("");
                texts.push(content.to_string());
            } else if let Some(content) = element.get("text").and_then(|t| t.as_str()) {
                texts.push(content.to_string());
            }
            texts.push("\n".to_string());
        }
        "a" => {
            let href = element.get("href").and_then(|h| h.as_str()).unwrap_or("");
            let text = element.get("text").and_then(|t| t.as_str()).unwrap_or("");
            if !text.is_empty() {
                texts.push(text.to_string());
            } else if !href.is_empty() {
                texts.push(format!("link: {}", href));
            }
        }
        "img" => {
            let alt = element.get("alt");
            let alt_text = alt
                .and_then(|a| a.as_str())
                .or_else(|| {
                    alt.and_then(|a| a.as_object())
                        .and_then(|o| o.get("content"))
                        .and_then(|c| c.as_str())
                })
                .unwrap_or("[image]");
            texts.push(format!("{}\n", alt_text));
        }
        "note" => {
            if let Some(elements) = element.get("elements").and_then(|e| e.as_array()) {
                for el in elements {
                    extract_element_content(el, texts);
                }
            }
        }
        "column_set" => {
            if let Some(columns) = element.get("columns").and_then(|c| c.as_array()) {
                for col in columns {
                    if let Some(elements) = col.get("elements").and_then(|e| e.as_array()) {
                        for el in elements {
                            extract_element_content(el, texts);
                        }
                    }
                }
            }
        }
        "table" => {
            // Tables are complex, just indicate presence
            texts.push("[table]\n".to_string());
        }
        _ => {
            // Recursively check for nested elements
            if let Some(elements) = element.get("elements").and_then(|e| e.as_array()) {
                for el in elements {
                    extract_element_content(el, texts);
                }
            }
        }
    }
}

/// Parse Feishu list/bullet message content into plain text
fn parse_list_content(content: &str) -> Result<(String, Option<MediaItem>), ChannelError> {
    let parsed = match serde_json::from_str::<serde_json::Value>(content) {
        Ok(p) => p,
        Err(_) => return Ok((content.to_string(), None)),
    };

    let items = parsed
        .get("items")
        .and_then(|i| i.as_array())
        .or_else(|| parsed.get("content").and_then(|c| c.as_array()));

    let Some(items) = items else {
        return Ok((content.to_string(), None));
    };

    let mut lines = Vec::new();
    collect_list_items(items, &mut lines, 0);

    let result = lines.join("\n").trim().to_string();
    if result.is_empty() {
        Ok((content.to_string(), None))
    } else {
        Ok((result, None))
    }
}

/// Recursively collect list item text with indentation
fn collect_list_items(items: &[serde_json::Value], lines: &mut Vec<String>, depth: usize) {
    let indent = "  ".repeat(depth);

    for item in items {
        // Items can be arrays of inline elements or objects with content/children
        let inline_elements = if let Some(arr) = item.as_array() {
            arr.as_slice()
        } else if let Some(obj) = item.as_object() {
            obj.get("content")
                .and_then(|c| c.as_array())
                .map(|a| a.as_slice())
                .unwrap_or(&[])
        } else {
            continue;
        };

        let mut text = String::new();
        for el in inline_elements {
            extract_inline_text(el, &mut text);
        }

        let trimmed = text.trim();
        if !trimmed.is_empty() {
            lines.push(format!("{}- {}", indent, trimmed));
        }

        // Handle nested children
        if let Some(obj) = item.as_object() {
            if let Some(children) = obj.get("children").and_then(|c| c.as_array()) {
                collect_list_items(children, lines, depth + 1);
            }
        } else if let Some(children_arr) = item.as_array().and_then(|arr| {
            arr.iter()
                .find(|child| child.as_object().and_then(|o| o.get("children")).is_some())
        }) && let Some(children) = children_arr
            .as_object()
            .and_then(|o| o.get("children"))
            .and_then(|c| c.as_array())
        {
            collect_list_items(children, lines, depth + 1);
        }
    }
}

/// Extract text from inline elements (text, link, at-mention)
fn extract_inline_text(el: &serde_json::Value, out: &mut String) {
    match el.get("tag").and_then(|t| t.as_str()).unwrap_or("") {
        "text" => {
            if let Some(text) = el.get("text").and_then(|t| t.as_str()) {
                out.push_str(text);
            }
        }
        "a" => {
            let text = el
                .get("text")
                .and_then(|t| t.as_str())
                .filter(|s| !s.is_empty())
                .or_else(|| el.get("href").and_then(|h| h.as_str()))
                .unwrap_or("");
            out.push_str(text);
        }
        "at" => {
            let name = el
                .get("user_name")
                .and_then(|n| n.as_str())
                .or_else(|| el.get("user_id").and_then(|i| i.as_str()))
                .unwrap_or("user");
            out.push_str(&format!("@{}", name));
        }
        _ => {}
    }
}

fn mention_open_id(mention: &serde_json::Value) -> Option<&str> {
    mention
        .pointer("/id/open_id")
        .or_else(|| mention.get("open_id"))
        .and_then(|value| value.as_str())
}

fn message_mentions_bot(message: &LarkMessage, bot_open_id: Option<&str>) -> bool {
    let Some(bot_open_id) = bot_open_id.filter(|value| !value.is_empty()) else {
        return false;
    };
    if message
        .mentions
        .iter()
        .any(|mention| mention_open_id(mention) == Some(bot_open_id))
    {
        return true;
    }

    fn post_contains_bot(value: &serde_json::Value, bot_open_id: &str) -> bool {
        match value {
            serde_json::Value::Object(map) => {
                let is_bot_mention = map.get("tag").and_then(|value| value.as_str()) == Some("at")
                    && map
                        .get("user_id")
                        .or_else(|| map.get("open_id"))
                        .and_then(|value| value.as_str())
                        == Some(bot_open_id);
                is_bot_mention
                    || map
                        .values()
                        .any(|value| post_contains_bot(value, bot_open_id))
            }
            serde_json::Value::Array(values) => values
                .iter()
                .any(|value| post_contains_bot(value, bot_open_id)),
            _ => false,
        }
    }

    serde_json::from_str::<serde_json::Value>(&message.content)
        .is_ok_and(|content| post_contains_bot(&content, bot_open_id))
}

/// Remove the bot's own placeholder while preserving human mentions as readable names.
fn normalize_mentions(
    text: &str,
    mentions: &[serde_json::Value],
    bot_open_id: Option<&str>,
) -> String {
    let mut normalized = text.to_string();
    for mention in mentions {
        let Some(key) = mention.get("key").and_then(|value| value.as_str()) else {
            continue;
        };
        let replacement = if mention_open_id(mention) == bot_open_id {
            String::new()
        } else {
            mention
                .get("name")
                .and_then(|value| value.as_str())
                .filter(|value| !value.is_empty())
                .map(|name| format!("@{name}"))
                .unwrap_or_else(|| key.to_string())
        };
        normalized = normalized.replace(key, &replacement);
    }
    normalized.trim().to_string()
}

fn resolve_image_ext(content_type: &str) -> &str {
    match content_type {
        "image/png" => "png",
        "image/gif" => "gif",
        "image/webp" => "webp",
        "image/bmp" => "bmp",
        _ => "jpg",
    }
}

fn resolve_file_ext(content_json: &serde_json::Value) -> String {
    if let Some(name) = content_json.get("file_name").and_then(|v| v.as_str())
        && let Some(ext) = std::path::Path::new(name)
            .extension()
            .and_then(|e| e.to_str())
    {
        return ext.to_string();
    }
    String::new()
}

async fn resolve_unique_path(dir: &Path, filename: &str) -> std::path::PathBuf {
    let candidate = dir.join(filename);
    if !tokio::fs::try_exists(&candidate).await.unwrap_or(false) {
        return candidate;
    }
    let stem = std::path::Path::new(filename)
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or(filename);
    let ext = std::path::Path::new(filename)
        .extension()
        .and_then(|s| s.to_str())
        .unwrap_or("");
    let mut n = 1;
    loop {
        let candidate = if ext.is_empty() {
            dir.join(format!("{}({})", stem, n))
        } else {
            dir.join(format!("{}({}).{}", stem, n, ext))
        };
        if !tokio::fs::try_exists(&candidate).await.unwrap_or(false) {
            return candidate;
        }
        n += 1;
    }
}

impl FeishuChannel {
    /// Build a Card JSON 2.0 interactive card with a single markdown element.
    fn build_card_content(markdown: &str) -> String {
        serde_json::json!({
            "schema": "2.0",
            "body": {
                "elements": [{
                    "tag": "markdown",
                    "content": markdown
                }]
            }
        })
        .to_string()
    }

    /// Max byte-size for markdown content in a single card.
    /// Card payloads have a ~30 KB limit; leave margin for JSON envelope.
    const CARD_MARKDOWN_MAX_BYTES: usize = 28_000;

    /// Split markdown content into chunks that fit within the card size limit.
    /// Splits on line boundaries to avoid breaking markdown syntax.
    fn split_markdown_chunks(text: &str) -> Vec<String> {
        if text.len() <= Self::CARD_MARKDOWN_MAX_BYTES {
            return vec![text.to_string()];
        }

        let mut chunks: Vec<String> = Vec::new();
        let mut start = 0;

        while start < text.len() {
            if start + Self::CARD_MARKDOWN_MAX_BYTES >= text.len() {
                chunks.push(text[start..].to_string());
                break;
            }

            let end = text.floor_char_boundary(start + Self::CARD_MARKDOWN_MAX_BYTES);
            let search_region = &text[start..end];
            let split_at = search_region
                .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(start)
            };

            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].to_string());
                start = forced;
            } else {
                chunks.push(text[start..split_at].to_string());
                start = split_at;
            }
        }

        chunks
    }

    /// Send an interactive card message to Feishu.
    async fn send_interactive_card(
        &self,
        target: &FeishuSendTarget,
        card_content: &str,
    ) -> Result<String, ChannelError> {
        let message_id = self
            .send_raw_message(target, "interactive", card_content)
            .await?;
        if message_id.is_empty() {
            Err(ChannelError::Other(
                "Feishu send response has no message_id".into(),
            ))
        } else {
            Ok(message_id)
        }
    }

    async fn update_interactive_card(
        &self,
        message_id: &str,
        card_content: &str,
    ) -> Result<(), ChannelError> {
        let card: serde_json::Value = serde_json::from_str(card_content)
            .map_err(|error| ChannelError::Other(format!("Invalid card JSON: {error}")))?;
        let response = self
            .send_authenticated("update Feishu card", |client, token| {
                client
                    .patch(format!("{}/im/v1/messages/{}", FEISHU_API_BASE, message_id))
                    .bearer_auth(token)
                    .json(&serde_json::json!({ "card": card }))
            })
            .await?;

        #[derive(Deserialize)]
        struct UpdateResp {
            code: i32,
            msg: String,
        }
        let result: UpdateResp = response.json().await.map_err(|error| {
            ChannelError::Other(format!("Parse update card response error: {error}"))
        })?;
        if result.code != 0 {
            return Err(ChannelError::Other(format!(
                "Update card failed: code={} msg={}",
                result.code, result.msg
            )));
        }
        Ok(())
    }
}

#[async_trait]
trait FeishuTurnApi: Send {
    async fn create_card(&mut self, markdown: &str) -> Result<String, ChannelError>;
    async fn update_card(&mut self, message_id: &str, markdown: &str) -> Result<(), ChannelError>;
    async fn cleanup(&mut self);
}

struct FeishuTurnBackend {
    channel: FeishuChannel,
    target: FeishuSendTarget,
    metadata: HashMap<String, String>,
}

#[async_trait]
impl FeishuTurnApi for FeishuTurnBackend {
    async fn create_card(&mut self, markdown: &str) -> Result<String, ChannelError> {
        let card = FeishuChannel::build_card_content(markdown);
        self.channel
            .send_interactive_card(&self.target, &card)
            .await
    }

    async fn update_card(&mut self, message_id: &str, markdown: &str) -> Result<(), ChannelError> {
        let card = FeishuChannel::build_card_content(markdown);
        self.channel
            .update_interactive_card(message_id, &card)
            .await
    }

    async fn cleanup(&mut self) {
        self.channel
            .remove_reaction_from_metadata(&self.metadata)
            .await;
    }
}

struct FeishuTurnSink {
    api: Box<dyn FeishuTurnApi>,
    message_id: Option<String>,
    cleaned_up: bool,
}

impl FeishuTurnSink {
    fn new(api: Box<dyn FeishuTurnApi>) -> Self {
        Self {
            api,
            message_id: None,
            cleaned_up: false,
        }
    }

    async fn cleanup(&mut self) {
        if !self.cleaned_up {
            self.api.cleanup().await;
            self.cleaned_up = true;
        }
    }

    async fn send_chunks(&mut self, chunks: &[String]) -> Result<(), ChannelError> {
        for chunk in chunks {
            self.api.create_card(chunk).await?;
        }
        Ok(())
    }

    async fn finish_snapshot(&mut self, snapshot: &TurnSnapshot) -> Result<(), ChannelError> {
        let markdown = render_feishu_turn(snapshot);
        let chunks = if markdown.is_empty() {
            Vec::new()
        } else {
            FeishuChannel::split_markdown_chunks(&markdown)
        };

        let result = if chunks.is_empty() {
            Ok(())
        } else if let Some(message_id) = self.message_id.clone() {
            match self.api.update_card(&message_id, &chunks[0]).await {
                Ok(()) => self.send_chunks(&chunks[1..]).await,
                Err(error) => {
                    tracing::warn!(error = %error, "Final Feishu card update failed; sending complete fallback");
                    self.send_chunks(&chunks).await
                }
            }
        } else {
            self.send_chunks(&chunks).await
        };
        self.cleanup().await;
        result
    }
}

#[async_trait]
impl TurnSink for FeishuTurnSink {
    async fn update(&mut self, snapshot: &TurnSnapshot) -> Result<(), ChannelError> {
        let markdown = render_feishu_turn(snapshot);
        if markdown.is_empty() {
            return Ok(());
        }
        let live_markdown = truncate_feishu_live_markdown(&markdown);
        if let Some(message_id) = self.message_id.clone() {
            self.api.update_card(&message_id, &live_markdown).await
        } else {
            self.message_id = Some(self.api.create_card(&live_markdown).await?);
            Ok(())
        }
    }

    async fn finish(&mut self, snapshot: &TurnSnapshot) -> Result<(), ChannelError> {
        self.finish_snapshot(snapshot).await
    }

    async fn abort(&mut self, snapshot: &TurnSnapshot) -> Result<(), ChannelError> {
        self.finish_snapshot(snapshot).await
    }
}

fn render_feishu_turn(snapshot: &TurnSnapshot) -> String {
    let mut sections = Vec::new();
    for block in &snapshot.blocks {
        match block {
            TurnBlock::Reasoning { text, .. } if !text.trim().is_empty() => {
                sections.push(format!(
                    "> **思考过程**\n> {}",
                    text.trim().replace('\n', "\n> ")
                ));
            }
            TurnBlock::Assistant { text, .. } if !text.trim().is_empty() => {
                sections.push(text.trim().to_string());
            }
            TurnBlock::Tool {
                name,
                status,
                preview,
                ..
            } => {
                let status = match status {
                    ToolStatus::Running => "执行中",
                    ToolStatus::Completed => "已完成",
                    ToolStatus::Cancelled => "已停止",
                    ToolStatus::Failed => "失败",
                };
                let mut section = format!("> 🔧 **{name}** · {status}");
                if let Some(preview) = preview.as_deref().filter(|value| !value.trim().is_empty()) {
                    section.push_str("\n> ");
                    section.push_str(&preview.trim().replace('\n', "\n> "));
                }
                sections.push(section);
            }
            _ => {}
        }
    }

    if sections.is_empty() {
        if snapshot.status == TurnStatus::Failed {
            sections.push(format!(
                "⚠️ 回复失败：{}",
                snapshot.error.as_deref().unwrap_or("未知错误")
            ));
        } else if snapshot.status == TurnStatus::Cancelled {
            sections.push("已停止生成。".to_string());
        } else {
            return String::new();
        }
    }

    let status = match snapshot.status {
        TurnStatus::Running => Some(match snapshot.phase {
            crate::session::TurnPhase::Queued => "排队中",
            crate::session::TurnPhase::Reasoning => "思考中",
            crate::session::TurnPhase::Responding => "生成中",
            crate::session::TurnPhase::Acting => "调用工具中",
            crate::session::TurnPhase::Finalizing => "收尾中",
        }),
        TurnStatus::Cancelled => Some("已停止"),
        TurnStatus::Failed => Some("失败"),
        TurnStatus::Completed => None,
    };
    if let Some(status) = status {
        sections.push(format!("_{status}_"));
    }
    sections.join("\n\n")
}

fn truncate_feishu_live_markdown(markdown: &str) -> String {
    if markdown.len() <= FeishuChannel::CARD_MARKDOWN_MAX_BYTES {
        return markdown.to_string();
    }
    const SUFFIX: &str = "\n\n_内容仍在生成，已暂时截断…_";
    let limit = FeishuChannel::CARD_MARKDOWN_MAX_BYTES.saturating_sub(SUFFIX.len());
    let boundary = markdown.floor_char_boundary(limit);
    format!("{}{SUFFIX}", &markdown[..boundary])
}

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

    fn is_allowed(&self, sender_id: &str) -> bool {
        self.config.allow_from.iter().any(|allowed| {
            let allowed = allowed.trim();
            allowed == "*" || allowed == sender_id
        })
    }

    /// Handle an inbound message: check for slash commands first, then publish to bus
    async fn handle_and_publish(
        &self,
        bus: &Arc<MessageBus>,
        msg: &crate::bus::InboundMessage,
    ) -> Result<(), ChannelError> {
        // All messages (including slash commands) go through the normal inbound flow
        // SessionManager handles session creation/reuse internally
        bus.publish_inbound(msg.clone()).await?;
        Ok(())
    }

    async fn start(&self, bus: Arc<MessageBus>) -> Result<(), ChannelError> {
        if self.config.app_id.is_empty() || self.config.app_secret.is_empty() {
            return Err(ChannelError::ConfigError(
                "Feishu app_id or app_secret is not configured".to_string(),
            ));
        }

        if self.config.require_mention {
            match self.refresh_bot_open_id().await {
                Ok(open_id) => {
                    tracing::info!(bot_open_id = %open_id, "Resolved Feishu bot identity")
                }
                Err(error) => tracing::warn!(
                    error = %error,
                    "Failed to resolve Feishu bot identity; group messages will be ignored"
                ),
            }
        }

        let mut run_task = self.run_task.lock().await;
        if run_task.as_ref().is_some_and(|task| !task.is_finished()) {
            return Ok(());
        }
        if let Some(finished) = run_task.take() {
            let _ = finished.await;
        }

        *self.running.write().await = true;

        let shutdown = CancellationToken::new();
        *self.shutdown.write().await = Some(shutdown.clone());

        let channel = self.clone();
        let bus = bus.clone();
        *run_task = Some(tokio::spawn(async move {
            let mut retry_attempt = 0_u32;

            loop {
                if !*channel.running.read().await {
                    break;
                }

                match channel.run_ws_loop(bus.clone(), shutdown.clone()).await {
                    Ok(connected_for) => {
                        if connected_for >= STABLE_CONNECTION_WINDOW {
                            retry_attempt = 0;
                        } else {
                            retry_attempt = retry_attempt.saturating_add(1).min(7);
                        }
                        tracing::info!(
                            connected_secs = connected_for.as_secs(),
                            "Feishu WebSocket disconnected"
                        );
                    }
                    Err(e) => {
                        retry_attempt = retry_attempt.saturating_add(1).min(6);
                        tracing::error!(attempt = retry_attempt, error = %e, "Feishu WebSocket error");
                    }
                }

                if !*channel.running.read().await || shutdown.is_cancelled() {
                    break;
                }

                let retry_delay = reconnect_delay(retry_attempt);
                tracing::info!(
                    delay_secs = retry_delay.as_secs(),
                    "Feishu channel reconnect scheduled"
                );
                tokio::select! {
                    _ = tokio::time::sleep(retry_delay) => {}
                    _ = shutdown.cancelled() => break,
                }
            }

            *channel.running.write().await = false;
            tracing::info!("Feishu channel stopped");
        }));

        tracing::info!("Feishu channel started");
        Ok(())
    }

    async fn stop(&self) -> Result<(), ChannelError> {
        *self.running.write().await = false;
        *self.connected.write().await = false;

        if let Some(shutdown) = self.shutdown.write().await.take() {
            shutdown.cancel();
        }

        let task = { self.run_task.lock().await.take() };
        if let Some(mut task) = task {
            match tokio::time::timeout(CHANNEL_STOP_GRACE, &mut task).await {
                Ok(result) => result.map_err(|error| {
                    ChannelError::Other(format!("Feishu channel task failed to join: {error}"))
                })?,
                Err(_) => {
                    tracing::warn!(
                        grace_ms = CHANNEL_STOP_GRACE.as_millis(),
                        "Feishu channel did not stop in time; aborting connection task"
                    );
                    task.abort();
                    let _ = task.await;
                }
            }
        }

        Ok(())
    }

    fn is_running(&self) -> bool {
        self.running.try_read().map(|r| *r).unwrap_or(false)
    }

    fn live_policy(&self) -> LivePolicy {
        if self.config.live_updates {
            LivePolicy::Snapshot {
                min_interval: Duration::from_millis(
                    self.config.live_update_interval_ms.clamp(250, 5_000),
                ),
            }
        } else {
            LivePolicy::FinalOnly
        }
    }

    fn presentation_policy(&self) -> crate::delivery::PresentationPolicy {
        crate::delivery::PresentationPolicy::external(self.config.live_updates)
    }

    async fn open_turn(&self, target: TurnTarget) -> Result<Box<dyn TurnSink>, ChannelError> {
        let send_target =
            FeishuSendTarget::from_message(target.chat_id, target.reply_to, &target.metadata);
        Ok(Box::new(FeishuTurnSink::new(Box::new(FeishuTurnBackend {
            channel: self.clone(),
            target: send_target,
            metadata: target.metadata,
        }))))
    }

    async fn send(&self, msg: OutboundMessage) -> Result<(), ChannelError> {
        let target = FeishuSendTarget::from_message(
            msg.chat_id.clone(),
            msg.reply_to.clone(),
            &msg.metadata,
        );

        // If no media, send as interactive card with raw markdown
        if msg.media.is_empty() {
            let content = msg.content.trim();

            // Empty content
            if content.is_empty() {
                self.remove_reaction_from_metadata(&msg.metadata).await;
                return Ok(());
            }

            let chunks = Self::split_markdown_chunks(content);
            for chunk in &chunks {
                let card = Self::build_card_content(chunk);
                if let Err(e) = self.send_interactive_card(&target, &card).await {
                    tracing::warn!(error = %e, "Failed to send interactive card, falling back to text");
                    let text_content = serde_json::json!({ "text": chunk }).to_string();
                    let result = self
                        .send_raw_message(&target, "text", &text_content)
                        .await
                        .map(|_| ());
                    self.remove_reaction_from_metadata(&msg.metadata).await;
                    return result;
                }
            }
            self.remove_reaction_from_metadata(&msg.metadata).await;
            return Ok(());
        }

        // Handle multimodal message - send with media
        // Separate images (can embed in post) from files (sent as separate messages)
        let mut image_items = Vec::new();
        let mut file_items = Vec::new();
        for media_item in &msg.media {
            match media_item.media_type.as_str() {
                "image" => image_items.push(media_item),
                "audio" | "video" | "file" => file_items.push(media_item),
                _ => {
                    tracing::warn!(media_type = %media_item.media_type, "Unsupported media type for sending");
                }
            }
        }

        // Upload and send files as separate messages (one per file)
        for item in &file_items {
            match self.upload_file(&item.path).await {
                Ok(file_key) => {
                    let file_msg_type = match item.media_type.as_str() {
                        "audio" => "audio",
                        "video" => "media",
                        _ => "file",
                    };
                    let file_content = serde_json::json!({"file_key": file_key}).to_string();
                    if let Err(e) = self
                        .send_raw_message(&target, file_msg_type, &file_content)
                        .await
                    {
                        tracing::warn!(error = %e, msg_type = file_msg_type, "Failed to send file message");
                    }
                }
                Err(e) => {
                    tracing::warn!(error = %e, path = %item.path, "Failed to upload file");
                }
            }
        }

        // Build content parts for post (text + images)
        let mut content_parts = Vec::new();

        if !msg.content.is_empty() {
            const MAX_TEXT_LENGTH: usize = 60_000;
            let truncated_text = if msg.content.len() > MAX_TEXT_LENGTH {
                format!(
                    "{}...\n\n[Content truncated due to length limit]",
                    &msg.content[..msg.content.ceil_char_boundary(MAX_TEXT_LENGTH)]
                )
            } else {
                msg.content.clone()
            };
            content_parts.push(serde_json::json!({
                "tag": "text",
                "text": truncated_text
            }));
        }

        for item in &image_items {
            match self.upload_image(&item.path).await {
                Ok(image_key) => {
                    content_parts.push(serde_json::json!({
                        "tag": "img",
                        "image_key": image_key
                    }));
                }
                Err(e) => {
                    tracing::warn!(error = %e, path = %item.path, "Failed to upload image");
                }
            }
        }

        // If no post content after processing (no text, no images), skip
        if content_parts.is_empty() {
            self.remove_reaction_from_metadata(&msg.metadata).await;
            return Ok(());
        }

        // Determine message type and build content
        let msg_type = if msg.content.is_empty() && image_items.len() == 1 {
            "image"
        } else {
            "post"
        };

        let content = if msg_type == "image" {
            // Image-only: content is just {"image_key": "..."}
            let image_key = content_parts[0]["image_key"].as_str().unwrap_or("");
            serde_json::json!({"image_key": image_key}).to_string()
        } else {
            // Post with media: zh_cn wrapped post structure
            let post_content: Vec<Vec<serde_json::Value>> =
                content_parts.into_iter().map(|part| vec![part]).collect();
            serde_json::json!({
                "zh_cn": {
                    "title": "",
                    "content": post_content
                }
            })
            .to_string()
        };

        self.send_raw_message(&target, msg_type, &content).await?;

        // Remove pending reaction after successfully sending
        self.remove_reaction_from_metadata(&msg.metadata).await;

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::agent::TurnEvent;
    use crate::delivery::{PresentationPolicy, project_snapshot};

    #[derive(Default)]
    struct MockTurnState {
        created: Vec<String>,
        updated: Vec<(String, String)>,
        cleanups: usize,
        fail_updates: usize,
    }

    struct MockTurnApi {
        state: Arc<Mutex<MockTurnState>>,
    }

    #[async_trait]
    impl FeishuTurnApi for MockTurnApi {
        async fn create_card(&mut self, markdown: &str) -> Result<String, ChannelError> {
            let mut state = self.state.lock().await;
            state.created.push(markdown.to_string());
            Ok(format!("card-{}", state.created.len()))
        }

        async fn update_card(
            &mut self,
            message_id: &str,
            markdown: &str,
        ) -> Result<(), ChannelError> {
            let mut state = self.state.lock().await;
            if state.fail_updates > 0 {
                state.fail_updates -= 1;
                return Err(ChannelError::Other("card can no longer be edited".into()));
            }
            state
                .updated
                .push((message_id.to_string(), markdown.to_string()));
            Ok(())
        }

        async fn cleanup(&mut self) {
            self.state.lock().await.cleanups += 1;
        }
    }

    fn mock_sink(state: Arc<Mutex<MockTurnState>>) -> FeishuTurnSink {
        FeishuTurnSink::new(Box::new(MockTurnApi { state }))
    }

    fn test_channel() -> FeishuChannel {
        FeishuChannel::new(
            FeishuChannelConfig {
                enabled: true,
                app_id: "test-app".to_string(),
                app_secret: "test-secret".to_string(),
                allow_from: vec!["*".to_string()],
                require_mention: true,
                agent: String::new(),
                media_dir: String::new(),
                reaction_emoji: "THUMBSUP".to_string(),
                live_updates: false,
                live_update_interval_ms: 500,
                max_image_bytes: 10 * 1024 * 1024,
                max_file_bytes: 25 * 1024 * 1024,
                media_dir_max_bytes: 512 * 1024 * 1024,
                request_timeout_secs: 30,
            },
            Path::new("/tmp"),
        )
        .expect("test channel should be valid")
    }

    fn inbound_frame(
        message_id: &str,
        sender_id: &str,
        chat_type: &str,
        text: &str,
        mentions: serde_json::Value,
    ) -> PbFrame {
        PbFrame {
            seq_id: 1,
            log_id: 1,
            service: 1,
            method: 1,
            headers: vec![],
            payload: Some(
                serde_json::json!({
                    "header": {
                        "event_type": "im.message.receive_v1",
                        "event_id": format!("event-{message_id}")
                    },
                    "event": {
                        "sender": {
                            "sender_id": { "open_id": sender_id },
                            "sender_type": "user"
                        },
                        "message": {
                            "message_id": message_id,
                            "chat_id": "oc_test",
                            "chat_type": chat_type,
                            "message_type": "text",
                            "content": serde_json::json!({ "text": text }).to_string(),
                            "mentions": mentions
                        }
                    }
                })
                .to_string()
                .into_bytes(),
            ),
        }
    }

    #[tokio::test]
    async fn inbound_admission_enforces_allowlist_before_parsing() {
        let mut channel = test_channel();
        channel.config.allow_from = vec!["ou_allowed".to_string()];
        let frame = inbound_frame(
            "om_denied",
            "ou_denied",
            "p2p",
            "hello",
            serde_json::json!([]),
        );

        assert!(channel.handle_frame(&frame).await.unwrap().is_none());
        assert!(channel.seen_message_ids.read().await.is_empty());
    }

    #[tokio::test]
    async fn group_message_requires_bot_mention_and_removes_only_self_mention() {
        let channel = test_channel();
        *channel.bot_open_id.write().await = Some("ou_bot".to_string());

        let ignored = inbound_frame(
            "om_ignored",
            "ou_user",
            "group",
            "hello",
            serde_json::json!([]),
        );
        assert!(channel.handle_frame(&ignored).await.unwrap().is_none());

        let admitted = inbound_frame(
            "om_admitted",
            "ou_user",
            "group",
            "@_user_1 ask @_user_2",
            serde_json::json!([
                {"key": "@_user_1", "id": {"open_id": "ou_bot"}, "name": "PicoBot"},
                {"key": "@_user_2", "id": {"open_id": "ou_peer"}, "name": "Alice"}
            ]),
        );
        let parsed = channel
            .handle_frame(&admitted)
            .await
            .unwrap()
            .expect("mentioned group message should be admitted");
        assert_eq!(parsed.content, "ask @Alice");
    }

    #[tokio::test]
    async fn inbound_context_replies_to_current_message_and_preserves_thread_metadata() {
        let mut channel = test_channel();
        channel.config.reaction_emoji.clear();
        *channel.bot_open_id.write().await = Some("ou_bot".to_string());
        let mut frame = inbound_frame(
            "om_current",
            "ou_user",
            "group",
            "@_user_1 hello",
            serde_json::json!([
                {"key": "@_user_1", "id": {"open_id": "ou_bot"}, "name": "PicoBot"}
            ]),
        );
        let mut payload: serde_json::Value =
            serde_json::from_slice(frame.payload.as_deref().unwrap()).unwrap();
        payload["event"]["message"]["root_id"] = serde_json::json!("om_root");
        payload["event"]["message"]["thread_id"] = serde_json::json!("omt_thread");
        frame.payload = Some(payload.to_string().into_bytes());
        let bus = Arc::new(MessageBus::new(4));

        channel.process_event_frame(&bus, &frame).await;
        let inbound = bus.consume_inbound().await.unwrap();

        assert_eq!(
            inbound.channel_context.reply_to.as_deref(),
            Some("om_current")
        );
        assert_eq!(
            inbound
                .channel_context
                .private
                .get("feishu.thread_id")
                .map(String::as_str),
            Some("omt_thread")
        );
        assert_eq!(
            inbound
                .channel_context
                .private
                .get("feishu.root_id")
                .map(String::as_str),
            Some("om_root")
        );
    }

    #[test]
    fn post_mentions_can_gate_group_messages_when_top_level_mentions_are_absent() {
        let message = LarkMessage {
            message_id: "om_post".to_string(),
            chat_id: "oc_test".to_string(),
            chat_type: "group".to_string(),
            message_type: "post".to_string(),
            content: serde_json::json!({
                "zh_cn": {"content": [[{"tag": "at", "user_id": "ou_bot"}]]}
            })
            .to_string(),
            parent_id: None,
            root_id: None,
            thread_id: None,
            mentions: vec![],
        };

        assert!(message_mentions_bot(&message, Some("ou_bot")));
        assert!(!message_mentions_bot(&message, Some("ou_other")));
    }

    fn fragment_frame(message_id: &str, seq: usize, sum: usize, payload: &[u8]) -> PbFrame {
        PbFrame {
            seq_id: seq as u64,
            log_id: 1,
            service: 1,
            method: 1,
            headers: vec![
                PbHeader {
                    key: "message_id".to_string(),
                    value: message_id.to_string(),
                },
                PbHeader {
                    key: "seq".to_string(),
                    value: seq.to_string(),
                },
                PbHeader {
                    key: "sum".to_string(),
                    value: sum.to_string(),
                },
            ],
            payload: Some(payload.to_vec()),
        }
    }

    #[test]
    fn fragmented_frames_are_reassembled_in_sequence_order() {
        let mut cache = HashMap::new();
        assert!(
            reassemble_data_frame(&fragment_frame("event", 1, 3, b"world"), &mut cache)
                .unwrap()
                .is_none()
        );
        assert!(
            reassemble_data_frame(&fragment_frame("event", 0, 3, b"hello "), &mut cache)
                .unwrap()
                .is_none()
        );
        let complete = reassemble_data_frame(&fragment_frame("event", 2, 3, b"!"), &mut cache)
            .unwrap()
            .expect("last fragment should complete the event");

        assert_eq!(
            complete.payload.as_deref(),
            Some(b"hello world!".as_slice())
        );
        assert!(cache.is_empty());
    }

    #[test]
    fn fragmented_frames_reject_invalid_shape_and_bound_total_size() {
        let mut cache = HashMap::new();
        assert!(
            reassemble_data_frame(
                &fragment_frame("event", MAX_WS_FRAGMENTS, MAX_WS_FRAGMENTS, b"x"),
                &mut cache,
            )
            .is_err()
        );
        assert!(
            reassemble_data_frame(
                &fragment_frame("event", 0, 2, &vec![0; MAX_WS_EVENT_BYTES + 1]),
                &mut cache,
            )
            .is_err()
        );
        assert!(cache.is_empty());
    }

    #[test]
    fn reconnect_backoff_is_bounded_and_resets_after_success() {
        assert_eq!(reconnect_delay(0), Duration::from_secs(1));
        assert_eq!(reconnect_delay(1), Duration::from_secs(1));
        assert_eq!(reconnect_delay(2), Duration::from_secs(2));
        assert_eq!(reconnect_delay(7), Duration::from_secs(64));
        assert_eq!(reconnect_delay(100), Duration::from_secs(64));
    }

    #[test]
    fn send_target_uses_reply_endpoint_and_thread_flag() {
        let metadata = HashMap::from([("feishu.thread_id".to_string(), "omt_thread".to_string())]);
        let target = FeishuSendTarget::from_message(
            "oc_chat".to_string(),
            Some("om_current".to_string()),
            &metadata,
        );
        let (url, body) = target.request("interactive", "{}");

        assert!(url.ends_with("/im/v1/messages/om_current/reply"));
        assert_eq!(body["reply_in_thread"], true);
        assert_eq!(body["msg_type"], "interactive");
        assert!(body.get("receive_id").is_none());
    }

    #[test]
    fn proactive_thread_target_creates_inside_thread() {
        let metadata = HashMap::from([("feishu.thread_id".to_string(), "omt_thread".to_string())]);
        let target = FeishuSendTarget::from_message("oc_chat".to_string(), None, &metadata);
        let (url, body) = target.request("text", "{}");

        assert!(url.ends_with("/im/v1/messages?receive_id_type=thread_id"));
        assert_eq!(body["receive_id"], "omt_thread");
    }

    #[test]
    fn inbound_filename_is_reduced_to_a_safe_basename() {
        assert_eq!(sanitize_filename("../../secret.txt"), "secret.txt");
        assert_eq!(sanitize_filename(".."), "file.bin");
        assert_eq!(sanitize_filename("report:\0.txt"), "report__.txt");
    }

    #[tokio::test]
    async fn media_capacity_and_file_size_are_checked_before_writing_or_uploading() {
        let dir = tempfile::tempdir().unwrap();
        let existing = dir.path().join("existing.bin");
        tokio::fs::write(&existing, vec![0_u8; 8]).await.unwrap();

        ensure_media_capacity(dir.path(), 2, 10).await.unwrap();
        assert!(ensure_media_capacity(dir.path(), 3, 10).await.is_err());
        ensure_local_media_size(existing.to_str().unwrap(), 8, "file")
            .await
            .unwrap();
        assert!(
            ensure_local_media_size(existing.to_str().unwrap(), 7, "file")
                .await
                .is_err()
        );
    }

    #[tokio::test]
    async fn authenticated_requests_retry_transient_http_failures() {
        use std::sync::atomic::{AtomicUsize, Ordering};

        let attempts = Arc::new(AtomicUsize::new(0));
        let route_attempts = attempts.clone();
        let app = axum::Router::new().route(
            "/test",
            axum::routing::get(move || {
                let route_attempts = route_attempts.clone();
                async move {
                    if route_attempts.fetch_add(1, Ordering::SeqCst) < 2 {
                        axum::http::StatusCode::SERVICE_UNAVAILABLE
                    } else {
                        axum::http::StatusCode::OK
                    }
                }
            }),
        );
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
        let address = listener.local_addr().unwrap();
        let server = tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
        let channel = test_channel();
        *channel.tenant_token.write().await = Some(CachedTenantToken {
            value: "test-token".to_string(),
            refresh_after: Instant::now() + Duration::from_secs(60),
        });
        let url = format!("http://{address}/test");

        let response = channel
            .send_authenticated("test request", |client, token| {
                client.get(&url).bearer_auth(token)
            })
            .await
            .unwrap();

        assert_eq!(response.status(), reqwest::StatusCode::OK);
        assert_eq!(attempts.load(Ordering::SeqCst), 3);
        server.abort();
    }

    #[tokio::test]
    async fn turn_sink_creates_once_updates_same_card_and_cleans_up_at_finish() {
        let state = Arc::new(Mutex::new(MockTurnState::default()));
        let mut sink = mock_sink(state.clone());
        let (controller, emitter, _) =
            crate::session::TurnController::start("feishu:chat:dialog", "message");
        emitter
            .emit(TurnEvent::TextDelta {
                iteration: 0,
                delta: "hello".into(),
            })
            .unwrap();
        sink.update(&project_snapshot(
            &controller.snapshot(),
            PresentationPolicy::external(true),
        ))
        .await
        .unwrap();
        emitter
            .emit(TurnEvent::TextDelta {
                iteration: 0,
                delta: " world".into(),
            })
            .unwrap();
        sink.update(&project_snapshot(
            &controller.snapshot(),
            PresentationPolicy::external(true),
        ))
        .await
        .unwrap();
        controller.complete(None);
        sink.finish(&project_snapshot(
            &controller.snapshot(),
            PresentationPolicy::external(true),
        ))
        .await
        .unwrap();

        let state = state.lock().await;
        assert_eq!(state.created.len(), 1);
        assert_eq!(state.updated.len(), 2);
        assert!(state.updated.iter().all(|(id, _)| id == "card-1"));
        assert!(state.updated.last().unwrap().1.contains("hello world"));
        assert_eq!(state.cleanups, 1);
    }

    #[tokio::test]
    async fn final_update_failure_sends_complete_fallback_and_cleanup_is_idempotent() {
        let state = Arc::new(Mutex::new(MockTurnState::default()));
        let mut sink = mock_sink(state.clone());
        let (controller, emitter, _) =
            crate::session::TurnController::start("feishu:chat:dialog", "message");
        emitter
            .emit(TurnEvent::TextDelta {
                iteration: 0,
                delta: "partial".into(),
            })
            .unwrap();
        sink.update(&controller.snapshot()).await.unwrap();
        state.lock().await.fail_updates = 1;
        emitter
            .emit(TurnEvent::TextDelta {
                iteration: 0,
                delta: " final".into(),
            })
            .unwrap();
        controller.complete(None);
        sink.finish(&controller.snapshot()).await.unwrap();
        sink.finish(&controller.snapshot()).await.unwrap();

        let state = state.lock().await;
        assert_eq!(state.created.len(), 2);
        assert!(state.created[1].contains("partial final"));
        assert_eq!(state.cleanups, 1);
    }

    #[tokio::test]
    async fn final_only_sink_sends_no_fragments_and_abort_without_text_is_visible() {
        let state = Arc::new(Mutex::new(MockTurnState::default()));
        let mut sink = mock_sink(state.clone());
        let (controller, _emitter, _) =
            crate::session::TurnController::start("feishu:chat:dialog", "message");
        controller.fail("provider unavailable");

        sink.abort(&controller.snapshot()).await.unwrap();

        let state = state.lock().await;
        assert_eq!(state.created.len(), 1);
        assert!(state.created[0].contains("provider unavailable"));
        assert_eq!(state.updated.len(), 0);
        assert_eq!(state.cleanups, 1);
    }

    #[test]
    fn external_projection_removes_reasoning_before_feishu_rendering() {
        let (controller, emitter, _) =
            crate::session::TurnController::start("feishu:chat:dialog", "message");
        emitter
            .emit(TurnEvent::ReasoningDelta {
                iteration: 0,
                delta: "private".into(),
            })
            .unwrap();
        emitter
            .emit(TurnEvent::TextDelta {
                iteration: 0,
                delta: "public".into(),
            })
            .unwrap();
        let projected =
            project_snapshot(&controller.snapshot(), PresentationPolicy::external(true));

        let markdown = render_feishu_turn(&projected);
        assert!(markdown.contains("public"));
        assert!(!markdown.contains("private"));
    }

    #[test]
    fn live_card_truncation_preserves_utf8_and_payload_limit() {
        let markdown = "你".repeat(FeishuChannel::CARD_MARKDOWN_MAX_BYTES);
        let truncated = truncate_feishu_live_markdown(&markdown);

        assert!(truncated.len() <= FeishuChannel::CARD_MARKDOWN_MAX_BYTES);
        assert!(truncated.ends_with("_内容仍在生成，已暂时截断…_"));
    }

    #[test]
    fn final_card_chunking_preserves_long_utf8_content() {
        let markdown = "你".repeat(FeishuChannel::CARD_MARKDOWN_MAX_BYTES);
        let chunks = FeishuChannel::split_markdown_chunks(&markdown);

        assert!(chunks.len() > 1);
        assert!(
            chunks
                .iter()
                .all(|chunk| chunk.len() <= FeishuChannel::CARD_MARKDOWN_MAX_BYTES)
        );
        assert_eq!(chunks.concat(), markdown);
    }

    #[test]
    fn live_policy_uses_configured_bounded_interval() {
        let mut channel = test_channel();
        assert_eq!(channel.live_policy(), LivePolicy::FinalOnly);
        channel.config.live_updates = true;
        channel.config.live_update_interval_ms = 10;

        assert_eq!(
            channel.live_policy(),
            LivePolicy::Snapshot {
                min_interval: Duration::from_millis(250)
            }
        );
    }

    #[tokio::test]
    async fn stop_aborts_connection_task_that_ignores_cancellation() {
        let channel = test_channel();
        let shutdown = CancellationToken::new();

        *channel.running.write().await = true;
        *channel.connected.write().await = true;
        *channel.shutdown.write().await = Some(shutdown.clone());
        *channel.run_task.lock().await = Some(tokio::spawn(std::future::pending()));

        tokio::time::timeout(Duration::from_secs(1), channel.stop())
            .await
            .expect("stop must have a hard deadline")
            .expect("stop should succeed after aborting the stuck task");

        assert!(shutdown.is_cancelled());
        assert!(!channel.is_running());
        assert!(!*channel.connected.read().await);
        assert!(channel.run_task.lock().await.is_none());
    }

    #[test]
    fn collect_post_image_keys_finds_nested_images() {
        let content = serde_json::json!({
            "zh_cn": {
                "title": "",
                "content": [[
                    {"tag": "img", "image_key": "img_v3_001"},
                    {"tag": "text", "text": "这是哪里？"},
                    {"tag": "img", "image_key": "img_v3_002"},
                    {"tag": "img", "image_key": "img_v3_001"}
                ]]
            }
        })
        .to_string();

        assert_eq!(
            collect_post_image_keys(&content),
            vec!["img_v3_001".to_string(), "img_v3_002".to_string()]
        );
    }

    #[test]
    fn parse_post_content_preserves_image_positions() {
        let content = serde_json::json!({
            "zh_cn": {
                "title": "",
                "content": [[
                    {"tag": "text", "text": "这是一张图："},
                    {"tag": "img", "image_key": "img_v3_001"},
                    {"tag": "text", "text": "看完继续说"}
                ]]
            }
        })
        .to_string();

        assert_eq!(
            parse_post_content(&content),
            "这是一张图：[image]看完继续说"
        );
    }
}
