use crate::{get_client, StdError};
use itertools::Itertools;
use std::borrow::Cow;
use std::convert::TryInto;
use std::fmt;
use std::io::prelude::*;
use std::{collections::HashMap, io::Write};
use tracing::{event, instrument, Level};

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";

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

pub fn user_token_from_env() -> oauth1::Token<'static> {
    oauth1::Token::new(
        std::env::var(USER_TOKEN_ENV_VAR).expect("No user token env var"),
        std::env::var(USER_SECRET_ENV_VAR).expect("No user secret env var"),
    )
}

pub enum TwitterEndpoint {
    OauthRequestToken,
    OauthAccessToken,
    OauthAuthenticate,
    UpdateStatus,
    UserTimeline,
    VerifyCredentials,
}

impl std::fmt::Display for TwitterEndpoint {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        const BASE: &str = "https://api.twitter.com";
        let endpoint = match self {
            Self::OauthAuthenticate => "oauth/authenticate",
            Self::OauthRequestToken => "oauth/request_token",
            Self::OauthAccessToken => "oauth/access_token",
            Self::UpdateStatus => "1.1/statuses/update.json",
            Self::UserTimeline => "1.1/statuses/user_timeline.json",
            Self::VerifyCredentials => "1.1/account/verify_credentials.json",
        };
        write!(f, "{}/{}", BASE, endpoint)
    }
}

impl TryInto<reqwest::Url> for TwitterEndpoint {
    type Error = url::ParseError;
    fn try_into(self) -> Result<reqwest::Url, Self::Error> {
        reqwest::Url::parse(&self.to_string())
    }
}

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

impl fmt::Debug for PostData<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}",
            match self {
                Self::Empty => "Empty",
                Self::Multipart(_) => "Multipart",
                Self::Data(_) => "Data",
            }
        )
    }
}

#[derive(Debug)]
pub enum APIAction<'a> {
    Get,
    Post(PostData<'a>),
}

impl APIAction<'_> {
    pub fn get_verb(&self) -> &'static str {
        match self {
            Self::Get => "GET",
            Self::Post(_) => "POST",
        }
    }
}
// Make an authed twitter API request
#[instrument(skip(user_token), fields(url=url.to_string()))]
pub 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 params: HashMap<&str, Cow<str>> = extra_oauth_params
        .iter()
        .cloned()
        .map(|(x, y)| (x, y.into()))
        .collect();

    // Copy all our query parameters and add them to the list of params for oauth1 signature
    // generation.
    // This is a bit awkward, as params is a map from &str to Cow<str> but query_pairs() returns
    // (Cow<str>, Cow<str>) tuples. So we call into_owned to make (String, String) tuples, and then
    // borrow from there. If there's a way to do it without copying, I couldn't find it.
    let pairs: Vec<_> = url.query_pairs().into_owned().collect();
    for (k, v) in &pairs {
        params.insert(k, Cow::Borrowed(v));
    }

    // If the request is a key/value form post, we also need to include those parameters when
    // generating the signature.
    if let APIAction::Post(PostData::Data(d)) = action {
        params.extend(d.to_owned())
    }

    // The url used to generate the signature must not include the query params
    let mut url_sans_query = url.clone();
    url_sans_query.set_query(None);
    headers.insert(
        reqwest::header::AUTHORIZATION,
        reqwest::header::HeaderValue::from_str(&oauth1::authorize(
            action.get_verb(),
            url_sans_query.as_str(),
            &consumer_token,
            user_token,
            Some(params),
        ))?,
    );
    let client = get_client(Some(headers))?;
    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),
    };
    event!(Level::INFO, "Sending request");
    let res = req.send()?;
    if !res.status().is_success() {
        return Err(format!(
            "Got non-200 response: status {}, {}",
            res.status(),
            res.text()?
        )
        .into());
    }
    Ok(res)
}

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

    // Oauth1 leg 1
    let res = twitter_api(
        TwitterEndpoint::OauthRequestToken.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(
        &TwitterEndpoint::OauthAuthenticate.to_string(),
        [("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' ').nth(1).ok_or("No target found")?)?;
    let oauth_verifier = reqwest::Url::parse("https://example.net/")?
        .join(target)?
        .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_all(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::OauthAccessToken.try_into()?,
        None,
        APIAction::Post(PostData::Data(&[(
            "oauth_verifier",
            Cow::Owned(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!")?)
}

pub fn tweet(text: &str, img: Option<Cow<'static, [u8]>>) -> StdError<()> {
    let user_token = oauth1::Token::new(
        std::env::var(USER_TOKEN_ENV_VAR)?,
        std::env::var(USER_SECRET_ENV_VAR)?,
    );

    let user: serde_json::Value = twitter_api(
        TwitterEndpoint::VerifyCredentials.try_into()?,
        Some(&user_token),
        APIAction::Get,
        &[],
    )?
    .json()?;
    println!(
        "Tweeting for user @{}, (id: {})",
        user["screen_name"], user["id"]
    );
    let mut post_data = vec![("status", Cow::Borrowed(text))];
    if let Some(img) = img {
        println!("Uploading image...");
        let img_id = upload_image(&user_token, img)?;
        post_data.push(("media_ids", Cow::Owned(img_id.to_string())))
    }
    event!(Level::INFO, "Sending tweet...");
    twitter_api(
        TwitterEndpoint::UpdateStatus.try_into()?,
        Some(&user_token),
        APIAction::Post(PostData::Data(&post_data[0..])),
        &[],
    )?;
    Ok(())
}
