use serde::{Deserialize, Serialize}; // Added Serialize for potential use, Deserialize is essential
use std::error::Error;
use std::path::Path;
use crate::{HttpClient};
use crate::models::repository::{RepositoryPlugin, RepositorySource};
use urlencoding;
use async_trait::async_trait;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH}; // For converting timestamp
use base64::{Engine as _, engine::general_purpose::STANDARD}; // Correct import with Engine trait
use serde_json::Value; // Import Value
use crate::crawlers::Repository; // Use the correct trait import

// --- Structs for SpiGet API Deserialization ---

#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct SpiGetIcon {
    url: Option<String>,
    // data: Option<String>, // Base64 data not typically needed if URL is present
}

#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct SpiGetRating {
    average: f32,
}

#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct SpiGetAuthor {
    id: u32,
    name: Option<String>, // SpiGet might not always return name in all contexts
    // icon: Option<SpiGetIcon>, // Icon data might be available in author details endpoint
}

#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct SpiGetFile {
    // #[serde(rename = "type")]
    // file_type: Option<String>, // e.g. ".jar"
    size: Option<Value>, // Use Value to accept string or number from API
    size_unit: Option<String>, // e.g. "MB"
    url: Option<String>, // Link to the spigotmc resource page, *not* a direct download
    // externalUrl: Option<String> // Field from docs, maybe add if needed later
}

// Represents a version summary, often nested or in arrays
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct SpiGetVersion {
    id: u32,
    name: Option<String>, // Make optional: The actual version string e.g., "1.19.4" or "v2.1"
    uuid: Option<String>,
    release_date: Option<u64>, // Timestamp
    downloads: Option<u32>,
}

#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct SpiGetUpdate {

    description: String, // Base64 encoded HTML description

}

#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct IdReference { // Used for arrays containing only IDs
    id: u32,
}

#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct SpiGetResource {
    id: u32,
    name: Option<String>, // Make name optional to handle potential missing field in search results
    tag: Option<String>, // Make tag optional as well for robustness
    version: SpiGetVersion, // Represents the *latest* version details here
    author: SpiGetAuthor, // Contains author ID, maybe name
    downloads: u32,
    tested_versions: Vec<String>, // List of MC versions plugin is tested against
    rating: SpiGetRating,
    icon: SpiGetIcon, // Contains URL to icon
    update_date: u64, // Timestamp
    file: SpiGetFile, // Details about the main file download
    external: bool, // If true, download link points externally
}

// --- SpigotMC Crawler Implementation (using SpiGet API) ---

pub struct SpigotMCCrawler {
    client: Arc<HttpClient>,
    base_url: String,
}

impl SpigotMCCrawler {
    pub fn new() -> Self {
        SpigotMCCrawler {
            client: Arc::new(HttpClient::new()),
            base_url: "https://api.spiget.org/v2".to_string(), // Use SpiGet API base URL
        }
    }

