#![deny(clippy::unwrap_used)]
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used, clippy::panic))]
// Don't warn on event sending macros
#![recursion_limit = "512"]

#[cfg(target_os = "wasi")]
compile_error!("Compiling for WASI targets is not supported!");

use pumpkin_data::packet::CURRENT_MC_VERSION;
use pumpkin_world::{CURRENT_BEDROCK_MC_PROTOCOL, CURRENT_BEDROCK_MC_VERSION};
use std::{
    backtrace::{Backtrace, BacktraceStatus},
    io::{self},
    panic::PanicHookInfo,
    process::exit,
    sync::{OnceLock, atomic::Ordering},
    thread::{self, ThreadId},
};
#[cfg(not(unix))]
use tokio::signal::ctrl_c;
#[cfg(unix)]
use tokio::signal::unix::{SignalKind, signal};

use pumpkin::{
    CRASH_REPORT, SERVER_EXIT_CODE, SERVER_IS_STOPPING,
    crash::{CrashReport, FullBacktrace},
    data::VanillaData,
    stop_or_exit_server,
};
use pumpkin::{PumpkinServer, stop_server};

use pumpkin_config::{LoadConfiguration, PumpkinConfig};
use pumpkin_util::text::{
    TextComponent,
    color::{Color, NamedColor},
};
use std::time::Instant;
use tracing::{debug, info, warn};

const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");

static MAIN_THREAD: OnceLock<ThreadId> = OnceLock::new();

// WARNING: All rayon calls from the tokio runtime must be non-blocking! This includes things
// like `par_iter`. These should be spawned in the the rayon pool and then passed to the tokio
// runtime with a channel! See `Level::fetch_chunks` as an example!
#[allow(clippy::too_many_lines)]
#[tokio::main]
async fn main() {
    let _ = MAIN_THREAD.set(thread::current().id());

    // Set the panic handler.
    std::panic::set_hook(Box::new(handle_panic));

    #[cfg(feature = "console-subscriber")]
    console_subscriber::init();
    let time = Instant::now();

    let exec_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));

    let config = PumpkinConfig::load(&exec_dir);

    let vanilla_data = VanillaData::load();

    pumpkin::init_logger(&config.advanced);

    info!(
        "{}",
        TextComponent::text(format!(
            "Starting {} {} Java Minecraft (Protocol {}) | {} Bedrock (Protocol {})",
            TextComponent::text("Pumpkin")
                .color_named(NamedColor::Gold)
                .to_pretty_console(),
            TextComponent::text(CARGO_PKG_VERSION.to_string())
                .color_named(NamedColor::Green)
                .to_pretty_console(),
            TextComponent::text(CURRENT_MC_VERSION.protocol_version().to_string())
                .color_named(NamedColor::DarkBlue)
                .to_pretty_console(),
            TextComponent::text(CURRENT_BEDROCK_MC_VERSION)
                .color_named(NamedColor::Gold)
                .to_pretty_console(),
            TextComponent::text(CURRENT_BEDROCK_MC_PROTOCOL.to_string())
                .color_named(NamedColor::DarkBlue)
                .to_pretty_console()
        ))
        .to_pretty_console(),
    );

    debug!(
        "Build info: FAMILY: \"{}\", OS: \"{}\", ARCH: \"{}\", BUILD: \"{}\"",
        std::env::consts::FAMILY,
        std::env::consts::OS,
        std::env::consts::ARCH,
        if cfg!(debug_assertions) {
            "Debug"
        } else {
            "Release"
        }
    );
    if cfg!(debug_assertions) {
        warn!(
            "Pumpkin is running an unoptimized debug build. Do not use this build for performance testing; run `cargo run --release` or use a release binary."
        );
    }
    print_support_links_and_warning();

    tokio::spawn(async {
        if let Err(err) = setup_sighandler().await {
            tracing::error!("Unable to setup signal handlers: {err}");
        }
    });

    let pumpkin_server = PumpkinServer::new(config.basic, config.advanced, vanilla_data).await;
    let plugin_wait_time = pumpkin_server.init_plugins().await;

    let time_elapsed = time.elapsed().saturating_sub(plugin_wait_time);

    info!(
        "Started server; took {}",
        TextComponent::text(format!("{}ms", time_elapsed.as_millis()))
            .color_named(NamedColor::Gold)
            .to_pretty_console()
    );
    let advanced_config = &pumpkin_server.server.advanced_config;
    info!(
        "Server is now running. Connect using port: {}{}{}",
        if advanced_config.networking.java.enabled {
            format!(
                "{} {}",
                TextComponent::text("Java Edition:")
                    .color_named(NamedColor::Yellow)
                    .to_pretty_console(),
                TextComponent::text(format!("{}", advanced_config.networking.java.address))
                    .color_named(NamedColor::DarkBlue)
                    .to_pretty_console()
            )
        } else {
            TextComponent::text(String::new()).to_pretty_console()
        },
        if advanced_config.networking.java.enabled && advanced_config.networking.bedrock.enabled {
            " | " // Separator if both are enabled
        } else {
            ""
        },
        if advanced_config.networking.bedrock.enabled {
            format!(
                "{} {}",
                TextComponent::text("Bedrock Edition:")
                    .color_named(NamedColor::Gold)
                    .to_pretty_console(),
                TextComponent::text(format!(
                    "{}",
                    advanced_config.networking.bedrock.nethernet.address
                ))
                .color_named(NamedColor::DarkBlue)
                .to_pretty_console()
            )
        } else {
            TextComponent::text(String::new()).to_pretty_console()
        }
    );

    pumpkin_server.start().await;

    info!(
        "{}",
        TextComponent::text("The server has stopped.")
            .color_named(NamedColor::Red)
            .to_pretty_console()
    );

    exit(SERVER_EXIT_CODE.load(Ordering::Acquire));
}
fn print_support_links_and_warning() {
    warn!(
        "{}",
        TextComponent::text("Pumpkin is currently under heavy development!")
            .color_named(NamedColor::DarkRed)
            .to_pretty_console(),
    );
    info!(
        "Report issues on {}",
        TextComponent::text("https://github.com/Pumpkin-MC/Pumpkin/issues")
            .color_named(NamedColor::DarkAqua)
            .to_pretty_console()
    );
    info!(
        "Join our {} for community support: {}",
        TextComponent::text("Discord")
            .color_named(NamedColor::DarkBlue)
            .to_pretty_console(),
        TextComponent::text("https://discord.gg/wT8XjrjKkf")
            .color_named(NamedColor::Aqua)
            .to_pretty_console()
    );
    info!(
        "Consider {} to {}",
        TextComponent::text("Donating")
            .color_named(NamedColor::DarkPurple)
            .to_pretty_console(),
        TextComponent::text("https://pumpkinmc.org/donate/")
            .color_named(NamedColor::Gold)
            .to_pretty_console()
    );
}

