mod shell_updater;

use std::io::Cursor;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};

use dsh_core::clipboard::{
    detect_image_mime, file_from_bytes, filename_for_mime, files_from_paths, parse_uri_list,
    ClipboardFile,
};
use dsh_core::launcher::{DshLauncher, DshProcess, URL_TIMEOUT_SECONDS};
use dsh_core::modlens::ensure_modlens;
use dsh_core::paths::BundledPaths;
use dsh_core::preset::ensure_anchored_standard;
use dsh_core::updater::{mark_update_checked, update_check_due, update_dsh};
use dsh_core::{APP_NAME, ENV_NO_UPDATE};
use serde::Serialize;
use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
use url::Url;

const INJECT: &str = concat!(
    include_str!("../../ui/inject/ingest.js"),
    include_str!("../../ui/inject/hide-twins.js"),
    include_str!("../../ui/inject/chrome.js"),
);

#[derive(Clone, Debug)]
pub struct Args {
    pub dsh: Option<String>,
    pub cwd: Option<PathBuf>,
    pub no_update: bool,
    pub force_update: bool,
    pub dev: bool,
    pub verbose: bool,
}

impl Args {
    pub fn parse() -> Self {
        let mut args = Args {
            dsh: std::env::var(dsh_core::ENV_BIN_OVERRIDE)
                .ok()
                .filter(|s| !s.is_empty()),
            cwd: std::env::var(dsh_core::ENV_CWD_OVERRIDE)
                .ok()
                .filter(|s| !s.is_empty())
                .map(PathBuf::from),
            no_update: std::env::var(ENV_NO_UPDATE)
                .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
                .unwrap_or(false),
            force_update: false,
            dev: false,
            verbose: false,
        };
        let mut argv = std::env::args().skip(1);
        while let Some(arg) = argv.next() {
            match arg.as_str() {
                "--dsh" => args.dsh = argv.next(),
                "--cwd" => args.cwd = argv.next().map(PathBuf::from),
                "--no-update" => args.no_update = true,
                "--update" => args.force_update = true,
                "--dev" => args.dev = true,
                "--verbose" => args.verbose = true,
                "--help" | "-h" => {
                    print_help();
                    std::process::exit(0);
                }
                "--version" | "-V" => {
                    println!("{APP_NAME} {}", dsh_core::VERSION);
                    std::process::exit(0);
                }
                _ => {}
            }
        }
        args
    }
}

fn print_help() {
    println!(
        "{APP_NAME} — native window around the official dsh WebUI.\n\n\
         --dsh PATH     dsh 可执行文件路径或命令\n\
         --cwd DIR      传给 dsh 的工作目录\n\
         --no-update    不检查 dsh 更新\n\
         --update       启动前强制检查/安装 dsh 更新\n\
         --dev          打开 WebView 开发者工具\n\
         --verbose      输出调试日志"
    );
}

struct AppState {
    args: Args,
    paths: BundledPaths,
    process: Mutex<Option<Arc<DshProcess>>>,
    url: Mutex<Option<String>>,
    initial_boot_started: AtomicBool,
}

impl AppState {
    fn stop(&self) {
        if let Ok(mut slot) = self.process.lock() {
            if let Some(proc) = slot.take() {
                proc.stop();
            }
        }
    }
}

#[derive(Clone, Serialize)]
struct StatusPayload {
    message: String,
}

#[derive(Clone, Serialize)]
struct ErrorPayload {
    message: String,
    detail: String,
}

#[derive(Clone, Serialize)]
struct ReadyPayload {
    url: String,
}

#[tauri::command]
fn restart(app: AppHandle) {
    thread::spawn(move || boot(app));
}
fn start_initial_boot(app: AppHandle) {
    let Some(state) = app.try_state::<AppState>() else {
        return;
    };
    if state.initial_boot_started.swap(true, Ordering::AcqRel) {
        return;
    }
    thread::spawn(move || boot(app));
}

#[tauri::command]
async fn check_shell_update(app: AppHandle) -> Option<shell_updater::UpdateInfo> {
    match shell_updater::check(&app).await {
        Ok(Some(update)) => Some(update),
        Ok(None) => {
            start_initial_boot(app);
            None
        }
        Err(error) => {
            log::warn!("shell update check failed: {error}");
            start_initial_boot(app);
            None
        }
    }
}

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

#[tauri::command]
fn skip_shell_update(app: AppHandle) {
    start_initial_boot(app);
}

#[tauri::command]
fn open_in_browser(state: tauri::State<AppState>) -> Result<(), String> {
    let url = state
        .url
        .lock()
        .ok()
        .and_then(|g| g.clone())
        .ok_or_else(|| "服务尚未就绪".to_string())?;
    open::that(&url).map_err(|e| e.to_string())
}

