pub mod gemini;
pub mod translator;

use crate::translator::{
    ModelId, ProviderCredentials, RewriteMode, RewriteProvider, RewriteRequest,
};
use enigo::{Enigo, Key, KeyboardControllable};
use futures_util::StreamExt;
use serde::Serialize;
use std::str::FromStr;
use std::sync::Mutex;
use std::thread;
use std::time::{Duration, Instant};
use tauri::menu::MenuBuilder;
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
use tauri::{AppHandle, Emitter, Manager, State, WebviewWindow, WindowEvent};
use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};

const KEYRING_SERVICE: &str = "thisper";
const KEYRING_ACCOUNT: &str = "gemini_api_key";
const MAIN_WINDOW_LABEL: &str = "main";
const TRAY_ID: &str = "main-tray";
const MENU_SHOW_ID: &str = "show";
const MENU_REWRITE_ID: &str = "rewrite";
const MENU_QUIT_ID: &str = "quit";
const EVENT_RUNTIME_STATUS: &str = "runtime-status";
const EVENT_TRAY_HINT: &str = "tray-hint";

struct AppState {
    provider: Box<dyn RewriteProvider + Send + Sync>,
    active_model: Mutex<ModelId>,
    api_key: Mutex<String>,
    runtime: Mutex<RuntimeState>,
}

#[derive(Debug, Clone, Serialize)]
struct RewriteMetrics {
    duration_ms: u64,
    input_length: usize,
    model: ModelId,
    success: bool,
    error_category: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
struct RuntimeStatus {
    hotkey_registered: bool,
    background_mode: bool,
    active_model: String,
    api_key_configured: bool,
    last_error: Option<String>,
    rewrite_attempts: u64,
    rewrite_successes: u64,
    rewrite_failures: u64,
    last_metrics: Option<RewriteMetrics>,
}

#[derive(Debug, Default)]
struct RuntimeState {
    hotkey_registered: bool,
    background_mode: bool,
    tray_hint_seen: bool,
    quitting: bool,
    rewrite_attempts: u64,
    rewrite_successes: u64,
    rewrite_failures: u64,
    last_error: Option<String>,
    last_metrics: Option<RewriteMetrics>,
}

fn load_saved_api_key() -> Result<Option<String>, String> {
    let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_ACCOUNT)
        .map_err(|err| format!("Failed to access credential store: {err}"))?;
    match entry.get_password() {
        Ok(password) if !password.trim().is_empty() => Ok(Some(password)),
        Ok(_) => Ok(None),
        Err(keyring::Error::NoEntry) => Ok(None),
        Err(err) => Err(format!("Failed to read credential store: {err}")),
    }
}

fn api_key_configured(state: &AppState) -> bool {
    let key = state.api_key.lock().unwrap();
    !key.trim().is_empty() || std::env::var("GEMINI_API_KEY").is_ok()
}

fn categorize_error(error: &str) -> String {
    let lower = error.to_ascii_lowercase();
    if lower.contains("api key") || lower.contains("credential") {
        "credentials".to_string()
    } else if lower.contains("clipboard") {
        "clipboard".to_string()
    } else if lower.contains("stream") || lower.contains("gemini") || lower.contains("http") {
        "provider".to_string()
    } else if lower.contains("window") || lower.contains("tray") {
        "window".to_string()
    } else if lower.contains("shortcut") {
        "shortcut".to_string()
    } else {
        "unknown".to_string()
    }
}

fn main_window(app: &AppHandle) -> Result<WebviewWindow, String> {
    app.get_webview_window(MAIN_WINDOW_LABEL)
        .ok_or_else(|| "Main window is unavailable.".to_string())
}

fn snapshot_runtime_status(state: &AppState) -> RuntimeStatus {
    let runtime = state.runtime.lock().unwrap();
    let active_model = state.active_model.lock().unwrap();

    RuntimeStatus {
        hotkey_registered: runtime.hotkey_registered,
        background_mode: runtime.background_mode,
        active_model: active_model.to_string(),
        api_key_configured: api_key_configured(state),
        last_error: runtime.last_error.clone(),
        rewrite_attempts: runtime.rewrite_attempts,
        rewrite_successes: runtime.rewrite_successes,
        rewrite_failures: runtime.rewrite_failures,
        last_metrics: runtime.last_metrics.clone(),
    }
}

fn emit_runtime_status(app: &AppHandle) {
    let state: State<'_, AppState> = app.state();
    let status = snapshot_runtime_status(&state);
    let _ = app.emit(EVENT_RUNTIME_STATUS, status);
}

