use safc::db::*;
use safc::sec::*;

// msg æ˜¯ bot ç‹¬ç”¨çš„ mod
mod msg;
use msg::*;

use teloxide::{
    dispatching::{dialogue, dialogue::InMemStorage, UpdateHandler},
    prelude::*,
    types::{
        InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, KeyboardMarkup, KeyboardRemove,
        ParseMode::MarkdownV2,
    },
    utils::command::BotCommands,
};

type MyDialogue = Dialogue<State, InMemStorage<State>>; // ? è¦�ä½¿ç”¨ sqlite å­˜å‚¨çŠ¶æ€�å�—
type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;

#[derive(BotCommands, Clone, PartialEq, Debug)]
#[command(
    rename_rule = "lowercase",
    // parse_with = "split",
    description = "è¿™æ˜¯å¤§å­¦ç”Ÿå��è¯ˆä¸­å¿ƒï¼ˆSAFC @SAFC_groupï¼‰çš„æœºå™¨äºº\næ”¯æŒ�ä»¥ä¸‹å‘½ä»¤ï¼š"
)]
enum Command {
    #[command(description = "æ˜¾ç¤ºå¸®åŠ©ä¿¡æ�¯")]
    Help,
    #[command(description = "å¼€å§‹")]
    Start,
    #[command(description = "ç»ˆæ­¢å¯¹è¯�")]
    Cancel,
    #[command(description = "ä¿¡æ�¯")]
    Info,
    #[command(description = "è¯„ä»·")]
    Comment(String),
    #[command(description = "æ�œç´¢")]
    Find(String),
}

#[tokio::main]
async fn main() {
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
    log::info!("Starting SAFT bot for telegram ... by Framecraft");

    let bot = Bot::from_env();

    bot.set_my_commands(Command::bot_commands()) // å�‘ telegram æ³¨å†Œå‘½ä»¤
        .await
        .expect("Failed to set bot commands to telegram");

    Dispatcher::builder(bot, schema())
        .dependencies(dptree::deps![InMemStorage::<State>::new()])
        .enable_ctrlc_handler()
        .build()
        .dispatch()
        .await;
}

/// è´£ä»»é“¾æ¨¡å¼� çš„ SAFC bot
/// branch æ˜¯åˆ†æ”¯çš„æ„�æ€� å�‚è€ƒï¼š
/// https://docs.rs/dptree/0.3.0/dptree/prelude/struct.Handler.html#the-difference-between-chaining-and-branching
fn schema() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
    use dptree::case;

    // å‘½ä»¤
    let command_handler = teloxide::filter_command::<Command, _>()
        .branch(case![Command::Start].endpoint(start))
        .branch(case![Command::Help].endpoint(help_command))
        .branch(case![Command::Cancel].endpoint(cancel_command))
        .branch(case![Command::Info].endpoint(info_command))
        .branch(case![Command::Find(arg)].endpoint(find_command))
        .branch(case![Command::Comment(arg)].endpoint(comment_command))
        .branch(dptree::endpoint(invalid_command));

    // æ–‡æœ¬æ¶ˆæ�¯
    #[rustfmt::skip]
    let text_handler = Message::filter_text()
        .branch(case![State::FindSupervisor].endpoint(find_supervisor))
        .branch(case![State::FindComment].endpoint(find_comment))
        .branch(case![State::SchoolCate].endpoint(choose_university))
        .branch(case![State::University { school_cate }].endpoint(choose_department))
        .branch(case![State::Department { school_cate, university }].endpoint(choose_supervisor))
        .branch(case![State::Supervisor { school_cate, university, department }].endpoint(read_or_comment))
        .branch(case![State::Comment { object_id, comment_type }].endpoint(add_comment))
        .branch(case![State::Publish { object_id, comment, comment_type }].endpoint(publish_comment));

    // æ¶ˆæ�¯
    let message_handler = Update::filter_message()
        .branch(command_handler) // å‘½ä»¤ä¹Ÿæ˜¯æ¶ˆæ�¯çš„ä¸€ç§�
        .branch(text_handler)
        .branch(dptree::endpoint(invalid_state));

    // å›�è°ƒ
    // todo è§£å†³å›�è°ƒä¿¡æ�¯çš„ä¸²æ‰°é—®é¢˜
    let callback_query_handler = Update::filter_callback_query()
        .branch(case![State::StartCb].endpoint(start_cb))
        .branch(case![State::Read { obj_teacher }].endpoint(read_or_comment_cb))
        .branch(case![State::PagingCb { data }].endpoint(paging_cb))
        .branch(dptree::endpoint(invalid_callback_query));

    dialogue::enter::<Update, InMemStorage<State>, State, _>()
        .branch(message_handler)
        .branch(callback_query_handler)
}

/// Send a message when the command /help is issued.
async fn help_command(bot: Bot, msg: Message) -> HandlerResult {
    bot.send_message(msg.chat.id, Command::descriptions().to_string())
        .reply_to_message_id(msg.id)
        .await?;
    Ok(())
}

/// Send a message when the command /info is issued.
async fn info_command(bot: Bot, msg: Message) -> HandlerResult {
    bot.send_message(msg.chat.id, TgResponse::Info.to_string())
        .reply_to_message_id(msg.id)
        .parse_mode(MarkdownV2)
        .await?;
    Ok(())
}