#[tauri::command]
fn read_clipboard_images(app: AppHandle) -> Result<Vec<ClipboardFile>, String> {
    let (tx, rx) = std::sync::mpsc::sync_channel(1);
    app.run_on_main_thread(move || {
        let _ = tx.send(read_images());
    })
    .map_err(|e| e.to_string())?;
    rx.recv().map_err(|e| e.to_string())?
}

fn read_images() -> Result<Vec<ClipboardFile>, String> {
    if let Some(file) = read_native_image() {
        return Ok(vec![file]);
    }
    if let Some(file) = read_arboard_image() {
        return Ok(vec![file]);
    }
    if let Some(file) = read_cli_image() {
        return Ok(vec![file]);
    }
    if let Some(files) = read_arboard_image_paths() {
        return Ok(files);
    }
    Ok(Vec::new())
}

#[cfg(target_os = "linux")]
fn read_native_image() -> Option<ClipboardFile> {
    let clipboard = gtk::Clipboard::get(&gdk::SELECTION_CLIPBOARD);
    let pixbuf = clipboard.wait_for_image()?;
    let png = pixbuf.save_to_bufferv("png", &[]).ok()?;
    file_from_bytes(filename_for_mime("image/png", 0), "image/png".into(), png)
}

#[cfg(not(target_os = "linux"))]
fn read_native_image() -> Option<ClipboardFile> {
    None
}

fn read_arboard_image() -> Option<ClipboardFile> {
    let mut clipboard = arboard::Clipboard::new().ok()?;
    let img = clipboard.get_image().ok()?;
    let width = img.width as u32;
    let height = img.height as u32;
    let bytes = img.bytes.into_owned();
    let buffer = image::RgbaImage::from_raw(width, height, bytes)?;
    let mut png = Vec::new();
    buffer
        .write_to(&mut Cursor::new(&mut png), image::ImageFormat::Png)
        .ok()?;
    file_from_bytes(filename_for_mime("image/png", 0), "image/png".into(), png)
}

fn read_arboard_image_paths() -> Option<Vec<ClipboardFile>> {
    let mut clipboard = arboard::Clipboard::new().ok()?;
    let text = clipboard.get_text().ok()?;
    let loaded = files_from_paths(parse_uri_list(&text));
    if loaded.is_empty() {
        None
    } else {
        Some(loaded)
    }
}

fn read_cli_image() -> Option<ClipboardFile> {
    const ATTEMPTS: &[(&str, &[&str])] = &[
        ("wl-paste", &["--type", "image/png"]),
        ("wl-paste", &["--type", "image/jpeg"]),
        ("wl-paste", &["--type", "image/webp"]),
        (
            "xclip",
            &["-selection", "clipboard", "-t", "image/png", "-o"],
        ),
        (
            "xclip",
            &["-selection", "clipboard", "-t", "image/jpeg", "-o"],
        ),
        (
            "xclip",
            &["-selection", "clipboard", "-t", "image/webp", "-o"],
        ),
    ];
    for (bin, args) in ATTEMPTS {
        let output = match Command::new(bin).args(*args).output() {
            Ok(output) => output,
            Err(_) => continue,
        };
        if !output.status.success() || output.stdout.is_empty() {
            continue;
        }
        let mime = detect_image_mime(&output.stdout).unwrap_or("image/png");
        if let Some(file) = file_from_bytes(filename_for_mime(mime, 0), mime.into(), output.stdout)
        {
            return Some(file);
        }
    }
    None
}

fn enable_microphone(window: &tauri::WebviewWindow) {
    #[cfg(target_os = "linux")]
    {
        let _ = window.with_webview(|platform| {
            use gtk::glib::{object::ObjectExt, StaticType};
            use webkit2gtk::{PermissionRequestExt, SettingsExt, WebViewExt};
            let webview = platform.inner();
            if let Some(settings) = webview.settings() {
                settings.set_enable_media_stream(true);
                settings.set_enable_mediasource(true);
            }
            webview.connect_permission_request(|_, request| {
                if request
                    .type_()
                    .is_a(webkit2gtk::UserMediaPermissionRequest::static_type())
                {
                    request.allow();
                    true
                } else {
                    false
                }
            });
        });
    }
    let _ = window;
}

fn is_internal(url: &Url) -> bool {
    matches!(url.scheme(), "tauri" | "asset" | "about" | "data" | "blob")
        || matches!(
            url.host_str(),
            Some("127.0.0.1" | "localhost" | "::1" | "tauri.localhost")
        )
}