fn set_background_mode(app: &AppHandle, hidden: bool) {
    let state: State<'_, AppState> = app.state();
    {
        let mut runtime = state.runtime.lock().unwrap();
        runtime.background_mode = hidden;
    }
    emit_runtime_status(app);
}

fn record_rewrite_attempt(app: &AppHandle) {
    let state: State<'_, AppState> = app.state();
    {
        let mut runtime = state.runtime.lock().unwrap();
        runtime.rewrite_attempts += 1;
    }
    emit_runtime_status(app);
}

fn record_rewrite_outcome(
    app: &AppHandle,
    input_length: usize,
    model: ModelId,
    started_at: Instant,
    result: &Result<(), String>,
) {
    let duration_ms = started_at.elapsed().as_millis() as u64;
    let state: State<'_, AppState> = app.state();
    let mut runtime = state.runtime.lock().unwrap();
    let success = result.is_ok();

    if success {
        runtime.rewrite_successes += 1;
        runtime.last_error = None;
    } else {
        runtime.rewrite_failures += 1;
        runtime.last_error = result.as_ref().err().cloned();
    }

    runtime.last_metrics = Some(RewriteMetrics {
        duration_ms,
        input_length,
        model,
        success,
        error_category: result.as_ref().err().map(|err| categorize_error(err)),
    });

    drop(runtime);
    emit_runtime_status(app);
}

fn show_main_window_inner(app: &AppHandle) -> Result<(), String> {
    let window = main_window(app)?;
    window
        .unminimize()
        .map_err(|err| format!("Failed to restore main window: {err}"))?;
    window
        .show()
        .map_err(|err| format!("Failed to show main window: {err}"))?;
    window
        .set_focus()
        .map_err(|err| format!("Failed to focus main window: {err}"))?;
    set_background_mode(app, false);
    Ok(())
}

fn hide_main_window_inner(app: &AppHandle) -> Result<(), String> {
    let window = main_window(app)?;
    window
        .hide()
        .map_err(|err| format!("Failed to hide main window: {err}"))?;
    set_background_mode(app, true);
    Ok(())
}

fn emit_tray_hint(app: &AppHandle) {
    let should_emit = {
        let state: State<'_, AppState> = app.state();
        let mut runtime = state.runtime.lock().unwrap();
        if runtime.tray_hint_seen {
            false
        } else {
            runtime.tray_hint_seen = true;
            true
        }
    };

    if should_emit {
        let _ = app.emit(
            EVENT_TRAY_HINT,
            "Thisper is still running in the tray. Use Show Thisper or Quit from the tray menu.",
        );
    }
}

fn build_tray(app: &AppHandle) -> Result<(), String> {
    let menu = MenuBuilder::new(app)
        .text(MENU_SHOW_ID, "Show Thisper")
        .text(MENU_REWRITE_ID, "Run Clipboard Rewrite")
        .separator()
        .text(MENU_QUIT_ID, "Quit")
        .build()
        .map_err(|err| format!("Failed to build tray menu: {err}"))?;

    let icon = app
        .default_window_icon()
        .cloned()
        .ok_or_else(|| "Tray icon is unavailable.".to_string())?;

    TrayIconBuilder::with_id(TRAY_ID)
        .icon(icon)
        .menu(&menu)
        .show_menu_on_left_click(false)
        .on_menu_event(|app, event| match event.id() {
            id if id == MENU_SHOW_ID => {
                let _ = show_main_window_inner(app);
            }
            id if id == MENU_REWRITE_ID => {
                let app_handle = app.clone();
                tauri::async_runtime::spawn(async move {
                    if let Err(err) = handle_global_rewrite(&app_handle).await {
                        eprintln!("Tray rewrite error: {err}");
                    }
                });
            }
            id if id == MENU_QUIT_ID => {
                let state: State<'_, AppState> = app.state();
                {
                    let mut runtime = state.runtime.lock().unwrap();
                    runtime.quitting = true;
                }
                app.exit(0);
            }
            _ => {}
        })
        .on_tray_icon_event(|tray: &tauri::tray::TrayIcon<_>, event| match event {
            TrayIconEvent::Click {
                button: MouseButton::Left,
                button_state: MouseButtonState::Up,
                ..
            }
            | TrayIconEvent::DoubleClick {
                button: MouseButton::Left,
                ..
            } => {
                let _ = show_main_window_inner(tray.app_handle());
            }
            _ => {}
        })
        .build(app)
        .map_err(|err| format!("Failed to create tray icon: {err}"))?;

    Ok(())
}

