use crate::{truncated, Guess};
use anyhow::Result;
use regex::{Regex, RegexBuilder};

const A: &str = "[Ğ°ağŸ…°ğŸ…°ï¸�ğŸ‡¦ğŸ‡¦Ğ¾]";
const C: &str = "[cC]";
const D: &str = "[dÔ�ğŸ‡©]";
const E: &str = "[eEĞµ3â‚¬â„¯ğŸ‡ª]";
const I: &str = "[Ñ–iI1lâ„¹ï¸�ğŸ‡®]";
const K: &str = "[kKğŸ‡°]";
const L: &str = "[lL1|â„“ğŸ‡±]";
const M: &str = "[mM]";
const N: &str = "[nNâ„•ğŸ‡³]";
const O: &str = "[Ğ¾o0ğŸ…¾ï¸�ğŸ‡´]";
const P: &str = "[Ñ€pÏ�Ï±ğŸ…¿ï¸�ğŸ‡µ]";
const R: &str = "[Ñ€rğŸ‡·]";
const S: &str = "[sSĞ…]";
const T: &str = "[tTĞ¢7â€ ğŸ‡¹]";
const W: &str = "[wWğŸ‡¼]";

#[derive(Clone)]
pub struct RegularExpression {
    airdrop: Regex,
    wallet: Regex,
    token: Regex,
    claim: Regex,
    swap: Regex,
    cleanup: Regex,
    reward: Regex,
}

fn to_regex<I>(chars: I) -> Result<Regex>
where
    I: IntoIterator,
    I::Item: AsRef<str>,
{
    Ok(RegexBuilder::new(
        chars
            .into_iter()
            .map(|s| s.as_ref().to_string())
            .collect::<Vec<_>>()
            .join(r"\s?")
            .as_str(),
    )
    .case_insensitive(true)
    .build()?)
}

impl RegularExpression {
    pub async fn new() -> Result<Self> {
        let airdrop = to_regex([A, I, R, D, R, O, P])?;
        let wallet = to_regex([W, A, L, L, E, T])?;
        let token = to_regex([T, O, K, E, N])?;
        let claim = to_regex([C, L, A, I, M])?;
        let swap = to_regex([S, W, A, P])?;
        let reward = to_regex([R, E, W, A, R, D])?;
        let cleanup = Regex::new(r"\s")?;
        Ok(Self {
            airdrop,
            wallet,
            token,
            claim,
            swap,
            reward,
            cleanup,
        })
    }