/// Cancels and ends the conversation.
async fn cancel_command(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
    bot.send_message(
        msg.chat.id,
        "æ‚¨ç»ˆæ­¢äº†æœ¬æ¬¡ä¼šè¯�\nå†�è§�ï¼�æœ¬æ¬¡å¯¹è¯�ç»“æ�Ÿï¼Œä½¿ç”¨ /start é‡�æ–°å¼€å§‹ã€‚\næˆ‘ä»¬æœŸå¾…æ‚¨çš„ä½¿ç”¨å��é¦ˆ",
    )
    .reply_to_message_id(msg.id)
    .reply_markup(KeyboardRemove::new())
    .await?;

    dialogue.exit().await?;
    Ok(())
}

/// find_command å¿«é€ŸæŸ¥æ‰¾
async fn find_command(bot: Bot, dialogue: MyDialogue, arg: String, msg: Message) -> HandlerResult {
    // arg æœ‰æ•ˆæ€§éªŒè¯�
    let args: Vec<&str> = arg.split(' ').collect();
    if args.len() >= 2 {
        // æ­¤å¤„å�–ä¸‹æ ‡å¿…ä¸�ä¼š panic
        match args[0] {
            "å®¢ä½“" => {
                find_supervisor_msg(&args[1..], &bot, &msg, dialogue).await?;
                return Ok(());
            }
            "è¯„ä»·" => {
                find_comment_msg(&args[1..], &bot, &msg, dialogue).await?;
                return Ok(());
            }
            _ => {}
        }
    }
    bot.send_message(
        msg.chat.id,
        "ä½¿ç”¨æ–¹æ³•ï¼š \n\
            - /find <å®¢ä½“ | è¯„ä»·> <å…³é”®å­— 1> [å…³é”®å­—...]\n\
            ä¾‹å¦‚ï¼š\n\
            - /find å®¢ä½“ èµµ__\n\
            - /find è¯„ä»· å‰�é€” æ— é‡�\n\
            å®¢ä½“çš„ä¸€èˆ¬æ˜¯å¯¼å¸ˆçš„æ„�æ€�ã€‚å°±å�¯é€‰çš„é«˜çº§æ“�ä½œï¼š\n\
            - æ‚¨å�¯ä»¥ç”¨ç™¾åˆ†å�·ï¼ˆ%ï¼‰ä»£è¡¨é›¶ä¸ªã€�ä¸€ä¸ªæˆ–å¤šä¸ªå­—ç¬¦ã€‚ä¸‹åˆ’çº¿ï¼ˆ_ï¼‰ä»£è¡¨ä¸€ä¸ªå�•ä¸€çš„å­—ç¬¦\n\n\
            æ‚¨ä¹Ÿå�¯ä»¥ä½¿ç”¨ /start ä¸­çš„åŠŸèƒ½æŒ‰é’®ä½¿ç”¨è¿™äº›åŠŸèƒ½",
    )
    .await?;
    Ok(())
}

/// ç›´æ�¥è¯„ä»·å‘½ä»¤å¤„ç�†å‡½æ•°
async fn comment_command(
    bot: Bot,
    dialogue: MyDialogue,
    arg: String,
    msg: Message,
) -> HandlerResult {
    // arg æœ‰æ•ˆæ€§éªŒè¯�
    if arg.is_empty() {
        bot.send_message(msg.chat.id, "ä½¿ç”¨æ–¹æ³•ï¼š /comment <id>")
            .await?;
        return Ok(());
    }

    if let Some(t) = SAFC_DB.if_object_exists(&arg)? {
        let object_id = arg;

        let text = format!(
            "ğŸ†” `{object_id}`\n\
            \nè¯·å†™ä¸‹æ‚¨å¯¹æ­¤å®¢ä½“çš„è¯„ä»·ï¼š"
        );

        bot.send_message(msg.chat.id, text)
            .reply_to_message_id(msg.id)
            .parse_mode(MarkdownV2)
            .reply_markup(KeyboardRemove::new())
            .await?;

        dialogue
            .update(State::Comment {
                object_id,
                comment_type: t,
            })
            .await?; // æ›´æ–°ä¼šè¯�çŠ¶æ€�
        Ok(())
    } else {
        bot.send_message(msg.chat.id, "â�Œ - é��æœ‰æ•ˆ id").await?;
        Ok(())
    }
}

async fn _unable_command(bot: Bot, msg: Message) -> HandlerResult {
    bot.send_message(msg.chat.id, TgResponse::NotImplemented.to_string())
        .await?;
    Ok(())
}

async fn invalid_state(_bot: Bot, msg: Message) -> HandlerResult {
    log::warn!("invalid state - Unable to handle the message: {:?}", msg);
    Ok(())
}

async fn invalid_command(bot: Bot, msg: Message) -> HandlerResult {
    bot.send_message(
        msg.chat.id,
        format!("â�Œ é”™è¯¯å‘½ä»¤ - usage: \n{}", Command::descriptions()),
    )
    .await?;
    Ok(())
}

/// å¼€å§‹
async fn start(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
    bot.send_message(msg.chat.id, TgResponse::Hello.to_string())
        .parse_mode(MarkdownV2)
        .reply_markup(start_op_keyboard())
        .reply_to_message_id(msg.id)
        .await?;
    dialogue.update(State::StartCb).await?; // æ›´æ–°ä¼šè¯�çŠ¶æ€�
    Ok(())
}