fn attach_window_lifecycle(app: &AppHandle) -> Result<(), String> {
    let window = main_window(app)?;
    let app_handle = app.clone();

    window.on_window_event(move |event| match event {
        WindowEvent::CloseRequested { api, .. } => {
            let should_quit = {
                let state: State<'_, AppState> = app_handle.state();
                let runtime = state.runtime.lock().unwrap();
                runtime.quitting
            };

            if should_quit {
                return;
            }

            api.prevent_close();
            let hint_seen = {
                let state: State<'_, AppState> = app_handle.state();
                let runtime = state.runtime.lock().unwrap();
                runtime.tray_hint_seen
            };

            if hint_seen {
                let _ = hide_main_window_inner(&app_handle);
            } else {
                emit_tray_hint(&app_handle);
            }
        }
        WindowEvent::Focused(false) => {
            if let Ok(window) = main_window(&app_handle) {
                if !window.is_visible().unwrap_or(true) {
                    set_background_mode(&app_handle, true);
                }
            }
        }
        _ => {}
    });

    Ok(())
}

#[tauri::command]
async fn save_api_key(
    key: String,
    app: AppHandle,
    state: State<'_, AppState>,
) -> Result<(), String> {
    let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_ACCOUNT)
        .map_err(|err| format!("Failed to access credential store: {err}"))?;
    entry
        .set_password(&key)
        .map_err(|err| format!("Failed to save API key securely: {err}"))?;

    {
        let mut key_state = state.api_key.lock().unwrap();
        *key_state = key;
    }

    emit_runtime_status(&app);
    Ok(())
}

#[tauri::command]
async fn get_api_key_status(state: State<'_, AppState>) -> Result<bool, String> {
    Ok(api_key_configured(&state))
}

#[tauri::command]
async fn set_active_model(
    model: String,
    app: AppHandle,
    state: State<'_, AppState>,
) -> Result<(), String> {
    let parsed_model = ModelId::from_str(&model)?;
    let mut active = state.active_model.lock().unwrap();
    *active = parsed_model;
    drop(active);
    emit_runtime_status(&app);
    Ok(())
}

#[tauri::command]
async fn rewrite_text(
    text: String,
    mode_str: String,
    model_str: String,
    app: AppHandle,
    state: State<'_, AppState>,
) -> Result<String, String> {
    let mode = RewriteMode::from_str(&mode_str)?;
    let model = ModelId::from_str(&model_str)?;
    let api_key = {
        let key = state.api_key.lock().unwrap();
        key.clone()
    };
    let started_at = Instant::now();
    let input_length = text.len();

    record_rewrite_attempt(&app);

    let request = RewriteRequest { text, mode, model };
    let credentials = ProviderCredentials { api_key };
    let result = state.provider.rewrite(&request, &credentials).await;

    let outcome = result.as_ref().map(|_| ()).map_err(|err| err.clone());
    record_rewrite_outcome(
        &app,
        input_length,
        request.model.clone(),
        started_at,
        &outcome,
    );

    result
}

#[tauri::command]
async fn get_runtime_status(state: State<'_, AppState>) -> Result<RuntimeStatus, String> {
    Ok(snapshot_runtime_status(&state))
}

#[tauri::command]
async fn show_main_window(app: AppHandle) -> Result<(), String> {
    show_main_window_inner(&app)
}

#[tauri::command]
async fn hide_main_window(app: AppHandle) -> Result<(), String> {
    hide_main_window_inner(&app)
}