fn boot(app: AppHandle) {
    let Some(state) = app.try_state::<AppState>() else {
        return;
    };
    state.stop();
    let started = Instant::now();
    let _ = app.emit(
        "status",
        StatusPayload {
            message: "正在启动…".into(),
        },
    );

    if state.args.force_update && !state.args.no_update {
        let _ = app.emit(
            "status",
            StatusPayload {
                message: "正在检查 dsh 更新…".into(),
            },
        );
        let result = update_dsh(true);
        mark_update_checked();
        log::info!("dsh update: {} ({})", result.status, result.message);
    }

    let plugin = ensure_modlens(&state.paths);
    log::info!(
        "modlens: {} ({}) in {:?}",
        plugin.status,
        plugin.message,
        started.elapsed()
    );
    let preset = ensure_anchored_standard(&state.paths);
    log::info!(
        "anchored-standard: {} ({}) in {:?}",
        preset.status,
        preset.message,
        started.elapsed()
    );
    let _ = app.emit(
        "status",
        StatusPayload {
            message: "正在启动 dsh web 服务…".into(),
        },
    );

    let launcher = DshLauncher::new(state.args.dsh.clone(), state.args.cwd.clone());
    let process = match launcher.start() {
        Ok(p) => Arc::new(p),
        Err(err) => {
            let _ = app.emit(
                "error",
                ErrorPayload {
                    message: err.to_string(),
                    detail: String::new(),
                },
            );
            return;
        }
    };
    if let Ok(mut slot) = state.process.lock() {
        *slot = Some(Arc::clone(&process));
    }
    log::info!("dsh web spawned in {:?}", started.elapsed());

    if !state.args.no_update && !state.args.force_update && update_check_due() {
        thread::spawn(|| {
            log::info!("background dsh update check");
            let result = update_dsh(true);
            mark_update_checked();
            log::info!(
                "background dsh update: {} ({})",
                result.status,
                result.message
            );
        });
    }

    let deadline = Instant::now() + Duration::from_secs(URL_TIMEOUT_SECONDS);
    loop {
        if let Some(url) = process.take_url() {
            if let Ok(mut slot) = state.url.lock() {
                *slot = Some(url.clone());
            }
            let _ = app.emit("ready", ReadyPayload { url });
            log::info!("dsh web ready in {:?}", started.elapsed());
            return;
        }
        if let Some(code) = process.poll() {
            let detail = process.snapshot_lines().join("\n");
            let _ = app.emit(
                "error",
                ErrorPayload {
                    message: format!("服务已退出（exit {code}）。"),
                    detail,
                },
            );
            return;
        }
        if Instant::now() >= deadline {
            let detail = process
                .snapshot_lines()
                .into_iter()
                .rev()
                .take(80)
                .collect::<Vec<_>>()
                .into_iter()
                .rev()
                .collect::<Vec<_>>()
                .join("\n");
            process.stop();
            let _ = app.emit(
                "error",
                ErrorPayload {
                    message: format!("等待 dsh web 输出 URL 超时（{URL_TIMEOUT_SECONDS} 秒）。"),
                    detail,
                },
            );
            return;
        }
        thread::sleep(Duration::from_millis(80));
    }
}

pub fn run() {
    let args = Args::parse();
    env_logger::Builder::from_env(
        env_logger::Env::default().default_filter_or(if args.verbose { "debug" } else { "info" }),
    )
    .init();

    let mut paths = BundledPaths::discover();
    let dev = args.dev;
    let updater = match shell_updater::public_key() {
        Some(public_key) => tauri_plugin_updater::Builder::new().pubkey(public_key),
        None => tauri_plugin_updater::Builder::new(),
    }
    .build();

    tauri::Builder::default()
        .plugin(updater)
        .plugin(tauri_plugin_opener::init())
        .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
            if let Some(window) = app.get_webview_window("main") {
                let _ = window.unminimize();
                let _ = window.set_focus();
            }
        }))
        .setup(move |app| {
            if let Ok(dir) = app.path().resource_dir() {
                paths = paths.with_resource_dir(dir);
            }
            app.manage(AppState {
                args: args.clone(),
                paths,
                process: Mutex::new(None),
                url: Mutex::new(None),
                initial_boot_started: AtomicBool::new(false),
            });

            let mut builder =
                WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into()))
                    .title(APP_NAME)
                    .inner_size(1320.0, 860.0)
                    .min_inner_size(800.0, 560.0)
                    .decorations(false)
                    .resizable(true)
                    .initialization_script(INJECT)
                    .on_navigation(|url| {
                        if is_internal(&url) {
                            true
                        } else {
                            let _ = open::that(url.as_str());
                            false
                        }
                    });

            if let Some(icon) = app.default_window_icon().cloned() {
                builder = builder.icon(icon)?;
            }

            if dev {
                builder = builder.devtools(true);
            }

            let window = builder.build()?;
            if dev {
                window.open_devtools();
            }
            enable_microphone(&window);

            let _ = window;
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            restart,
            open_in_browser,
            read_clipboard_images,
            check_shell_update,
            install_shell_update,
            skip_shell_update,
        ])
        .on_window_event(|window, event| {
            if let tauri::WindowEvent::CloseRequested { .. } = event {
                if let Some(state) = window.try_state::<AppState>() {
                    state.stop();
                }
            }
        })
        .run(tauri::generate_context!())
        .expect("error while running DeepSeek Harness Desktop");
}
