//! 文章 slug 生成与唯一性校验。
//!
//! 将标题转换为小写、仅含字母数字与连字符/下划线的 URL 友好形式，
//! 并检测数据库中是否已存在，必要时追加数字后缀。
//! 仅在 `feature = "server"` 时访问数据库。

#![allow(clippy::unused_unit, deprecated)]

#[cfg(feature = "server")]
use dioxus::prelude::*;

#[cfg(feature = "server")]
use pinyin::ToPinyin;

#[cfg(feature = "server")]
/// 将标题转换为 URL 友好的 slug。
///
/// 汉字转为无声调拼音（每字独立成词，用 `-` 分隔，如 `你好` → `ni-hao`）；
/// ASCII 字母数字与 `-`/`_` 保留；其余字符替换为 `-`，连续 `-` 合并。
/// 结果截断至 100 字符；若全部被过滤则回退为当前时间戳。
///
/// 多音字取默认读音（不处理歧义），博客 slug 场景足够。
///
/// 单遍状态机实现：旧实现 `to_lowercase` 分配一次 String + `split.collect::<Vec>` +
/// `join` 再分配 + `take.collect` 第三次分配，共 4 次堆分配、3 遍扫描。
/// 现用 `prev_dash` 状态在单遍内合并连续 `-` 并按需截断，1 次分配、1 遍扫描。
pub fn slugify(title: &str) -> String {
    let mut out = String::with_capacity(title.len());
    // prev_dash = true 表示当前不应再输出 `-`（处于开头，或上一输出字符已是 `-`）。
    // 等价于旧实现字符流 + split('-').filter(!empty).join('-') 的合并/去首尾效果。
    let mut prev_dash = true;
    let mut len = 0usize;

    /// 输出一个字符，截断到 100。返回 false 表示已达上限应停止。
    fn push_char(out: &mut String, len: &mut usize, c: char) -> bool {
        if *len >= 100 {
            return false;
        }
        out.push(c);
        *len += 1;
        true
    }

    for c in title.chars() {
        // 汉字优先转拼音：to_pinyin() 对非汉字（含 ascii）返回 None。
        if let Some(py) = c.to_pinyin() {
            // 拼音内部不含 `-`；连续字母直接拼接，与旧实现 push_str 行为一致。
            for pc in py.plain().chars() {
                if !push_char(&mut out, &mut len, pc) {
                    break;
                }
            }
            // 汉字成词，后接 `-`（合并连续、去除尾部由末尾清理负责）。
            // 拼音后必然接 `-`，所以 prev_dash 在此无条件置 true，中间无读。
            if !push_char(&mut out, &mut len, '-') {
                break;
            }
            prev_dash = true;
        } else {
            // ASCII 小写化避免对整串 to_lowercase 的全量分配。
            let lc = c.to_ascii_lowercase();
            if lc.is_ascii_alphanumeric() {
                if !push_char(&mut out, &mut len, lc) {
                    break;
                }
                prev_dash = false;
            } else if lc == '_' {
                // 下划线保留原样（split('-') 不把它当分隔符，行为一致）。
                if !push_char(&mut out, &mut len, '_') {
                    break;
                }
                prev_dash = false;
            } else {
                // `-` 或其他字符（空格/标点）：仅当前面非分隔符时输出一个 `-`，
                // 连续分隔符合并为一个；首尾的 `-` 由 prev_dash 初值和末尾清理负责。
                if !prev_dash {
                    if !push_char(&mut out, &mut len, '-') {
                        break;
                    }
                    prev_dash = true;
                }
            }
        }
    }

    // 去除尾部 `-`（开头已被 prev_dash 初值 true 挡住）。
    while out.ends_with('-') {
        out.pop();
    }

    if out.is_empty() {
        return chrono::Utc::now().timestamp().to_string();
    }

    out
}

#[cfg(feature = "server")]
/// 校验 slug 是否为空且仅含合法字符、长度不超过 200。
pub fn is_valid_slug(slug: &str) -> bool {
    if slug.is_empty() || slug.len() > 200 {
        return false;
    }
    slug.chars()
        .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
}

