use itertools::Itertools;
use rand::seq::SliceRandom;
use std::borrow::Cow;
use std::convert::TryInto;
use std::io::prelude::*;
use std::{collections::HashMap, io::Write};

const APP_TOKEN_ENV_VAR: &str = "TWITTER_APP_TOKEN";
const APP_SECRET_ENV_VAR: &str = "TWITTER_APP_SECRET";
const USER_TOKEN_ENV_VAR: &str = "TWITTER_USER_TOKEN";
const USER_SECRET_ENV_VAR: &str = "TWITTER_USER_SECRET";
const IMG_HEIGHT: u32 = 1000;

static APP_USER_AGENT: &str = concat!(
    "bot_",
    env!("CARGO_PKG_NAME"),
    "/",
    env!("CARGO_PKG_VERSION"),
    " ",
    "reqwest",
);

static CB_URL: &str = "http://localhost:6969/cb";

fn render_svg(data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    let opt = usvg::Options::default();
    let rtree = usvg::Tree::from_data(&data, &opt.to_ref()).expect("couldn't parse");
    let mut pixmap =
        tiny_skia::Pixmap::new(IMG_HEIGHT, IMG_HEIGHT).ok_or("Error creating pixmap")?;
    resvg::render(
        &rtree,
        usvg::FitTo::Size(IMG_HEIGHT, IMG_HEIGHT),
        pixmap.as_mut(),
    )
    .ok_or_else(|| "Error rendering svg")?;
    let mut bigger_pixmap = tiny_skia::Pixmap::new(IMG_HEIGHT / 9 * 16, IMG_HEIGHT)
        .ok_or("Error creating bigger pixmap")?;
    bigger_pixmap
        .draw_pixmap(
            (bigger_pixmap.width() / 2 - IMG_HEIGHT / 2)
                .try_into()
                .unwrap(),
            0,
            pixmap.as_ref(),
            &tiny_skia::PixmapPaint::default(),
            tiny_skia::Transform::identity(),
            None,
        )
        .ok_or("Error drawing onto bigger pixmap")?;
    let png_data = bigger_pixmap.encode_png()?;
    Ok(png_data)
}

enum PostData<'a> {
    Empty,
    Multipart(reqwest::blocking::multipart::Form),
    Data(&'a [(&'a str, &'a str)]),
}

enum APIAction<'a> {
    Get,
    Post(PostData<'a>),
}

struct TwitterEndpoint<'a>(&'a str);

impl TryInto<reqwest::Url> for TwitterEndpoint<'_> {
    type Error = url::ParseError;
    fn try_into(self) -> Result<reqwest::Url, Self::Error> {
        reqwest::Url::parse(&format!("https://api.twitter.com/{}", self.0))
    }
}

// Make an authed twitter API request
fn twitter_api<'a>(
    url: reqwest::Url,
    user_token: Option<&oauth1::Token>,
    action: APIAction,
    extra_oauth_params: &[(&str, &str)],
) -> StdError<reqwest::blocking::Response> {
    let consumer_token = oauth1::Token::new(
        std::env::var(APP_TOKEN_ENV_VAR)?,
        std::env::var(APP_SECRET_ENV_VAR)?,
    );
    let mut headers = reqwest::header::HeaderMap::new();
    let mut oauth_params: HashMap<&str, Cow<str>> = extra_oauth_params
        .iter()
        .cloned()
        .map(|(x, y)| (x, y.into()))
        .collect();

    // If the request is a key/value form post, we need to include those parameters when
    // generating the signature.
    match action {
        APIAction::Post(PostData::Data(d)) => {
            oauth_params.extend(d.iter().cloned().map(|(x, y)| (x, y.into())))
        }
        _ => {}
    }

    headers.insert(
        reqwest::header::AUTHORIZATION,
        reqwest::header::HeaderValue::from_str(&oauth1::authorize(
            if matches!(action, APIAction::Post(_)) {
                "POST"
            } else {
                "GET"
            },
            url.as_str(),
            &consumer_token,
            user_token,
            Some(oauth_params),
        ))?,
    );

    let client = reqwest::blocking::Client::builder()
        .user_agent(APP_USER_AGENT)
        .default_headers(headers)
        .build()?;
    let req = match action {
        APIAction::Get => client.get(url),
        APIAction::Post(PostData::Empty) => client.post(url),
        APIAction::Post(PostData::Data(data)) => client.post(url).form(data),
        APIAction::Post(PostData::Multipart(form)) => client.post(url).multipart(form),
    };

    let res = req.send()?;
    if !res.status().is_success() {
        return Err(format!(
            "Got non-200 response: status {}, {}",
            res.status(),
            res.text()?
        )
        .into());
    }
    Ok(res)
}

type StdError<T> = Result<T, Box<dyn std::error::Error>>;

fn main() -> StdError<()> {
    let matches = clap::App::new(env!("CARGO_PKG_NAME"))
        .version(env!("CARGO_PKG_VERSION"))
        .subcommand(clap::SubCommand::with_name("authorize").about("Authorize the twitter application to access a user's account by popping open a web browser and returning the credentials once authorized.")).get_matches();
    match matches.subcommand() {
        ("authorize", _) => do_authorize(),
        _ => run_bot(),
    }
}