    // Helper to convert SpiGetResource to RepositoryPlugin
    fn map_resource_to_plugin(&self, resource: &SpiGetResource) -> RepositoryPlugin {
        // Construct SpigotMC page URL
        let page_url = format!("https://www.spigotmc.org/resources/{}", resource.id);

        // Construct potential download URL (may differ for external resources)
        let download_url = if resource.external {
             // For external resources, the 'url' in file info is the download link
            resource.file.url.clone().unwrap_or_default()
        } else {
            // For internal resources, use the SpiGet download endpoint for the *latest* version
            format!("{}/resources/{}/download", self.base_url, resource.id)
        };

        // Format update date
        let last_updated = SystemTime::UNIX_EPOCH
            .checked_add(std::time::Duration::from_secs(resource.update_date))
            .map(|_st| { // Remove chrono formatting for now
                // Simple ISO 8601 format or similar - requires chrono for better formatting
                // For now, just return the timestamp as string for simplicity
                format!("{}", resource.update_date)
            });

        // Safely get author name
        let author_name = resource.author.name.clone().unwrap_or_else(|| format!("#{}", resource.author.id));

        // Convert file size, handling potential string in 'size' field
        let file_size_bytes = resource.file.size.as_ref().and_then(|s| s.as_f64()).map(|s_num| {
            match resource.file.size_unit.as_deref() {
                Some("KB") => (s_num * 1024.0) as u64,
                Some("MB") => (s_num * 1024.0 * 1024.0) as u64,
                Some("GB") => (s_num * 1024.0 * 1024.0 * 1024.0) as u64,
                _ => s_num as u64, // Assume bytes if unit is missing or unknown
            }
        });

        // Prepend base URL to icon URL if it's relative (SpiGet usually provides full URL)
        let icon_url = resource.icon.url.clone();

        // Use id as fallback if name is missing
        let plugin_name = resource.name.clone().unwrap_or_else(|| format!("Unnamed Resource #{}", resource.id));

        // Extract version information properly
        let version_name = resource.version.name.clone().unwrap_or_else(|| {
            println!("[SpigotMCCrawler::map_resource_to_plugin] Warning: Missing version name for resource ID: {}", resource.id);
            // DO NOT use tested Minecraft versions as fallbacks for plugin versions
            // since they are different types of versions
            "Unknown".to_string()
        });

        println!("[SpigotMCCrawler::map_resource_to_plugin] Version for {}: {}", plugin_name, version_name);

        RepositoryPlugin {
            id: resource.id.to_string(),
            name: plugin_name, // Use the potentially fallback name
            version: version_name, // Use the potentially fallback version name
            description: resource.tag.clone(), // Use tagline as description (already Option<String>)
            authors: vec![author_name],
            download_url,
            repository: RepositorySource::SpigotMC,
            page_url,
            download_count: Some(resource.downloads as u64),
            last_updated,
            icon_url, // Use the potentially prefixed URL
            minecraft_versions: resource.tested_versions.clone(),
            categories: Vec::new(), // SpiGet only gives category ID, fetching name requires another call
            rating: Some(resource.rating.average),
            file_size: file_size_bytes,
            file_hash: None, // SpiGet does not provide hashes
            changelog: None, // Needs separate call to /updates/latest
        }
    }

    // Helper to fetch and decode the latest update's description as changelog
    async fn get_latest_changelog(&self, resource_id: &str) -> Result<Option<String>, Box<dyn Error + Send + Sync>> {
        let url = format!("{}/resources/{}/updates/latest", self.base_url, resource_id);
        match self.client.get(&url).await {
            Ok(body) => {
                match serde_json::from_str::<SpiGetUpdate>(&body) {
                    Ok(update) => {
                        // Description is Base64 encoded HTML
                        match STANDARD.decode(&update.description) {
                            Ok(decoded_bytes) => {
                                // Convert bytes to string (assuming UTF-8)
                                // decoded_bytes is now a Vec<u8>, which implements Sized
                                Ok(Some(String::from_utf8_lossy(&decoded_bytes).to_string()))
                            }
                            Err(e) => {
                                println!("Failed to decode base64 changelog for {}: {}", resource_id, e);
                                Ok(None) // Return None if decoding fails
                            }
                        }
                    }
                    Err(e) => {
                        println!("Failed to parse latest update JSON for {}: {}", resource_id, e);
                        Ok(None) // Return None if parsing fails
                    }
                }
            }
            Err(e) => {
                 // If the request itself fails (e.g., 404 if no updates), treat as no changelog
                println!("Failed to fetch latest update for {}: {}", resource_id, e);
                Ok(None)
            }
        }
    }

