use serde::{Deserialize, Serialize};
use teloxide::types::InlineKeyboardButton;
use teloxide::types::InlineKeyboardMarkup;
pub use teloxide::utils::markdown::escape;
use url::Url;

use safc::db::*;

// æœ‰æ²¡æœ‰æ›´ä¼˜é›…çš„æ–¹æ³•ï¼Ÿ
// æ”¾åœ¨è¿™é‡Œæ˜¯ä¸ºäº†è®© `main` å¼•ç”¨ï¼Œè€Œä¸�æ˜¯ `msg` å¼•ç”¨ `main`
use lazy_static::lazy_static;
lazy_static! {
    /// bot çš„å…¨å±€å”¯ä¸€æ•°æ�®åº“æ± 
    pub static ref SAFC_DB: SAFCdb = SAFCdb::new();
}

/// æœ€å¤§åˆ†é¡µå¤§å°�
pub const MSG_MAX_PAGES: usize = 99;

const GITHUB_URL: &str = "https://github.com/framist/SAFC-bot";
const WEB_URL: &str = "https://framist.github.io/safc";
const BOT_INFO: &str = r"*å¤§å­¦ç”Ÿå��è¯ˆä¸­å¿ƒ*

_å…ƒå¹³å�°ï¼Œåˆ†å¸ƒå¼�_

è‡ªä»�æœ€åˆ�çš„å¯¼å¸ˆè¯„ä»·ç½‘ï¼ˆurfireï¼‰å…³é—­ï¼Œæ—¶è‡³ä»Šæ—¥ï¼Œä¸€æ‰¹ä¸€æ‰¹çš„æ–°å¯¼å¸ˆè¯„ä»·æ•°æ�®åˆ†äº«å¹³å�°çš„è¿­èµ·å…´è¡°ï¼Œæœ€ç»ˆéƒ½è�½äº� 404 æˆ–æ”¶è´¹é—­å¡�ã€‚
ä¸�çŸ¥æ˜¯ä½•ç­‰é˜»åŠ›ï¼Œè®©å�—è¿‡æ¬ºéª—çš„å­¦ç”Ÿå’ŒäºŸéœ€ä¿¡æ�¯çš„å­¦ç”Ÿæ•£è‹¥æ¸ºèŒ«æ˜Ÿç�«ã€‚æ•…å»ºæ­¤å¹³å�°ä¸�æœºå™¨äººï¼Œé�©æ–°æ–¹å¼�ï¼Œå�šæŒ�â€œç¤¾ç¾¤ï¼Œä¿�æŠ¤ï¼Œå¼€æ”¾â€�çš„ç�†å¿µï¼Œä¿¡å¥‰||å¯†ç �æœ‹å…‹||ã€�å¼€æº�ç²¾ç¥�ï¼Œæ„¿æ­¤å’Œè°�å…±èµ¢åœ°æŒ�ä¹…æ€§å�‘å±•ä¼ æ‰¿ä¸‹å�»ã€‚

*telegram æœºå™¨äºº* ~@SAFC\_bot~ @SAFC\_bak\_bot â€”â€” å­¦æ ¡ã€�ä¸“ä¸šã€�å­¦é™¢ã€�è¯¾ç¨‹ã€�å¯¼å¸ˆçš„äº¤å�‰è¯„ä»·ä¸�æŸ¥è¯¢
*telegram ç¾¤ç»„ç¤¾åŒº* @SAFC\_group â€”â€” å…¬å‘Šä¸�äº¤æµ�å¹³å�°