async fn start_cb(bot: Bot, dialogue: MyDialogue, q: CallbackQuery) -> HandlerResult {
    bot.answer_callback_query(q.id).await?; // todo å…ˆåˆ« await
    if let Some(op) = &q.data {
        match serde_json::from_str(op)? {
            StartOp::Tree => {
                let data = SAFC_DB.find_school_cate()?;
                let keyboard = convert_to_n_columns_keyboard(data, 3);
                let text = "æ‚¨æƒ³æŸ¥è¯¢æˆ–è¯„ä»·çš„ã€Œå­¦æ ¡ç±»åˆ«ã€�æ˜¯ï¼Ÿæ‚¨å�¯ä»¥ç›´æ�¥è¾“å…¥æˆ–è€…åœ¨ä¸‹é�¢çš„é”®ç›˜é€‰æ‹©æ¡†ä¸­é€‰æ‹©\n\
                    _é”®ç›˜é€‰æ‹©æ¡†ä¸­æ²¡æœ‰çš„ä¹Ÿå�¯ä»¥ç›´æ�¥è¾“å…¥æ�¥æ–°å»ºï¼›å¦‚æ�œæ˜¯ä¸Šä¸ªç±»åˆ«æœ¬èº«è¯·é€‰æ‹©æˆ–è¾“å…¥ `self`ã€‚ä¸‹å�Œ_\n";
                bot.send_message(dialogue.chat_id(), text)
                    .parse_mode(MarkdownV2)
                    .reply_markup(KeyboardMarkup::new(keyboard))
                    .await?;
                dialogue.update(State::SchoolCate).await?; // æ›´æ–°ä¼šè¯�çŠ¶æ€�
            }
            StartOp::FindSupervisor => {
                let text = "è¯·å›�å¤�ä½ è¦�æŸ¥æ‰¾çš„ ğŸ‘”\n\
                    å�¯é€‰ï¼šæ‚¨å�¯ä»¥ç”¨ç™¾åˆ†å�·ï¼ˆ%ï¼‰ä»£è¡¨é›¶ä¸ªã€�ä¸€ä¸ªæˆ–å¤šä¸ªå­—ç¬¦ã€‚ä¸‹åˆ’çº¿ï¼ˆ_ï¼‰ä»£è¡¨ä¸€ä¸ªå�•ä¸€çš„å­—ç¬¦\n\n\
                    ä¾‹å¦‚ï¼šèµµ__\n\
                    æ­¤åŠŸèƒ½ç­‰æ•ˆä½¿ç”¨å‘½ä»¤ /find å®¢ä½“ èµµ__\n";
                bot.send_message(dialogue.chat_id(), text).await?;
                dialogue.update(State::FindSupervisor).await?;
            }
            StartOp::FindComment => {
                let text = "è¯·å›�å¤�ä½ è¦�æŸ¥æ‰¾çš„è¯„ä»·å…³é”®å­—ï¼Œç©ºæ ¼åˆ†éš”\n\
                    å�¯é€‰ï¼šæ‚¨å�¯ä»¥ç”¨ç™¾åˆ†å�·ï¼ˆ%ï¼‰ä»£è¡¨é›¶ä¸ªã€�ä¸€ä¸ªæˆ–å¤šä¸ªå­—ç¬¦ã€‚ä¸‹åˆ’çº¿ï¼ˆ_ï¼‰ä»£è¡¨ä¸€ä¸ªå�•ä¸€çš„å­—ç¬¦\n\n\
                    ä¾‹å¦‚ï¼šå‰�é€” æ— é‡�\n\
                    æ­¤åŠŸèƒ½ç­‰æ•ˆä½¿ç”¨å‘½ä»¤ /find è¯„ä»· å‰�é€” æ— é‡�"; // todo
                bot.send_message(dialogue.chat_id(), text).await?;
                dialogue.update(State::FindComment).await?;
            }
            StartOp::Status => {
                let text = SAFC_DB.db_status()?;
                bot.send_message(dialogue.chat_id(), text).await?;
            }
        }
    }
    Ok(())
}

/// ç”¨äº�æ¨¡ç³Šæ�œç´¢çš„å…³é”®å­—åˆ—è¡¨çš„å¤„ç�†å‡½æ•°
/// todo å�»é™¤é¡ºåº�çš„å½±å“�
fn search_keys_helper(s: &[&str]) -> String {
    format!("%{}%", s.join("%"))
}

/// å¿«é€ŸæŸ¥æ‰¾å®¢ä½“çš„æ¶ˆæ�¯
/// è¿›å…¥åˆ†é¡µçŠ¶æ€�ï¼Œæœ€ç»ˆçš„è¿”å›�çŠ¶æ€�ä¸º [`State::StartCb`]
async fn find_supervisor_msg(
    args: &[&str],
    bot: &Bot,
    msg: &Message,
    dialogue: MyDialogue,
) -> HandlerResult {
    let mut objs = SAFC_DB.find_supervisor_like(&search_keys_helper(args))?;
    if objs.is_empty() {
        bot.send_message(msg.chat.id, ":( æ‰¾ä¸�åˆ°æ‰€æŸ¥è¯¢çš„å¯¼å¸ˆ")
            .reply_to_message_id(msg.id)
            .await?;
        return Ok(());
    }
    let action_name = "é€‰å®š".to_string();
    objs.truncate(MSG_MAX_PAGES);
    let pages: Vec<String> = objs
        .clone()
        .into_iter()
        .map(|x| display_teacher_md(&x))
        .collect();
    let action_states = objs
        .clone()
        .into_iter()
        .map(|x| State::Read { obj_teacher: x })
        .collect();
    let action_msgs = objs
        .clone()
        .into_iter()
        .map(|x| format!("{}\nè¯·é€‰æ‹©æ“�ä½œï¼š", x.display_path()))
        .collect();
    let text = &pages[0]; // assert!(pages.len() >= 1);
    bot.send_message(msg.chat.id, text)
        .reply_markup(build_paging_keyboard(pages.len(), 0, Some(&action_name)))
        .parse_mode(MarkdownV2)
        .reply_to_message_id(msg.id)
        .await?;
    dialogue
        .update(State::PagingCb {
            data: PagingCbData {
                pages,
                actions: Some(PagingCbActions {
                    name: action_name,
                    action_states,
                    action_msgs,
                    action_op_keyboard: obj_op_keyboard(),
                }),
                prev_state: Box::new(State::StartCb),
                prev_msg: "è¯·é€‰æ‹©æ“�ä½œï¼š".to_string(),
                prev_op_keyboard: start_op_keyboard(),
            },
        })
        .await?;
    Ok(())
}