fn handle_interrupt() {
    warn!(
        "{}",
        TextComponent::text("Received interrupt signal; stopping server...")
            .color_named(NamedColor::Red)
            .to_pretty_console()
    );
    stop_or_exit_server();
}

fn handle_panic(panic_info: &PanicHookInfo<'_>) {
    // Generate a crash report.
    let crash_report = {
        // We capture the backtraces here, and not in the
        // crash report, so that the backtrace doesn't show
        // the CrashReport's `new` function.
        let captured_backtrace = Backtrace::capture();
        let full_backtrace = if captured_backtrace.status() == BacktraceStatus::Captured {
            FullBacktrace::Captured
        } else {
            FullBacktrace::ForceCaptured(Backtrace::force_capture())
        };

        CrashReport::new(panic_info, captured_backtrace, full_backtrace)
    };

    let payload = panic_info.payload();

    if is_main_thread() {
        // It's the first panic;
        // We cannot gracefully shut down as the main thread
        // has panicked. However, we can still generate the crash report.

        if let Some(crash_report) = try_set_crash_report(crash_report) {
            crash_report.print_to_console();
            crash_report.save_and_log();

            tracing::error!(
                "{}",
                TextComponent::text("Aborting due to the main thread panicking.")
                    .color(Color::Named(NamedColor::Red))
                    .to_pretty_console()
            );
        } else {
            // It's a subsequent panic.
            tracing::error!(
                "{}: {}",
                TextComponent::text(
                    "The main thread panicked while stopping the server; aborting."
                )
                .color(Color::Named(NamedColor::Red))
                .bold()
                .to_pretty_console(),
                payload
                    .downcast_ref::<&str>()
                    .copied()
                    .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
                    .unwrap_or("<unknown>")
            );
        }

        exit(1);
    }

    if try_set_crash_report(crash_report).is_some() {
        // It's the first panic; let's stop the server.
        stop_server();
    } else {
        // It's a subsequent panic; let's just alert about it.
        tracing::error!(
            "{}: {}",
            TextComponent::text("Encountered panic while shutting down")
                .color(Color::Named(NamedColor::Red))
                .bold()
                .to_pretty_console(),
            payload
                .downcast_ref::<&str>()
                .copied()
                .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
                .unwrap_or("<unknown>")
        );
    }
}

fn is_main_thread() -> bool {
    Some(&thread::current().id()) == MAIN_THREAD.get()
}

/// Returns `Some` if the crash report was successfully set. That
/// means it is the first panic, and it must be logged and saved later.
///
/// Returns `None` otherwise as the panic is subsequent.
fn try_set_crash_report(crash_report: CrashReport) -> Option<&'static CrashReport> {
    if !SERVER_IS_STOPPING.load(Ordering::Acquire) && CRASH_REPORT.set(crash_report).is_ok() {
        CRASH_REPORT.get()
    } else {
        None
    }
}

// Non-UNIX Ctrl-C handling
#[cfg(not(unix))]
async fn setup_sighandler() -> io::Result<()> {
    if ctrl_c().await.is_ok() {
        handle_interrupt();
    }

    Ok(())
}

// Unix signal handling
#[cfg(unix)]
async fn setup_sighandler() -> io::Result<()> {
    if signal(SignalKind::interrupt())?.recv().await.is_some() {
        handle_interrupt();
    }

    if signal(SignalKind::hangup())?.recv().await.is_some() {
        handle_interrupt();
    }

    if signal(SignalKind::terminate())?.recv().await.is_some() {
        handle_interrupt();
    }

    Ok(())
}
