mod commands;
mod db;
mod error;
mod models;
mod services;

use std::sync::{Arc, Mutex};
use tauri::Manager;

pub struct AppState {
    pub db: Arc<Mutex<rusqlite::Connection>>,
    pub encryption_key: [u8; 32],
    pub http_client: reqwest::Client,
    pub process_registry: services::process_registry::ProcessRegistry,
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_notification::init())
        .setup(|app| {
            let db_dir = app.path().app_data_dir()?;
            std::fs::create_dir_all(&db_dir)?;

            let db_path = db_dir.join("orchai.db");
            let conn = db::init(&db_path).expect("Failed to initialize database");

            let key_path = db_dir.join("orchai.key");
            let encryption_key = load_or_generate_key(&key_path)?;

            let http_client = reqwest::Client::new();
            let process_registry = services::process_registry::ProcessRegistry::default();

            let db_arc = Arc::new(Mutex::new(conn));
            app.manage(AppState {
                db: db_arc.clone(),
                encryption_key,
                http_client: http_client.clone(),
                process_registry: process_registry.clone(),
            });

            // Start background poller
            services::poller::start(
                db_arc.clone(),
                encryption_key,
                http_client,
                app.handle().clone(),
            );

            // Start agent orchestrator
            services::orchestrator::start(
                db_arc.clone(),
                app.handle().clone(),
                process_registry.clone(),
            );

            // Start agent task runner
            services::task_runner::start(db_arc, app.handle().clone(), process_registry);

            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            commands::agent::create_agent,
            commands::agent::list_agents,
            commands::agent::get_agent,
            commands::agent::update_agent,
            commands::agent::delete_agent,
            commands::agent::improve_agent_prompt,
            commands::project::create_project,
            commands::project::list_projects,
            commands::project::get_project,
            commands::project::update_project,
            commands::project::delete_project,
            commands::credential::set_tuleap_credentials,
            commands::credential::get_tuleap_credentials,
            commands::credential::delete_tuleap_credentials,
            commands::credential::test_tuleap_connection,
            commands::tracker::add_tracker,
            commands::tracker::list_trackers,
            commands::tracker::update_tracker,
            commands::tracker::remove_tracker,
            commands::tracker::get_tracker_fields,
            commands::tracker::list_processed_tickets,
            commands::poller::manual_poll,
            commands::poller::get_queue_status,
            commands::poller::get_project_throughput,
            commands::notification::list_notifications,
            commands::notification::mark_notification_read,
            commands::notification::mark_all_notifications_read,
            commands::module::list_project_modules,
            commands::module::set_project_module_enabled,
            commands::orchestrator::get_ticket_result,
            commands::orchestrator::retry_ticket,
            commands::orchestrator::cancel_ticket,
            commands::live_agent::create_live_session,
            commands::live_agent::list_live_sessions,
            commands::live_agent::list_live_messages,
            commands::live_agent::set_live_session_archived,
            commands::live_agent::send_live_message,
            commands::task::create_agent_task,
            commands::task::list_agent_tasks,
            commands::task::retry_agent_task,
            commands::task::cancel_agent_task,
            commands::worktree::list_worktrees,
            commands::worktree::get_worktree_diff,
            commands::worktree::apply_fix_to_branch,
            commands::worktree::delete_worktree_cmd,
            commands::worktree::list_local_branches,
            commands::worktree::list_local_branches_for_worktree,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

fn load_or_generate_key(path: &std::path::Path) -> Result<[u8; 32], Box<dyn std::error::Error>> {
    use rand::RngCore;

    if path.exists() {
        return read_key(path);
    }

    let mut key = [0u8; 32];
    rand::rngs::OsRng.fill_bytes(&mut key);

    match write_new_key(path, &key) {
        Ok(()) => Ok(key),
        Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => read_key(path),
        Err(err) => Err(err.into()),
    }
}

fn read_key(path: &std::path::Path) -> Result<[u8; 32], Box<dyn std::error::Error>> {
    enforce_key_permissions(path)?;

    let bytes = std::fs::read(path)?;
    if bytes.len() != 32 {
        return Err("Invalid key file size".into());
    }

    let mut key = [0u8; 32];
    key.copy_from_slice(&bytes);
    Ok(key)
}

fn write_new_key(path: &std::path::Path, key: &[u8; 32]) -> std::io::Result<()> {
    #[cfg(unix)]
    {
        use std::fs::OpenOptions;
        use std::io::Write;
        use std::os::unix::fs::OpenOptionsExt;

        let mut file = OpenOptions::new()
            .create_new(true)
            .write(true)
            .mode(0o600)
            .open(path)?;
        file.write_all(key)?;
        file.sync_all()?;
    }

    #[cfg(not(unix))]
    {
        std::fs::write(path, key)?;
    }

    Ok(())
}

fn enforce_key_permissions(path: &std::path::Path) -> std::io::Result<()> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;

        let metadata = std::fs::metadata(path)?;
        let current_mode = metadata.permissions().mode() & 0o777;
        if current_mode != 0o600 {
            let mut permissions = metadata.permissions();
            permissions.set_mode(0o600);
            std::fs::set_permissions(path, permissions)?;
        }
    }

    #[cfg(not(unix))]
    {
        let _ = path;
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn test_load_or_generate_key_creates_key() {
        let dir = tempdir().expect("tempdir should be created");
        let path = dir.path().join("orchai.key");

        let key = load_or_generate_key(&path).expect("key generation should succeed");
        assert_eq!(key.len(), 32);

        let persisted = fs::read(&path).expect("key file should exist");
        assert_eq!(persisted.len(), 32);
        assert_eq!(persisted, key);
    }

    #[test]
    fn test_load_or_generate_key_reads_existing_key() {
        let dir = tempdir().expect("tempdir should be created");
        let path = dir.path().join("orchai.key");
        let expected = [42u8; 32];
        fs::write(&path, expected).expect("existing key should be written");

        let loaded = load_or_generate_key(&path).expect("existing key should be loaded");
        assert_eq!(loaded, expected);
    }

    #[cfg(unix)]
    #[test]
    fn test_load_or_generate_key_creates_private_permissions() {
        use std::os::unix::fs::PermissionsExt;

        let dir = tempdir().expect("tempdir should be created");
        let path = dir.path().join("orchai.key");

        load_or_generate_key(&path).expect("key generation should succeed");

        let mode = fs::metadata(&path)
            .expect("metadata should be readable")
            .permissions()
            .mode()
            & 0o777;
        assert_eq!(mode, 0o600);
    }

    #[cfg(unix)]
    #[test]
    fn test_load_or_generate_key_hardens_existing_permissions() {
        use std::os::unix::fs::PermissionsExt;

        let dir = tempdir().expect("tempdir should be created");
        let path = dir.path().join("orchai.key");
        fs::write(&path, [7u8; 32]).expect("existing key should be written");

        let mut permissions = fs::metadata(&path)
            .expect("metadata should be readable")
            .permissions();
        permissions.set_mode(0o644);
        fs::set_permissions(&path, permissions).expect("permissions should be set");

        load_or_generate_key(&path).expect("existing key should be loaded");

        let mode = fs::metadata(&path)
            .expect("metadata should be readable")
            .permissions()
            .mode()
            & 0o777;
        assert_eq!(mode, 0o600);
    }
}