/// å¿«é€ŸæŸ¥æ‰¾è¯„è®ºçš„æ¶ˆæ�¯
/// è¿›å…¥åˆ†é¡µçŠ¶æ€�ï¼Œæœ€ç»ˆçš„è¿”å›�çŠ¶æ€�ä¸º [`State::StartCb`]
async fn find_comment_msg(
    args: &[&str],
    bot: &Bot,
    msg: &Message,
    dialogue: MyDialogue,
) -> HandlerResult {
    let mut objs = SAFC_DB.find_comment_like(&search_keys_helper(args))?;
    if objs.is_empty() {
        bot.send_message(msg.chat.id, ":( æŸ¥è¯¢æ— ç»“æ�œ")
            .reply_to_message_id(msg.id)
            .await?;
        return Ok(());
    }
    let action_name = "å›�å¤�æ­¤è¯„ä»·".to_string();
    objs.truncate(MSG_MAX_PAGES);
    let pages: Vec<String> = objs
        .clone()
        .into_iter()
        .map(|c: ObjComment| {
            format!(
                "ğŸ’¬ *é’ˆå¯¹ object `{}` çš„è¯„ä»·ï¼š*\n\
                *data {} \\| from {} \\| id `{}`*\n\
                {}\n",
                c.object,
                escape(c.date.as_str()),
                c.source_cate,
                c.id,
                escape(c.description.replace("<br>", "\n").as_str())
            )
        })
        .collect();
    let action_states = objs
        .clone()
        .into_iter()
        .map(|c| State::Comment {
            object_id: c.id,
            comment_type: CommentType::Nest,
        })
        .collect();
    let action_msgs = objs
        .clone()
        .into_iter()
        .map(|c| {
            format!(
                "ğŸ†” `{}`\n\
                \nè¯·å†™ä¸‹æ‚¨å¯¹æ­¤å®¢ä½“çš„è¯„ä»·ï¼š",
                c.id
            )
        })
        .collect();
    let text = &pages[0]; // assert!(pages.len() >= 1);

    bot.send_message(msg.chat.id, text)
        .reply_markup(build_paging_keyboard(pages.len(), 0, Some(&action_name)))
        .parse_mode(MarkdownV2)
        .reply_to_message_id(msg.id)
        .await?;

    dialogue
        .update(State::PagingCb {
            data: PagingCbData {
                pages,
                actions: Some(PagingCbActions {
                    name: action_name,
                    action_states,
                    action_msgs,
                    ..Default::default()
                }),
                prev_state: Box::new(State::StartCb),
                prev_msg: "è¯·é€‰æ‹©æ“�ä½œï¼š".to_string(),
                prev_op_keyboard: start_op_keyboard(),
            },
        })
        .await?;
    Ok(())
}

/// å¿«é€ŸæŸ¥æ‰¾æ•™å¸ˆ çŠ¶æ€�çš„å¤„ç�†å‡½æ•°
async fn find_supervisor(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
    let args: Vec<&str> = msg.text().ok_or("None msg err")?.split(' ').collect();
    find_supervisor_msg(&args, &bot, &msg, dialogue).await?;
    Ok(())
}

/// å¿«é€ŸæŸ¥æ‰¾æ•™å¸ˆ çŠ¶æ€�çš„å¤„ç�†å‡½æ•°
async fn find_comment(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
    let args: Vec<&str> = msg.text().ok_or("None msg err")?.split(' ').collect();
    find_comment_msg(&args, &bot, &msg, dialogue).await?;
    Ok(())
}

/// å­˜å‚¨é€‰å®šçš„ school_cateï¼Œå¹¶è¯¢é—® universityã€‚
async fn choose_university(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
    if let Some(s_c) = msg.text().map(ToOwned::to_owned) {
        choose_university_msg(&s_c, &bot, &msg).await?;
        dialogue
            .update(State::University { school_cate: s_c })
            .await?; // æ›´æ–°ä¼šè¯�çŠ¶æ€�
    } else {
        bot.send_message(msg.chat.id, TgResponse::RetryErrNone.to_string())
            .await?;
    }

    Ok(())
}

