use crossterm::{cursor::MoveTo, event, terminal, ExecutableCommand};
use dirs::home_dir;
use serde::{Deserialize, Serialize};
use text_io::read;

use egg_mode::{
    auth::{access_token, authorize_url, request_token},
    error::{Error::TwitterError, TwitterErrors},
    tweet::{like, mentions_timeline, DraftTweet, Tweet},
    KeyPair, Token,
};

use std::{
    clone::Clone,
    fs,
    io::{stdin, stdout, Read, Stdout, Write},
    iter::Iterator,
    process::exit,
};

use tui::{
    backend::{Backend, CrosstermBackend},
    layout::{Constraint, Layout},
    style,
    text::{Span, Spans},
    widgets::{Block, Borders, Paragraph},
    Terminal,
};

#[derive(Clone, Deserialize, Serialize)]
struct Keys {
    key: Option<String>,
    secret: Option<String>,
    access_token: Option<String>,
    access_token_secret: Option<String>,
}

type Term = Terminal<CrosstermBackend<Stdout>>;
type TwitterResult = Result<(), TwitterErrors>;

const CFG_PATH: &str = ".config/qt/keys.toml";
const MENU_TEXT: &str = "(l)ike | (r)eply | (t)weet | (n)ext | (p)revious | e(x)it";

async fn authenticate() -> Token {
    let key_file = home_dir().unwrap().join(CFG_PATH);
    let key_map: Keys = toml::from_str(&fs::read_to_string(&key_file).unwrap()).unwrap();
    if !(key_map.key.is_some() && key_map.secret.is_some()) {
        println!("Please put your consumer key and secret in ~/.config/qt/keys.toml");
        exit(0);
    };
    if key_map.access_token.is_some() {
        let consumer_pair = KeyPair::new(key_map.key.unwrap(), key_map.secret.unwrap());
        let access_token_pair = KeyPair::new(
            key_map.access_token.unwrap(),
            key_map.access_token_secret.unwrap(),
        );

        return Token::Access {
            consumer: consumer_pair,
            access: access_token_pair,
        };
    }

    let mut config_file = key_map.clone();
    let consumer_pair = KeyPair::new(key_map.key.unwrap(), key_map.secret.unwrap());

    let auth_token = request_token(&consumer_pair, "oob").await.unwrap();
    let auth_url = authorize_url(&auth_token);
    print!(
        "\nGo to this URL to authenticate: {}\n\nEnter PIN: ",
        auth_url
    );
    stdout().flush().unwrap();
    let verifier: String = read!();
    println!();
    let token = access_token(consumer_pair, &auth_token, verifier)
        .await
        .unwrap()
        .0;

    match token {
        Token::Access {
            consumer: _,
            access: ref a,
        } => {
            config_file.access_token = Some(a.key.to_string());
            config_file.access_token_secret = Some(a.secret.to_string());
        }
        Token::Bearer(_) => panic!(),
    };
    let key_toml = toml::to_string(&config_file).unwrap();
    fs::write(key_file, key_toml).unwrap();

    token
}

async fn get_mentions(token: &Token) -> Vec<Tweet> {
    mentions_timeline(&token).start().await.unwrap().1.to_vec()
}

async fn send_like(tweet: &Tweet, token: &Token) -> TwitterResult {
    match like(tweet.id, &token).await {
        Ok(_) => Ok(()),
        Err(TwitterError(_, e)) => Err(e),
        Err(_) => panic!(),
    }
}

async fn send_reply(tweet: &Tweet, reply_text: String, token: &Token) -> TwitterResult {
    // TODO should mention everyone in the thread, not just who was directly replied to
    let reply_text = format!(
        "@{} {}",
        tweet.user.as_ref().unwrap().screen_name,
        reply_text
    );
    let result = DraftTweet::new(reply_text)
        .in_reply_to(tweet.id)
        .send(&token)
        .await;
    match result {
        Ok(_) => Ok(()),
        Err(TwitterError(_, e)) => Err(e),
        Err(_) => panic!(),
    }
}

async fn send_tweet(text: String, token: &Token) -> TwitterResult {
    let result = DraftTweet::new(text).send(&token).await;
    match result {
        Ok(_) => Ok(()),
        Err(TwitterError(_, e)) => Err(e),
        Err(_) => panic!(),
    }
}

fn result_view(result: TwitterResult) -> String {
    match result {
        Ok(()) => "Done!".to_string(),
        Err(errs) => errs.errors[0].to_string(),
    }
}

fn tweet_view<'a>(tweet: &'a Tweet) -> Vec<Spans<'a>> {
    let screen_name = &tweet.user.as_ref().unwrap().screen_name;
    let screen_name_msg = format!("@{} says:\n", screen_name);
    let in_response_msg = match &tweet.in_reply_to_screen_name {
        None => "".to_string(),
        Some(name) => format!("in response to @{}\n", name),
    };

    vec![
        Spans::from(Span::styled(
            screen_name_msg,
            style::Style::default().add_modifier(style::Modifier::BOLD),
        )),
        Spans::from(Span::styled(
            in_response_msg,
            style::Style::default().add_modifier(style::Modifier::ITALIC),
        )),
        Spans::from(Span::raw(&tweet.text)),
    ]
}

