mod tunnel_manager;

use std::{fs, path::PathBuf, sync::Mutex};

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

const PROFILE_NAME: &str = "NexaVPN";
const MAIN_WINDOW_LABEL: &str = "main";

struct AppState {
    session: Mutex<Option<SessionState>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SessionState {
    access_token: String,
    refresh_token: String,
    server_url: String,
    profile_path: 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, Serialize)]
struct LoginRequest<'a> {
    username: &'a str,
    password: &'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_path = write_profile(&app, &enroll.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(),
        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);

    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();
    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;
    Ok(())
}

#[tauri::command]
async fn sync_profile(app: AppHandle, state: State<'_, AppState>) -> Result<EnrollmentResult, String> {
    let existing = {
        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 response = client
        .get(format!("{}/api/v1/me/profile", existing.server_url.trim_end_matches('/')))
        .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_path = write_profile(&app, &enroll.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(),
        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);

    Ok(result)
}

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

#[tauri::command]
fn disconnect_tunnel(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> {
    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())?;
    tunnel_manager::disconnect(&app, std::path::Path::new(&session.profile_path))
}

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 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 restore_window(window: &Window) {
    let _ = window.show();
    let _ = window.unminimize();
    let _ = window.set_focus();
}

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();
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            let open_item = MenuItemBuilder::with_id("open", "Open NexaVPN").build(app)?;
            let quit_item = MenuItemBuilder::with_id("quit", "Quit NexaVPN").build(app)?;
            let menu = MenuBuilder::new(app).items(&[&open_item, &quit_item]).build()?;

            let mut tray = TrayIconBuilder::new().menu(&menu).show_menu_on_left_click(false);
            if let Some(icon) = app.default_window_icon() {
                tray = tray.icon(icon.clone());
            }

            tray
                .on_menu_event(|app, event| match event.id().as_ref() {
                    "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)?;

            Ok(())
        })
        .manage(AppState {
            session: Mutex::new(None),
        })
        .on_window_event(|window, event| match event {
            WindowEvent::CloseRequested { api, .. } => {
                api.prevent_close();
                hide_main_window(window);
            }
            WindowEvent::Resized(_) => {
                if window.is_minimized().unwrap_or(false) {
                    hide_main_window(window);
                }
            }
            WindowEvent::Focused(true) => {
                restore_window(window);
            }
            _ => {}
        })
        .invoke_handler(tauri::generate_handler![load_state, clear_session, enroll_device, sync_profile, connect_tunnel, disconnect_tunnel])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