async fn choose_university_msg(s_c: &String, bot: &Bot, msg: &Message) -> HandlerResult {
    let keyboard = convert_to_n_columns_keyboard(SAFC_DB.find_university(s_c)?, 2);
    bot.send_message(msg.chat.id, format!("ğŸ§­ {s_c}\næ‚¨æƒ³æŸ¥è¯¢çš„ã€Œå­¦æ ¡ã€�æ˜¯ï¼š"))
        .reply_markup(KeyboardMarkup::new(keyboard).input_field_placeholder("å­¦æ ¡ï¼Ÿ".to_string()))
        .reply_to_message_id(msg.id)
        .await?;
    Ok(())
}

/// å­˜å‚¨é€‰å®šçš„ university å¹¶è¦�æ±‚ä¸€ä¸ª departmentã€‚
async fn choose_department(
    bot: Bot,
    dialogue: MyDialogue,
    s_c: String, // Available from `State::...`.
    msg: Message,
) -> HandlerResult {
    if let Some(university) = msg.text().map(ToOwned::to_owned) {
        choose_department_msg(&s_c, &university, &bot, &msg).await?;
        dialogue
            .update(State::Department {
                school_cate: s_c,
                university,
            })
            .await?; // æ›´æ–°ä¼šè¯�çŠ¶æ€�
    } else {
        bot.send_message(msg.chat.id, TgResponse::RetryErrNone.to_string())
            .await?;
    }
    Ok(())
}

async fn choose_department_msg(
    s_c: &String,
    university: &String,
    bot: &Bot,
    msg: &Message,
) -> HandlerResult {
    let keyboard = convert_to_n_columns_keyboard(SAFC_DB.find_department(s_c, university)?, 1);
    bot.send_message(
        msg.chat.id,
        format!("ğŸ§­ {s_c} ğŸ�« {university}\næ‚¨æƒ³æŸ¥è¯¢çš„ã€Œå­¦é™¢ã€�æ˜¯ï¼š"),
    )
    .reply_markup(KeyboardMarkup::new(keyboard).input_field_placeholder("å­¦é™¢ï¼Ÿ".to_string()))
    .reply_to_message_id(msg.id)
    .await?;
    Ok(())
}

/// å­˜å‚¨æ‰€é€‰éƒ¨é—¨å¹¶é€‰æ‹© å®¢ä½“
async fn choose_supervisor(
    bot: Bot,
    dialogue: MyDialogue,
    (s_c, university): (String, String), // Available from `State::...`.
    msg: Message,
) -> HandlerResult {
    if let Some(department) = msg.text().map(ToOwned::to_owned) {
        choose_supervisor_msg(&s_c, &university, &department, &bot, &msg).await?;
        dialogue
            .update(State::Supervisor {
                school_cate: s_c,
                university,
                department,
            })
            .await?; // æ›´æ–°ä¼šè¯�çŠ¶æ€�
    } else {
        bot.send_message(msg.chat.id, TgResponse::RetryErrNone.to_string())
            .await?;
    }
    Ok(())
}

async fn choose_supervisor_msg(
    school_cate: &String,
    university: &String,
    department: &String,
    bot: &Bot,
    msg: &Message,
) -> HandlerResult {
    let keyboard = convert_to_n_columns_keyboard(
        SAFC_DB.find_supervisor(school_cate, university, department)?,
        3,
    );
    bot.send_message(
        msg.chat.id,
        format!("ğŸ§­ {school_cate} ğŸ�« {university} ğŸ�¢ {department}\næ‚¨æƒ³æŸ¥è¯¢çš„ã€Œå¯¼å¸ˆç­‰å®¢ä½“ã€�æ˜¯ï¼š"),
    )
    .reply_markup(KeyboardMarkup::new(keyboard).input_field_placeholder("å®¢ä½“ï¼Ÿ".to_string()))
    .reply_to_message_id(msg.id)
    .await?;
    Ok(())
}

/// å­˜å‚¨é€‰å®šçš„å®¢ä½“å¹¶è¯¢é—®ä¸‹ä¸€æ­¥æ“�ä½œ
async fn read_or_comment(
    bot: Bot,
    dialogue: MyDialogue,
    (school_cate, university, department): (String, String, String), // Available from `State::...`.
    msg: Message,
) -> HandlerResult {
    if let Some(supervisor) = msg.text().map(ToOwned::to_owned) {
        let obj = SAFC_DB.find_object_with_path(&university, &department, &supervisor)?;
        match obj {
            None => {
                let object_id = hash_object_id(&university, &department, &supervisor);
                bot.send_message(
                    msg.chat.id,
                    format!(
                        "ğŸ§­ {school_cate} ğŸ�« {university} ğŸ�¢ {department} ğŸ‘” {supervisor}\n\
                        ğŸ¤— ç›®å‰�è¿˜æ²¡æœ‰è¿™ä¸ªå¯¹è±¡çš„ä¿¡æ�¯ï¼Œæ˜¯å�¦å¢�åŠ æ­¤å¯¹è±¡ï¼Ÿ"
                    ),
                )
                .reply_markup(InlineKeyboardMarkup::new([[
                    InlineKeyboardButton::callback(
                        "â�• å¢�åŠ ",
                        serde_json::to_string(&ObjectOp::Add).unwrap(),
                    ),
                    InlineKeyboardButton::callback(
                        "ğŸ�� ç»“æ�Ÿ",
                        serde_json::to_string(&ObjectOp::End).unwrap(),
                    ),
                ]]))
                .reply_to_message_id(msg.id)
                .await?;
                dialogue
                    .update(State::Read {
                        obj_teacher: ObjTeacher {
                            school_cate,
                            university,
                            department,
                            supervisor,
                            date: get_current_date(),
                            info: None,
                            object_id,
                        },
                    })
                    .await?; // æ›´æ–°ä¼šè¯�çŠ¶æ€�
            }
            Some(obj_teacher) => {
                bot.send_message(
                    msg.chat.id,
                    format!(
                        "{}\n\
                        è¯·é€‰æ‹©æ“�ä½œï¼š",
                        display_teacher_md(&obj_teacher)
                    ),
                )
                .reply_to_message_id(msg.id)
                .parse_mode(MarkdownV2)
                .reply_markup(obj_op_keyboard())
                .await?;
                dialogue.update(State::Read { obj_teacher }).await?; // æ›´æ–°ä¼šè¯�çŠ¶æ€�
            }
        }
    } else {
        bot.send_message(msg.chat.id, TgResponse::RetryErrNone.to_string())
            .await?;
    }
    Ok(())
}