#[tauri::command]
async fn run_clipboard_rewrite(app: AppHandle) -> Result<(), String> {
    handle_global_rewrite(&app).await
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    let initial_key = load_saved_api_key()
        .ok()
        .flatten()
        .or_else(|| std::env::var("GEMINI_API_KEY").ok())
        .unwrap_or_default();

    let provider =
        Box::new(gemini::GeminiProvider::new().expect("Gemini provider initialization failed"))
            as Box<dyn RewriteProvider + Send + Sync>;

    let rewrite_shortcut = Shortcut::new(
        Some(
            tauri_plugin_global_shortcut::Modifiers::CONTROL
                | tauri_plugin_global_shortcut::Modifiers::ALT,
        ),
        tauri_plugin_global_shortcut::Code::KeyR,
    );
    let shortcut_clone = rewrite_shortcut.clone();

    tauri::Builder::default()
        .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
            let _ = show_main_window_inner(app);
        }))
        .manage(AppState {
            provider,
            active_model: Mutex::new(ModelId::default()),
            api_key: Mutex::new(initial_key),
            runtime: Mutex::new(RuntimeState::default()),
        })
        .plugin(tauri_plugin_clipboard_manager::init())
        .plugin(
            tauri_plugin_global_shortcut::Builder::new()
                .with_handler(move |app, shortcut, event| {
                    if event.state() == tauri_plugin_global_shortcut::ShortcutState::Pressed
                        && shortcut == &rewrite_shortcut
                    {
                        let app_handle = app.clone();
                        tauri::async_runtime::spawn(async move {
                            if let Err(err) = handle_global_rewrite(&app_handle).await {
                                eprintln!("Global rewrite error: {err}");
                            }
                        });
                    }
                })
                .build(),
        )
        .setup(move |app| {
            build_tray(&app.handle()).map_err(std::io::Error::other)?;
            attach_window_lifecycle(&app.handle()).map_err(std::io::Error::other)?;
            app.global_shortcut().register(shortcut_clone.clone())?;

            {
                let state: State<'_, AppState> = app.state();
                let mut runtime = state.runtime.lock().unwrap();
                runtime.hotkey_registered = true;
            }

            emit_runtime_status(&app.handle());
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            rewrite_text,
            set_active_model,
            save_api_key,
            get_api_key_status,
            get_runtime_status,
            show_main_window,
            hide_main_window,
            run_clipboard_rewrite
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

async fn handle_global_rewrite(app: &AppHandle) -> Result<(), String> {
    let state: State<'_, AppState> = app.state();
    let model = {
        let active = state.active_model.lock().unwrap();
        active.clone()
    };

    record_rewrite_attempt(app);

    let api_key = {
        let key = state.api_key.lock().unwrap();
        key.clone()
    };

    if api_key.trim().is_empty() {
        let _ = show_main_window_inner(app);
        let result = Err(
            "Gemini API key is not configured. Open Thisper and save a key in Settings."
                .to_string(),
        );
        record_rewrite_outcome(app, 0, model, Instant::now(), &result);
        return result;
    }

    let started_at = Instant::now();
    let mut enigo = Enigo::new();
    let clipboard = app.clipboard();
    let original_clipboard = clipboard.read_text().ok();

    enigo.key_up(Key::Alt);
    enigo.key_up(Key::Control);
    thread::sleep(Duration::from_millis(100));

    enigo.key_down(Key::Control);
    enigo.key_click(Key::Layout('c'));
    enigo.key_up(Key::Control);
    thread::sleep(Duration::from_millis(350));

    let text = match clipboard.read_text() {
        Ok(text) => text,
        Err(err) => {
            let result = Err(format!("Failed to read clipboard text: {err}"));
            record_rewrite_outcome(app, 0, model, started_at, &result);
            return result;
        }
    };

    if text.trim().is_empty() {
        let result = Ok(());
        record_rewrite_outcome(app, 0, model, started_at, &result);
        return result;
    }

    let request = RewriteRequest {
        text,
        mode: RewriteMode::PreserveVoice,
        model: model.clone(),
    };
    let input_length = request.text.len();
    let credentials = ProviderCredentials { api_key };

    let result = async {
        let mut stream = state
            .provider
            .rewrite_stream(&request, &credentials)
            .await?;
        let mut rewritten = String::new();

        while let Some(chunk) = stream.next().await {
            match chunk {
                Ok(part) => rewritten.push_str(&part),
                Err(err) => {
                    if let Some(previous) = original_clipboard.as_ref() {
                        let _ = clipboard.write_text(previous.clone());
                    }
                    return Err(err);
                }
            }
        }

        let rewritten = rewritten.trim().to_string();
        if rewritten.is_empty() {
            if let Some(previous) = original_clipboard.as_ref() {
                let _ = clipboard.write_text(previous.clone());
            }
            return Err("Rewrite returned no text.".to_string());
        }

        clipboard
            .write_text(rewritten)
            .map_err(|err| format!("Failed to stage rewritten text in clipboard: {err}"))?;

        thread::sleep(Duration::from_millis(50));
        enigo.key_down(Key::Control);
        enigo.key_click(Key::Layout('v'));
        enigo.key_up(Key::Control);
        thread::sleep(Duration::from_millis(50));

        if let Some(previous) = original_clipboard {
            let _ = clipboard.write_text(previous);
        }

        Ok(())
    }
    .await;

    record_rewrite_outcome(app, input_length, model, started_at, &result);
    result
}
