use rand::seq::SliceRandom;
use std::convert::TryInto;
use std::io::Cursor;
use tracing::{event, Level};
mod twitter;
mod wiki;
use clap::{Parser, Subcommand};
use image::{DynamicImage, RgbaImage};
use tiny_skia::{Paint, PathBuilder, Pixmap, PixmapPaint, Stroke, Transform};
use twitter::*;
use wiki::*;

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

// Render the raw SVG data to an image
fn render_svg(data: &[u8], height: u32, with_border: bool) -> StdError<DynamicImage> {
    let opt = usvg::Options::default();
    let rtree = usvg::Tree::from_data(data, &opt.to_ref()).expect("couldn't parse");
    let svg_size = rtree.svg_node().size;
    // Work out how wide the pixmap of height `height` needs to be to entirely fit the SVG.
    let pm_width = ((height as f64 / svg_size.height()) * svg_size.width()).ceil() as u32;
    let mut pixmap = Pixmap::new(pm_width, height).ok_or("Error creating pixmap")?;
    // Render the svg into a pixmap.
    resvg::render(&rtree, usvg::FitTo::Height(height), pixmap.as_mut())
        .ok_or("Error rendering svg")?;
    // Make a wider pixmap with a 16:9 AR and the same height. This is a blesséd ratio by twitter
    // and means we see the whole image nicely in the timeline with no truncation.
    let mut bigger_pixmap =
        Pixmap::new(height / 9 * 16, height).ok_or("Error creating bigger pixmap")?;
    // Then draw our freshly rendered SVG into the middle of the bigger pixmap.
    bigger_pixmap
        .draw_pixmap(
            ((bigger_pixmap.width() - pm_width) / 2).try_into().unwrap(),
            0,
            pixmap.as_ref(),
            &PixmapPaint::default(),
            Transform::identity(),
            None,
        )
        .ok_or("Error drawing onto bigger pixmap")?;
    let (w, h) = (bigger_pixmap.width(), bigger_pixmap.height());
    // Render a red border for debug purposes
    if with_border {
        let mut paint = Paint::default();
        paint.set_color_rgba8(255, 0, 0, 255);
        let stroke = Stroke {
            width: 1.0,
            ..Default::default()
        };

        let path = {
            let mut pb = PathBuilder::new();
            pb.move_to(0.0, 0.0);
            pb.line_to(0.0, h as f32 - stroke.width);
            pb.line_to(w as f32, h as f32 - stroke.width);
            pb.line_to(w as f32 - stroke.width, 0.0);
            pb.line_to(0.0, 0.0);
            pb.finish().unwrap()
        };
        bigger_pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
    }
    let img = RgbaImage::from_raw(
        bigger_pixmap.width(),
        bigger_pixmap.height(),
        bigger_pixmap.data().to_vec(),
    )
    .ok_or("Error creating image from pixmap")?;
    Ok(DynamicImage::ImageRgba8(img))
}

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

#[derive(Parser)]
#[clap(author, version, about)]
struct Cli {
    #[clap(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Authorize the twitter application to acccess a user's account
    Authorize,
    /// Scrape images from the category on wikimedia commons
    ScrapeCategory,
    /// Scrape images from the iso7010 wikipedia page
    ScrapeWeb,
    /// List tweets from the authed user's timeline
    ListTweets,
    /// Run the bot - scrape, pick a random entry and tweet it
    RunBot {
        #[clap(short, long, action)]
        dry_run: bool,
        #[clap(short, long, value_parser)]
        target: Option<String>,
    },
    /// Print details about the currently authed user
    Whoami,
}

fn main() -> StdError<()> {
    tracing_subscriber::fmt::init();
    let cli = Cli::parse();
    match &cli.command {
        Commands::Authorize => do_authorize(),
        Commands::ScrapeCategory => do_scrape_category(),
        Commands::ScrapeWeb => do_scrape_web(),
        Commands::ListTweets => do_list_tweets(),
        Commands::Whoami => do_whoami(),
        Commands::RunBot { dry_run, target } => run_bot(*dry_run, target.to_owned()),
    }
}