/// è§£æ�� CallbackQuery å¹¶æ›´æ–°æ¶ˆæ�¯æ–‡æœ¬ã€‚
async fn read_or_comment_cb(
    bot: Bot,
    dialogue: MyDialogue,
    obj_teacher: ObjTeacher, // Available from `State::...`.
    q: CallbackQuery,
) -> HandlerResult {
    let ObjTeacher {
        school_cate,
        university,
        department,
        supervisor,
        date,
        info,
        object_id,
    } = obj_teacher.clone();
    bot.answer_callback_query(q.id).await?;
    if let Some(op) = &q.data {
        match serde_json::from_str(op)? {
            ObjectOp::Read => {
                let action_name = "å›�å¤�æ­¤è¯„ä»·".to_string();
                let pages: Vec<String> = get_comment_pages(&object_id)?
                    .iter()
                    .enumerate()
                    .map(|(i, x)| {
                        format!(
                            "*ğŸ‘” {} `{}` çš„è¯„ä»· ç¬¬ {} é¡µï¼š*\n\
                            {}\n\
                            _ä½¿ç”¨ /comment \\<id\\> ç»™ä»»æ„�è¯„ä»·å†™è¯„ä»·ã€‚_ ",
                            escape(&supervisor),
                            &object_id,
                            i + 1,
                            x
                        )
                    })
                    .collect();
                if pages.is_empty() {
                    if let Some(Message { id, chat, .. }) = q.message {
                        bot.edit_message_text(chat.id, id, "ğŸˆ³ _æ­¤å®¢ä½“æš‚æ— è¯„ä»·ï¼�_".to_string())
                            .reply_markup(obj_op_keyboard())
                            .parse_mode(MarkdownV2)
                            .await?;
                    }
                    // dialogue.update(State::Read { obj_teacher }).await?; // æ›´æ–°ä¼šè¯�çŠ¶æ€�
                } else {
                    let text = &pages[0];
                    if let Some(Message { id, chat, .. }) = q.message {
                        bot.edit_message_text(chat.id, id, text)
                            .reply_markup(build_paging_keyboard(pages.len(), 0, Some(&action_name)))
                            .parse_mode(MarkdownV2)
                            .await?;
                    }
                    let comments = SAFC_DB.find_comment(&object_id)?;
                    let action_msgs = comments
                        .iter()
                        .map(|x| format!("å›�å¤�è¯„ä»· `{}`\n/cancel å�–æ¶ˆ", &x.id))
                        .collect();
                    let action_states = comments
                        .iter()
                        .map(|x| State::Comment {
                            object_id: x.id.clone(),
                            comment_type: CommentType::Nest,
                        })
                        .collect();

                    // è¿›å…¥è¯»è¯„ä»·çš„åˆ†é¡µçŠ¶æ€�
                    dialogue
                        .update(State::PagingCb {
                            data: PagingCbData {
                                pages,
                                actions: Some(PagingCbActions {
                                    name: action_name,
                                    action_states,
                                    action_msgs,
                                    ..Default::default()
                                }),
                                prev_state: Box::new(State::Read { obj_teacher }),
                                prev_msg: escape(
                                    format!(
                                    "ğŸ§­ {school_cate} ğŸ�« {university} ğŸ�¢ {department} ğŸ‘” {supervisor}\n\
                                    è¯·é€‰æ‹©æ“�ä½œï¼š"
                                )
                                    .as_str(),
                                ),
                                prev_op_keyboard: obj_op_keyboard(),
                            },
                        })
                        .await?;
                }
            }
            ObjectOp::Add => {
                // å¢�åŠ è¯„ä»·å®¢ä½“
                SAFC_DB.add_object(&obj_teacher)?;
                let text = format!(
                    "ğŸ§­ {school_cate} ğŸ�« {university} ğŸ�¢ {department} ğŸ‘” {supervisor}\n\
                    è¯„ä»·å®¢ä½“å·²å¢�åŠ ï¼�æ„Ÿè°¢æ‚¨çš„è´¡çŒ® ğŸŒ·"
                );
                log::info!("è¯„ä»·å®¢ä½“å·²å¢�åŠ ï¼�");
                if let Some(Message { id, chat, .. }) = q.message {
                    bot.edit_message_text(chat.id, id, text)
                        .reply_markup(obj_op_keyboard())
                        .await?;
                } // else ... todo
                  // dialogue.update(State::Read { obj_teacher }).await?; // æ›´æ–°ä¼šè¯�çŠ¶æ€�
            }
            ObjectOp::Commet => {
                let text = format!(
                    "ğŸ§­ {school_cate} ğŸ�« {university} ğŸ�¢ {department} ğŸ‘” {supervisor}\n\
                    \nè¯·å†™ä¸‹æ‚¨å¯¹æ­¤å®¢ä½“çš„è¯„ä»·ï¼š"
                );
                if let Some(Message { id, chat, .. }) = q.message {
                    bot.edit_message_text(chat.id, id, text).await?;
                } // else ... todo
                dialogue
                    .update(State::Comment {
                        object_id,
                        comment_type: CommentType::Teacher,
                    })
                    .await?; // æ›´æ–°ä¼šè¯�çŠ¶æ€�
            }
            ObjectOp::End => {
                bot.send_message(
                    dialogue.chat_id(),
                    "è°¢è°¢ï¼�æœ¬æ¬¡å¯¹è¯�ç»“æ�Ÿï¼Œä½¿ç”¨ /start é‡�æ–°å¼€å§‹ã€‚\nç›®å‰�ä¸ºæµ‹è¯•ç‰ˆæœ¬ï¼Œæˆ‘ä»¬æœŸå¾…æ‚¨çš„ä½¿ç”¨å��é¦ˆ".to_string(),
                )
                .reply_markup(KeyboardRemove::new())
                .await?;
                dialogue.exit().await?; // ç»“æ�Ÿä¼šè¯�
            }
            ObjectOp::Info => {
                // todo è½¬ä¸ºä½¿ç”¨ display_teacher_md
                let text = format!(
                    "ğŸ§­ {school_cate} ğŸ�« {university} ğŸ�¢ {department} ğŸ‘” {supervisor}\n\
                    è¯¥å®¢ä½“çš„åˆ�æ¬¡æ·»åŠ æ—¥æœŸï¼š{}\n\
                    {:?} \nï¼ˆæ­¤åŠŸèƒ½æœ‰å¾…å¼€å�‘ï¼‰",
                    date, info
                );
                if let Some(Message { id, chat, .. }) = q.message {
                    bot.edit_message_text(chat.id, id, text)
                        .reply_markup(obj_op_keyboard())
                        .await?;
                } // else ... todo
                  // dialogue.update(State::Read { obj_teacher }).await?; // æ›´æ–°ä¼šè¯�çŠ¶æ€�
            }
            ObjectOp::ReturnU => {
                choose_university_msg(&school_cate, &bot, &q.message.unwrap()).await?;
                dialogue.update(State::University { school_cate }).await?;
            }
            ObjectOp::ReturnD => {
                choose_department_msg(&school_cate, &university, &bot, &q.message.unwrap()).await?;
                dialogue
                    .update(State::Department {
                        school_cate,
                        university,
                    })
                    .await?;
            }
            ObjectOp::ReturnS => {
                choose_supervisor_msg(
                    &school_cate,
                    &university,
                    &department,
                    &bot,
                    &q.message.unwrap(),
                )
                .await?;
                dialogue
                    .update(State::Supervisor {
                        school_cate,
                        university,
                        department,
                    })
                    .await?;
            }
        }
    }

    Ok(())
}

