use std::path::PathBuf;
use tauri::{AppHandle, State};
use tauri_plugin_http::reqwest;
use tauri_plugin_store::StoreExt;

use crate::achievements::{get_achievement_info, load_achievements, AchievementUnlockedEvent};
use crate::bridge_manager::SharedBridgeManager;
use crate::config::{ClaudeStartOptions, HikariConfig};
use crate::stats::UsageStats;
use crate::temp_manager::SharedTempFileManager;

const CONFIG_STORE_KEY: &str = "config";

#[tauri::command]
pub async fn start_claude(
    bridge_manager: State<'_, SharedBridgeManager>,
    conversation_id: String,
    options: ClaudeStartOptions,
) -> Result<(), String> {
    let mut manager = bridge_manager.lock();
    manager.start_claude(&conversation_id, options)
}

#[tauri::command]
pub async fn stop_claude(
    bridge_manager: State<'_, SharedBridgeManager>,
    conversation_id: String,
) -> Result<(), String> {
    let mut manager = bridge_manager.lock();
    manager.stop_claude(&conversation_id)
}

#[tauri::command]
pub async fn interrupt_claude(
    bridge_manager: State<'_, SharedBridgeManager>,
    conversation_id: String,
) -> Result<(), String> {
    let mut manager = bridge_manager.lock();
    manager.interrupt_claude(&conversation_id)
}

#[tauri::command]
pub async fn send_prompt(
    bridge_manager: State<'_, SharedBridgeManager>,
    conversation_id: String,
    message: String,
) -> Result<(), String> {
    let mut manager = bridge_manager.lock();
    manager.send_prompt(&conversation_id, message)
}

#[tauri::command]
pub async fn is_claude_running(
    bridge_manager: State<'_, SharedBridgeManager>,
    conversation_id: String,
) -> Result<bool, String> {
    let manager = bridge_manager.lock();
    Ok(manager.is_claude_running(&conversation_id))
}

#[tauri::command]
pub async fn get_working_directory(
    bridge_manager: State<'_, SharedBridgeManager>,
    conversation_id: String,
) -> Result<String, String> {
    let manager = bridge_manager.lock();
    manager.get_working_directory(&conversation_id)
}

#[tauri::command]
pub async fn select_wsl_directory() -> Result<String, String> {
    Ok("/home".to_string())
}

#[tauri::command]
pub async fn get_config(app: AppHandle) -> Result<HikariConfig, String> {
    let store = app.store("hikari-config.json").map_err(|e| e.to_string())?;

    match store.get(CONFIG_STORE_KEY) {
        Some(value) => serde_json::from_value(value.clone()).map_err(|e| e.to_string()),
        None => Ok(HikariConfig::default()),
    }
}

#[tauri::command]
pub async fn save_config(app: AppHandle, config: HikariConfig) -> Result<(), String> {
    let store = app.store("hikari-config.json").map_err(|e| e.to_string())?;

    let value = serde_json::to_value(&config).map_err(|e| e.to_string())?;
    store.set(CONFIG_STORE_KEY, value);
    store.save().map_err(|e| e.to_string())?;

    Ok(())
}

#[tauri::command]
pub async fn get_usage_stats(
    bridge_manager: State<'_, SharedBridgeManager>,
    conversation_id: String,
) -> Result<UsageStats, String> {
    let manager = bridge_manager.lock();
    manager.get_usage_stats(&conversation_id)
}

/// Load persisted lifetime stats from store (no bridge required)
#[tauri::command]
pub async fn get_persisted_stats(app: AppHandle) -> Result<UsageStats, String> {
    let mut stats = UsageStats::new();

    // Load persisted stats if available
    if let Some(persisted) = crate::stats::load_stats(&app).await {
        stats.apply_persisted(persisted);
    }

    Ok(stats)
}

#[tauri::command]
pub async fn validate_directory(
    path: String,
    current_dir: Option<String>,
) -> Result<String, String> {
    use std::path::Path;

    let path = Path::new(&path);

    // Expand ~ to home directory
    let expanded_path = if path.starts_with("~") {
        if let Some(home) = std::env::var_os("HOME") {
            let home_path = Path::new(&home);
            if path == Path::new("~") {
                home_path.to_path_buf()
            } else {
                home_path.join(path.strip_prefix("~").unwrap())
            }
        } else {
            return Err("Could not determine home directory".to_string());
        }
    } else if path.is_relative() {
        // Handle relative paths (., .., or any relative path) by resolving against current_dir
        if let Some(ref cwd) = current_dir {
            Path::new(cwd).join(path)
        } else {
            path.to_path_buf()
        }
    } else {
        path.to_path_buf()
    };

    // Check if the path exists and is a directory
    if !expanded_path.exists() {
        return Err(format!(
            "Directory does not exist: {}",
            expanded_path.display()
        ));
    }

    if !expanded_path.is_dir() {
        return Err(format!(
            "Path is not a directory: {}",
            expanded_path.display()
        ));
    }

    // Return the canonicalized (absolute) path
    expanded_path
        .canonicalize()
        .map(|p| p.to_string_lossy().to_string())
        .map_err(|e| format!("Failed to resolve path: {}", e))
}

