use core::fmt;
use std::{error::Error, path::PathBuf, process::exit};

use crate::{http, Config, UA};

use colored::Colorize;
use num::integer::gcd;
use reqwest::{header::USER_AGENT, Client};
use serde_json::Value;

#[derive(Debug)]
enum FilterCondition {
    MaxRes,
    MinRes,
    AspectRatio,
    None,
}

impl fmt::Display for FilterCondition {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::MaxRes => write!(f, "Image is larger than the set maximum resolution"),
            Self::MinRes => write!(f, "Image is smaller than the set maximum resolution"),
            Self::AspectRatio => write!(f, "Image's aspect ratio doesn't match the listed ones"),
            Self::None => write!(f, "No reason"),
        }
    }
}

pub async fn dl(
    cfg: Config,
    client: Client,
    combo: Option<(String, String)>,
) -> Result<(usize, usize), Box<dyn Error>> {
    let (json_url, board) = match combo {
        Some((json_url, board)) => (json_url, board),
        None => parse_url(cfg.target.clone()),
    };

    let (images, fl) = parse_json(cfg.clone(), &client, json_url.clone(), board).await?;
    let (dl, sk) = http::concurrent_dl(images, json_url).await?;

    Ok((dl, fl + sk))
}

fn parse_url(url: String) -> (String, String) {
    let url_split: Vec<&str> = url.split('/').collect();
    let thread_id = url_split.last().unwrap();
    let board = url_split.get(url_split.len() - 3).unwrap();

    (
        format!("https://a.4cdn.org/{board}/thread/{thread_id}.json"),
        board.to_string(),
    )
}

async fn parse_json(
    cfg: Config,
    client: &Client,
    url: String,
    board: String,
) -> Result<(Vec<(String, PathBuf, String)>, usize), Box<dyn Error>> {
    let mut filtered = 0;
    let mut images = Vec::new();

    let res_txt = match client.get(url.clone()).header(USER_AGENT, UA).send().await {
        Ok(res) => res.text().await?,
        Err(e) => {
            eprintln!(
                "{}",
                format!("Failed to request the thread JSON data from {url}: {e}")
                    .red()
                    .bold()
            );
            exit(1);
        }
    };

    let data: Value = match serde_json::from_str(&res_txt) {
        Ok(data) => data,
        Err(e) => {
            eprintln!(
                "{}",
                format!("Failed to parse the raw data from {url}: {e}")
                    .red()
                    .bold()
            );
            exit(1);
        }
    };

    for entry in data["posts"].as_array().unwrap() {
        // Reply without an attachment
        if !entry["tim"].is_i64() {
            continue;
        }

        let (is_filtered, cond) = img_filter(cfg.clone(), entry);

        if is_filtered {
            if cfg.verbose {
                println!("Image filtered: {cond}");
            }

            filtered += 1;
            continue;
        }

        // Format: (img url, output file, md5 hash)
        images.push(parse_img(cfg.clone(), entry, &board));
    }

    Ok((images, filtered))
}

fn parse_img(cfg: Config, entry: &Value, board: &str) -> (String, PathBuf, String) {
    let id = entry["tim"].to_string();
    // Chained conversions for `ext` and `md5` to get rid of the quotes
    let ext = entry["ext"].as_str().unwrap().to_string();
    let md5 = entry["md5"].as_str().unwrap().to_string();
    let path = cfg.out_dir.join(format!("{id}{ext}"));
    let url = format!("https://i.4cdn.org/{board}/{id}{ext}");

    (url, path, md5)
}

fn img_filter(cfg: Config, entry: &Value) -> (bool, FilterCondition) {
    let img_w = entry["w"].as_u64().unwrap();
    let img_h = entry["h"].as_u64().unwrap();
    let aspect_ratio = img_aspect_ratio(img_w, img_h);

    if let Some(min_res) = cfg.min_res {
        if min_res.0 > img_w || min_res.1 > img_h {
            return (true, FilterCondition::MinRes);
        }
    }

    if let Some(max_res) = cfg.max_res {
        if max_res.0 < img_w || max_res.1 < img_h {
            return (true, FilterCondition::MaxRes);
        }
    }

    if let Some(aspect_ratios) = &cfg.aspect_ratios {
        // Non-empty list of preferred aspect ratios
        if !aspect_ratios.contains(&aspect_ratio) {
            return (true, FilterCondition::AspectRatio);
        }
    }

    (false, FilterCondition::None)
}

fn img_aspect_ratio(w: u64, h: u64) -> (u32, u32) {
    let div = gcd(w, h);
    let simpl_w = (w / div) as u32;
    let simpl_h = (h / div) as u32;

    (simpl_w, simpl_h)
}