/// åˆ†é¡µä¼šè¯�å›�è°ƒå¤„ç�†å‡½æ•°
/// æ³¨æ„�ï¼Œparse_mode(MarkdownV2) ç�°åœ¨è¿˜æ˜¯å¿…é¡»çš„
/// ä»�å›�è°ƒä¸­è�·å�–ç›®å‰�çš„é¡µç � `index`
async fn paging_cb(
    bot: Bot,
    dialogue: MyDialogue,
    data: PagingCbData,
    q: CallbackQuery,
) -> HandlerResult {
    let PagingCbData {
        pages,
        actions,
        prev_state,
        prev_msg,
        prev_op_keyboard,
    } = data;
    bot.answer_callback_query(q.id).await?;
    if let Some(op) = &q.data {
        match serde_json::from_str(op)? {
            PagingOp::Page(index) => {
                let text = &pages[index];
                if let Some(Message { id, chat, .. }) = q.message {
                    bot.edit_message_text(chat.id, id, text)
                        .parse_mode(MarkdownV2)
                        .reply_markup(build_paging_keyboard(
                            pages.len(),
                            index,
                            actions.map(|x| x.name.clone()).as_ref(),
                        ))
                        .await?;
                }
            }
            PagingOp::Back => {
                if let Some(Message { id, chat, .. }) = q.message {
                    bot.edit_message_text(chat.id, id, prev_msg)
                        .parse_mode(MarkdownV2)
                        .reply_markup(prev_op_keyboard)
                        .await?;
                }
                dialogue.update(*prev_state).await?;
            }
            PagingOp::Action(index) => {
                if let Some(PagingCbActions {
                    name: _,
                    action_states,
                    action_msgs,
                    action_op_keyboard,
                }) = actions
                {
                    if let Some(Message { id, chat, .. }) = q.message {
                        bot.edit_message_text(chat.id, id, &action_msgs[index])
                            .parse_mode(MarkdownV2)
                            .reply_markup(action_op_keyboard)
                            .await?;
                    }
                    dialogue.update(action_states[index].clone()).await?;
                }
            }
        }
    }
    Ok(())
}

async fn invalid_callback_query(bot: Bot, q: CallbackQuery) -> HandlerResult {
    bot.answer_callback_query(q.id).await?;
    if let Some(Message { id, chat, .. }) = q.message {
        bot.edit_message_text(chat.id, id, "â�� å¯¹è¯�è¿‡æœŸã€‚ä½¿ç”¨ /start é‡�æ–°å¼€å§‹")
            .await?;
    }
    Ok(())
}

