use std::{error::Error, time::Duration};

use serde::{Deserialize, Serialize};

use crate::fetcher::{DockerTagsFetcher, QueryArgs};

#[derive(Serialize, Deserialize)]
struct DockerResult {
    content_type: Option<String>,
    creator: u64,
    full_size: u64,
    id: u64,
    last_updated: Option<String>,
    last_updater: u64,
    last_updater_username: Option<String>,
    media_type: Option<String>,
    name: Option<String>,
    repository: u32,
    tag_last_pulled: Option<String>,
    tag_last_pushed: Option<String>,
    tag_status: Option<String>,
    v2: bool,
    images: Vec<DockerImage>,
}

#[derive(Serialize, Deserialize)]
struct DockerImage {
    architecture: Option<String>,
    digest: Option<String>,
    features: Option<String>,
    last_pulled: Option<String>,
    last_pushed: Option<String>,
    os: Option<String>,
    os_features: Option<String>,
    os_version: Option<String>,
    size: u64,
    status: Option<String>,
    variant: Option<String>,
}

#[derive(Serialize, Deserialize)]
struct DockerResponse {
    count: u32,
    next: Option<String>,
    previous: Option<String>,
    results: Vec<DockerResult>,
}

pub(crate) struct DockerHubTagsFetcher {}

impl DockerHubTagsFetcher {
    pub fn new() -> DockerHubTagsFetcher {
        DockerHubTagsFetcher {}
    }
}

impl DockerTagsFetcher for DockerHubTagsFetcher {
    async fn get_tags(
        &self,
        args: &QueryArgs,
    ) -> Result<Vec<String>, Box<dyn Error + Send + Sync>> {
        let mut page = 1;
        let page_size = 100;

        let namespace = args.namespace.as_str();
        let repository = args.repository.as_str();
        let url = format!("https://hub.docker.com/v2/namespaces/{namespace}/repositories/{repository}/tags?page={page}&page_size={page_size}");
        let mut results: Vec<DockerResult> = Vec::new();
        let client = reqwest::Client::builder()
            .pool_max_idle_per_host(10)
            .connect_timeout(Duration::from_secs(5))
            .timeout(Duration::from_secs(10))
            .pool_idle_timeout(Duration::from_secs(90))
            .build()?;
        let mut count = 0;
        let mut handles = vec![];

        let resp = client.get(&url).send().await?;
        if resp.status().is_success() {
            let mut response: DockerResponse = resp.json().await?;
            results.append(&mut response.results);
            if count == 0 {
                count = response.count;
            }
            while page * page_size < count {
                page += 1;
                let next = format!("https://hub.docker.com/v2/namespaces/{namespace}/repositories/{repository}/tags?page={page}&page_size={page_size}");
                let client_clone = client.clone();
                let handle = tokio::spawn(async move { client_clone.get(&next).send().await });
                handles.push(handle);
            }

            let results_from_handles = futures::future::join_all(handles).await;

            for result in results_from_handles.into_iter().flatten() {
                let mut response: DockerResponse = result.unwrap().json().await?;
                results.append(&mut response.results);
            }
        }

        let mut filtered = results
            .into_iter()
            .filter(|x| !x.images.is_empty())
            .collect::<Vec<DockerResult>>();
        if args.name.is_some() {
            let pat = args.name.clone().unwrap();
            filtered = filtered
                .into_iter()
                .filter(|x| x.name.clone().unwrap().contains(&pat))
                .collect::<Vec<DockerResult>>();
        };
        if args.arch.is_some() {
            let pat = args.arch.clone().unwrap();
            filtered = filtered
                .into_iter()
                .filter(|x| {
                    x.images
                        .first()
                        .as_ref()
                        .unwrap()
                        .architecture
                        .clone()
                        .unwrap()
                        .contains(&pat)
                })
                .collect::<Vec<DockerResult>>();
        };
        let mut results = Vec::<String>::new();
        for x in &filtered {
            results.push(x.name.clone().unwrap());
        }
        Ok(results)
    }
}
