// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use serde::{Serialize, Deserialize};
use std::path::Path;
use std::fs;
use std::io::Read;
use tauri::command;
use zip::ZipArchive;
use yaml_rust::{YamlLoader, Yaml};
use std::fs::File;
use sha2::{Sha256, Digest};
use reqwest;
use std::error::Error;

// Add the crawlers module
mod crawlers;
use crawlers::HangarCrawler;

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum ServerType {
    Paper,
    Spigot,
    Bukkit,
    Vanilla,
    Forge,
    Fabric,
    Velocity,
    BungeeCord,
    Waterfall,
    Unknown,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ServerInfo {
    server_type: ServerType,
    minecraft_version: Option<String>,
    plugins_directory: String,
    plugins_count: usize,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Plugin {
    name: String,
    version: String,
    latest_version: Option<String>,
    description: Option<String>,
    authors: Vec<String>,
    has_update: bool,
    api_version: Option<String>,
    main_class: Option<String>,
    depend: Option<Vec<String>>,
    soft_depend: Option<Vec<String>>,
    load_before: Option<Vec<String>>,
    commands: Option<serde_json::Value>,
    permissions: Option<serde_json::Value>,
    file_path: String,
    file_hash: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct PluginMeta {
    pub name: String,
    pub version: String,
    pub description: Option<String>,
    pub authors: Vec<String>,
    pub api_version: Option<String>,
    pub main_class: Option<String>,
    pub depend: Option<Vec<String>>,
    pub soft_depend: Option<Vec<String>>,
    pub load_before: Option<Vec<String>>,
    pub commands: Option<serde_json::Value>,
    pub permissions: Option<serde_json::Value>,
    pub file_path: String,
    pub file_size: u64,
    pub file_hash: String,
}

/// Calculates SHA-256 hash for a given file path
pub fn calculate_file_hash(file_path: &str) -> Result<String, String> {
    let mut file = File::open(file_path).map_err(|e| format!("Failed to open file for hashing: {}", e))?;
    let mut hasher = Sha256::new();
    let mut buffer = [0; 1024];

    loop {
        let bytes_read = file.read(&mut buffer).map_err(|e| format!("Failed to read file for hashing: {}", e))?;
        if bytes_read == 0 {
            break;
        }
        hasher.update(&buffer[..bytes_read]);
    }

    let hash = hasher.finalize();
    Ok(format!("{:x}", hash))
}

/// Extract metadata from a plugin.yml file inside a JAR
fn extract_plugin_metadata(jar_path: &Path) -> Result<PluginMeta, String> {
    let file = fs::File::open(jar_path)
        .map_err(|e| format!("Failed to open JAR file: {}", e))?;

    let file_size = file.metadata()
        .map_err(|e| format!("Failed to read file metadata: {}", e))?
        .len();

    let mut archive = ZipArchive::new(file)
        .map_err(|e| format!("Invalid JAR file: {}", e))?;

    // Try to find and read plugin.yml or bungee.yml
    let yaml_content = match read_yaml_from_archive(&mut archive, "plugin.yml") {
        Ok(content) => content,
        Err(_) => match read_yaml_from_archive(&mut archive, "bungee.yml") {
            Ok(content) => content,
            Err(_) => {
                // If no plugin metadata file is found, try to infer from filename
                let filename = jar_path.file_name()
                    .and_then(|n| n.to_str())
                    .unwrap_or("unknown.jar");

                // Extract name and version from filename (e.g., "WorldEdit-7.2.8.jar" → name: "WorldEdit", version: "7.2.8")
                let mut parts: Vec<&str> = filename.trim_end_matches(".jar").split('-').collect();
                let version = if parts.len() > 1 {
                    parts.pop().unwrap_or("1.0.0").to_string()
                } else {
                    "1.0.0".to_string()
                };

                let name = parts.join("-");

                return Ok(PluginMeta {
                    name,
                    version,
                    description: None,
                    authors: Vec::new(),
                    api_version: None,
                    main_class: None,
                    depend: None,
                    soft_depend: None,
                    load_before: None,
                    commands: None,
                    permissions: None,
                    file_path: jar_path.to_string_lossy().to_string(),
                    file_size,
                    file_hash: calculate_file_hash(jar_path.to_str().unwrap_or("unknown.jar")).unwrap_or_else(|_| "unknown".to_string()),
                });
            }
        }
    };

    // Parse the YAML content
    let docs = match YamlLoader::load_from_str(&yaml_content) {
        Ok(docs) => docs,
        Err(e) => {
            println!("Failed to parse plugin YAML: {}", e);
            return fallback_plugin_meta(jar_path, file_size);
        }
    };

    if docs.is_empty() {
        return fallback_plugin_meta(jar_path, file_size);
    }

    let doc = &docs[0];

    // Extract basic metadata with fallbacks for missing fields
    let name = yaml_str_with_fallback(doc, "name", jar_path);
    let version = yaml_str_with_fallback(doc, "version", jar_path);

    // Extract optional fields
    let description = yaml_str_opt(doc, "description");

    // Handle authors (can be a single string or an array)
    let authors = match &doc["authors"] {
        Yaml::Array(arr) => {
            arr.iter()
                .filter_map(|a| a.as_str().map(|s| s.to_string()))
                .collect()
        },
        Yaml::String(s) => vec![s.clone()],
        _ => {
            // Fallback to 'author' field which is sometimes used
            match &doc["author"] {
                Yaml::String(s) => vec![s.clone()],
                _ => Vec::new(),
            }
        }
    };

    // Extract other optional metadata
    let api_version = yaml_str_opt(doc, "api-version");
    let main_class = yaml_str_opt(doc, "main");

    // Handle dependency lists
    let depend = yaml_str_array(doc, "depend");
    let soft_depend = yaml_str_array(doc, "softdepend");
    let load_before = yaml_str_array(doc, "loadbefore");

    // Handle complex structures as generic JSON values
    let commands = match &doc["commands"] {
        Yaml::Hash(_) => {
            Some(serde_json::Value::String("Commands data present".to_string()))
        },
        _ => None
    };

    let permissions = match &doc["permissions"] {
        Yaml::Hash(_) => {
            Some(serde_json::Value::String("Permissions data present".to_string()))
        },
        _ => None
    };

    // Calculate the file hash
    let file_hash = calculate_file_hash(jar_path.to_str().unwrap_or("unknown.jar")).unwrap_or_else(|_| "unknown".to_string());

    Ok(PluginMeta {
        name,
        version,
        description,
        authors,
        api_version,
        main_class,
        depend,
        soft_depend,
        load_before,
        commands,
        permissions,
        file_path: jar_path.to_string_lossy().to_string(),
        file_size,
        file_hash,
    })
}

// Helper function to read a YAML file from the ZIP archive
fn read_yaml_from_archive(archive: &mut ZipArchive<fs::File>, file_name: &str) -> Result<String, String> {
    match archive.by_name(file_name) {
        Ok(mut file) => {
            let mut contents = String::new();
            file.read_to_string(&mut contents)
                .map_err(|e| format!("Failed to read {}: {}", file_name, e))?;
            Ok(contents)
        },
        Err(e) => Err(format!("Failed to find {}: {}", file_name, e))
    }
}

// Helper function to create plugin metadata with fallback values
fn fallback_plugin_meta(jar_path: &Path, file_size: u64) -> Result<PluginMeta, String> {
    let filename = jar_path.file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("unknown.jar");

    // Extract name and version from filename (e.g., "WorldEdit-7.2.8.jar" → name: "WorldEdit", version: "7.2.8")
    let mut parts: Vec<&str> = filename.trim_end_matches(".jar").split('-').collect();
    let version = if parts.len() > 1 {
        parts.pop().unwrap_or("1.0.0").to_string()
    } else {
        "1.0.0".to_string()
    };

    let name = parts.join("-");

    Ok(PluginMeta {
        name,
        version,
        description: None,
        authors: Vec::new(),
        api_version: None,
        main_class: None,
        depend: None,
        soft_depend: None,
        load_before: None,
        commands: None,
        permissions: None,
        file_path: jar_path.to_string_lossy().to_string(),
        file_size,
        file_hash: calculate_file_hash(jar_path.to_str().unwrap_or("unknown.jar")).unwrap_or_else(|_| "unknown".to_string()),
    })
}

// Extract a string from YAML with fallback to filename
fn yaml_str_with_fallback(yaml: &Yaml, key: &str, jar_path: &Path) -> String {
    match yaml[key].as_str() {
        Some(s) => s.to_string(),
        None => {
            // Extract from filename as fallback
            let filename = jar_path.file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("unknown.jar");

            if key == "name" {
                // Extract name (e.g., "WorldEdit-7.2.8.jar" → "WorldEdit")
                let parts: Vec<&str> = filename.trim_end_matches(".jar").split('-').collect();
                parts[0].to_string()
            } else if key == "version" {
                // Extract version (e.g., "WorldEdit-7.2.8.jar" → "7.2.8")
                let parts: Vec<&str> = filename.trim_end_matches(".jar").split('-').collect();
                if parts.len() > 1 {
                    parts[1].to_string()
                } else {
                    "1.0.0".to_string()
                }
            } else {
                String::new()
            }
        }
    }
}

fn yaml_str_opt(yaml: &Yaml, key: &str) -> Option<String> {
    yaml[key].as_str().map(|s| s.to_string())
}

fn yaml_str_array(yaml: &Yaml, key: &str) -> Option<Vec<String>> {
    match &yaml[key] {
        Yaml::Array(arr) => {
            let strings: Vec<String> = arr.iter()
                .filter_map(|a| a.as_str().map(|s| s.to_string()))
                .collect();
            if strings.is_empty() { None } else { Some(strings) }
        },
        _ => None
    }
}

/// Detect the server type based on files in the server directory
fn detect_server_type(server_path: &Path) -> ServerType {
    // Check for Paper
    if server_path.join("cache").join("patched_1.19.2.jar").exists() ||
       server_path.join("paper.yml").exists() {
        return ServerType::Paper;
    }

    // Check for Spigot
    if server_path.join("spigot.yml").exists() {
        return ServerType::Spigot;
    }

    // Check for Bukkit
    if server_path.join("bukkit.yml").exists() {
        return ServerType::Bukkit;
    }

    // Check for Forge
    if server_path.join("forge-server.jar").exists() ||
       server_path.join("mods").exists() {
        return ServerType::Forge;
    }

    // Check for Fabric
    if server_path.join("fabric-server-launch.jar").exists() ||
       (server_path.join("mods").exists() && server_path.join("fabric-server-launcher.properties").exists()) {
        return ServerType::Fabric;
    }

    // Check for Velocity
    if server_path.join("velocity.toml").exists() {
        return ServerType::Velocity;
    }

    // Check for BungeeCord
    if server_path.join("BungeeCord.jar").exists() ||
       server_path.join("config.yml").exists() {
        return ServerType::BungeeCord;
    }

    // Check for Waterfall
    if server_path.join("waterfall.jar").exists() ||
       server_path.join("waterfall.yml").exists() {
        return ServerType::Waterfall;
    }

    // Check if it's at least a vanilla server
    if server_path.join("server.properties").exists() ||
       server_path.join("vanilla_server.jar").exists() {
        return ServerType::Vanilla;
    }

    // If no server type detected
    ServerType::Unknown
}

/// Guess the Minecraft version from various files in the server directory
fn detect_minecraft_version(server_path: &Path, server_type: &ServerType) -> Option<String> {
    // Try from version.json if it exists
    if let Ok(content) = fs::read_to_string(server_path.join("version.json")) {
        if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
            if let Some(version) = json["name"].as_str() {
                return Some(version.to_string());
            }
        }
    }

    // Try from the server jar name pattern
    if let Ok(entries) = fs::read_dir(server_path) {
        for entry in entries {
            if let Ok(entry) = entry {
                let path = entry.path();
                if path.is_file() && path.extension().map_or(false, |ext| ext == "jar") {
                    let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");

                    // Extract version from various common patterns in jar names
                    if filename.starts_with("paper-") ||
                       filename.starts_with("spigot-") ||
                       filename.starts_with("craftbukkit-") {
                        // Pattern: paper-1.19.2.jar, spigot-1.19.2.jar
                        let parts: Vec<&str> = filename.split('-').collect();
                        if parts.len() > 1 {
                            let version_part = parts[1].trim_end_matches(".jar");
                            if version_part.contains('.') { // Basic version format check
                                return Some(version_part.to_string());
                            }
                        }
                    }

                    // Look for version patterns like minecraft_server.1.19.2.jar
                    if filename.starts_with("minecraft_server.") {
                        let version_part = filename
                            .trim_start_matches("minecraft_server.")
                            .trim_end_matches(".jar");
                        if version_part.contains('.') {
                            return Some(version_part.to_string());
                        }
                    }
                }
            }
        }
    }

    // If server type is proxy, look in config files
    if server_type == &ServerType::BungeeCord ||
       server_type == &ServerType::Waterfall ||
       server_type == &ServerType::Velocity {
        // Velocity uses TOML, others use YAML
        if server_type == &ServerType::Velocity {
            if let Ok(content) = fs::read_to_string(server_path.join("velocity.toml")) {
                // Very basic TOML parsing just for this field
                for line in content.lines() {
                    if line.contains("minecraft-version") {
                        if let Some(version) = line.split('=').nth(1) {
                            return Some(version.trim().trim_matches('"').to_string());
                        }
                    }
                }
            }
        } else {
            // Try to parse config.yml for BungeeCord/Waterfall
            if let Ok(content) = fs::read_to_string(server_path.join("config.yml")) {
                if let Ok(docs) = YamlLoader::load_from_str(&content) {
                    if !docs.is_empty() {
                        let doc = &docs[0];
                        if let Some(version) = doc["minecraft_version"].as_str() {
                            return Some(version.to_string());
                        }
                    }
                }
            }
        }
    }

    // Default fallback
    None
}

/// Get plugins directory path based on server type
fn get_plugins_directory(server_path: &Path, server_type: &ServerType) -> String {
    match server_type {
        ServerType::Velocity => server_path.join("plugins").to_string_lossy().to_string(),
        ServerType::BungeeCord => server_path.join("plugins").to_string_lossy().to_string(),
        ServerType::Waterfall => server_path.join("plugins").to_string_lossy().to_string(),
        _ => server_path.join("plugins").to_string_lossy().to_string(),
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ScanResult {
    server_info: ServerInfo,
    plugins: Vec<Plugin>,
}

#[command]
fn scan_server_directory(path: &str) -> Result<ScanResult, String> {
    let server_path = Path::new(path);

    if !server_path.exists() {
        return Err(format!("Server path does not exist: {}", path));
    }

    // Detect server type and version
    let server_type = detect_server_type(server_path);
    let minecraft_version = detect_minecraft_version(server_path, &server_type);

    println!("Detected server type: {:?}", server_type);
    if let Some(version) = &minecraft_version {
        println!("Detected Minecraft version: {}", version);
    }

    // Determine plugins directory based on server type
    let plugins_dir_str = get_plugins_directory(server_path, &server_type);
    let plugins_dir = Path::new(&plugins_dir_str);

    if !plugins_dir.exists() {
        return Err(format!("Plugins directory not found at: {}", plugins_dir.display()));
    }

    // Scan for JAR files in the plugins directory
    let mut plugins = Vec::new();

    match fs::read_dir(&plugins_dir) {
        Ok(entries) => {
            for entry in entries {
                if let Ok(entry) = entry {
                    let path = entry.path();

                    // Check if this is a JAR file
                    if path.is_file() && path.extension().map_or(false, |ext| ext.eq_ignore_ascii_case("jar")) {
                        match extract_plugin_metadata(&path) {
                            Ok(meta) => {
                                // Create a Plugin from PluginMeta
                                let plugin = Plugin {
                                    name: meta.name,
                                    version: meta.version,
                                    latest_version: None, // Will be filled by update checker
                                    description: meta.description,
                                    authors: meta.authors,
                                    has_update: false, // Will be determined by update checker
                                    api_version: meta.api_version,
                                    main_class: meta.main_class,
                                    depend: meta.depend,
                                    soft_depend: meta.soft_depend,
                                    load_before: meta.load_before,
                                    commands: meta.commands,
                                    permissions: meta.permissions,
                                    file_path: meta.file_path,
                                    file_hash: meta.file_hash,
                                };

                                plugins.push(plugin);
                            },
                            Err(e) => {
                                // Log error but continue with other plugins
                                println!("Error reading plugin from {}: {}", path.display(), e);
                            }
                        }
                    }
                }
            }
        },
        Err(e) => {
            return Err(format!("Failed to read plugins directory: {}", e));
        }
    }

    // If no plugins were found, fall back to mock data for testing
    if plugins.is_empty() && server_type == ServerType::Unknown {
        // For testing only - in production, we'd just return an empty list
        plugins = vec![
            Plugin {
                name: "EssentialsX".to_string(),
                version: "2.19.0".to_string(),
                latest_version: Some("2.20.0".to_string()),
                description: Some("Essential server tools for Minecraft".to_string()),
                authors: vec!["md_5".to_string(), "SupaHam".to_string()],
                has_update: true,
                api_version: Some("1.13".to_string()),
                main_class: Some("com.earth2me.essentials.Essentials".to_string()),
                depend: None,
                soft_depend: None,
                load_before: None,
                commands: None,
                permissions: None,
                file_path: "EssentialsX.jar".to_string(),
                file_hash: calculate_file_hash("EssentialsX.jar").unwrap_or_else(|_| "unknown".to_string()),
            },
            Plugin {
                name: "WorldEdit".to_string(),
                version: "7.2.8".to_string(),
                latest_version: Some("7.2.8".to_string()),
                description: Some("In-game map editor".to_string()),
                authors: vec!["sk89q".to_string(), "wizjany".to_string()],
                has_update: false,
                api_version: Some("1.13".to_string()),
                main_class: Some("com.sk89q.worldedit.bukkit.WorldEditPlugin".to_string()),
                depend: None,
                soft_depend: None,
                load_before: None,
                commands: None,
                permissions: None,
                file_path: "WorldEdit.jar".to_string(),
                file_hash: calculate_file_hash("WorldEdit.jar").unwrap_or_else(|_| "unknown".to_string()),
            },
        ];
    }

    // Create server info
    let server_info = ServerInfo {
        server_type,
        minecraft_version,
        plugins_directory: plugins_dir_str,
        plugins_count: plugins.len(),
    };

    Ok(ScanResult {
        server_info,
        plugins,
    })
}

#[command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

// Web Crawler Module
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum RepositorySource {
    HangarMC,
    SpigotMC,
    Modrinth,
    GitHub,
    BukkitDev,
    Custom(String),
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RepositoryPlugin {
    id: String,               // Unique identifier in the repository
    name: String,             // Plugin name
    version: String,          // Latest version
    description: Option<String>,
    authors: Vec<String>,
    download_url: String,     // URL to download the plugin
    repository: RepositorySource,
    page_url: String,         // URL to the plugin page
    download_count: Option<u64>,
    last_updated: Option<String>,
    icon_url: Option<String>,
    minecraft_versions: Vec<String>,
    categories: Vec<String>,
    rating: Option<f32>,
    file_size: Option<u64>,
    file_hash: Option<String>,
}

// Trait for implementing different repository crawlers
pub trait RepositoryCrawler {
    fn search(&self, query: &str) -> Result<Vec<RepositoryPlugin>, Box<dyn Error>>;
    fn get_plugin_details(&self, plugin_id: &str) -> Result<RepositoryPlugin, Box<dyn Error>>;
    fn get_plugin_versions(&self, plugin_id: &str) -> Result<Vec<String>, Box<dyn Error>>;
    fn download_plugin(&self, plugin_id: &str, version: &str, destination: &Path) -> Result<String, Box<dyn Error>>;
    fn get_repository_name(&self) -> RepositorySource;
}

// Basic HTTP client for crawler implementations
pub struct HttpClient {
    client: reqwest::blocking::Client,
}

impl HttpClient {
    pub fn new() -> Self {
        let client = reqwest::blocking::Client::builder()
            .user_agent("PlugSnatcher/0.1.0")
            .build()
            .unwrap_or_else(|_| reqwest::blocking::Client::new());

        HttpClient { client }
    }

    pub fn get(&self, url: &str) -> Result<String, Box<dyn Error>> {
        let response = self.client.get(url).send()?;

        if response.status().is_success() {
            Ok(response.text()?)
        } else {
            Err(format!("HTTP error: {}", response.status()).into())
        }
    }

    pub fn download(&self, url: &str, destination: &Path) -> Result<(), Box<dyn Error>> {
        let response = self.client.get(url).send()?;

        if response.status().is_success() {
            let bytes = response.bytes()?;
            fs::write(destination, bytes)?;
            Ok(())
        } else {
            Err(format!("Download failed: {}", response.status()).into())
        }
    }
}

// Helper function to get crawler for a specific repository
fn get_crawler(repository: &RepositorySource) -> Option<Box<dyn RepositoryCrawler>> {
    match repository {
        RepositorySource::HangarMC => Some(Box::new(HangarCrawler::new())),
        // Other repositories will be implemented later
        _ => None,
    }
}

// Command to search for plugins in specified repositories
#[command]
pub fn search_repository_plugins(query: &str, repositories: Vec<RepositorySource>) -> Result<Vec<RepositoryPlugin>, String> {
    let mut results: Vec<RepositoryPlugin> = Vec::new();

    // Try each requested repository
    for repo in repositories {
        if let Some(crawler) = get_crawler(&repo) {
            match crawler.search(query) {
                Ok(repo_results) => {
                    results.extend(repo_results);
                },
                Err(e) => {
                    println!("Error searching in repository {:?}: {}", repo, e);
                    // Continue with other repositories even if one fails
                }
            }
        } else {
            println!("Repository crawler for {:?} not implemented yet", repo);
        }
    }

    if results.is_empty() {
        Err("No plugins found or repositories not implemented yet".to_string())
    } else {
        Ok(results)
    }
}

// Command to get plugin details from a specific repository
#[command]
pub fn get_repository_plugin_details(plugin_id: &str, repository: RepositorySource) -> Result<RepositoryPlugin, String> {
    if let Some(crawler) = get_crawler(&repository) {
        crawler.get_plugin_details(plugin_id).map_err(|e| e.to_string())
    } else {
        Err(format!("Repository crawler for {:?} not implemented yet", repository))
    }
}

// Command to download a plugin from a repository
#[command]
pub fn download_repository_plugin(plugin_id: &str, version: &str, repository: RepositorySource, destination: &str) -> Result<String, String> {
    if let Some(crawler) = get_crawler(&repository) {
        crawler
            .download_plugin(plugin_id, version, Path::new(destination))
            .map_err(|e| e.to_string())
    } else {
        Err(format!("Repository crawler for {:?} not implemented yet", repository))
    }
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .invoke_handler(tauri::generate_handler![
            greet,
            scan_server_directory,
            search_repository_plugins,
            get_repository_plugin_details,
            download_repository_plugin
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