    // Moved get_plugin_versions back here as it's not part of the Repository trait
    async fn get_plugin_versions(&self, plugin_id: &str) -> Result<Vec<String>, String> {
        println!("Fetching versions for resource ID: {}", plugin_id);
        let url = format!("{}/resources/{}/versions?sort=-releaseDate&size=10", self.base_url, plugin_id);

        let body = match self.client.get(&url).await {
            Ok(b) => b,
            Err(e) => return Err(format!("Failed SpiGet versions request: {}", e))
        };

        match serde_json::from_str::<Vec<SpiGetVersion>>(&body) {
            Ok(versions) => {
                let version_names: Vec<String> = versions.into_iter()
                    .filter_map(|v| v.name)
                    .collect();

                if version_names.is_empty() {
                    // If no version names available, try to extract from the version ID
                    println!("No named versions found for resource {}. Trying alternate methods.", plugin_id);

                    // Try to get full resource details for version info
                    let resource_url = format!("{}/resources/{}", self.base_url, plugin_id);
                    match self.client.get(&resource_url).await {
                        Ok(resource_body) => {
                            match serde_json::from_str::<SpiGetResource>(&resource_body) {
                                Ok(resource) => {
                                    // Check if we can get a version from the resource directly
                                    if let Some(name) = resource.version.name {
                                        println!("Found version from resource details: {}", name);
                                        return Ok(vec![name]);
                                    }
                                },
                                Err(e) => println!("Failed to parse resource details: {}", e)
                            }
                        },
                        Err(e) => println!("Failed to fetch resource details: {}", e)
                    }

                    // If still no version, return a fallback
                    println!("Using fallback version for resource ID: {}", plugin_id);
                    Ok(vec!["Unknown".to_string()])
                } else {
                    println!("Found {} versions for resource ID: {}", version_names.len(), plugin_id);
                    Ok(version_names)
                }
            },
            Err(e) => {
                println!("Failed to parse versions JSON: {}. Body: {}", e, body);
                Err(format!("Failed to parse SpiGet versions: {}", e))
            }
        }
    }
}

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

    async fn search(&self, query: &str) -> Result<Vec<RepositoryPlugin>, String> {
        let encoded_query = urlencoding::encode(query);
        let url = format!(
            "{}/search/resources/{}?field=name&fields=name,tag,author,version,downloads,rating,icon,updateDate,premium,file,external,testedVersions",
            self.base_url,
            encoded_query
        );

        let body = match self.client.get(&url).await {
            Ok(b) => b,
            Err(e) => return Err(format!("Failed SpiGet search request: {}", e))
        };

        match serde_json::from_str::<Vec<SpiGetResource>>(&body) {
            Ok(resources) => {
                let mut results = Vec::new();
                for res in &resources {
                    let mut plugin = self.map_resource_to_plugin(res);
                    // Try fetching versions if missing
                    if plugin.version == "Unknown" {
                        println!("Searching for version information for resource ID: {}", plugin.id);
                        match self.get_plugin_versions(&plugin.id).await {
                            Ok(versions) => {
                                if let Some(latest) = versions.first() {
                                    println!("Found version for {}: {}", plugin.name, latest);
                                    plugin.version = latest.clone();
                                }
                            },
                            Err(e) => println!("Failed to fetch versions for {}: {}", plugin.id, e),
                        }
                    }
                    results.push(plugin);
                }
                Ok(results)
            }
            Err(e) => {
                // Handle case where search returns a single object instead of array (e.g., direct ID match?)
                // Or just return the parsing error
                Err(format!("Failed to parse SpiGet search results: {}. Body: {}", e, body))
            }
        }
    }

    async fn get_plugin_details(&self, plugin_id: &str) -> Result<RepositoryPlugin, String> {
        let url = format!(
            "{}/resources/{}?fields=name,tag,author,version,downloads,rating,icon,updateDate,premium,file,external,testedVersions",
            self.base_url,
            plugin_id
        );

        let body = match self.client.get(&url).await {
            Ok(b) => b,
            Err(e) => return Err(format!("Failed SpiGet details request: {}", e))
        };

        match serde_json::from_str::<SpiGetResource>(&body) {
            Ok(resource) => {
                let mut plugin = self.map_resource_to_plugin(&resource);
                // Fetch changelog
                match self.get_latest_changelog(&plugin.id).await {
                    Ok(changelog_opt) => plugin.changelog = changelog_opt,
                    Err(e) => println!("Failed to fetch changelog for {}: {}", plugin.id, e),
                }
                 // Try fetching versions if missing
                 if plugin.version == "Unknown" {
                    println!("Fetching versions for detail view of resource ID: {}", plugin.id);
                    match self.get_plugin_versions(&plugin.id).await {
                        Ok(versions) => {
                            if let Some(latest) = versions.first() {
                                println!("Found version for {} in detail view: {}", plugin.name, latest);
                                plugin.version = latest.clone();
                            }
                        },
                        Err(e) => println!("Failed to fetch versions for {} in detail view: {}", plugin.id, e),
                    }
                }
                Ok(plugin)
            }
            Err(e) => Err(format!("Failed to parse SpiGet resource details: {}. Body: {}", e, body))
        }
    }

    async fn download_plugin(&self, plugin_id: &str, version: &str, destination: &Path) -> Result<String, String> {
        // Going with Option 1: Download latest version associated with the resource ID
        let details = self.get_plugin_details(plugin_id).await?;
        let download_url = &details.download_url;

        if download_url.is_empty() {
            return Err(format!("No download URL found for SpigotMC resource {}", plugin_id));
        }

        // We ignore the 'version' parameter here because SpiGet usually only provides the latest download
        println!("Warning: SpigotMC download via SpiGet usually fetches the LATEST version, requested version '{}' might be ignored.", version);

        match self.client.download(download_url, destination).await {
            Ok(_) => Ok(destination.to_string_lossy().to_string()), // Restore returning path
            Err(e) => Err(format!("Failed to download from SpiGet: {}", e))
        }
    }
}