[GitHub é¡¹ç›®ä¸»é¡µ](https://github.com/framist/SAFC-bot)
";

pub enum TgResponse {
    Hello,
    Info,
    RetryErrNone,
    #[allow(unused)]
    NotImplemented,
}

impl ToString for TgResponse {
    fn to_string(&self) -> String {
        // escape(&self.to_unescaped_string())
        match self {
            Self::Hello => concat!(
                "ğŸ‘‹ å—¨ï¼�æˆ‘æ˜¯å¤§å­¦ç”Ÿå��è¯ˆä¸­å¿ƒï¼ˆSAFC @SAFC\\_groupï¼‰çš„å®¢æœ�æœºå™¨äºº\n",
                "_ç›®å‰�ä»�ä¸ºæ—©æœŸå¼€å�‘ç‰ˆæœ¬_ é—®é¢˜æ•¬è¯·å��é¦ˆï¼›*è¶Šå¢™ä¸�æ˜“ï¼Œå»¶è¿Ÿä¸¢åŒ…æ•¬è¯·è§�è°…*\n",
                "æ‚¨å�¯ä»¥å�‘é€� /cancel æ�¥å�œæ­¢æ­¤æ¬¡å¯¹è¯�ï¼›",
                "å�‘é€� /info äº†è§£å��è¯ˆä¸­å¿ƒ\n",
                "æ‚¨å�¯ä»¥å…ˆæŸ¥è¯¢å®¢ä½“ï¼ˆä¸€èˆ¬æ�¥è®²å®¢ä½“å°±æ˜¯å¯¼å¸ˆå•¦ï¼‰ï¼Œç„¶å��æŸ¥çœ‹æˆ–å�‘èµ·å¯¹å®¢ä½“çš„è¯„ä»·ã€‚\n\n",
                "è¯·é€‰æ‹©ä»¥ä¸‹åŠŸèƒ½ä¹‹ä¸€ï¼š",
            )
            .to_owned(),
            Self::Info => BOT_INFO.to_owned(),
            Self::RetryErrNone => "ç©ºæ¶ˆæ�¯é”™è¯¯ã€‚å¯¹ä¸�èµ·ï¼Œè¯·é‡�è¯•".to_owned(),
            Self::NotImplemented => "ğŸ˜¢ åŠŸèƒ½å°šæœªå®�ç�°ï¼Œæ•¬è¯·æœŸå¾…".to_owned(),
        }
    }
}

/// æµ�ç¨‹
#[derive(Clone, Default, Serialize, Deserialize, Debug)]
pub enum State {
    #[default]
    Start,
    /// å¼€å§‹åŠŸèƒ½é€‰å�•å›�è°ƒçŠ¶æ€�
    StartCb,
    /// å¿«é€Ÿæ�œç´¢æ•™å¸ˆ
    FindSupervisor,
    /// å¿«é€Ÿæ�œç´¢è¯„è®º
    FindComment,
    SchoolCate,
    University {
        school_cate: String,
    },
    Department {
        school_cate: String,
        university: String,
    },
    Supervisor {
        school_cate: String,
        university: String,
        department: String,
    },
    Read {
        obj_teacher: ObjTeacher,
    },
    Comment {
        object_id: String, // todo å¾…é‡�æ�„ä¸º Obj
        comment_type: CommentType,
    },
    Publish {
        object_id: String, // todo å¾…é‡�æ�„ä¸º Obj
        comment: String,
        comment_type: CommentType,
    },
    /// åˆ†é¡µæ˜¾ç¤ºå›�è°ƒçŠ¶æ€�
    PagingCb {
        data: PagingCbData,
    },
}

/// åˆ†é¡µæ˜¾ç¤ºå›�è°ƒçŠ¶æ€�çš„æ•°æ�®
#[derive(Clone, Default, Serialize, Deserialize, Debug)]
pub struct PagingCbData {
    /// å�„ä¸ªé¡µé�¢çš„æ–‡å­—
    pub pages: Vec<String>,
    /// å�¯é€‰çš„å¯¹å�„é¡µçš„è¿›ä¸€æ­¥æ“�ä½œ
    pub actions: Option<PagingCbActions>,
    /// ä¸Šä¸€ä¸ªçŠ¶æ€�
    pub prev_state: Box<State>,
    /// ç”¨äº�è¿”å›�å��çš„æ¶ˆæ�¯æ˜¾ç¤º
    /// msg ä¸€å®šæ˜¯ Markdown æ ¼å¼�çš„
    pub prev_msg: String,
    /// ç”¨äº�è¿”å›�å��çš„å†…è�”å›�è°ƒé”®ç›˜
    pub prev_op_keyboard: InlineKeyboardMarkup,
}

#[derive(Clone, Default, Serialize, Deserialize, Debug)]
pub struct PagingCbActions {
    /// æ“�ä½œå��ï¼Œç®€çŸ­ï¼�
    pub name: String,
    /// å¦‚æ“�ä½œï¼Œå�„ä¸ªé¡µé�¢çš„ä¸‹ä¸€ä¸ªçŠ¶æ€�
    pub action_states: Vec<State>,
    /// å¦‚æ“�ä½œï¼Œå�„ä¸ªé¡µé�¢çš„ä¸‹ä¸€ä¸ªæ¶ˆæ�¯æ˜¾ç¤º
    /// msg ä¸€å®šæ˜¯ Markdown æ ¼å¼�çš„
    pub action_msgs: Vec<String>,
    /// å¦‚æ“�ä½œï¼Œé¡µé�¢ç»Ÿä¸€çš„å�¯é€‰çš„å†…è�”å›�è°ƒé”®ç›˜
    /// è‹¥ä¸�éœ€è¦�ï¼Œåˆ™ default å�³å�¯
    pub action_op_keyboard: InlineKeyboardMarkup,
}

/// å¼€å§‹åŠŸèƒ½é€‰æ‹©çš„å›�è°ƒ
#[derive(Serialize, Deserialize, Debug)]
pub enum StartOp {
    Tree,           // å¼€å§‹åœ¨æ ‘ç»“æ�„ä¸­å®šä½�
    FindSupervisor, // å¿«é€ŸæŸ¥æ‰¾æ•™å¸ˆ
    FindComment,    // å¿«é€ŸæŸ¥æ‰¾è¯„ä»·
    Status,         // ç»Ÿè®¡ä¸�çŠ¶æ€�
                    // Find,   // å¿«é€ŸæŸ¥æ‰¾
}

/// å¯¹è±¡æ“�ä½œçš„å›�è°ƒ
#[derive(Serialize, Deserialize, Debug)]
pub enum ObjectOp {
    Read,
    Commet,
    Info,
    End,
    Add,
    // æœ€é•¿å�ªèƒ½ 64 å­—ç¬¦ï¼Œæ‰€ä»¥é€‰æ‹©è¿™ç§� hack çš„æ–¹æ³•ï¼Œæœ‰å¾…æ”¹è¿›
    ReturnU,
    ReturnD,
    ReturnS,
}

/// åˆ†é¡µæ“�ä½œçš„å›�è°ƒ
#[derive(Serialize, Deserialize, Debug)]
pub enum PagingOp {
    /// é¡µç �
    Page(usize),
    /// è¿”å›�
    Back,
    /// å¯¹å½“é¡µå¯¹è±¡çš„æ“�ä½œ
    Action(usize),
}

impl From<StartOp> for String {
    fn from(val: StartOp) -> Self {
        serde_json::to_string(&val).unwrap()
    }
}

impl From<ObjectOp> for String {
    fn from(val: ObjectOp) -> Self {
        serde_json::to_string(&val).unwrap()
    }
}

impl From<PagingOp> for String {
    fn from(val: PagingOp) -> Self {
        serde_json::to_string(&val).unwrap()
    }
}

impl From<String> for ObjectOp {
    fn from(value: String) -> Self {
        serde_json::from_str(&value).unwrap()
    }
}

pub fn start_op_keyboard() -> InlineKeyboardMarkup {
    InlineKeyboardMarkup::new([
        vec![InlineKeyboardButton::callback(
            "ğŸŒ³ å¼€å§‹æŸ¥è¯¢ & è¯„ä»·ï¼ˆè��ï¼‰",
            StartOp::Tree,
        )],
        vec![
            InlineKeyboardButton::callback("ğŸ‘” å¿«æ�œæ•™å¸ˆ", StartOp::FindSupervisor),
            InlineKeyboardButton::callback("ğŸ’¬ å¿«æ�œè¯„è®º", StartOp::FindComment),
        ],
        vec![
            InlineKeyboardButton::callback("ğŸ“Š", StartOp::Status),
            InlineKeyboardButton::url("ğŸ�›ï¸�", Url::parse("https://t.me/SAFC_group").unwrap()),
            InlineKeyboardButton::url("ğŸŒ�", Url::parse(WEB_URL).unwrap()),
            InlineKeyboardButton::url("ğŸ�±", Url::parse(GITHUB_URL).unwrap()),
        ],
    ])
}

pub fn obj_op_keyboard() -> InlineKeyboardMarkup {
    InlineKeyboardMarkup::new([
        vec![
            InlineKeyboardButton::callback("ğŸ‘€ æŸ¥çœ‹è¯„ä»·", ObjectOp::Read),
            InlineKeyboardButton::callback("ğŸ’¬ å¢�åŠ è¯„ä»·", ObjectOp::Commet),
        ],
        vec![
            InlineKeyboardButton::callback("ğŸ¤— è¯¦ç»†ä¿¡æ�¯", ObjectOp::Info),
            InlineKeyboardButton::callback("ğŸ�� ç»“æ�Ÿä¼šè¯�", ObjectOp::End),
        ],
        vec![
            InlineKeyboardButton::callback("â†©ï¸� ğŸ�«", ObjectOp::ReturnU),
            InlineKeyboardButton::callback("â†©ï¸� ğŸ�¢", ObjectOp::ReturnD),
            InlineKeyboardButton::callback("â†©ï¸� ğŸ‘”", ObjectOp::ReturnS),
        ],
    ])
}

/// `index` ä»� 0 å¼€å§‹çš„é¡µç �
/// `total` ä¸ºæ€»å…±çš„é¡µæ•°
/// `action` ç”¨äº�å½“å‰�é¡µçš„å›�è°ƒæŒ‰é’®
pub fn build_paging_keyboard(
    total: usize,
    index: usize,
    action: Option<&String>,
) -> InlineKeyboardMarkup {
    let mut buttons_2 = vec![InlineKeyboardButton::callback("â†©ï¸� è¿”å›�", PagingOp::Back)];
    if total <= 1 && action.is_none() {
        return InlineKeyboardMarkup::new([buttons_2]);
    }

    const COLS: usize = 2; // COLS * 2 + 1 == ä¸€è¡Œæ˜¾ç¤ºæœ€å¤šçš„é¡µç �æŒ‰é’®æ•°
    let start = index
        .saturating_sub(COLS)
        .min(total.saturating_sub(2 * COLS + 1));
    let mut buttons_1: Vec<InlineKeyboardButton> = (start..(start + 2 * COLS + 1).min(total))
        .map(|x| InlineKeyboardButton::callback(format!("{}/{}", x + 1, total), PagingOp::Page(x)))
        .collect();

    // buttons_1.retain(|x| x.text != format!("{}/{}", index + 1, total)); // å¼ƒç”¨
    for x in buttons_1.iter_mut() {
        if x.text == format!("{}/{}", index + 1, total) {
            *x = match action {
                Some(b) => InlineKeyboardButton::callback(b.clone(), PagingOp::Action(index)),
                None => InlineKeyboardButton::callback(
                    format!(">{}<", index + 1),
                    PagingOp::Action(index),
                ),
            };
            break;
        }
    }

    if index > 0 {
        buttons_2.push(InlineKeyboardButton::callback(
            "â¬…ï¸� ä¸Šé¡µ",
            PagingOp::Page(index - 1),
        ));
    }
    if index < total - 1 {
        buttons_2.push(InlineKeyboardButton::callback(
            "â�¡ï¸� ä¸‹é¡µ",
            PagingOp::Page(index + 1),
        ));
    }
    InlineKeyboardMarkup::new([buttons_1, buttons_2])
}

/// æ˜¾ç¤º [`ObjTeacher`] è¯¦ç»†çš„ä¿¡æ�¯
/// markdown æ ¼å¼�
pub fn display_teacher_md(obj: &ObjTeacher) -> String {
    format!(
        "*{}*\n\
        ä¿¡æ�¯ï¼š{}\n\
        è¯„ä»·æ•°ï¼š {}\n\
        è¯¥å®¢ä½“çš„åˆ�æ¬¡æ·»åŠ æ—¥æœŸï¼š{}",
        escape(obj.display_path().as_str()),
        escape(obj.info.clone().unwrap_or("æš‚æ— ".to_string()).as_str()),
        escape(
            SAFC_DB
                .find_comment(&obj.object_id)
                .unwrap()
                .len()
                .to_string()
                .as_str()
        ),
        escape(obj.date.as_str())
    )
}

/// ç”Ÿæˆ�åˆ†é¡µçš„è¯„ä»· markdown
pub fn get_comment_pages(
    object_id: &String,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
    comments_msg_helper(object_id)
}

fn comments_msg_helper(
    object_id: &String,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
    SAFC_DB
        .find_comment(object_id)?
        .iter()
        .map(|c: &ObjComment| {
            Ok(format!(
                "ğŸ’¬ *data {} \\| from {} \\| id `{}`*\n\
                {}\n\
                {}\n",
                escape(c.date.as_str()),
                c.source_cate,
                c.id,
                escape(c.description.replace("<br>", "\n").as_str()),
                format_nested_comments(comments_msg_helper(&c.id)?)
            ))
        })
        .collect()
}

/// æ ¼å¼�åŒ–åµŒå¥—è¯„ä»·
fn format_nested_comments(comments: Vec<String>) -> String {
    if !comments.is_empty() {
        comments
            .iter()
            .map(|c| {
                c.lines()
                    .map(|l| format!(" \\| {}", l))
                    .collect::<Vec<_>>()
                    .join("\n")
            })
            .collect::<Vec<_>>()
            .join(escape("---\n").as_str())
    } else {
        // " \\| _æ²™å�‘è™šä½�ä»¥å¾…_".to_owned()
        escape(" â—»\n")
    }
}

#[test]
fn my_test() {
    println!("{}", serde_json::to_string(&ObjectOp::Read).unwrap());
    let msg = get_comment_pages(&"2ac4ae281b9a2528".to_string()).unwrap();
    println!("{:#?}", msg);
}
