use crate::observability::config::{LogFormat, LogLocalConfig};
use crate::observability::filter::SharedOrderedFilter;
use chrono::Utc;
use chrono_tz::Tz;
use std::fmt::{self, Write};
use tracing::{Event, Subscriber};
use tracing_subscriber::{
    fmt::{
        format::{FormatEvent, FormatFields, Writer},
        time::FormatTime,
        FmtContext,
    },
    registry::LookupSpan,
    Layer, Registry,
};

#[derive(Clone, Copy)]
struct TimezoneTimer(Tz);

struct ProcessContextFormat<E> {
    inner: E,
    pid: u32,
}

impl<E> ProcessContextFormat<E> {
    fn new(inner: E) -> Self {
        Self {
            inner,
            pid: std::process::id(),
        }
    }
}

impl<S, N, E> FormatEvent<S, N> for ProcessContextFormat<E>
where
    S: Subscriber + for<'lookup> LookupSpan<'lookup>,
    N: for<'writer> FormatFields<'writer> + 'static,
    E: FormatEvent<S, N>,
{
    fn format_event(
        &self,
        ctx: &FmtContext<'_, S, N>,
        mut writer: Writer<'_>,
        event: &Event<'_>,
    ) -> fmt::Result {
        let mut rendered = String::new();
        self.inner
            .format_event(ctx, Writer::new(&mut rendered), event)?;

        let thread = std::thread::current();
        let thread_name = thread.name().unwrap_or("unnamed");
        let process_context = format!(
            "process.pid={} thread.name={thread_name:?} thread.id={:?} ",
            self.pid,
            thread.id()
        );
        if let Some(index) = rendered.rfind("log.target") {
            rendered.insert_str(index, &process_context);
        } else if let Some(index) = rendered.rfind('\n') {
            rendered.insert_str(index, &format!(" {process_context}"));
        } else {
            rendered.push(' ');
            rendered.push_str(&process_context);
        }
        writer.write_str(&rendered)
    }
}

impl FormatTime for TimezoneTimer {
    fn format_time(&self, writer: &mut Writer<'_>) -> fmt::Result {
        write!(writer, "{}", Utc::now().with_timezone(&self.0).to_rfc3339())
    }
}

/// Builds the stdout JSON layer.
///
/// This uses the same shared ordered filter as local file and remote outputs.
pub fn build_console_layer(
    format: LogFormat,
    timezone: Tz,
    include_process_id: bool,
    filter: SharedOrderedFilter,
) -> Box<dyn Layer<Registry> + Send + Sync> {
    let base = tracing_subscriber::fmt::layer()
        .with_timer(TimezoneTimer(timezone))
        .with_target(true)
        .with_file(true)
        .with_line_number(true)
        .with_thread_ids(true)
        .with_thread_names(true)
        .with_writer(std::io::stdout);

    match (format, include_process_id) {
        (LogFormat::Json, _) => base
            .json()
            .with_current_span(true)
            .with_span_list(true)
            .with_filter(filter)
            .boxed(),
        (LogFormat::Compact, false) => base.compact().with_filter(filter).boxed(),
        (LogFormat::Full, false) => base.with_filter(filter).boxed(),
        (LogFormat::Pretty, false) => base.pretty().with_filter(filter).boxed(),
        (LogFormat::Compact, true) => base
            .event_format(ProcessContextFormat::new(
                tracing_subscriber::fmt::format()
                    .with_timer(TimezoneTimer(timezone))
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .compact(),
            ))
            .with_filter(filter)
            .boxed(),
        (LogFormat::Full, true) => base
            .event_format(ProcessContextFormat::new(
                tracing_subscriber::fmt::format()
                    .with_timer(TimezoneTimer(timezone))
                    .with_thread_ids(false)
                    .with_thread_names(false),
            ))
            .with_filter(filter)
            .boxed(),
        (LogFormat::Pretty, true) => base
            .event_format(ProcessContextFormat::new(
                tracing_subscriber::fmt::format()
                    .with_timer(TimezoneTimer(timezone))
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .pretty(),
            ))
            .with_filter(filter)
            .boxed(),
    }
}

/// Builds the local rolling JSON file layer.
///
/// The supplied `filter` is the shared ordered filter used by all log outputs.
pub fn build_local_layer(
    config: &LogLocalConfig,
    format: LogFormat,
    timezone: Tz,
    include_process_id: bool,
    filter: SharedOrderedFilter,
) -> (
    Box<dyn Layer<Registry> + Send + Sync>,
    tracing_appender::non_blocking::WorkerGuard,
) {
    let file_appender = tracing_appender::rolling::daily(&config.file_dir, &config.file_name);
    let (writer, guard) = tracing_appender::non_blocking(file_appender);

    let base = tracing_subscriber::fmt::layer()
        .with_timer(TimezoneTimer(timezone))
        .with_target(true)
        .with_file(true)
        .with_line_number(true)
        .with_thread_ids(true)
        .with_thread_names(true)
        .with_writer(writer);

    let layer = match (format, include_process_id) {
        (LogFormat::Json, _) => base
            .json()
            .with_current_span(true)
            .with_span_list(true)
            .with_filter(filter)
            .boxed(),
        (LogFormat::Compact, false) => base.compact().with_filter(filter).boxed(),
        (LogFormat::Full, false) => base.with_filter(filter).boxed(),
        (LogFormat::Pretty, false) => base.pretty().with_filter(filter).boxed(),
        (LogFormat::Compact, true) => base
            .event_format(ProcessContextFormat::new(
                tracing_subscriber::fmt::format()
                    .with_timer(TimezoneTimer(timezone))
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .compact(),
            ))
            .with_filter(filter)
            .boxed(),
        (LogFormat::Full, true) => base
            .event_format(ProcessContextFormat::new(
                tracing_subscriber::fmt::format()
                    .with_timer(TimezoneTimer(timezone))
                    .with_thread_ids(false)
                    .with_thread_names(false),
            ))
            .with_filter(filter)
            .boxed(),
        (LogFormat::Pretty, true) => base
            .event_format(ProcessContextFormat::new(
                tracing_subscriber::fmt::format()
                    .with_timer(TimezoneTimer(timezone))
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .pretty(),
            ))
            .with_filter(filter)
            .boxed(),
    };

    (layer, guard)
}