// TODO: https://github.com/tokio-rs/tracing/blob/main/examples/examples/panic_hook.rs
// TODO: Use a custom language ID in `window.createOutputChannel` to better format log messages

use napi::bindgen_prelude::{self, JsObjectValue};
use napi::threadsafe_function::ThreadsafeFunctionCallMode;
use tracing::{Level, Subscriber};
use tracing_subscriber::Layer;
use tracing_subscriber::fmt::{FormatFields, FormattedFields};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::registry::LookupSpan;

struct VscodeLayer<F>
where
    F: for<'writer> FormatFields<'writer> + 'static,
{
    threadsafe_function: napi::threadsafe_function::ThreadsafeFunction<
        (&'static str, String),
        (),
        (&'static str, String),
        napi::Status,
        false,
        false,
        0,
    >,
    field_formatter: F,
}

impl<F> VscodeLayer<F>
where
    F: for<'writer> FormatFields<'writer> + 'static,
{
    fn new(env: &napi::Env, name: &str, field_formatter: F) -> Result<Self, napi::Error> {
        let output_channel = super::window::create_log_output_channel(env, name)?;

        let threadsafe_function: bindgen_prelude::Function<(&str, String), ()> = env
            .create_function_from_closure("tracing_event_handler", |function_context| {
                let ((method, message),): ((String, String),) = function_context.args()?;
                let output_channel: bindgen_prelude::Object = function_context.this()?;

                let log_function: bindgen_prelude::Function<String, ()> =
                    output_channel.get_named_property(&method)?;
                log_function.apply(output_channel, message)
            })?;

        // TODO: build_callback so can pass rust values
        let threadsafe_function = threadsafe_function
            .bind(output_channel)?
            .build_threadsafe_function()
            .callee_handled::<false>()
            .max_queue_size::<0>()
            .weak::<false>()
            .build()?;

        Ok(Self {
            threadsafe_function,
            field_formatter,
        })
    }

    fn call_output_channel<S: Into<String>>(&self, level: Level, message: S) {
        let level = match level {
            Level::TRACE => "trace",
            Level::DEBUG => "debug",
            Level::INFO => "info",
            Level::WARN => "warn",
            Level::ERROR => "error",
        };

        let status = self.threadsafe_function.call(
            // Ok((level, message.into())),
            (level, message.into()),
            ThreadsafeFunctionCallMode::Blocking,
        );
    }

    fn internal_message<S: core::fmt::Display>(&self, level: Level, message: S) {
        self.call_output_channel(level, format!("Internal tracing message: {message}"));
    }
}

impl<F, S> Layer<S> for VscodeLayer<F>
where
    F: for<'writer> FormatFields<'writer> + 'static,
    S: Subscriber + for<'registry> LookupSpan<'registry>,
{
    fn on_new_span(
        &self,
        attributes: &tracing::span::Attributes<'_>,
        id: &tracing::span::Id,
        context: tracing_subscriber::layer::Context<'_, S>,
    ) {
        let Some(span) = context.span(id) else {
            self.internal_message(Level::ERROR, format!("no matching span for ID: {id:#?}"));
            return;
        };

        let mut extensions = span.extensions_mut();

        if extensions.get_mut::<FormattedFields<F>>().is_none() {
            let mut formatted_fields: FormattedFields<F> = FormattedFields::new(String::new());
            let format_result = self
                .field_formatter
                .format_fields(formatted_fields.as_writer(), attributes);

            match format_result {
                Ok(()) => extensions.insert(formatted_fields),
                Err(error) => {
                    self.internal_message(Level::ERROR, format!("failed to format fields: {error}"))
                }
            }
        }
    }

    fn on_event(
        &self,
        event: &tracing::Event<'_>,
        context: tracing_subscriber::layer::Context<'_, S>,
    ) {
        let mut message = String::new();

        if let Some(event_scope) = context.event_scope(event) {
            for span in event_scope.from_root() {
                message.push_str(span.name());

                match span.extensions().get::<FormattedFields<F>>() {
                    Some(formatted_fields) => {
                        if !formatted_fields.is_empty() {
                            message.push('{');
                            message.push_str(formatted_fields);
                            message.push('}');
                        }
                    }
                    None => {
                        self.internal_message(
                            Level::WARN,
                            format!("no formatted fields stored for {:#?}", span.id()),
                        );

                        message.push_str("{missing fields}");
                    }
                }

                message.push(':');
            }
        }

        match event.metadata().module_path() {
            Some(module_path) => {
                if !message.is_empty() {
                    message.push(' ');
                }
                message.push_str(module_path);
                message.push_str(": ");
            }
            None => {
                self.internal_message(
                    Level::WARN,
                    format!(
                        "missing module path for event at callsite {:#?}",
                        event.metadata().callsite()
                    ),
                );
            }
        }

        // TODO: https://github.com/tokio-rs/tracing/issues/3335
        let writer = tracing_subscriber::fmt::format::Writer::new(&mut message);
        if let Err(error) = self.field_formatter.format_fields(writer, event) {
            self.internal_message(
                Level::ERROR,
                format!("failed to write event fields: {error}"),
            );
        }

        self.call_output_channel(*event.metadata().level(), message);
    }
}

pub fn init<F>(env: &napi::Env, name: &str, field_formatter: F) -> Result<(), napi::Error>
where
    F: for<'writer> FormatFields<'writer> + Send + Sync + 'static,
{
    let vscode_layer = VscodeLayer::new(env, name, field_formatter)?;

    // TODO: https://docs.rs/tracing-appender/latest/tracing_appender/
    let subscriber = tracing_subscriber::registry().with(vscode_layer);

    tracing::subscriber::set_global_default(subscriber).map_err(|error| {
        napi::Error::from_reason(format!("Failed to set global default subscriber: {error}"))
    })?;

    std::panic::set_hook(Box::new(|panic_hook_info| {
        tracing::error!(message = "Extension panic", %panic_hook_info);
    }));

    Ok(())
}