mod tunnel_manager;

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

use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _};
use rand_core::OsRng;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager, State};
use x25519_dalek::{PublicKey, StaticSecret};

const PROFILE_NAME: &str = "NexaVPN";

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)]
#[serde(rename_all = "camelCase")]
struct LoginRequest<'a> {
    username: &'a str,
    password: &'a str,
}

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

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

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct EnrollResponse {
    peer: PeerView,
    profile: ProfileView,
    resources: Vec<ResourceView>,
}

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

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
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() {
        return Err(format!("Enrollment failed with status {}", enroll_response.status()));
    }

    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: "just now".into(),
        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 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_NO_PAD.encode(private.to_bytes()),
        STANDARD_NO_PAD.encode(public.to_bytes()),
    )
}

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

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .manage(AppState {
            session: Mutex::new(None),
        })
        .invoke_handler(tauri::generate_handler![load_state, enroll_device, connect_tunnel, disconnect_tunnel])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