fn do_authorize() -> StdError<()> {
    println!("Authorizing you lol!");

    // Oauth1 leg 1
    let res = twitter_api(
        TwitterEndpoint("oauth/request_token").try_into()?,
        None,
        APIAction::Post(PostData::Empty),
        &[("oauth_callback", CB_URL)],
    )?
    .text()?;

    let returned_params: HashMap<&str, &str> = res
        .split("&")
        .map(|s| s.split("=").collect_tuple())
        .collect::<Option<_>>()
        .ok_or("Unexpected oauth step 1 response")?;

    // Oauth1 leg 2
    let user_url = reqwest::Url::parse_with_params(
        "https://api.twitter.com/oauth/authenticate",
        [("oauth_token", returned_params["oauth_token"])],
    )?;
    println!("Plz do the thing in the browser");
    webbrowser::open(user_url.as_str())?;

    let listener = std::net::TcpListener::bind("127.0.0.1:6969")?;
    let mut stream = listener.incoming().next().ok_or("Error getting stream")??;
    let mut buf = [0u8; 4096];
    stream.read(&mut buf[..])?;

    let target = std::str::from_utf8(
        buf.split(|c| *c == b' ')
            .skip(1)
            .next()
            .ok_or("No target found")?,
    )?;
    let oauth_verifier = reqwest::Url::parse("https://example.net/")?
        .join(target.into())?
        .query_pairs()
        .find_map(|(k, v)| {
            if k == "oauth_verifier" {
                Some(v.into_owned())
            } else {
                None
            }
        })
        .ok_or("no oauth_verifier in response")?;
    stream.write(b"HTTP/1.1 200 OK\r\n\r\nThanks lmao\r\n")?;
    stream.shutdown(std::net::Shutdown::Read)?;

    // Oauth1 leg 3
    let res = twitter_api(
        TwitterEndpoint("oauth/access_token").try_into()?,
        None,
        APIAction::Post(PostData::Data(&[("oauth_verifier", &oauth_verifier)])),
        &[("oauth_token", returned_params["oauth_token"])],
    )?
    .text()?;
    let returned_params: HashMap<&str, &str> = res
        .split("&")
        .map(|s| s.split("=").collect_tuple())
        .collect::<Option<_>>()
        .ok_or("Unexpected oauth step 3 response")?;

    println!(
        "Authorized for {}.\nRun with {}={} {}={}",
        returned_params["screen_name"],
        USER_TOKEN_ENV_VAR,
        returned_params["oauth_token"],
        USER_SECRET_ENV_VAR,
        returned_params["oauth_token_secret"]
    );

    Ok(())
}

fn upload_image(user_token: &oauth1::Token, img: Cow<'static, [u8]>) -> StdError<u64> {
    let form = reqwest::blocking::multipart::Form::new()
        .part("media", reqwest::blocking::multipart::Part::bytes(img));
    let res: serde_json::Value = twitter_api(
        "https://upload.twitter.com/1.1/media/upload.json".try_into()?,
        Some(&user_token),
        APIAction::Post(PostData::Multipart(form)),
        &[],
    )?
    .json()?;
    Ok(res["media_id"].as_u64().ok_or("media_id not u64!")?)
}

fn run_bot() -> StdError<()> {
    let user_token = oauth1::Token::new(
        std::env::var(USER_TOKEN_ENV_VAR)?,
        std::env::var(USER_SECRET_ENV_VAR)?,
    );
    let args: Vec<String> = std::env::args().collect();
    if args.len() < 2 {
        println!("usage: ./thing out.png");
    }
    // Parse CSS selectors to scrape elements
    let gallerybox_sel = scraper::Selector::parse(".mw-body-content li.gallerybox")
        .map_err(|e| format!("{:?}", e))?;
    let link_sel = scraper::Selector::parse("a.image").map_err(|e| format!("{:?}", e))?;
    let title_sel = scraper::Selector::parse(".gallerytext p").map_err(|e| format!("{:?}", e))?;
    let original_sel = scraper::Selector::parse(".fullMedia a").map_err(|e| format!("{:?}", e))?;

    // Fetch stuff!
    let client = reqwest::blocking::Client::builder()
        .user_agent(APP_USER_AGENT)
        .build()?;
    println!("Fetching main page");
    let txt = client
        .get("https://en.wikipedia.org/wiki/ISO_7010")
        .send()?
        .text()?;
    let page = scraper::Html::parse_document(txt.as_str());
    let things = page
        .select(&gallerybox_sel)
        .map(|a| {
            let link = a
                .select(&link_sel)
                .next()
                .unwrap()
                .value()
                .attr("href")
                .unwrap();
            let title = a
                .select(&title_sel)
                .next()
                .unwrap()
                .text()
                .collect::<String>()
                .trim()
                .to_owned();
            (title, link)
        })
        .collect::<Vec<(String, &str)>>();
    // Pick a random entry and fetch the original file

    let (title, link) = things
        .choose(&mut rand::thread_rng())
        .ok_or_else(|| "got no images m8")?;
    println!("Fetching image page");
    let media_page = client
        .get(format!("https://en.wikipedia.org{}", link))
        .send()?
        .text()?;
    let page = scraper::Html::parse_document(media_page.as_str());
    let link = page
        .select(&original_sel)
        .next()
        .unwrap()
        .value()
        .attr("href")
        .unwrap();
    let svg = client.get(format!("https:{}", link)).send()?.bytes()?;
    let png_data = render_svg(&svg)?;

    let user: serde_json::Value = twitter_api(
        TwitterEndpoint("1.1/account/verify_credentials.json").try_into()?,
        Some(&user_token),
        APIAction::Get,
        &[],
    )?
    .json()?;
    println!(
        "Tweeting for user @{}, (id: {})",
        user["screen_name"], user["id"]
    );

    println!("Uploading image...");
    let img_id = upload_image(&user_token, Cow::from(png_data))?;
    let tweet = title;
    println!("Sending tweet...");
    twitter_api(
        TwitterEndpoint("1.1/statuses/update.json").try_into()?,
        Some(&user_token),
        APIAction::Post(PostData::Data(&[
            ("media_ids", &img_id.to_string()),
            ("status", tweet),
        ])),
        &[],
    )?;
    Ok(())
}
