use std::{
    backtrace::{Backtrace, BacktraceStatus},
    fmt::{Display, Write as _},
    fs::{File, create_dir_all},
    panic::{Location as PanicLocation, PanicHookInfo},
    path::{Path, PathBuf},
    thread::{self, Thread},
};

use pumpkin_util::text::{
    TextComponent,
    color::{Color, NamedColor},
};
use pumpkin_world::CURRENT_MC_VERSION;
use rustc_hash::FxHashMap;
use sysinfo::{Cpu, System};
use time::OffsetDateTime;
use tracing::error;

pub const BYTES_PER_MEBIBYTE: u64 = 1024 * 1024;

/// Writes to a string which cannot fail.
macro_rules! writeln_output {
    ($dst:expr $(,)?) => {
        let _ = writeln!($dst);
    };
    ($dst:expr, $($arg:tt)*) => {
        let _ = writeln!($dst, $($arg)*);
    };
}

/// A backtrace that either references
/// a full backtrace already generated,
/// or a new one.
pub enum FullBacktrace {
    Captured,
    ForceCaptured(Backtrace),
}

/// Represents the location of a character
/// in a file.
pub struct Location {
    pub file_name: String,
    pub line: u32,
    pub column: u32,
}

impl From<&PanicLocation<'_>> for Location {
    fn from(value: &PanicLocation) -> Self {
        Self {
            file_name: value.file().to_string(),
            line: value.line(),
            column: value.column(),
        }
    }
}

impl Display for Location {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}:{}:{}", self.file_name, self.line, self.column)
    }
}

/// Represents a crash report, containing
/// the required information.
///
/// This is stored without a lifetime related to a [`PanicHookInfo`]
/// so that information from a reference to a [`PanicHookInfo`]
/// can be stored globally without lifetime issues.
pub struct CrashReport {
    utc_time: OffsetDateTime,
    payload: Option<String>,
    thread: Thread,
    panic_location: Option<Location>,
    full_backtrace: FullBacktrace,
    captured_backtrace: Backtrace,
}

impl CrashReport {
    /// Creates a new crash report detailing the state of the
    /// server at the panic handler call site.
    pub fn new(
        panic_info: &PanicHookInfo<'_>,
        captured_backtrace: Backtrace,
        full_backtrace: FullBacktrace,
    ) -> Self {
        Self {
            utc_time: OffsetDateTime::now_utc(),
            payload: {
                let payload = panic_info.payload();
                payload.downcast_ref::<&str>().map_or_else(
                    || payload.downcast_ref::<String>().cloned(),
                    |s| Some(s.to_string()),
                )
            },
            thread: thread::current(),
            full_backtrace,
            captured_backtrace,
            panic_location: panic_info.location().map(Into::into),
        }
    }

    /// Prints information about the crash to the console,
    /// but not the full report.
    #[allow(clippy::print_stderr)]
    pub fn print_to_console(&self) {
        const RED: Color = Color::Named(NamedColor::Red);

        error!(
            "{}",
            TextComponent::text("Pumpkin has encountered a panic!")
                .color(RED)
                .bold()
                .to_pretty_console()
        );

        error!("");

        // Printing panic info.
        let thread_name = self.thread.name().unwrap_or("<unnamed>");
        let thread_id = self.thread.id();

        let message = self.panic_location.as_ref().map_or_else(
            || format!("Thread '{thread_name}' {thread_id:?} panicked"),
            |location| format!("Thread '{thread_name}' with {thread_id:?} panicked at {location}"),
        );

        if let Some(payload) = &self.payload {
            error!("{}:", RED.console_color(&message));
            error!("{payload}");
        } else {
            error!("{}", RED.console_color(&message));
        }

        error!("");

        let backtrace_status = self.full_backtrace().status();

        match backtrace_status {
            BacktraceStatus::Unsupported => {
                error!(
                    "{}",
                    RED.console_color("Backtracing is not supported for this platform.")
                );
            }
            // It cannot possibly be BacktraceStatus::Disabled
            // as it is a forced backtrace.
            BacktraceStatus::Captured => {
                error!(
                    "{}",
                    RED.console_color("The full backtrace will be printed to the crash report.")
                );

                if self.captured_backtrace.status() == BacktraceStatus::Captured {
                    eprintln!(
                        "{}\n{}",
                        RED.console_color("Backtrace:"),
                        self.captured_backtrace
                    );
                }
            }
            _ => {
                error!("{}", RED.console_color("Backtrace status is unknown, so no backtrace will be generated for the crash report."));
            }
        }
    }