/// å¢�åŠ è¯„ä»·å¤„ç�†å‡½æ•°
/// ? è¿”å›�å­—ç¬¦ä¸²ä½¿ç”¨çš„æ ‡è®°è¯­è¨€æ˜¯ä»€ä¹ˆ
async fn add_comment(
    bot: Bot,
    dialogue: MyDialogue,
    (object_id, comment_type): (String, CommentType), // Available from `State::...`.
    msg: Message,
) -> HandlerResult {
    if let Some(comment) = msg.text().map(ToOwned::to_owned) {
        // let date = get_current_date();
        // let comment_id = hash_comment_id(&object_id, &comment, &date);
        bot.send_message(
            msg.chat.id,
            format!(
                "æ‚¨å¯¹ `{}` çš„è¯„ä»·æ˜¯\n\n\
                ```\n{}\n```\n\
                ç¡®è®¤å�‘å¸ƒï¼Ÿå¦‚ç¡®è®¤è¯·è¾“å…¥ã€Œå�‘å¸ƒäºº OTPã€�ï¼Œä¹‹å��å°†å�‘å¸ƒè¯„ä»·;\
                å�–æ¶ˆè¯· /cancel  *æ‚¨å�ªèƒ½åœ¨æ­¤å�–æ¶ˆï¼�*\n\
                _æ³¨ï¼šã€Œå�‘å¸ƒäºº OTPã€�å�³ä¸€æ¬¡æ€§å¯†é’¥ï¼Œæ˜¯å�¯ä»¥è®©æ‚¨æ—¥å��è¯�æ˜�æœ¬è¯„ä»·ç”±æ‚¨å�‘å¸ƒï¼Œç”±æ­¤æ‚¨å�¯ä»¥ä¿®æ”¹/é”€æ¯�æ­¤è¯„è®ºï¼Œ\
                å¦‚ä¸�éœ€è¦�ï¼Œè¾“å…¥éš�æœºå€¼å�³å�¯_",
                &object_id,
                escape(comment.as_str())
            ),
        )
        .reply_to_message_id(msg.id)
        .parse_mode(MarkdownV2)
        .await?;
        dialogue
            .update(State::Publish {
                object_id,
                comment,
                comment_type,
            })
            .await?; // æ›´æ–°ä¼šè¯�çŠ¶æ€�
    } else {
        bot.send_message(msg.chat.id, TgResponse::RetryErrNone.to_string())
            .await?;
    }
    Ok(())
}

/// å¢�åŠ è¯„ä»·å¤„ç�†å‡½æ•°
async fn publish_comment(
    bot: Bot,
    dialogue: MyDialogue,
    (object_id, comment, comment_type): (String, String, CommentType), // Available from `State::...`.
    msg: Message,
) -> HandlerResult {
    if let Some(otp) = msg.text().map(ToOwned::to_owned) {
        let c = ObjComment::new_with_otp(
            object_id.clone(),
            comment,
            SourceCate::Telegram,
            comment_type,
            otp,
        );
        SAFC_DB.add_comment(&c)?; // ? æœ‰äº›å�¯èƒ½çš„é”™è¯¯éœ€æ��ç¤ºç”¨æˆ·
        log::info!("{} è¯„ä»·å·²å�‘å¸ƒ", c.id);

        match SAFC_DB.find_objteacher_with_id(object_id.as_str())? {
            Some(obj_teacher) => {
                bot.send_message(
                    msg.chat.id,
                    format!(
                        "_æ‚¨çš„ OTP å·²é”€æ¯�_\n\
                        è¯„ä»·ã€Œ`{}`ã€�å·²å�‘å¸ƒï¼�æ„Ÿè°¢æ‚¨çš„è´¡çŒ® ğŸŒ·",
                        c.id
                    ),
                )
                .reply_to_message_id(msg.id)
                .parse_mode(MarkdownV2)
                .reply_markup(obj_op_keyboard())
                .await?;
                dialogue.update(State::Read { obj_teacher }).await?;
            }
            None => {
                bot.send_message(
                    msg.chat.id,
                    format!(
                        "_æ‚¨çš„ OTP å·²é”€æ¯�_\n\
                        åµŒå¥—è¯„ä»·ã€Œ`{}`ã€�å·²å�‘å¸ƒï¼�æ„Ÿè°¢æ‚¨çš„è´¡çŒ® ğŸŒ·\n\
                        ä½¿ç”¨ /start é‡�æ–°å¼€å§‹",
                        c.id
                    ),
                )
                .reply_to_message_id(msg.id)
                .parse_mode(MarkdownV2)
                .await?;
                dialogue.exit().await?; // TODO åµŒå¥—è¯„ä»·é�¢æ�¿
            }
        }
    } else {
        bot.send_message(msg.chat.id, TgResponse::RetryErrNone.to_string())
            .await?;
    }
    Ok(())
}

/// ä¸€ç»´å�‘é‡�è½¬æ�¢ä¸º n åˆ—çºµå�‘é”®ç›˜
fn convert_to_n_columns_keyboard(data: Vec<String>, n: usize) -> Vec<Vec<KeyboardButton>> {
    data.chunks(n)
        .map(|chunk| chunk.iter().map(KeyboardButton::new).collect())
        .collect()
}