    pub async fn is_spam(&self, txt: &str) -> Result<Guess> {
        let cleaned = self.cleanup.replace_all(txt, " ").to_string();
        let result = self.airdrop.is_match(cleaned.as_str())
            || (self.wallet.is_match(cleaned.as_str()) && self.token.is_match(cleaned.as_str()))
            || (self.wallet.is_match(cleaned.as_str()) && self.reward.is_match(cleaned.as_str()))
            || (self.claim.is_match(cleaned.as_str()) && self.swap.is_match(cleaned.as_str()));
        if result {
            log::info!("Message detected as spam by RegularExpression");
            log::debug!("{}", truncated(txt));
        }
        Ok(Guess {
            is_spam: result,
            score: None,
            scores: vec![],
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tokio::{fs, io::AsyncReadExt};

    #[tokio::test]
    async fn test_is_spam() {
        let test_cases = vec![
            ("airdrop", true),
            ("Ğ°irdrop", true), // Cyrillic Ğ°
            ("Ğ°irdrĞ¾Ñ€", true), // Cyrillic Ğ° and Cyrillic Ğ¾
            ("Ğ°irdrĞ¾p", true), // Cyrillic Ğ° and Cyrillic Ğ¾
            ("Ğ°irdrĞ¾Ñ€", true), // Cyrillic Ğ°, Cyrillic Ğ¾, and Cyrillic Ñ€
            ("airdrĞ¾Ñ€", true), // Cyrillic Ğ¾
            ("airdrĞ¾p", true), // Cyrillic Ğ¾
            ("airdrĞ¾Ñ€", true), // Cyrillic Ğ¾ and Cyrillic Ñ€
            ("Ğ°irdrĞ¾p", true), // Cyrillic Ğ°, and Cyrillic Ğ¾
            ("Ğ°irdrĞ¾Ñ€", true), // Cyrillic Ğ°, Cyrillic Ğ¾, and Cyrillic Ñ€
            ("Ğ°irdrop", true), // Cyrillic Ğ°
            ("Ğ°irdrĞ¾Ñ€", true), // Cyrillic Ğ° and Cyrillic Ñ€
            ("Ğ°irdrop", true), // Cyrillic Ğ°
            ("airÔ�rop", true), // Cyrillic Ô�
            ("aÑ–rÔ�rop", true), // Cyrillic Ñ– and Cyrillic Ô�
            ("airÔ�roÑ€", true), // Cyrillic Ñ€
            ("Ğ°irdrĞ¾Ñ€", true), // Cyrillic Ğ° and Cyrillic Ñ€
            ("aÑ–rdrop", true), // Cyrillic Ñ–
            ("Ğ°Ñ–rdrop", true), // Cyrillic Ğ° and Cyrillic Ñ–
            ("Ğ°Ñ–rdrop", true), // Cyrillic Ğ° and Cyrillic Ñ–
            ("Ğ°Ñ–rdrop", true), // Cyrillic Ğ° and Cyrillic Ñ–
            ("aÑ–rdroÑ€", true), // Cyrillic Ñ– and Cyrillic Ñ€
            ("Ğ°Ñ–rdroÑ€", true), // Cyrillic Ğ°, Cyrillic Ñ–, and Cyrillic Ñ€
            ("Ğ°irdrĞ¾Ñ€", true), // Cyrillic Ğ° and Cyrillic Ñ€
            ("aÑ–rÔ�rĞ¾Ñ€", true), // Cyrillic Ñ–, Cyrillic Ô�, and Cyrillic Ñ€
            ("aÑ–rÔ�rop", true), // Cyrillic Ñ– and Cyrillic Ô�
            ("airdroÑ€", true), // Cyrillic Ñ€
            ("airÔ�rop", true), // Greek delta, Î”
            ("Ğ°irdrĞ¾Ñ€", true), // Greek o, Î¿
            ("Ğ°Ñ–rÔ�rop", true), // Greek iota, Î¹
            ("airÔ�roÑ€", true), // Greek rho, Ï�
            ("Ğ°irdrĞ¾Ñ€", true), // Greek omicron, Î¿
            ("aÑ–rdrop", true), // Greek iota, Î¹
            ("Ğ°Ñ–rdrop", true), // Greek alpha, Î±
            ("Ğ°Ñ–rdrop", true), // Greek iota, Î¹
            ("aÑ–rdroÑ€", true), // Greek iota, Î¹, and rho, Ï�
            ("Ğ°irdrĞ¾Ñ€", true), // Greek omicron, Î¿, and rho, Ï�
            ("aÑ–rÔ�rĞ¾Ñ€", true), // Greek iota, Î¹, delta, Î”, and rho, Ï�
            ("aÑ–rÔ�rop", true), // Greek iota, Î¹, and delta, Î”
            ("airdroÑ€", true), // Greek rho, Ï�
            ("Ğ¡lĞ°im  Q Ğ¡OMMUNITY Ğ�IRDROP\n Join the Q movement.", true), // snippet from a real one
            ("ğŸ…°irdrop", true), // with emoji
            ("ğŸ…°ï¸�â„¹ï¸�irdrğŸ…¾ï¸�ğŸ‡µ", true), // with emojis
            ("air drop", true), // with space
            ("a i r d r o p", true), // with single spaces
            ("a i r d r o p", true), // with different kids of spaces
            ("ğŸ‡¦ ğŸ‡® ğŸ‡· ğŸ‡© ğŸ‡· ğŸ‡´ ğŸ‡µ", true), // with special characters and spaces
            ("42", false),
            ("", false),
            ("token", false),
            ("wallet", false),
            ("wallet tokens", true),
            ("tokens wallet", true),
            ("wallĞµt and tokĞµn", true), // with Cyrillic Ğµ
        ];
        for (word, expected) in test_cases {
            for w in [word, word.to_uppercase().as_str()] {
                let model = RegularExpression::new().await.unwrap();
                let got = model.is_spam(w).await.unwrap();
                assert_eq!(
                    got.is_spam, expected,
                    "expected: {:?} for {:?}, got: {:?}",
                    expected, w, got.is_spam
                );
                assert_eq!(got.score, None);
            }
        }
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn test_is_spam_with_test_data() {
        let model = RegularExpression::new().await.unwrap();
        let mut entries = fs::read_dir("test_data").await.unwrap();
        while let Some(entry) = entries.next_entry().await.unwrap() {
            let path = entry.path();
            let mut contents = String::new();
            let mut file = fs::File::open(&path).await.unwrap();
            file.read_to_string(&mut contents).await.unwrap();
            let got = model.is_spam(contents.as_str()).await.unwrap();
            assert!(got.is_spam, "{} was not flagged as spam", path.display(),);
        }
    }
}
