use tun2proxy::{ArgVerbosity, Args, BoxError};

fn main() -> Result<(), BoxError> {
    dotenvy::dotenv().ok();
    let args = Args::parse_args();

    #[cfg(unix)]
    if args.daemonize {
        let stdout = std::fs::File::create("/tmp/tun2proxy.out")?;
        let stderr = std::fs::File::create("/tmp/tun2proxy.err")?;
        let daemonize = daemonize::Daemonize::new()
            .working_directory("/tmp")
            .umask(0o777)
            .stdout(stdout)
            .stderr(stderr)
            .privileged_action(|| "Executed before drop privileges");
        let _ = daemonize.start()?;
    }

    #[cfg(target_os = "windows")]
    if args.daemonize {
        tun2proxy::win_svc::start_service()?;
        return Ok(());
    }

    let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?;
    rt.block_on(async move {
        let res = main_async(args).await;
        // Start a timer to force exit after FORCE_EXIT_TIMEOUT seconds. Use a std thread
        // so the timer is not cancelled when the tokio runtime is being shut down.
        let _h = std::thread::spawn(move || {
            log::info!("Starting {:?} exit timer", tun2proxy::FORCE_EXIT_TIMEOUT);
            // Delay some seconds then try to exit current process if not exited yet, normally this case should not happen
            std::thread::sleep(tun2proxy::FORCE_EXIT_TIMEOUT);
            log::info!("Forcing exit now.");
            std::process::exit(-1);
        });

        log::info!("Runtime.block_on exiting...");
        tokio::time::sleep(std::time::Duration::from_micros(100)).await;

        res
    })
}

fn setup_logging(args: &Args) {
    let avoid_trace = match args.verbosity {
        ArgVerbosity::Trace => ArgVerbosity::Debug,
        _ => args.verbosity,
    };
    let default = format!(
        "{:?},hickory_proto=warn,ipstack={:?},netlink_proto={:?},netlink_sys={:?}",
        args.verbosity, avoid_trace, avoid_trace, avoid_trace
    );
    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default)).init();
}

async fn main_async(args: Args) -> Result<(), BoxError> {
    setup_logging(&args);

    let shutdown_token = tokio_util::sync::CancellationToken::new();
    let main_loop_handle = tokio::spawn({
        let args = args.clone();
        let shutdown_token = shutdown_token.clone();
        async move {
            #[cfg(target_os = "linux")]
            if args.unshare && args.socket_transfer_fd.is_none() {
                if let Err(err) = namespace_proxy_main(args, shutdown_token).await {
                    log::error!("namespace proxy error: {err}");
                }
                return Ok(0);
            }

            unsafe extern "C" fn traffic_cb(status: *const tun2proxy::TrafficStatus, _: *mut std::ffi::c_void) {
                let status = unsafe { &*status };
                log::debug!("Traffic: ▲ {} : ▼ {}", status.tx, status.rx);
            }
            unsafe { tun2proxy::tun2proxy_set_traffic_status_callback(1, Some(traffic_cb), std::ptr::null_mut()) };

            let ret = tun2proxy::general_run_async(args, tun::DEFAULT_MTU, cfg!(target_os = "macos"), shutdown_token).await;
            if let Err(err) = &ret {
                log::error!("main loop error: {err}");
            }
            ret
        }
    });

    let ctrlc_fired = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
    let ctrlc_fired_clone = ctrlc_fired.clone();
    let ctrlc_handel = ctrlc2::AsyncCtrlC::new(move || {
        log::info!("Ctrl-C received, exiting...");
        ctrlc_fired_clone.store(true, std::sync::atomic::Ordering::SeqCst);
        shutdown_token.cancel();
        true
    })?;

    let _tasks = main_loop_handle.await??;

    if ctrlc_fired.load(std::sync::atomic::Ordering::SeqCst) {
        log::info!("Ctrl-C fired, waiting the handler to finish...");
        match tokio::time::timeout(std::time::Duration::from_secs(1), ctrlc_handel).await {
            Ok(Ok(())) => log::info!("Ctrl-C handler finished"),
            Ok(Err(e)) => log::warn!("Ctrl-C handler error: {e}"),
            Err(_) => log::warn!("Ctrl-C handler timeout, continuing..."),
        }
    }

    Ok(())
}

#[cfg(target_os = "linux")]
async fn namespace_proxy_main(
    _args: Args,
    _shutdown_token: tokio_util::sync::CancellationToken,
) -> Result<std::process::ExitStatus, tun2proxy::Error> {
    use nix::fcntl::{OFlag, open};
    use nix::sys::stat::Mode;
    use std::os::fd::AsRawFd;

    let (socket, remote_fd) = tun2proxy::socket_transfer::create_transfer_socket_pair().await?;

    let fd = open("/proc/self/exe", OFlag::O_PATH, Mode::empty())?;

    let child = tokio::process::Command::new("unshare")
        .args("--user --map-current-user --net --mount --keep-caps --kill-child --fork".split(' '))
        .arg(format!("/proc/self/fd/{}", fd.as_raw_fd()))
        .arg("--socket-transfer-fd")
        .arg(remote_fd.as_raw_fd().to_string())
        .args(std::env::args().skip(1))
        .kill_on_drop(true)
        .spawn();

    let mut child = match child {
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
            log::error!("`unshare(1)` executable wasn't located in PATH.");
            log::error!("Consider installing linux utils package: `apt install util-linux`");
            log::error!("Or similar for your distribution.");
            return Err(err.into());
        }
        child => child?,
    };

    let unshare_pid = child.id().unwrap_or(0);
    log::info!("The tun proxy is running in unprivileged mode. See `namespaces(7)`.");
    log::info!("");
    log::info!("If you need to run a process that relies on root-like capabilities (e.g. `openvpn`)");
    log::info!("Use `tun2proxy-bin --unshare --setup [...] -- openvpn --config [...]`");
    log::info!("");
    log::info!("To run a new process in the created namespace (e.g. a flatpak app)");
    log::info!("Use `nsenter --preserve-credentials --user --net --mount  --target {unshare_pid} /bin/sh`");
    log::info!("");
    if let Some(pidfile) = _args.unshare_pidfile.as_ref() {
        log::info!("Writing unshare pid to {pidfile}");
        std::fs::write(pidfile, unshare_pid.to_string()).ok();
    }
    tokio::spawn(async move { tun2proxy::socket_transfer::process_socket_requests(&socket).await });

    Ok(child.wait().await?)
}
