use serde::{Deserialize, Serialize};
use std::{fs, fs::File, sync::Mutex, env};
use std::path::PathBuf;
use std::collections::HashMap;
use tokio::io::AsyncWriteExt;
use futures_util::StreamExt;
use tauri::{AppHandle, Emitter, Manager};
use xxhash_rust::xxh3::xxh3_64;
use rayon::prelude::*;
use memmap2::Mmap;
use walkdir::WalkDir;

// TODO HANDLE ERRORS PROPERLY
// TODO SUPPORT REGEX
// TODO SUPPORT ALL FILE MOVES
pub const SERVER: &str = "http://127.0.0.1";

#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
    #[serde(rename = "$schema")]
    pub schema: String,

    pub locale: Locale,
    pub display: Display,
    pub gamma: i32,

    pub volume: Volume,

    pub show_mob: ShowMob,

    pub shadow_level: ShadowLevel,
    pub field_of_view: f64,
    pub fog_mode_on: bool,
    pub camera_mode: CameraMode,
    pub private_shop: PrivateShop,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Locale {
    En,
    Pt,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(try_from = "u8", into = "u8")]
pub enum ShadowLevel {
    L0 = 0,
    L1 = 1,
    L2 = 2,
    L3 = 3,
}

impl From<ShadowLevel> for u8 {
    fn from(sl: ShadowLevel) -> Self {
        sl as u8
    }
}