fn do_whoami() -> StdError<()> {
    let user_token = user_token_from_env();

    let user: serde_json::Value = twitter_api(
        TwitterEndpoint::VerifyCredentials.try_into()?,
        Some(&user_token),
        APIAction::Get,
        &[],
    )?
    .json()?;
    println!("User @{}, (id: {})", user["screen_name"], user["id"]);
    Ok(())
}

fn do_list_tweets() -> StdError<()> {
    let user_token = user_token_from_env();

    let user = twitter_api(
        TwitterEndpoint::VerifyCredentials.try_into()?,
        Some(&user_token),
        APIAction::Get,
        &[],
    )?
    .json::<serde_json::Value>()?;

    let id = user["id"].as_u64().unwrap();

    let timeline: serde_json::Value = twitter_api(
        reqwest::Url::parse_with_params(
            &TwitterEndpoint::UserTimeline.to_string(),
            [
                ("count", "200"),
                ("exclude_replies", "true"),
                ("include_retweets", "false"),
                ("trim_user", "true"),
                ("user_id", id.to_string().as_ref()),
            ],
        )?,
        Some(&user_token),
        APIAction::Get,
        &[],
    )?
    .json()?;
    for tweet in timeline.as_array().unwrap() {
        let tweet = tweet.as_object().unwrap();
        println!("{}, \"{}\"", tweet["id"], tweet["text"]);
    }
    Ok(())
}

fn do_scrape_category() -> StdError<()> {
    let mut files = get_files_in_category("Category:ISO_7010_safety_signs_(vector_drawings)")?;
    files.sort();
    for f in files {
        println!("{}", f);
    }

    Ok(())
}

fn do_scrape_web() -> StdError<()> {
    let mut files: Vec<_> = scrape_web()?.into_iter().map(|(_, file)| file).collect();
    files.sort();
    for f in files {
        println!("{}", f);
    }

    Ok(())
}

fn get_client(headers: Option<reqwest::header::HeaderMap>) -> StdError<reqwest::blocking::Client> {
    let mut c = reqwest::blocking::Client::builder().user_agent(APP_USER_AGENT);
    if let Some(headers) = headers {
        c = c.default_headers(headers);
    }
    Ok(c.build()?)
}

fn run_bot(dry_run: bool, target: Option<String>) -> StdError<()> {
    let all = scrape_web()?;
    let (title, filename) = if let Some(target) = target {
        all.iter().find(|(title, _)| title.to_lowercase().contains(&target.to_lowercase()))
    } else {
        all
        .choose(&mut rand::thread_rng())
    }.ok_or("got no images m8")?;
    event!(Level::INFO, title, filename, "Picked random thing");
    let client = get_client(None)?;
    event!(Level::INFO, "Fetching metadata...");
    // TODO: could crash, probably doesn't matter
    let meta = get_file_metadata(vec![filename])?.remove(0);
    event!(Level::INFO, %meta, "Got metadata");
    event!(Level::INFO, url = meta.url.to_string(), "Fetching image");
    let svg = client.get(meta.url).send()?.bytes()?;

    let text = format!(
        "{}\n\nImage source: {}\nAuthor: Wikimedia Commons user {}\n{}{}",
        title,
        meta.html_url,
        meta.author,
        meta.license_short_name,
        meta.license_url
            .map_or("".to_owned(), |u| format!(" ({})", u))
    );

    if !dry_run {
        // Render the image nice and big for twitter
        let img = render_svg(&svg, 1000, false)?;
        let mut buf = Cursor::new(Vec::new());
        img.write_to(&mut buf, image::ImageFormat::Png)?;
        tweet(&text, Some(buf.into_inner().into()))?;
    } else {
        // Render the image smaller for output to terminal
        let img = render_svg(&svg, 128, true)?;
        println!("Dry run - would tweet:\n \"{}\"", text);
        viuer::print(
            &img,
            &viuer::Config {
                absolute_offset: false,
                width: Some(32),
                ..Default::default()
            },
        )?;
    }

    Ok(())
}