fn get_key() -> char {
    loop {
        match event::read().unwrap() {
            event::Event::Key(event) => match event.code {
                event::KeyCode::Char(c) => return c,
                _ => (),
            },
            _ => (),
        }
    }
}

fn get_line(terminal: &mut Term) -> Option<String> {
    let mut line = String::new();
    let mut abort: bool = false;
    for byte in stdin().bytes() {
        let next_char = byte.unwrap() as char;
        match next_char {
            '\x1B' => {
                abort = true;
                break;
            }
            // TODO handle newlines in input
            '\n' | '\r' => {
                abort = false;
                break;
            }
            c => {
                print!("{}", c);
                stdout().flush().unwrap();
                line.push(c);
            }
        }
    }
    terminal.clear().unwrap();

    if abort {
        None
    } else {
        Some(line)
    }
}

fn prepare_exit() {
    stdout()
        .execute(terminal::Clear(terminal::ClearType::All))
        .unwrap()
        .execute(MoveTo(0, 0))
        .unwrap();
    terminal::disable_raw_mode().unwrap();
}

async fn run_action(tweet: &Tweet, key: char, token: &Token, terminal: &mut Term) -> String {
    if key == 'x' {
        prepare_exit();
        exit(0);
    };
    draw(
        tweet_view(&tweet),
        vec![Spans::from("Working...")],
        terminal,
    );

    if key == 'l' {
        result_view(send_like(tweet, token).await)
    } else if key == 'r' {
        draw(
            tweet_view(tweet),
            vec![Spans::from("Your reply:")],
            terminal,
        );
        let height = terminal::size().unwrap().1;
        stdout().execute(MoveTo(13, height - 2)).unwrap();
        let reply = get_line(terminal);

        if reply.is_some() {
            result_view(send_reply(tweet, reply.unwrap(), token).await)
        } else {
            "Nevermind.".to_string()
        }
    } else if key == 't' {
        draw(
            tweet_view(tweet),
            vec![Spans::from("Your tweet:")],
            terminal,
        );
        let height = terminal::size().unwrap().1;
        stdout().execute(MoveTo(13, height - 2)).unwrap();
        let text = get_line(terminal);
        if text.is_some() {
            result_view(send_tweet(text.unwrap(), token).await)
        } else {
            "Nevermind.".to_string()
        }
    } else {
        "That doesn't do anything.".to_string()
    }
}

fn draw(main_text: Vec<Spans>, bottom_text: Vec<Spans>, terminal: &mut Term) {
    terminal
        .draw(|f| {
            let layout = Layout::default()
                .constraints([
                    Constraint::Length(3),
                    Constraint::Min(5),
                    Constraint::Length(3),
                ])
                .split(f.size());

            f.render_widget(
                Paragraph::new(vec![Spans::from(MENU_TEXT)])
                    .block(Block::default().borders(Borders::ALL)),
                layout[0],
            );
            f.render_widget(
                Paragraph::new(main_text)
                    .block(Block::default().title("Mentions").borders(Borders::ALL)),
                layout[1],
            );
            f.render_widget(
                Paragraph::new(bottom_text).block(Block::default().borders(Borders::ALL)),
                layout[2],
            );
        })
        .unwrap();
}

async fn render_tweets(tweets: Vec<Tweet>, terminal: &mut Term, token: &Token) {
    let length = tweets.len();
    let mut i = 0;
    while i < length {
        let tweet = &tweets[i];
        draw(tweet_view(&tweet), vec![Spans::from("")], terminal);
        loop {
            let key = get_key();
            let message = match key {
                'n' => {
                    i = i + 1;
                    break;
                }
                'p' => {
                    if i == 0 {
                        prepare_exit();
                        exit(0);
                    }
                    i = i - 1;
                    break;
                }
                c => run_action(&tweet, c, token, terminal).await,
            };
            draw(tweet_view(&tweet), vec![Spans::from(message)], terminal);
        }
    }
}

#[tokio::main]
async fn main() {
    println!("Authenticating...");
    let token = authenticate().await;

    let mut term_backend = CrosstermBackend::new(stdout());
    term_backend.clear().unwrap();
    let mut terminal = Terminal::new(term_backend).unwrap();

    draw(
        vec![Spans::from("Getting mentions...")],
        vec![],
        &mut terminal,
    );
    let tweets = get_mentions(&token).await;
    terminal::enable_raw_mode().unwrap();
    render_tweets(tweets, &mut terminal, &token).await;

    prepare_exit();
}