impl TryFrom<u8> for ShadowLevel {
    type Error = &'static str;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(ShadowLevel::L0),
            1 => Ok(ShadowLevel::L1),
            2 => Ok(ShadowLevel::L2),
            3 => Ok(ShadowLevel::L3),
            _ => Err("Invalid shadow level"),
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub enum CameraMode {
    #[serde(rename = "close")]
    Close,
    #[serde(rename = "far")]
    Far,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Display {
    pub width: i32,
    pub height: i32,
    pub frequency: i32,
    pub windowed: bool,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Volume {
    pub music: f64,
    pub voice: f64,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ShowMob {
    pub level: bool,
    pub ai_flag: bool,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct PrivateShop {
    pub view_distance: f64,
    pub show_sales_text: bool,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            schema: "https://raw.githubusercontent.com/WildEgo/m2-json-schemas/refs/heads/main/config.json".into(),
            locale: Locale::En,
            display: Display {
                width: 800,
                height: 600,
                frequency: 60,
                windowed: true,
            },
            volume: Volume {
                music: 50.0,
                voice: 50.0,
            },
            gamma: 3,
            show_mob: ShowMob {
                level: true,
                ai_flag: true,
            },
            shadow_level: ShadowLevel::L3,
            field_of_view: 30.0,
            fog_mode_on: false,
            camera_mode: CameraMode::Close,
            private_shop: PrivateShop {
                view_distance: 100.0,
                show_sales_text: true,
            },
        }
    }
}

fn get_config_path(_app: &AppHandle) -> PathBuf {
    std::env::current_exe()
        .expect("Failed to get executable path")
        .parent()
        .expect("Failed to get executable directory")
        .to_path_buf()
        .join("config.json")
}

#[tauri::command]
fn get_config(app: AppHandle) -> Config {
    let config_path = get_config_path(&app);

    match fs::read_to_string(&config_path) {
        Ok(content) => match serde_json::from_str::<Config>(&content) {
            Ok(cfg) => cfg,
            Err(_) => Config::default(),
        },
        Err(_) => Config::default(),
    }
}

#[tauri::command]
fn set_config(config: Config, app: AppHandle) -> bool {
    let config_path = get_config_path(&app);

    match serde_json::to_string_pretty(&config) {
        Ok(json) => match fs::write(&config_path, json) {
            Ok(_) => {
                // app.notification()
                //     .builder()
                //     .title("Configuration")
                //     .body("Saved Metin2 client settings")
                //     .show()
                //     .unwrap();

                true
            }
            Err(_e) => {
                // app.notification()
                //     .builder()
                //     .title("Configuration")
                //     .body(&format!("Could not save config: {}", e))
                //     .show()
                //     .unwrap();

                false
            }
        },
        Err(_e) => {
            // app.notification()
            //     .builder()
            //     .title("Configuration")
            //     .body(&format!("Could not serialize config: {}", e))
            //     .show()
            //     .unwrap();

            false
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Resolution {
    pub width: u32,
    pub height: u32,
    pub frequency: f32,
}

#[tauri::command]
fn get_resolutions() -> Vec<Resolution> {
    #[cfg(target_os = "windows")]
    {
        use windows::Win32::Graphics::Direct3D9::*;

        let mut resolutions = Vec::new();

        unsafe {
            // Returns Option<IDirect3D9>
            let d3d = Direct3DCreate9(D3D_SDK_VERSION);

            let d3d = match d3d {
                Some(d) => d,
                None => {
                    // Fallback if D3D fails
                    resolutions.push(Resolution {
                        width: 800,
                        height: 600,
                        frequency: 60.0,
                    });
                    return resolutions;
                }
            };

            let allowed_formats = [D3DFMT_X8R8G8B8, D3DFMT_R5G6B5];

            for &format in &allowed_formats {
                let count = d3d.GetAdapterModeCount(0, format);
                if count == 0 {
                    resolutions.push(Resolution {
                        width: 800,
                        height: 600,
                        frequency: 60.0,
                    });
                    continue;
                }

                for i in 0..count {
                    let mut mode = D3DDISPLAYMODE::default();
                    let _ = d3d.EnumAdapterModes(0, format, i, &mut mode);

                    if mode.Width < 800 || mode.Height < 600 {
                        continue;
                    }

                    if resolutions
                        .iter()
                        .any(|r| r.width == mode.Width && r.height == mode.Height)
                    {
                        continue;
                    }

                    resolutions.push(Resolution {
                        width: mode.Width,
                        height: mode.Height,
                        frequency: mode.RefreshRate as f32,
                    });
                }
            }
        }

        resolutions
    }

    #[cfg(not(target_os = "windows"))]
    {
        use display_info::DisplayInfo;

        let display_infos = DisplayInfo::all().unwrap();

        display_infos
            .into_iter()
            .map(|display| Resolution {
                width: display.width,
                height: display.height,
                frequency: display.frequency,
            })
            .collect()
    }
}

#[tauri::command]
fn close(app: AppHandle) {
    app.exit(0);
}

pub fn pack_hash(app: AppHandle) -> std::io::Result<Vec<(String, u64)>> {
    let base = base_dir(&app);
    let entries: Vec<PathBuf> = WalkDir::new(&base)
        .into_iter()
        .filter_map(|e| e.ok())
        .filter(|e| e.path().is_file())
        .map(|e| e.path().to_path_buf())
        .collect();

    let results = Mutex::new(Vec::with_capacity(entries.len()));

    entries.par_iter().try_for_each(|path| -> std::io::Result<()> {
        // Relative path to DIR
        let rel_path = path.strip_prefix(&base)
            .unwrap()
            .to_string_lossy()
            .replace("\\", "/"); // normalize

        app.emit("file", &rel_path)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;

        let file = File::open(path)?;
        let mmap = unsafe { Mmap::map(&file)? };
        let hash = xxh3_64(&mmap);

        results.lock().unwrap().push((rel_path, hash));

        Ok(())
    })?;

    Ok(results.into_inner().unwrap())
}

#[derive(Debug, Deserialize)]
struct ChecksumResponse {
    checksums: HashMap<String, String>,
}

async fn fetch_server_checksums() -> Result<HashMap<String, u64>, String> {
    let response = reqwest::get(format!("{}/checksum", SERVER))
        .await
        .map_err(|e| format!("Failed to fetch checksums: {}", e))?;

    let parsed: ChecksumResponse = response
        .json()
        .await
        .map_err(|e| format!("Failed to parse checksums: {}", e))?;

    let mut out = HashMap::new();

    for (name, hash_str) in parsed.checksums {
        let hash = hash_str
            .parse::<u64>()
            .map_err(|_| format!("Invalid checksum number for file: {}", name))?;

        // Normalize separators
        let normalized_name = name.replace("\\", "/");
        out.insert(normalized_name, hash);
    }

    Ok(out)
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(into = "u8", try_from = "u8")]
pub enum Status {
    Awaiting = 0,
    Fetching = 1,
    Checksumming = 2,
    Verifying = 3,
    Updating = 4,
    Ready = 5,
}

impl From<Status> for u8 {
    fn from(status: Status) -> Self {
        status as u8
    }
}

impl TryFrom<u8> for Status {
    type Error = String;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(Status::Awaiting),
            1 => Ok(Status::Fetching),
            2 => Ok(Status::Checksumming),
            3 => Ok(Status::Verifying),
            4 => Ok(Status::Updating),
            5 => Ok(Status::Ready),
            _ => Err(format!("Invalid status value: {}", value)),
        }
    }
}

async fn download_file(app_handle: &AppHandle, url: &str, dest_path: &str, file_name: &str) -> Result<(), String> {
    if tokio::fs::metadata(dest_path).await.is_ok() {
        let _ = tokio::fs::remove_file(dest_path).await;
    }

    let client = reqwest::Client::new();
    let response = client.get(url)
        .send()
        .await
        .map_err(|e| format!("Failed to download {}: {}", file_name, e))?;

    let total_size = response
        .content_length()
        .ok_or_else(|| format!("Failed to get content length for {}", file_name))?;

    let mut stream = response.bytes_stream();
    let mut file = tokio::fs::File::create(dest_path)
        .await
        .map_err(|e| format!("Failed to create file {}: {}", dest_path, e))?;

    let mut downloaded: u64 = 0;

    while let Some(chunk) = stream.next().await {
        let chunk = chunk
            .map_err(|e| format!("Error while downloading {}: {}", file_name, e))?;
        
        file.write_all(&chunk)
            .await
            .map_err(|e| format!("Failed to write {}: {}", dest_path, e))?;
        
        downloaded += chunk.len() as u64;

        // Emit progress safely
        if let Err(e) = app_handle.emit("download", serde_json::json!({
            "file": file_name,
            "downloaded": downloaded,
            "total": total_size,
            "percent": (downloaded as f64 / total_size as f64 * 100.0) as u8
        })) {
            eprintln!("Failed to emit download progress: {}", e);
        }
    }

    Ok(())
}


pub struct WindowState {
    config_open: Mutex<bool>,
}

impl Default for WindowState {
    fn default() -> Self {
        Self {
            config_open: Mutex::new(false),
        }
    }
}

#[tauri::command]
fn open_config(app: AppHandle) -> Result<(), String> {
    let state = app.state::<WindowState>();
    let mut is_open = state.config_open.lock().unwrap();

    if *is_open {
        if let Some(window) = app.get_webview_window("config") {
            let _ = window.close();
            return Ok(());
        } else {
            *is_open = false;
        }
    }

    *is_open = true;

    let window = tauri::WebviewWindowBuilder::new(
        &app,
        "config",
        tauri::WebviewUrl::App("config.html".into())
    )
    .title("Configuration")
    .inner_size(800.0, 600.0)
    .resizable(false)
    .decorations(false)
    .visible(true)
    .build()
    .map_err(|e| format!("Failed to create config window: {}", e))?;

    let app_handle = app.clone();
    window.on_window_event(move |event| {
        if let tauri::WindowEvent::Destroyed = event {
            let state = app_handle.state::<WindowState>();
            *state.config_open.lock().unwrap() = false;
        }
    });

    Ok(())
}

fn base_dir(_app: &AppHandle) -> PathBuf {
    std::env::current_exe()
        .expect("failed to get current executable")
        .to_path_buf()
}

pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .manage(WindowState::default())
        .invoke_handler(tauri::generate_handler![
            open_config,
            get_resolutions,
            get_config,
            set_config,
            close,
        ])
        .setup(|app| {
            let app_handle = app.handle().clone();

            let base = base_dir(&app_handle);
            let _ = fs::create_dir_all(&base);

            tauri::async_runtime::spawn(async move {
                let _ = app_handle.emit("status", Status::Fetching).unwrap();

                let server_hashes = match fetch_server_checksums().await {
                    Ok(hashes) => hashes,
                    Err(e) => {
                        let _ = app_handle.emit("error", e.to_string()).unwrap();
                        return;
                    }
                };

                let _ = app_handle.emit("status", Status::Checksumming).unwrap();

                let local_hashes = match tokio::task::spawn_blocking({
                    let app_handle = app_handle.clone();
                    move || pack_hash(app_handle)
                })
                .await
                {
                    Ok(Ok(hashes)) => hashes,
                    Ok(Err(e)) => {
                        let _ = app_handle.emit("error", e.to_string()).unwrap();
                        return;
                    }
                    Err(e) => {
                        let _ = app_handle.emit("error", e.to_string()).unwrap();
                        return;
                    }
                };
                let local_map: HashMap<String, u64> = local_hashes.into_iter().collect();
                let _ = app_handle.emit("status", Status::Verifying).unwrap();

                let mut updatable: HashMap<String, u64> = HashMap::new();

                for (filename, server_hash) in &server_hashes {
                    match local_map.get(filename) {
                        Some(local_hash) => {
                            if local_hash != server_hash {
                                updatable.insert(filename.clone(), *server_hash);
                            }
                        }
                        None => {
                            updatable.insert(filename.clone(), *server_hash);
                        }
                    }
                }

                if !updatable.is_empty() {
                    let _ = app_handle.emit("status", Status::Updating).unwrap();

                    for (file, _hash) in &updatable {
                        let url = format!("{}/{}", SERVER, file);

                        let mut dest_path = base.clone();
                        dest_path.push(file);

                        if let Some(parent) = dest_path.parent() {
                            if let Err(e) = fs::create_dir_all(parent) {
                                let _ = app_handle.emit("error", format!("Failed to create dir: {}", e.to_string()));
                                continue;
                            }
                        }

                        match download_file(&app_handle, &url, dest_path.to_str().unwrap(), file).await {
                            Ok(_) => {},
                            Err(e) => {
                                let _ = app_handle.emit("error", e.to_string());
                            }
                        }
                    }
                }

                let _ = app_handle.emit("status", Status::Ready).unwrap();
            });

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
