mod tunnel_manager;

use std::{fs, io::Cursor, net::TcpListener, path::PathBuf, sync::Mutex};

use base64::{engine::general_purpose::STANDARD, Engine as _};
use png::{ColorType, Decoder};
use rand_core::OsRng;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tauri::{
    image::Image,
    menu::{MenuBuilder, MenuItem, MenuItemBuilder, PredefinedMenuItem},
    tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent},
    AppHandle, Manager, State, WebviewWindow, Window, WindowEvent, Wry,
};
use x25519_dalek::{PublicKey, StaticSecret};

const PROFILE_NAME: &str = "NexaVPN";
const MAIN_WINDOW_LABEL: &str = "main";
const SINGLE_INSTANCE_ADDR: &str = "127.0.0.1:53190";

struct AppState {
    session: Mutex<Option<SessionState>>,
    tray: Mutex<Option<TrayState>>,
    single_instance_lock: TcpListener,
}

struct TrayState {
    tray_icon: TrayIcon<Wry>,
    disconnected_icon: Image<'static>,
    connected_icon: Image<'static>,
    status_item: MenuItem<Wry>,
    received_item: MenuItem<Wry>,
    sent_item: MenuItem<Wry>,
    toggle_item: MenuItem<Wry>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SessionState {
    access_token: String,
    refresh_token: String,
    server_url: String,
    profile_path: String,
    private_key: String,
    enrollment: EnrollmentResult,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct EnrollmentPayload {
    server_url: String,
    username: String,
    password: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct EnrollmentResult {
    assigned_ip: String,
    resources: Vec<String>,
    profile_revision: u32,
    gateway_endpoint: String,
    profile_path: String,
    last_sync_time: String,
    tunnel_strategy: String,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TunnelMetrics {
    active: bool,
    rx_bytes: u64,
    tx_bytes: u64,
}

#[derive(Debug, Serialize)]
struct LoginRequest<'a> {
    username: &'a str,
    password: &'a str,
}

#[derive(Debug, Serialize)]
struct RefreshRequest<'a> {
    refresh_token: &'a str,
}

#[derive(Debug, Deserialize)]
struct LoginResponse {
    #[serde(rename = "access_token")]
    access_token: String,
    #[serde(rename = "refresh_token")]
    refresh_token: String,
}

#[derive(Debug, Serialize)]
struct EnrollRequest<'a> {
    name: &'a str,
    platform: &'a str,
    #[serde(rename = "os_version")]
    os_version: &'a str,
    #[serde(rename = "app_version")]
    app_version: &'a str,
    #[serde(rename = "device_fingerprint")]
    device_fingerprint: String,
    #[serde(rename = "public_key")]
    public_key: String,
}

#[derive(Debug, Deserialize)]
struct EnrollResponse {
    device: DeviceView,
    peer: PeerView,
    profile: ProfileView,
    resources: Vec<ResourceView>,
}

#[derive(Debug, Deserialize)]
struct DeviceView {
    id: String,
}

#[derive(Debug, Deserialize)]
struct PeerView {
    #[serde(rename = "assigned_ip")]
    assigned_ip: String,
    gateway: GatewayView,
    #[serde(rename = "profile_revision")]
    profile_revision: u32,
}

#[derive(Debug, Deserialize)]
struct GatewayView {
    endpoint: String,
}

#[derive(Debug, Deserialize)]
struct ResourceView {
    value: String,
}

#[derive(Debug, Deserialize)]
struct ProfileView {
    content: String,
}

#[tauri::command]
async fn enroll_device(
    app: AppHandle,
    payload: EnrollmentPayload,
    state: State<'_, AppState>,
) -> Result<EnrollmentResult, String> {
    if payload.server_url.trim().is_empty() || payload.username.trim().is_empty() || payload.password.trim().is_empty() {
        return Err("Server URL, username, and password are required".into());
    }

    let client = Client::builder()
        .use_rustls_tls()
        .build()
        .map_err(|err| err.to_string())?;

    let login_response = client
        .post(format!("{}/api/v1/auth/login", payload.server_url.trim_end_matches('/')))
        .json(&LoginRequest {
            username: &payload.username,
            password: &payload.password,
        })
        .send()
        .await
        .map_err(|err| format!("Login failed: {}", err))?;

    if !login_response.status().is_success() {
        return Err(format!("Login failed with status {}", login_response.status()));
    }

    let login = login_response
        .json::<LoginResponse>()
        .await
        .map_err(|err| format!("Unable to decode login response: {}", err))?;

    let (private_key, public_key) = generate_keypair();
    let enroll_response = client
        .post(format!("{}/api/v1/devices/enroll", payload.server_url.trim_end_matches('/')))
        .bearer_auth(&login.access_token)
        .json(&EnrollRequest {
            name: "NexaVPN Desktop",
            platform: if cfg!(target_os = "macos") { "macos" } else { "windows" },
            os_version: std::env::consts::OS,
            app_version: env!("CARGO_PKG_VERSION"),
            device_fingerprint: build_fingerprint(&payload.server_url, &payload.username, &public_key),
            public_key,
        })
        .send()
        .await
        .map_err(|err| format!("Enrollment failed: {}", err))?;

    if !enroll_response.status().is_success() {
        let status = enroll_response.status();
        let body = enroll_response
            .text()
            .await
            .unwrap_or_else(|_| "<unable to read response body>".into());
        return Err(format!("Enrollment failed with status {}: {}", status, body));
    }

    let enroll = enroll_response
        .json::<EnrollResponse>()
        .await
        .map_err(|err| format!("Unable to decode enrollment response: {}", err))?;

    let profile_content = materialize_profile(&enroll.profile.content, &private_key);
    let profile_path = write_profile(&app, &profile_content)?;
    let result = EnrollmentResult {
        assigned_ip: enroll.peer.assigned_ip,
        resources: enroll.resources.into_iter().map(|resource| resource.value).collect(),
        profile_revision: enroll.peer.profile_revision,
        gateway_endpoint: enroll.peer.gateway.endpoint,
        profile_path: profile_path.display().to_string(),
        last_sync_time: now_label(),
        tunnel_strategy: tunnel_manager::current_tunnel_strategy().into(),
    };

    let session_state = SessionState {
        access_token: login.access_token,
        refresh_token: login.refresh_token,
        server_url: payload.server_url,
        profile_path: result.profile_path.clone(),
        private_key,
        enrollment: result.clone(),
    };

    write_session_state(&app, &session_state)?;
    let mut session = state.session.lock().map_err(|_| "Unable to store client state".to_string())?;
    *session = Some(session_state);
    drop(session);
    refresh_tray_menu(&app);

    Ok(result)
}

#[tauri::command]
fn load_state(app: AppHandle, state: State<'_, AppState>) -> Result<Option<EnrollmentResult>, String> {
    let loaded = read_session_state(&app)?;
    let mut session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
    *session = loaded.clone();
    drop(session);
    refresh_tray_menu(&app);
    Ok(loaded.map(|value| value.enrollment))
}

#[tauri::command]
fn clear_session(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> {
    let app_dir = ensure_app_dir(&app)?;
    let session_path = app_dir.join("session.json");
    let profile_path = app_dir.join(format!("{}.conf", PROFILE_NAME));

    if session_path.exists() {
        fs::remove_file(&session_path).map_err(|err| format!("Unable to remove session state: {}", err))?;
    }
    if profile_path.exists() {
        fs::remove_file(&profile_path).map_err(|err| format!("Unable to remove profile: {}", err))?;
    }

    let mut session = state.session.lock().map_err(|_| "Unable to clear client state".to_string())?;
    *session = None;
    drop(session);
    refresh_tray_menu(&app);
    Ok(())
}

#[tauri::command]
async fn sync_profile(app: AppHandle, _state: State<'_, AppState>) -> Result<EnrollmentResult, String> {
    let session_state = sync_current_session(&app).await?;
    refresh_tray_menu(&app);
    Ok(session_state.enrollment)
}

#[tauri::command]
async fn connect_tunnel(app: AppHandle) -> Result<EnrollmentResult, String> {
    let session_state = sync_current_session(&app).await?;
    let result = tunnel_manager::connect(&app, std::path::Path::new(&session_state.profile_path));
    refresh_tray_menu(&app);
    result?;
    Ok(session_state.enrollment)
}

#[tauri::command]
fn disconnect_tunnel(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> {
    let profile_path = {
        let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
        let session = session.as_ref().ok_or_else(|| "No active session is available".to_string())?;
        session.profile_path.clone()
    };
    let result = tunnel_manager::disconnect(&app, std::path::Path::new(&profile_path));
    refresh_tray_menu(&app);
    result
}

#[tauri::command]
fn tunnel_status(app: AppHandle, state: State<'_, AppState>) -> Result<bool, String> {
    let profile_path = {
        let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
        let session = session.as_ref().ok_or_else(|| "No active session is available".to_string())?;
        session.profile_path.clone()
    };
    tunnel_manager::is_active(&app, std::path::Path::new(&profile_path))
}

#[tauri::command]
fn tunnel_metrics(app: AppHandle, state: State<'_, AppState>) -> Result<TunnelMetrics, String> {
    let profile_path = {
        let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
        let session = session.as_ref().ok_or_else(|| "No active session is available".to_string())?;
        session.profile_path.clone()
    };
    let metrics = tunnel_manager::metrics(&app, std::path::Path::new(&profile_path))?;
    let mapped = TunnelMetrics {
        active: metrics.active,
        rx_bytes: metrics.rx_bytes,
        tx_bytes: metrics.tx_bytes,
    };
    let _ = update_tray_menu(&app, mapped);
    Ok(mapped)
}

fn generate_keypair() -> (String, String) {
    let private = StaticSecret::random_from_rng(OsRng);
    let public = PublicKey::from(&private);
    (
        STANDARD.encode(private.to_bytes()),
        STANDARD.encode(public.to_bytes()),
    )
}

fn now_label() -> String {
    "Just now".into()
}

fn build_fingerprint(server_url: &str, username: &str, public_key: &str) -> String {
    format!("nexavpn:{}:{}:{}", server_url, username, public_key)
}

fn materialize_profile(profile_content: &str, private_key: &str) -> String {
    profile_content
        .replace("__CLIENT_GENERATED_PRIVATE_KEY__", private_key)
        .replace("__CLIENT_PRIVATE_KEY_REQUIRED__", private_key)
}

fn write_profile(app: &AppHandle, profile_content: &str) -> Result<PathBuf, String> {
    let app_dir = ensure_app_dir(app)?;
    let profile_path = app_dir.join(format!("{}.conf", PROFILE_NAME));
    fs::write(&profile_path, profile_content).map_err(|err| format!("Unable to store profile: {}", err))?;
    Ok(profile_path)
}

fn write_session_state(app: &AppHandle, session: &SessionState) -> Result<(), String> {
    let app_dir = ensure_app_dir(app)?;
    let session_path = app_dir.join("session.json");
    let json = serde_json::to_vec_pretty(session).map_err(|err| err.to_string())?;
    fs::write(session_path, json).map_err(|err| format!("Unable to persist session state: {}", err))
}

fn read_session_state(app: &AppHandle) -> Result<Option<SessionState>, String> {
    let app_dir = ensure_app_dir(app)?;
    let session_path = app_dir.join("session.json");
    if !session_path.exists() {
        return Ok(None);
    }
    let raw = fs::read(session_path).map_err(|err| format!("Unable to read session state: {}", err))?;
    let value = serde_json::from_slice::<SessionState>(&raw).map_err(|err| format!("Unable to parse session state: {}", err))?;
    Ok(Some(value))
}

fn ensure_app_dir(app: &AppHandle) -> Result<PathBuf, String> {
    let dir = app
        .path()
        .app_config_dir()
        .map_err(|err| format!("Unable to resolve app config dir: {}", err))?;
    fs::create_dir_all(&dir).map_err(|err| format!("Unable to create app dir: {}", err))?;
    Ok(dir)
}

fn format_data_size(bytes: u64) -> String {
    if bytes == 0 {
        return "0 B".into();
    }

    let units = ["B", "KB", "MB", "GB", "TB"];
    let mut value = bytes as f64;
    let mut unit_index = 0;

    while value >= 1024.0 && unit_index < units.len() - 1 {
        value /= 1024.0;
        unit_index += 1;
    }

    if value >= 100.0 || unit_index == 0 {
        format!("{value:.0} {}", units[unit_index])
    } else {
        format!("{value:.1} {}", units[unit_index])
    }
}

fn current_metrics(app: &AppHandle) -> Result<TunnelMetrics, String> {
    let state = app.state::<AppState>();
    let profile_path = {
        let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
        let session = session.as_ref().ok_or_else(|| "No active session is available".to_string())?;
        session.profile_path.clone()
    };
    match tunnel_manager::metrics(app, std::path::Path::new(&profile_path)) {
        Ok(metrics) => Ok(TunnelMetrics {
            active: metrics.active,
            rx_bytes: metrics.rx_bytes,
            tx_bytes: metrics.tx_bytes,
        }),
        Err(_) => {
            let active = tunnel_manager::is_active(app, std::path::Path::new(&profile_path))?;
            Ok(TunnelMetrics {
                active,
                rx_bytes: 0,
                tx_bytes: 0,
            })
        }
    }
}

async fn sync_current_session(app: &AppHandle) -> Result<SessionState, String> {
    let mut existing = {
        let state = app.state::<AppState>();
        let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
        session.clone().ok_or_else(|| "No enrolled profile is available yet".to_string())?
    };

    let client = Client::builder()
        .use_rustls_tls()
        .build()
        .map_err(|err| err.to_string())?;

    let profile_url = format!("{}/api/v1/me/profile", existing.server_url.trim_end_matches('/'));
    let mut response = client
        .get(&profile_url)
        .bearer_auth(&existing.access_token)
        .send()
        .await
        .map_err(|err| format!("Profile sync failed: {}", err))?;

    if response.status().as_u16() == 401 {
        let refresh = client
            .post(format!("{}/api/v1/auth/refresh", existing.server_url.trim_end_matches('/')))
            .json(&RefreshRequest {
                refresh_token: &existing.refresh_token,
            })
            .send()
            .await
            .map_err(|err| format!("Session refresh failed: {}", err))?;

        if !refresh.status().is_success() {
            let status = refresh.status();
            let body = refresh
                .text()
                .await
                .unwrap_or_else(|_| "<unable to read response body>".into());
            return Err(format!("Session refresh failed with status {}: {}", status, body));
        }

        let refreshed = refresh
            .json::<LoginResponse>()
            .await
            .map_err(|err| format!("Unable to decode refresh response: {}", err))?;

        existing.access_token = refreshed.access_token;
        existing.refresh_token = refreshed.refresh_token;

        response = client
            .get(&profile_url)
            .bearer_auth(&existing.access_token)
            .send()
            .await
            .map_err(|err| format!("Profile sync failed: {}", err))?;
    }

    if !response.status().is_success() {
        let status = response.status();
        let body = response
            .text()
            .await
            .unwrap_or_else(|_| "<unable to read response body>".into());
        return Err(format!("Profile sync failed with status {}: {}", status, body));
    }

    let enroll = response
        .json::<EnrollResponse>()
        .await
        .map_err(|err| format!("Unable to decode profile sync response: {}", err))?;

    let profile_content = materialize_profile(&enroll.profile.content, &existing.private_key);
    let profile_path = write_profile(app, &profile_content)?;
    let result = EnrollmentResult {
        assigned_ip: enroll.peer.assigned_ip,
        resources: enroll.resources.into_iter().map(|resource| resource.value).collect(),
        profile_revision: enroll.peer.profile_revision,
        gateway_endpoint: enroll.peer.gateway.endpoint,
        profile_path: profile_path.display().to_string(),
        last_sync_time: now_label(),
        tunnel_strategy: tunnel_manager::current_tunnel_strategy().into(),
    };

    let session_state = SessionState {
        access_token: existing.access_token,
        refresh_token: existing.refresh_token,
        server_url: existing.server_url,
        profile_path: result.profile_path.clone(),
        private_key: existing.private_key,
        enrollment: result.clone(),
    };

    write_session_state(app, &session_state)?;
    let state = app.state::<AppState>();
    let mut session = state.session.lock().map_err(|_| "Unable to store client state".to_string())?;
    *session = Some(session_state.clone());
    Ok(session_state)
}

fn update_tray_menu(app: &AppHandle, metrics: TunnelMetrics) -> Result<(), String> {
    let state = app.state::<AppState>();
    let tray = state.tray.lock().map_err(|_| "Unable to update tray state".to_string())?;
    let Some(tray) = tray.as_ref() else {
        return Ok(());
    };

    let status_text = if metrics.active {
        "Status: Connected"
    } else {
        "Status: Disconnected"
    };
    let toggle_text = if metrics.active { "Disconnect NexaVPN" } else { "Connect NexaVPN" };

    let _ = tray.status_item.set_text(status_text);
    let _ = tray.received_item.set_text(format!("Received: {}", format_data_size(metrics.rx_bytes)));
    let _ = tray.sent_item.set_text(format!("Sent: {}", format_data_size(metrics.tx_bytes)));
    let _ = tray.toggle_item.set_text(toggle_text);
    let icon = if metrics.active {
        tray.connected_icon.clone()
    } else {
        tray.disconnected_icon.clone()
    };
    let _ = tray.tray_icon.set_icon(Some(icon));
    Ok(())
}

fn refresh_tray_menu(app: &AppHandle) {
    let metrics = current_metrics(app).unwrap_or(TunnelMetrics {
        active: false,
        rx_bytes: 0,
        tx_bytes: 0,
    });
    let _ = update_tray_menu(app, metrics);
}

fn toggle_tray_connection(app: &AppHandle) {
    let metrics = current_metrics(app).unwrap_or(TunnelMetrics {
        active: false,
        rx_bytes: 0,
        tx_bytes: 0,
    });

    let state = app.state::<AppState>();
    let profile_path = match state.session.lock() {
        Ok(session) => match session.as_ref() {
            Some(session) => session.profile_path.clone(),
            None => return,
        },
        Err(_) => return,
    };

    if metrics.active {
        if tunnel_manager::disconnect(app, std::path::Path::new(&profile_path)).is_ok() {
            refresh_tray_menu(app);
        }
        return;
    }

    let app_handle = app.clone();
    tauri::async_runtime::spawn(async move {
        if let Ok(session_state) = sync_current_session(&app_handle).await {
            let _ = tunnel_manager::connect(&app_handle, std::path::Path::new(&session_state.profile_path));
            refresh_tray_menu(&app_handle);
        }
    });
}

fn restore_webview_window(window: &WebviewWindow) {
    let _ = window.show();
    let _ = window.unminimize();
    let _ = window.set_focus();
}

fn hide_main_window(window: &Window) {
    let _ = window.hide();
}

fn load_tray_icons() -> Result<(Image<'static>, Image<'static>), String> {
    let disconnected = Image::from_bytes(include_bytes!("../icons/icon.png"))
        .map_err(|err| format!("Unable to load tray icon: {err}"))?
        .to_owned();
    let connected = build_connected_tray_icon()?;
    Ok((disconnected, connected))
}

fn build_connected_tray_icon() -> Result<Image<'static>, String> {
    let decoder = Decoder::new(Cursor::new(include_bytes!("../icons/icon.png")));
    let mut reader = decoder
        .read_info()
        .map_err(|err| format!("Unable to decode tray icon: {err}"))?;
    let mut buffer = vec![0; reader.output_buffer_size()];
    let info = reader
        .next_frame(&mut buffer)
        .map_err(|err| format!("Unable to read tray icon frame: {err}"))?;

    if info.color_type != ColorType::Rgba {
        return Err("Unsupported tray icon color format".into());
    }

    let width = info.width;
    let height = info.height;
    let pixels = &mut buffer[..info.buffer_size()];

    draw_check_badge(pixels, width as usize, height as usize);

    Ok(Image::new_owned(pixels.to_vec(), width, height))
}

fn draw_check_badge(pixels: &mut [u8], width: usize, height: usize) {
    let radius = ((width.min(height) as f32) * 0.22_f32).max(5.0) as i32;
    let center_x = width as i32 - radius - 1;
    let center_y = height as i32 - radius - 1;

    for y in 0..height as i32 {
        for x in 0..width as i32 {
            let dx = x - center_x;
            let dy = y - center_y;
            if dx * dx + dy * dy <= radius * radius {
                blend_pixel(pixels, width, x as usize, y as usize, [42, 199, 105, 255]);
            }
        }
    }

    let start_x = center_x - radius / 2;
    let start_y = center_y + radius / 10;
    let mid_x = center_x - radius / 8;
    let mid_y = center_y + radius / 2;
    let end_x = center_x + radius / 2;
    let end_y = center_y - radius / 3;

    draw_line(pixels, width, start_x, start_y, mid_x, mid_y, [255, 255, 255, 255]);
    draw_line(pixels, width, mid_x, mid_y, end_x, end_y, [255, 255, 255, 255]);
    draw_line(pixels, width, start_x, start_y - 1, mid_x, mid_y - 1, [255, 255, 255, 255]);
    draw_line(pixels, width, mid_x, mid_y - 1, end_x, end_y - 1, [255, 255, 255, 255]);
}

fn draw_line(
    pixels: &mut [u8],
    width: usize,
    mut x0: i32,
    mut y0: i32,
    x1: i32,
    y1: i32,
    color: [u8; 4],
) {
    let dx = (x1 - x0).abs();
    let sx = if x0 < x1 { 1 } else { -1 };
    let dy = -(y1 - y0).abs();
    let sy = if y0 < y1 { 1 } else { -1 };
    let mut err = dx + dy;

    loop {
        if x0 >= 0 && y0 >= 0 {
            blend_pixel(pixels, width, x0 as usize, y0 as usize, color);
        }
        if x0 == x1 && y0 == y1 {
            break;
        }
        let e2 = 2 * err;
        if e2 >= dy {
            err += dy;
            x0 += sx;
        }
        if e2 <= dx {
            err += dx;
            y0 += sy;
        }
    }
}

fn blend_pixel(pixels: &mut [u8], width: usize, x: usize, y: usize, color: [u8; 4]) {
    let index = (y * width + x) * 4;
    if index + 3 >= pixels.len() {
        return;
    }

    let alpha = color[3] as f32 / 255.0;
    let inverse = 1.0 - alpha;

    pixels[index] = (color[0] as f32 * alpha + pixels[index] as f32 * inverse) as u8;
    pixels[index + 1] = (color[1] as f32 * alpha + pixels[index + 1] as f32 * inverse) as u8;
    pixels[index + 2] = (color[2] as f32 * alpha + pixels[index + 2] as f32 * inverse) as u8;
    pixels[index + 3] = ((color[3] as f32) + pixels[index + 3] as f32 * inverse).min(255.0) as u8;
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            let single_instance_lock = TcpListener::bind(SINGLE_INSTANCE_ADDR)
                .map_err(|_| format!("{} is already running.", app.package_info().name))?;
            let (disconnected_icon, connected_icon) = load_tray_icons()?;

            let status_item = MenuItemBuilder::with_id("status", "Status: Disconnected").build(app)?;
            let received_item = MenuItemBuilder::with_id("received", "Received: 0 B").build(app)?;
            let sent_item = MenuItemBuilder::with_id("sent", "Sent: 0 B").build(app)?;
            let toggle_item = MenuItemBuilder::with_id("toggle", "Connect NexaVPN").build(app)?;
            let separator_top = PredefinedMenuItem::separator(app)?;
            let separator_bottom = PredefinedMenuItem::separator(app)?;
            let open_item = MenuItemBuilder::with_id("open", "Open NexaVPN Client").build(app)?;
            let quit_item = MenuItemBuilder::with_id("quit", "Quit NexaVPN Client").build(app)?;
            let menu = MenuBuilder::new(app)
                .items(&[
                    &status_item,
                    &received_item,
                    &sent_item,
                    &separator_top,
                    &toggle_item,
                    &separator_bottom,
                    &open_item,
                    &quit_item,
                ])
                .build()?;

            let tray_icon = TrayIconBuilder::new()
                .menu(&menu)
                .show_menu_on_left_click(false)
                .icon(disconnected_icon.clone())
                .on_menu_event(|app, event| match event.id().as_ref() {
                    "status" | "received" | "sent" => {}
                    "toggle" => {
                        toggle_tray_connection(&app);
                    }
                    "open" => {
                        if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) {
                            restore_webview_window(&window);
                        }
                    }
                    "quit" => {
                        app.exit(0);
                    }
                    _ => {}
                })
                .on_tray_icon_event(|tray, event| {
                    if let TrayIconEvent::Click {
                        button: MouseButton::Left,
                        button_state: MouseButtonState::Up,
                        ..
                    } = event
                    {
                        let app = tray.app_handle();
                        if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) {
                            restore_webview_window(&window);
                        }
                    }
                })
                .build(app)?;

            app.manage(AppState {
                session: Mutex::new(None),
                tray: Mutex::new(Some(TrayState {
                    tray_icon,
                    disconnected_icon,
                    connected_icon,
                    status_item,
                    received_item,
                    sent_item,
                    toggle_item,
                })),
                single_instance_lock,
            });
            refresh_tray_menu(app.handle());

            let app_handle = app.handle().clone();
            std::thread::spawn(move || loop {
                refresh_tray_menu(&app_handle);
                std::thread::sleep(std::time::Duration::from_secs(5));
            });

            Ok(())
        })
        .on_window_event(|window, event| match event {
            WindowEvent::CloseRequested { .. } => {
                window.app_handle().exit(0);
            }
            WindowEvent::Resized(_) => {
                if window.is_minimized().unwrap_or(false) {
                    hide_main_window(window);
                }
            }
            _ => {}
        })
        .invoke_handler(tauri::generate_handler![load_state, clear_session, enroll_device, sync_profile, connect_tunnel, disconnect_tunnel, tunnel_status, tunnel_metrics])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
