use std::error::Error;
use std::path::Path;
use serde::{Serialize, Deserialize};
use crate::{HttpClient, RepositorySource};
use urlencoding;
use async_trait::async_trait;
use std::sync::Arc;
use crate::models::repository::RepositoryPlugin;
use crate::crawlers::Repository;

// GitHub API response structures (Based on https://docs.github.com/en/rest/releases/releases)

#[derive(Debug, Serialize, Deserialize)]
struct GitHubRelease {
    // Structure for release details
    tag_name: String,
    name: Option<String>,
    body: Option<String>,
    published_at: String,
    assets: Vec<GitHubAsset>,
    html_url: String, // URL to the release page
    prerelease: bool,
    draft: bool,
}

#[derive(Debug, Serialize, Deserialize)]
struct GitHubAsset {
    // Structure for release asset details
    name: String,
    browser_download_url: String,
    size: u64,
    updated_at: String,
    download_count: u64,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct GitHubRepo {
    // Structure for repository details (used for searching potentially)
    id: u64,
    name: String,
    full_name: String, // "owner/repo"
    owner: GitHubUser,
    description: Option<String>,
    html_url: String,
    stargazers_count: u64, // Can use this as a proxy for rating/popularity
    updated_at: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct GitHubUser {
    login: String,
    avatar_url: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
struct GitHubSearchResponse {
    // Structure for repository search results
    items: Vec<GitHubRepo>,
    // Ignoring total_count, incomplete_results for now
}

// GitHub crawler implementation
#[derive(Clone)]
pub struct GitHubCrawler {
    client: Arc<HttpClient>,
    api_base_url: String,
}

// Inherent methods for GitHubCrawler
impl GitHubCrawler {
    pub fn new() -> Self {
        GitHubCrawler {
            client: Arc::new(HttpClient::new()),
            api_base_url: "https://api.github.com".to_string(),
        }
    }

    // Helper function to get the latest release for a repo, now async
    async fn get_latest_release_details(&self, repo_full_name: &str) -> Result<GitHubRelease, Box<dyn Error + Send + Sync>> {
        let url = format!("{}/repos/{}/releases/latest", self.api_base_url, repo_full_name);
        let response_body = self.client.get(&url).await?;
        let release: GitHubRelease = serde_json::from_str(&response_body)?;
        Ok(release)
    }

    // Helper function to get all releases, now async
    async fn get_all_release_details(&self, repo_full_name: &str) -> Result<Vec<GitHubRelease>, Box<dyn Error + Send + Sync>> {
        let url = format!("{}/repos/{}/releases?per_page=100", self.api_base_url, repo_full_name);
        let response_body = self.client.get(&url).await?;
        let releases: Vec<GitHubRelease> = serde_json::from_str(&response_body)?;
        Ok(releases)
    }

    // Helper function to get repo details, now async
    async fn get_repo_details(&self, repo_full_name: &str) -> Result<GitHubRepo, Box<dyn Error + Send + Sync>> {
        let repo_url = format!("{}/repos/{}", self.api_base_url, repo_full_name);
        let repo_response_body = self.client.get(&repo_url).await?;
        let repo: GitHubRepo = serde_json::from_str(&repo_response_body)?;
        Ok(repo)
    }

    async fn get_plugin_versions(&self, repo_full_name: &str) -> Result<Vec<String>, String> {
        let releases = match self.get_all_release_details(repo_full_name).await {
            Ok(r) => r,
            Err(e) => return Err(format!("Failed GitHub get releases for versions: {}", e)),
        };
        let versions = releases.into_iter()
            .filter(|r| !r.draft && !r.prerelease && r.assets.iter().any(|a| a.name.ends_with(".jar")))
            .map(|r| r.tag_name)
            .collect();
        Ok(versions)
    }
}

#[async_trait]
impl Repository for GitHubCrawler {
    fn get_repository_name(&self) -> String {
        "GitHub".to_string()
    }

    async fn search(&self, query: &str) -> Result<Vec<RepositoryPlugin>, String> {
        let search_terms = format!("{} topic:minecraft-plugin topic:spigot topic:paper topic:bukkit fork:false", query);
        let encoded_query = urlencoding::encode(&search_terms);
        let url = format!(
            "{}/search/repositories?q={}&sort=stars&order=desc",
            self.api_base_url,
            encoded_query
        );

        let response_body = match self.client.get(&url).await {
            Ok(body) => body,
            Err(e) => return Err(format!("GitHub search request failed: {}", e)),
        };
        let search_response: GitHubSearchResponse = match serde_json::from_str(&response_body) {
            Ok(res) => res,
            Err(e) => return Err(format!("Failed to parse GitHub search results: {}", e)),
        };

        let fetch_tasks = search_response.items.into_iter().map(|repo| {
            let self_clone = self.clone();
            async move {
                self_clone.get_plugin_details(&repo.full_name).await
            }
        });

        let results: Vec<RepositoryPlugin> = futures::future::join_all(fetch_tasks).await
            .into_iter()
            .filter_map(|result| match result {
                Ok(plugin) => Some(plugin),
                Err(e) => {
                    println!("Error fetching details during search: {}", e);
                    None
                }
            })
            .collect();

        Ok(results)
    }

    async fn get_plugin_details(&self, plugin_id: &str) -> Result<RepositoryPlugin, String> {
        let repo_full_name = plugin_id;
        let repo = match self.get_repo_details(repo_full_name).await {
            Ok(r) => r,
            Err(e) => return Err(format!("Failed GitHub get repo details: {}", e)),
        };
        let mut releases = match self.get_all_release_details(repo_full_name).await {
             Ok(r) => r,
            Err(e) => return Err(format!("Failed GitHub get releases: {}", e)),
        };

        releases.sort_by(|a, b| b.published_at.cmp(&a.published_at));

        let latest_valid_release = releases.into_iter()
            .filter(|r| !r.draft && !r.prerelease)
            .find(|r| r.assets.iter().any(|a| a.name.ends_with(".jar")));

        if let Some(release) = latest_valid_release {
            if let Some(asset) = release.assets.iter().find(|a| a.name.ends_with(".jar")) {
                Ok(RepositoryPlugin {
                    id: repo_full_name.to_string(),
                    name: repo.name,
                    version: release.tag_name,
                    description: repo.description,
                    authors: vec![repo.owner.login],
                    download_url: asset.browser_download_url.clone(),
                    repository: RepositorySource::GitHub,
                    page_url: repo.html_url,
                    download_count: Some(asset.download_count),
                    last_updated: Some(release.published_at),
                    icon_url: repo.owner.avatar_url,
                    minecraft_versions: Vec::new(),
                    categories: Vec::new(),
                    rating: Some(repo.stargazers_count as f32),
                    file_size: Some(asset.size),
                    file_hash: None,
                    changelog: release.body,
                })
            } else {
                 Err(format!("No suitable JAR asset found in the latest valid release for {}", repo_full_name))
            }
        } else {
            Err(format!("No valid release with a JAR asset found for {}", repo_full_name))
        }
    }

    async fn download_plugin(&self, plugin_id: &str, version_number_str: &str, destination: &Path) -> Result<String, String> {
        let repo_full_name = plugin_id;
        let tag_name = version_number_str;

        let release_url = format!("{}/repos/{}/releases/tags/{}", self.api_base_url, repo_full_name, tag_name);
        let release_response_body = match self.client.get(&release_url).await {
            Ok(body) => body,
            Err(e) => return Err(format!("GitHub get release by tag failed: {}", e)),
        };
        let release: GitHubRelease = match serde_json::from_str(&release_response_body) {
            Ok(r) => r,
            Err(e) => return Err(format!("Failed to parse GitHub release by tag: {}", e)),
        };

        let asset = release.assets.iter()
            .find(|a| a.name.ends_with(".jar"))
            .ok_or_else(|| format!("No suitable JAR asset found in release tag '{}' for {}", tag_name, repo_full_name))?;

        match self.client.download(&asset.browser_download_url, destination).await {
             Ok(_) => Ok(destination.to_string_lossy().to_string()),
             Err(e) => Err(format!("Failed to download from GitHub: {}", e))
         }
    }
}