use futures_util::TryStreamExt;
use game::{Command, Game};
use mastodon_async::{
    entities::notification::NotificationType,
    helpers::{cli, toml},
    prelude::*,
    Language, Result, StatusBuilder,
};
use once_cell::sync::Lazy;
use regex::Regex;
use reqwest::ClientBuilder;
use std::cmp::min;
use tokio::{
    select,
    sync::mpsc,
    time::{sleep, Duration},
};
use tokio_util::sync::CancellationToken;

mod game;
mod rps;

/// Admin account. It can send administrative commands.
const ADMIN: &str = "modulux@node.isonomia.net";

/// Quit command.
const QUIT: &str = "!QUIT!";

/// Instance to connect to.
const INSTANCE: &str = "https://botsin.space";

// Maximum backing off level in powers of 2 of seconds (default: 8 = 256 sec = about 4 min).
const MAX_BL: u8 = 8;

/// Clean a toot.
async fn clean(s: &str) -> String {
    static CLEAN: Lazy<Regex> =
        Lazy::new(|| Regex::new(r"<a.*?</a>").expect("Problem compiling cleanup regexp."));
    nanohtml2text::html2text(&CLEAN.replace_all(s, ""))
        .trim()
        .to_string()
        .to_lowercase()
}

/// Print out the text of a toot as plain text.
async fn print_status(s: &Status) {
    println!(
        "Received from {}: {}",
        s.account.acct,
        clean(&s.content).await
    );
}

#[tokio::main]
async fn main() -> Result<()> {
    let token = CancellationToken::new();
    let token2 = token.clone();

    // g is a vector of games.
    let mut g: Vec<Box<dyn Game>> = vec![Box::new(rps::Rps::new())];

    // We add Rock Paper Scissors..
    // If you write another game you need to create a file, use mod, and add it like this.
    // g.push(Box::new(rps::Rps::new()));

    let (cs, mut cr) = mpsc::unbounded_channel();

    let web_client = ClientBuilder::new()
        .user_agent("Werewolf 0.1")
        .build()
        .expect("Problem building Reqwest client.");

    let mastodon = if let Ok(data) = toml::from_file("mastodon-data.toml") {
        Mastodon::new(web_client, data)
    } else {
        register().await?
    };

    let m = mastodon.clone();
    tokio::spawn(async move {
        println!("Initiating main event loop.");
        while let Some(event) = cr.recv().await {
            match event {
                Event::Notification(n) if n.notification_type == NotificationType::Mention => {
                    let sender = n.account.acct;
                    // Time to check if we got a quit message.
                    let s = n
                        .status
                        .expect("Event is a notification but does not have a status.");
                    let content = clean(&s.content).await;
                    if sender == ADMIN && s.content.contains(QUIT) {
                        println!("Admin sent us the shut down command.");
                        token.cancel();
                        break;
                    }
                    print_status(&s).await;
                    // Treat the toot  like a command.
                    let comm = Command { sender, content };
                    // We iterate on each game.
                    for i in g.iter_mut() {
                        // Asking it if we must reply.
                        let r = i.next(&comm);
                        // And for each reply.
                        // Take the visibility.
                        let v = r.1;
                        for j in r.0 {
                            // We send it.
                            println!("Sent: {}", j);
                            let status = StatusBuilder::new()
                                .status(j.to_string())
                                .language(Language::Eng)
                                .visibility(v)
                                .build();
                            let _ =
                                post_status(&m, status.expect("Problem posting a status.")).await;
                        }
                    }
                }
                _ => (),
            }
        }
    });

    let mut l = 0;
    let mut bl = 0;
    'stream: loop {
        println!(
            "Initiating streaming API... Loop: {}. Backing off level: {}.",
            l, bl
        );
        let stream = match mastodon.stream_notifications().await {
            Ok(stream) => {
                if bl > 0 {
                    bl -= 1;
                }
                stream
            }
            _ => {
                sleep(Duration::from_secs(2u64.pow(min(MAX_BL, bl).into()))).await;
                if bl < MAX_BL.into() {
                    bl += 1;
                }
                println!("Backing off level now {}.", bl);
                continue;
            }
        };

        select! {
        _ = token2.cancelled() =>
        {
        println!("Shutting down...");
        break 'stream;
        }

        _ =
        stream.try_for_each(|(event, _)| async {
        cs.send(event).expect("Problem sending event through channel.");
        Ok(())
        })
        => {
        if bl < MAX_BL {
            bl += 1;
            println!("Streaming terminated. Backing off level now {}", bl);
        }}
        None = async {
        loop {
        sleep(Duration::from_secs(30)).await;
        if bl > 0 {
            bl -= 1;
            println!("Backing off level now {}.", bl);}
        }
        Some(())
        }
        =>
        {}
        };
        println!("Streaming terminated. Loop: {}. Backing off: {}", l, bl);
        l += 1;
    }

    Ok(())
}

async fn register() -> Result<Mastodon> {
    let client = ClientBuilder::new()
        .user_agent("Werewolf 0.1")
        .build()
        .expect("Problem building Reqwest client for registration.");
    let registration = Registration::new_with_client(INSTANCE, client)
        .client_name("Werewolf 0.1")
        .scopes(Scopes::read_all() | Scopes::write_all() | Scopes::follow())
        .build()
        .await?;
    let mastodon = cli::authenticate(registration).await?;

    // Save app data for using on the next run.
    toml::to_file(&mastodon.data, "mastodon-data.toml")?;

    Ok(mastodon)
}

async fn post_status(m: &Mastodon, s: NewStatus) -> Result<()> {
    m.new_status(s).await?;
    // println!("Sent: {}", clean(&st.content.to_string()).await);
    Ok(())
}