#[tauri::command]
pub async fn load_saved_achievements(
    app: AppHandle,
) -> Result<Vec<AchievementUnlockedEvent>, String> {
    use chrono::Utc;

    // Load achievements from persistent store
    let progress = load_achievements(&app).await;

    // Create events for all previously unlocked achievements
    let mut events = Vec::new();
    for achievement_id in &progress.unlocked {
        let mut info = get_achievement_info(achievement_id);
        info.unlocked_at = Some(Utc::now()); // We don't store timestamps, so just use now
        events.push(AchievementUnlockedEvent { achievement: info });
    }

    Ok(events)
}

#[tauri::command]
pub async fn answer_question(
    bridge_manager: State<'_, SharedBridgeManager>,
    conversation_id: String,
    tool_use_id: String,
    answers: serde_json::Value,
) -> Result<(), String> {
    let mut manager = bridge_manager.lock();
    manager.send_tool_result(&conversation_id, &tool_use_id, answers)
}

#[tauri::command]
pub async fn list_skills() -> Result<Vec<String>, String> {
    use std::fs;
    use std::path::Path;

    // Get the home directory
    let home =
        std::env::var_os("HOME").ok_or_else(|| "Could not determine home directory".to_string())?;

    let skills_dir = Path::new(&home).join(".claude").join("skills");

    // If the skills directory doesn't exist, return empty list
    if !skills_dir.exists() {
        return Ok(Vec::new());
    }

    // Read the directory and collect skill names
    let mut skills = Vec::new();
    let entries =
        fs::read_dir(&skills_dir).map_err(|e| format!("Failed to read skills directory: {}", e))?;

    for entry in entries {
        let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
        let path = entry.path();

        // Only include directories that contain a SKILL.md file
        if path.is_dir() {
            let skill_file = path.join("SKILL.md");
            if skill_file.exists() {
                if let Some(name) = path.file_name() {
                    skills.push(name.to_string_lossy().to_string());
                }
            }
        }
    }

    // Sort alphabetically
    skills.sort();

    Ok(skills)
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct UpdateInfo {
    pub current_version: String,
    pub latest_version: String,
    pub has_update: bool,
    pub release_url: String,
    pub release_notes: Option<String>,
}

#[derive(Debug, serde::Deserialize)]
struct GiteaRelease {
    tag_name: String,
    html_url: String,
    body: Option<String>,
    prerelease: bool,
}

#[tauri::command]
pub async fn check_for_updates() -> Result<UpdateInfo, String> {
    const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
    const RELEASES_API: &str =
        "https://git.nhcarrigan.com/api/v1/repos/nhcarrigan/hikari-desktop/releases";

    // Fetch releases from Gitea API
    let client = reqwest::Client::new();
    let response = client
        .get(RELEASES_API)
        .header("Accept", "application/json")
        .send()
        .await
        .map_err(|e| format!("Failed to fetch releases: {}", e))?;

    if !response.status().is_success() {
        return Err(format!("API returned status: {}", response.status()));
    }

    let text = response
        .text()
        .await
        .map_err(|e| format!("Failed to read response: {}", e))?;

    let releases: Vec<GiteaRelease> =
        serde_json::from_str(&text).map_err(|e| format!("Failed to parse releases: {}", e))?;

    // Find the latest non-prerelease, or fall back to latest prerelease
    let latest = releases
        .iter()
        .find(|r| !r.prerelease)
        .or_else(|| releases.first());

    let latest = match latest {
        Some(r) => r,
        None => return Err("No releases found".to_string()),
    };

    // Parse version strings (remove 'v' prefix if present)
    let current = semver::Version::parse(CURRENT_VERSION)
        .map_err(|e| format!("Failed to parse current version: {}", e))?;

    let latest_tag = latest.tag_name.trim_start_matches('v');
    let latest_ver = semver::Version::parse(latest_tag)
        .map_err(|e| format!("Failed to parse latest version: {}", e))?;

    Ok(UpdateInfo {
        current_version: CURRENT_VERSION.to_string(),
        latest_version: latest.tag_name.clone(),
        has_update: latest_ver > current,
        release_url: latest.html_url.clone(),
        release_notes: latest.body.clone(),
    })
}

#[derive(Debug, Clone, serde::Serialize)]
pub struct SavedFileInfo {
    pub path: String,
    pub filename: String,
}

#[tauri::command]
pub async fn save_temp_file(
    temp_manager: State<'_, SharedTempFileManager>,
    conversation_id: String,
    data: Vec<u8>,
    filename: Option<String>,
) -> Result<SavedFileInfo, String> {
    let mut manager = temp_manager.lock();
    let path = manager.save_file(&conversation_id, &data, filename.as_deref())?;

    let filename = path
        .file_name()
        .map(|n| n.to_string_lossy().to_string())
        .unwrap_or_else(|| "unknown".to_string());

    Ok(SavedFileInfo {
        path: path.to_string_lossy().to_string(),
        filename,
    })
}

#[tauri::command]
pub async fn register_temp_file(
    temp_manager: State<'_, SharedTempFileManager>,
    conversation_id: String,
    file_path: String,
) -> Result<(), String> {
    let mut manager = temp_manager.lock();
    manager.register_file(&conversation_id, PathBuf::from(file_path));
    Ok(())
}

#[tauri::command]
pub async fn get_temp_files(
    temp_manager: State<'_, SharedTempFileManager>,
    conversation_id: String,
) -> Result<Vec<String>, String> {
    let manager = temp_manager.lock();
    let files = manager.get_files_for_conversation(&conversation_id);
    Ok(files.iter().map(|p| p.to_string_lossy().to_string()).collect())
}

#[tauri::command]
pub async fn cleanup_temp_files(
    temp_manager: State<'_, SharedTempFileManager>,
    conversation_id: String,
) -> Result<(), String> {
    let mut manager = temp_manager.lock();
    manager.cleanup_conversation(&conversation_id)
}

#[tauri::command]
pub async fn cleanup_all_temp_files(
    temp_manager: State<'_, SharedTempFileManager>,
) -> Result<(), String> {
    let mut manager = temp_manager.lock();
    manager.cleanup_all()
}

#[tauri::command]
pub async fn cleanup_orphaned_temp_files(
    temp_manager: State<'_, SharedTempFileManager>,
) -> Result<usize, String> {
    let mut manager = temp_manager.lock();
    manager.cleanup_orphaned_files()
}

#[tauri::command]
pub async fn get_file_size(file_path: String) -> Result<u64, String> {
    let metadata = std::fs::metadata(&file_path)
        .map_err(|e| format!("Failed to get file metadata: {}", e))?;
    Ok(metadata.len())
}

// ==================== Editor File Operations ====================

#[derive(Debug, Clone, serde::Serialize)]
pub struct FileEntry {
    pub name: String,
    pub path: String,
    #[serde(rename = "isDirectory")]
    pub is_directory: bool,
}

#[tauri::command]
pub async fn list_directory(path: String) -> Result<Vec<FileEntry>, String> {
    use std::fs;
    use std::path::Path;

    let dir_path = Path::new(&path);

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

    if !dir_path.is_dir() {
        return Err(format!("Path is not a directory: {}", path));
    }

    let entries = fs::read_dir(dir_path)
        .map_err(|e| format!("Failed to read directory: {}", e))?;

    let mut file_entries = Vec::new();

    for entry in entries {
        let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
        let path = entry.path();
        let name = entry
            .file_name()
            .to_string_lossy()
            .to_string();

        // Skip hidden files by default (can be made configurable later)
        if name.starts_with('.') {
            continue;
        }

        file_entries.push(FileEntry {
            name,
            path: path.to_string_lossy().to_string(),
            is_directory: path.is_dir(),
        });
    }

    Ok(file_entries)
}

#[tauri::command]
pub async fn read_file_content(path: String) -> Result<String, String> {
    use std::fs;

    fs::read_to_string(&path)
        .map_err(|e| format!("Failed to read file: {}", e))
}

#[tauri::command]
pub async fn write_file_content(path: String, content: String) -> Result<(), String> {
    use std::fs;

    fs::write(&path, content)
        .map_err(|e| format!("Failed to write file: {}", e))
}

#[tauri::command]
pub async fn create_file(path: String) -> Result<(), String> {
    use std::fs::File;
    use std::path::Path;

    let file_path = Path::new(&path);

    if file_path.exists() {
        return Err("File already exists".to_string());
    }

    File::create(file_path).map_err(|e| format!("Failed to create file: {}", e))?;

    Ok(())
}

#[tauri::command]
pub async fn create_directory(path: String) -> Result<(), String> {
    use std::fs;
    use std::path::Path;

    let dir_path = Path::new(&path);

    if dir_path.exists() {
        return Err("Directory already exists".to_string());
    }

    fs::create_dir_all(dir_path).map_err(|e| format!("Failed to create directory: {}", e))?;

    Ok(())
}

#[tauri::command]
pub async fn delete_file(path: String) -> Result<(), String> {
    use std::fs;
    use std::path::Path;

    let file_path = Path::new(&path);

    if !file_path.exists() {
        return Err("File does not exist".to_string());
    }

    if file_path.is_dir() {
        return Err("Path is a directory, use delete_directory instead".to_string());
    }

    fs::remove_file(file_path).map_err(|e| format!("Failed to delete file: {}", e))?;

    Ok(())
}

#[tauri::command]
pub async fn delete_directory(path: String) -> Result<(), String> {
    use std::fs;
    use std::path::Path;

    let dir_path = Path::new(&path);

    if !dir_path.exists() {
        return Err("Directory does not exist".to_string());
    }

    if !dir_path.is_dir() {
        return Err("Path is not a directory".to_string());
    }

    fs::remove_dir_all(dir_path).map_err(|e| format!("Failed to delete directory: {}", e))?;

    Ok(())
}

#[tauri::command]
pub async fn rename_path(old_path: String, new_path: String) -> Result<(), String> {
    use std::fs;
    use std::path::Path;

    let old = Path::new(&old_path);
    let new = Path::new(&new_path);

    if !old.exists() {
        return Err("Path does not exist".to_string());
    }

    if new.exists() {
        return Err("Destination already exists".to_string());
    }

    fs::rename(old, new).map_err(|e| format!("Failed to rename: {}", e))?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs::{self, File};
    use std::io::Write;
    use tempfile::TempDir;

    // Helper to run async tests
    fn run_async<F: std::future::Future>(f: F) -> F::Output {
        tokio::runtime::Runtime::new().unwrap().block_on(f)
    }

    // ==================== validate_directory tests ====================

    #[test]
    fn test_validate_directory_absolute_path_exists() {
        let temp_dir = TempDir::new().unwrap();
        let path = temp_dir.path().to_string_lossy().to_string();

        let result = run_async(validate_directory(path.clone(), None));
        assert!(result.is_ok());
        // Canonicalized path should be returned
        assert!(result.unwrap().contains(&temp_dir.path().file_name().unwrap().to_string_lossy().to_string()));
    }

    #[test]
    fn test_validate_directory_path_not_exists() {
        let result = run_async(validate_directory(
            "/nonexistent/path/that/does/not/exist".to_string(),
            None,
        ));
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("does not exist"));
    }

    #[test]
    fn test_validate_directory_path_is_file() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test_file.txt");
        File::create(&file_path).unwrap();

        let result = run_async(validate_directory(
            file_path.to_string_lossy().to_string(),
            None,
        ));
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("not a directory"));
    }

    #[test]
    fn test_validate_directory_home_expansion() {
        // This test assumes HOME is set (which it should be on most systems)
        if std::env::var_os("HOME").is_some() {
            let result = run_async(validate_directory("~".to_string(), None));
            assert!(result.is_ok());
            // Should not contain ~ after expansion
            assert!(!result.unwrap().contains("~"));
        }
    }

    #[test]
    fn test_validate_directory_home_subpath_expansion() {
        // This test assumes HOME is set and has some subdirectory
        if let Some(home) = std::env::var_os("HOME") {
            let home_path = std::path::Path::new(&home);
            // Find any subdirectory in home
            if let Ok(entries) = fs::read_dir(home_path) {
                for entry in entries.flatten() {
                    if entry.path().is_dir() {
                        let subdir_name = entry.file_name().to_string_lossy().to_string();
                        let tilde_path = format!("~/{}", subdir_name);
                        let result = run_async(validate_directory(tilde_path, None));
                        assert!(result.is_ok());
                        assert!(!result.unwrap().contains("~"));
                        break;
                    }
                }
            }
        }
    }

    #[test]
    fn test_validate_directory_relative_path_with_current_dir() {
        let temp_dir = TempDir::new().unwrap();
        let subdir = temp_dir.path().join("subdir");
        fs::create_dir(&subdir).unwrap();

        let result = run_async(validate_directory(
            "subdir".to_string(),
            Some(temp_dir.path().to_string_lossy().to_string()),
        ));
        assert!(result.is_ok());
        assert!(result.unwrap().contains("subdir"));
    }

    #[test]
    fn test_validate_directory_dot_path() {
        let temp_dir = TempDir::new().unwrap();

        let result = run_async(validate_directory(
            ".".to_string(),
            Some(temp_dir.path().to_string_lossy().to_string()),
        ));
        assert!(result.is_ok());
    }

    #[test]
    fn test_validate_directory_dotdot_path() {
        let temp_dir = TempDir::new().unwrap();
        let subdir = temp_dir.path().join("subdir");
        fs::create_dir(&subdir).unwrap();

        let result = run_async(validate_directory(
            "..".to_string(),
            Some(subdir.to_string_lossy().to_string()),
        ));
        assert!(result.is_ok());
        // Should resolve to parent
        let resolved = result.unwrap();
        assert!(resolved.contains(&temp_dir.path().file_name().unwrap().to_string_lossy().to_string()));
    }

    #[test]
    fn test_validate_directory_relative_without_current_dir() {
        // Relative path without current_dir - should fail since relative path likely won't exist
        let result = run_async(validate_directory(
            "some_random_nonexistent_relative_path".to_string(),
            None,
        ));
        assert!(result.is_err());
    }

    // ==================== get_file_size tests ====================

    #[test]
    fn test_get_file_size_empty_file() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("empty.txt");
        File::create(&file_path).unwrap();

        let result = run_async(get_file_size(file_path.to_string_lossy().to_string()));
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), 0);
    }

    #[test]
    fn test_get_file_size_with_content() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("content.txt");
        let mut file = File::create(&file_path).unwrap();
        file.write_all(b"Hello, Hikari!").unwrap();

        let result = run_async(get_file_size(file_path.to_string_lossy().to_string()));
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), 14); // "Hello, Hikari!" is 14 bytes
    }

    #[test]
    fn test_get_file_size_larger_file() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("large.txt");
        let mut file = File::create(&file_path).unwrap();
        // Write 1000 bytes
        let data = vec![b'x'; 1000];
        file.write_all(&data).unwrap();

        let result = run_async(get_file_size(file_path.to_string_lossy().to_string()));
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), 1000);
    }

    #[test]
    fn test_get_file_size_nonexistent_file() {
        let result = run_async(get_file_size(
            "/nonexistent/path/file.txt".to_string(),
        ));
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("Failed to get file metadata"));
    }

    #[test]
    fn test_get_file_size_directory() {
        let temp_dir = TempDir::new().unwrap();

        // Getting "size" of a directory should work but return directory metadata
        // This is actually valid - directories have metadata too
        let result = run_async(get_file_size(temp_dir.path().to_string_lossy().to_string()));
        assert!(result.is_ok());
        // Directory size is platform-dependent, just check it returns something
    }

    // ==================== list_skills tests ====================

    #[test]
    fn test_list_skills_no_skills_dir() {
        // This test is tricky because it depends on HOME being set
        // and potentially affecting real user data, so we'll just
        // verify the function doesn't panic
        let result = run_async(list_skills());
        // Should either return Ok with a list or Ok with empty vec
        assert!(result.is_ok());
    }

    // ==================== select_wsl_directory tests ====================

    #[test]
    fn test_select_wsl_directory_returns_home() {
        let result = run_async(select_wsl_directory());
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), "/home");
    }

    // ==================== UpdateInfo struct tests ====================

    #[test]
    fn test_update_info_serialization() {
        let info = UpdateInfo {
            current_version: "1.0.0".to_string(),
            latest_version: "0.4.0".to_string(),
            has_update: true,
            release_url: "https://example.com/release".to_string(),
            release_notes: Some("New features!".to_string()),
        };

        let json = serde_json::to_string(&info).unwrap();
        assert!(json.contains("1.0.0"));
        assert!(json.contains("0.4.0"));
        assert!(json.contains("true"));
        assert!(json.contains("New features!"));
    }

    #[test]
    fn test_update_info_without_notes() {
        let info = UpdateInfo {
            current_version: "1.0.0".to_string(),
            latest_version: "1.0.0".to_string(),
            has_update: false,
            release_url: "https://example.com/release".to_string(),
            release_notes: None,
        };

        let json = serde_json::to_string(&info).unwrap();
        assert!(json.contains("null") || json.contains("release_notes"));
    }

    // ==================== SavedFileInfo struct tests ====================

    #[test]
    fn test_saved_file_info_serialization() {
        let info = SavedFileInfo {
            path: "/tmp/test.txt".to_string(),
            filename: "test.txt".to_string(),
        };

        let json = serde_json::to_string(&info).unwrap();
        assert!(json.contains("/tmp/test.txt"));
        assert!(json.contains("test.txt"));
    }
}