#[cfg(feature = "server")]
/// 确保生成的 slug 在数据库中唯一。
///
/// 若 `exclude_id` 不为空，则排除该文章自身；
/// 当冲突时依次尝试 `base-2`、`base-3` …… 直到生成唯一值。
///
/// 该函数应在事务内调用，确保与后续 INSERT/UPDATE 的 slug 唯一性检查
/// 在同一个事务中完成，避免并发竞态。
pub async fn ensure_unique_slug(
    tx: &deadpool_postgres::Transaction<'_>,
    base: &str,
    exclude_id: Option<i32>,
) -> Result<String, ServerFnError> {
    use crate::api::error::AppError;

    let mut candidate = base.to_string();
    let mut suffix = 2;

    loop {
        // 查询当前候选 slug 是否已存在（排除指定文章 ID）。
        let exists = if let Some(exclude) = exclude_id {
            tx.query_opt(
                "SELECT 1 FROM posts WHERE slug = $1 AND deleted_at IS NULL AND id != $2",
                &[&candidate, &exclude],
            )
            .await
            .map_err(AppError::query)?
            .is_some()
        } else {
            tx.query_opt(
                "SELECT 1 FROM posts WHERE slug = $1 AND deleted_at IS NULL",
                &[&candidate],
            )
            .await
            .map_err(AppError::query)?
            .is_some()
        };

        if !exists {
            return Ok(candidate);
        }

        candidate = format!("{}-{}", base, suffix);
        suffix += 1;

        // 防止无限循环：slug 总长度超过 200 时直接报错。
        if candidate.len() > 200 {
            return Err(AppError::Internal("无法生成唯一 slug").into());
        }
    }
}

#[cfg(all(test, feature = "server"))]
mod tests {
    use super::*;

    #[test]
    fn slugify_ascii_title() {
        assert_eq!(slugify("Hello World"), "hello-world");
    }

    #[test]
    fn slugify_special_characters() {
        assert_eq!(slugify("Hello, World! (2024)"), "hello-world-2024");
    }

    #[test]
    fn slugify_chinese_characters() {
        // 汉字逐字转拼音，用 `-` 分隔；混入的 ascii 自然衔接到末尾。
        assert_eq!(slugify("你好世界"), "ni-hao-shi-jie");
        assert_eq!(slugify("你好世界 hello"), "ni-hao-shi-jie-hello");
    }

    #[test]
    fn slugify_mixed_chinese_ascii() {
        // Rust 入门指南 → rust + ru-men-zhi-nan
        assert_eq!(slugify("Rust 入门指南"), "rust-ru-men-zhi-nan");
    }

    #[test]
    fn slugify_chinese_with_punctuation() {
        // 标点替换为 `-`，随后与拼音分隔符合并。
        assert_eq!(slugify("你好，世界！"), "ni-hao-shi-jie");
    }

    #[test]
    fn slugify_collapses_dashes() {
        assert_eq!(slugify("a---b"), "a-b");
    }

    #[test]
    fn slugify_empty_returns_timestamp() {
        let slug = slugify("");
        let _: i64 = slug.parse().expect("should be a valid timestamp");
    }

    #[test]
    fn slugify_truncates_at_100_chars() {
        let long_title = "a".repeat(200);
        assert!(slugify(&long_title).len() <= 100);
    }

    #[test]
    fn slugify_preserves_underscores() {
        assert_eq!(slugify("hello_world"), "hello_world");
    }

    #[test]
    fn is_valid_slug_normal() {
        assert!(is_valid_slug("hello-world_123"));
    }

    #[test]
    fn is_valid_slug_rejects_empty() {
        assert!(!is_valid_slug(""));
    }

    #[test]
    fn is_valid_slug_rejects_too_long() {
        let long_slug = "a".repeat(201);
        assert!(!is_valid_slug(&long_slug));
    }

    #[test]
    fn is_valid_slug_accepts_max_length() {
        let slug = "a".repeat(200);
        assert!(is_valid_slug(&slug));
    }

    #[test]
    fn is_valid_slug_rejects_special_chars() {
        assert!(!is_valid_slug("hello world"));
        assert!(!is_valid_slug("hello.world"));
        assert!(!is_valid_slug("hello!world"));
    }

    #[test]
    fn is_valid_slug_accepts_chinese() {
        assert!(is_valid_slug("你好-world"));
    }

    #[test]
    fn slugify_all_special_characters_returns_timestamp() {
        let slug = slugify("!@#$%^&*()+=[]{}|\\;:'\",.<>/?`~");
        let _: i64 = slug.parse().expect("should be a valid timestamp");
    }

    #[test]
    fn slugify_only_whitespace_returns_timestamp() {
        let slug = slugify("   \t\n  ");
        let _: i64 = slug.parse().expect("should be a valid timestamp");
    }

    #[test]
    fn slugify_leading_and_trailing_dashes() {
        assert_eq!(slugify("-hello-world-"), "hello-world");
        assert_eq!(slugify("---hello---world---"), "hello-world");
    }

    #[test]
    fn is_valid_slug_mixed_chinese_and_digits() {
        assert!(is_valid_slug("你好123"));
        assert!(is_valid_slug("123你好456"));
    }

    #[test]
    fn is_valid_slug_exact_200_char_boundary() {
        let slug = "a".repeat(200);
        assert!(is_valid_slug(&slug));
        let slug = "a".repeat(201);
        assert!(!is_valid_slug(&slug));
    }
}