    /// Generates the file content of the crash report file
    /// that would be generated from this report.
    pub fn generate_file_content(&self) -> String {
        let mut output = String::new();

        writeln_output!(&mut output, "====== Pumpkin Crash Report ======");
        writeln_output!(&mut output);
        writeln_output!(&mut output, "Time: {}", self.utc_time);
        writeln_output!(
            &mut output,
            "Message: {}",
            self.payload.as_deref().unwrap_or("<unknown>")
        );

        if let Some(panic_location) = &self.panic_location {
            writeln_output!(&mut output, "Panic Location: {}", panic_location);
        }

        writeln_output!(&mut output);
        writeln_output!(&mut output, "--- Panicking Thread ---");
        writeln_output!(&mut output, "ID: {:?}", self.thread.id());
        if let Some(thread_name) = self.thread.name() {
            writeln_output!(&mut output, "Name: {}", thread_name);
        }
        writeln_output!(&mut output, "Backtrace:");
        writeln_output!(&mut output, "{}", self.full_backtrace());

        writeln_output!(&mut output, "--- Server Details ---");

        writeln_output!(
            &mut output,
            "Pumpkin Version: {}",
            Self::get_pumpkin_version()
        );
        writeln_output!(&mut output, "Minecraft Version: {}", CURRENT_MC_VERSION);
        writeln_output!(
            &mut output,
            "Server compiled with Rust {}",
            rustc_version_runtime::version()
        );

        if sysinfo::IS_SUPPORTED_SYSTEM {
            writeln_output!(&mut output, "\n--- System Details ---");

            let mut sys = System::new_all();
            sys.refresh_all();

            writeln_output!(
                &mut output,
                "Operating System: {}",
                System::long_os_version().unwrap_or("Unknown".to_string())
            );
            writeln_output!(
                &mut output,
                "Kernel: {}",
                System::kernel_version().unwrap_or_else(|| "Unknown".to_string())
            );
            writeln_output!(
                &mut output,
                "Physical Memory: {} MiB/{} MiB used, {} MiB free",
                sys.used_memory() / BYTES_PER_MEBIBYTE,
                sys.total_memory() / BYTES_PER_MEBIBYTE,
                sys.free_memory() / BYTES_PER_MEBIBYTE
            );
            writeln_output!(
                &mut output,
                "Swap Memory: {} MiB/{} MiB used, {} MiB free",
                sys.used_swap() / BYTES_PER_MEBIBYTE,
                sys.total_swap() / BYTES_PER_MEBIBYTE,
                sys.free_swap() / BYTES_PER_MEBIBYTE
            );

            Self::write_cpus(&mut output, &sys);
        }

        output
    }

    fn write_cpus(output: &mut String, sys: &System) {
        writeln_output!(output);
        let cpus = sys.cpus();

        writeln_output!(output, "Total cores: {}", cpus.len());
        writeln_output!(output);

        let mut different_brands: FxHashMap<(&str, &str), Vec<&Cpu>> = FxHashMap::default();

        // `sysinfo` provides us a CPU for each core, so we try to group them.
        for cpu in cpus {
            different_brands
                .entry((cpu.brand(), cpu.vendor_id()))
                .or_default()
                .push(cpu);
        }

        for (i, ((brand, vendor_id), cpus)) in different_brands.iter().enumerate() {
            let prefix = format!(" CPU #{:<5}", i + 1);
            let padded = " ".repeat(prefix.len());

            let names = cpus
                .iter()
                .map(|cpu| cpu.name())
                .collect::<Vec<&str>>()
                .join(", ");

            let avg_freq = cpus.iter().map(|cpu| cpu.frequency()).sum::<u64>() / cpus.len() as u64;

            writeln_output!(output, "|{prefix} Cores: {}", cpus.len());
            writeln_output!(output, "|{padded} Names: {}", names);
            writeln_output!(output, "|{padded} Brand: {}", brand);
            writeln_output!(output, "|{padded} Average Frequency: {} MHz", avg_freq);
            writeln_output!(output, "|{padded} Vendor ID: {}", vendor_id);
            writeln_output!(output);
        }
    }

    #[must_use]
    pub fn get_pumpkin_version() -> String {
        let profile = if cfg!(debug_assertions) {
            "debug"
        } else {
            "release"
        };
        format!(
            "{} (Commit: {}/{})",
            env!("CARGO_PKG_VERSION"),
            env!("GIT_HASH"),
            profile
        )
    }

    /// Saves this report to the `crash-reports` directory.
    ///
    /// Returns a `Result` containing a path if successful.
    pub fn save(&self) -> std::io::Result<PathBuf> {
        const CRASH_REPORTS_DIR: &str = "./crash-reports";

        let file_name = format!(
            "crash-{}-{:02}-{:02}_{:02}.{:02}.{:02}.txt",
            self.utc_time.year(),
            self.utc_time.month() as u8,
            self.utc_time.day(),
            self.utc_time.hour(),
            self.utc_time.minute(),
            self.utc_time.second()
        );

        let path = Path::new(CRASH_REPORTS_DIR).join(file_name);
        Self::write_text_to_path(&path, &self.generate_file_content()).map(|()| path)
    }

    /// Saves the crash report to a file and
    /// prints about whether and where it saved.
    ///
    /// Returns `true` if the file successfully saved.
    pub fn save_and_log(&self) -> bool {
        match self.save() {
            Ok(path) => {
                tracing::info!(
                    "{} {}",
                    Color::Named(NamedColor::Green)
                        .console_color("Successfully saved the crash report to file:"),
                    path.display()
                );
                true
            }
            Err(error) => {
                tracing::error!(
                    "{} {}",
                    Color::Named(NamedColor::Red).console_color("Could not save the crash report:"),
                    error
                );
                false
            }
        }
    }

    fn write_text_to_path(path: &Path, text: &str) -> std::io::Result<()> {
        if let Some(parent) = path.parent() {
            create_dir_all(parent)?;
        }

        let mut file = File::create_new(path)?;
        <File as std::io::Write>::write_all(&mut file, text.as_bytes())?;

        Ok(())
    }

    /// Convenient method to return a reference to the report's full backtrace.
    const fn full_backtrace(&self) -> &Backtrace {
        match &self.full_backtrace {
            FullBacktrace::Captured => &self.captured_backtrace,
            FullBacktrace::ForceCaptured(backtrace) => backtrace,
        }
    }
}
