use crate::{formatter::LogFormat, LayerInfo, LogFilterReloadHandle};
#[cfg(feature = "otlp-logs")]
use reth_tracing_otlp::{log_layer, OtlpLogsConfig};
#[cfg(feature = "otlp")]
use reth_tracing_otlp::{span_layer, OtlpConfig};
use rolling_file::{RollingConditionBasic, RollingFileAppender};
use std::{
    fmt,
    fs::File,
    path::{Path, PathBuf},
};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{filter::Directive, reload, EnvFilter, Layer, Registry};

/// A worker guard returned by the file layer.
///
///  When a guard is dropped, all events currently in-memory are flushed to the log file this guard
///  belongs to.
pub type FileWorkerGuard = tracing_appender::non_blocking::WorkerGuard;

/// Guards for tracing layers that must stay alive until shutdown.
#[derive(Default)]
pub struct TracingGuards {
    _file: Option<FileWorkerGuard>,
    _chrome: Option<tracing_chrome::FlushGuard>,
}

impl fmt::Debug for TracingGuards {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("TracingGuards")
            .field("file", &self._file.is_some())
            .field("chrome", &self._chrome.is_some())
            .finish()
    }
}

impl TracingGuards {
    /// Creates tracing guards from active layer guards.
    pub const fn new(
        file: Option<FileWorkerGuard>,
        chrome: Option<tracing_chrome::FlushGuard>,
    ) -> Self {
        Self { _file: file, _chrome: chrome }
    }
}

///  A boxed tracing [Layer].
pub(crate) type BoxedLayer<S> = Box<dyn Layer<S> + Send + Sync>;

/// Default [directives](Directive) for [`EnvFilter`] which:
/// 1. Disable high-frequency debug logs from dependencies such as `hyper`, `hickory-resolver`,
///    `hickory_proto`, `discv5`, `jsonrpsee-server`, and `hyper_util::client::legacy::pool`.
/// 2. Set noisy crates like `opentelemetry_*`, `rustls`, and `tungstenite` to `WARN`.
const DEFAULT_ENV_FILTER_DIRECTIVES: [&str; 11] = [
    "hyper::proto::h1=off",
    "hickory_resolver=off",
    "hickory_proto=off",
    "discv5=off",
    "jsonrpsee-server=off",
    "opentelemetry-otlp=warn",
    "opentelemetry_sdk=warn",
    "opentelemetry-http=warn",
    "hyper_util::client::legacy::pool=off",
    "rustls=warn",
    "tungstenite=warn",
];

/// Manages the collection of layers for a tracing subscriber.
///
/// `Layers` acts as a container for different logging layers such as stdout, file, or journald.
/// Each layer can be configured separately and then combined into a tracing subscriber.
#[derive(Default)]
pub struct Layers {
    inner: Vec<BoxedLayer<Registry>>,
}

impl fmt::Debug for Layers {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Layers").field("layers_count", &self.inner.len()).finish()
    }
}

impl Layers {
    /// Creates a new `Layers` instance.
    pub fn new() -> Self {
        Self::default()
    }

    /// Adds a layer to the collection of layers.
    pub fn add_layer<L>(&mut self, layer: L)
    where
        L: Layer<Registry> + Send + Sync,
    {
        self.inner.push(layer.boxed());
    }

    /// Consumes the `Layers` instance, returning the inner vector of layers.
    pub(crate) fn into_inner(self) -> Vec<BoxedLayer<Registry>> {
        self.inner
    }

    /// Adds a journald layer to the layers collection.
    ///
    /// # Arguments
    /// * `filter` - A string containing additional filter directives for this layer.
    ///
    /// # Returns
    /// An `eyre::Result<()>` indicating the success or failure of the operation.
    pub(crate) fn journald(&mut self, filter: &str) -> eyre::Result<()> {
        let journald_filter = build_env_filter(None, filter)?;
        let layer = tracing_journald::layer()?.with_filter(journald_filter);
        self.add_layer(layer);
        Ok(())
    }

    /// Adds a stdout layer with specified formatting and filtering.
    ///
    /// # Arguments
    /// * `format` - The log message format.
    /// * `default_directive` - Directive for the default logging level.
    /// * `filters` - Additional filter directives as a string.
    /// * `color` - Optional color configuration for the log messages.
    /// * `reloadable` - If true, wraps the filter in a reload layer so it can be changed at
    ///   runtime, and returns the reload handle.
    ///
    /// # Returns
    /// An `eyre::Result` with an optional [`LogFilterReloadHandle`] (present when `reloadable` is
    /// true).
    pub(crate) fn stdout(
        &mut self,
        format: LogFormat,
        default_directive: Directive,
        filters: &str,
        color: Option<String>,
        reloadable: bool,
    ) -> eyre::Result<Option<LogFilterReloadHandle>> {
        let filter = build_env_filter(Some(default_directive), filters)?;

        // When reloadable, always show target since the user may switch to DEBUG/TRACE
        // at runtime via RPC — freezing target=false at init would hide module paths.
        // Otherwise, only show target when initial level is higher than INFO.
        let show_target = reloadable ||
            filter.max_level_hint().is_none_or(|max_level| max_level > tracing::Level::INFO);

        if reloadable {
            let (reloadable_filter, handle) = reload::Layer::new(filter);
            let layer = format.apply(reloadable_filter, color, show_target, None);
            self.add_layer(layer);
            Ok(Some(handle))
        } else {
            let layer = format.apply(filter, color, show_target, None);
            self.add_layer(layer);
            Ok(None)
        }
    }

    /// Adds a file logging layer to the layers collection.
    ///
    /// # Arguments
    /// * `format` - The format for log messages.
    /// * `filter` - Additional filter directives as a string.
    /// * `file_info` - Information about the log file including path and rotation strategy.
    /// * `reloadable` - If true, wraps the filter in a reload layer so it can be changed at
    ///   runtime, and returns the reload handle.
    ///
    /// # Returns
    /// An `eyre::Result` with the file worker guard and an optional [`LogFilterReloadHandle`]
    /// (present when `reloadable` is true).
    pub(crate) fn file(
        &mut self,
        format: LogFormat,
        filter: &str,
        file_info: FileInfo,
        reloadable: bool,
    ) -> eyre::Result<(FileWorkerGuard, Option<LogFilterReloadHandle>)> {
        let (writer, guard) = file_info.create_log_writer()?;
        let file_filter = build_env_filter(None, filter)?;

        if reloadable {
            let (reloadable_filter, handle) = reload::Layer::new(file_filter);
            self.add_layer(format.apply(reloadable_filter, None, true, Some(writer)));
            Ok((guard, Some(handle)))
        } else {
            self.add_layer(format.apply(file_filter, None, true, Some(writer)));
            Ok((guard, None))
        }
    }

    pub(crate) fn samply(&mut self, config: LayerInfo) -> eyre::Result<()> {
        self.add_layer(
            tracing_samply::SamplyLayer::new()
                .map_err(|e| eyre::eyre!("Failed to create samply layer: {e}"))?
                .with_filter(build_env_filter(
                    Some(config.default_directive.parse()?),
                    &config.filters,
                )?),
        );
        Ok(())
    }

    pub(crate) fn chrome(
        &mut self,
        config: LayerInfo,
        file: &Path,
    ) -> eyre::Result<tracing_chrome::FlushGuard> {
        let writer = File::create(file)
            .map_err(|err| eyre::eyre!("Failed to create Chrome trace file {file:?}: {err}"))?;
        let (layer, guard) = tracing_chrome::ChromeLayerBuilder::new()
            .writer(writer)
            .include_args(true)
            .include_locations(false)
            .build();
        self.add_layer(layer.with_filter(build_env_filter(
            Some(config.default_directive.parse()?),
            &config.filters,
        )?));
        Ok(guard)
    }

    #[cfg(feature = "tracy")]
    pub(crate) fn tracy(&mut self, config: LayerInfo) -> eyre::Result<()> {
        // Newtype wrapper around `DefaultFields` so that `FormattedFields<TracyFields>` uses a
        // distinct extension key from the fmt layer's `FormattedFields<DefaultFields>`. Without
        // this, when both layers are active the fmt layer may insert ANSI-colored fields first,
        // and the Tracy layer reuses them — leaking escape codes into Tracy zone text.
        struct TracyFields(tracing_subscriber::fmt::format::DefaultFields);
        impl<'writer> tracing_subscriber::fmt::FormatFields<'writer> for TracyFields {
            fn format_fields<R: tracing_subscriber::field::RecordFields>(
                &self,
                writer: tracing_subscriber::fmt::format::Writer<'writer>,
                fields: R,
            ) -> core::fmt::Result {
                self.0.format_fields(writer, fields)
            }
        }

        struct Config(TracyFields);
        impl tracing_tracy::Config for Config {
            type Formatter = TracyFields;
            fn formatter(&self) -> &Self::Formatter {
                &self.0
            }
            fn format_fields_in_zone_name(&self) -> bool {
                false
            }
        }

        self.add_layer(
            tracing_tracy::TracyLayer::new(Config(TracyFields(Default::default()))).with_filter(
                build_env_filter(Some(config.default_directive.parse()?), &config.filters)?,
            ),
        );
        Ok(())
    }

    /// Add OTLP spans layer to the layer collection
    #[cfg(feature = "otlp")]
    pub fn with_span_layer(
        &mut self,
        otlp_config: OtlpConfig,
        filter: EnvFilter,
    ) -> eyre::Result<()> {
        // Create the span provider

        let span_layer = span_layer(otlp_config)
            .map_err(|e| eyre::eyre!("Failed to build OTLP span exporter {}", e))?
            .with_filter(filter);

        self.add_layer(span_layer);

        Ok(())
    }

    /// Add OTLP logs layer to the layer collection
    #[cfg(feature = "otlp-logs")]
    pub fn with_log_layer(
        &mut self,
        otlp_config: OtlpLogsConfig,
        filter: EnvFilter,
    ) -> eyre::Result<()> {
        let log_layer = log_layer(otlp_config)
            .map_err(|e| eyre::eyre!("Failed to build OTLP log exporter {}", e))?
            .with_filter(filter);

        self.add_layer(log_layer);

        Ok(())
    }
}

/// Holds configuration information for file logging.
///
/// Contains details about the log file's path, name, size, and rotation strategy.
#[derive(Debug, Clone)]
pub struct FileInfo {
    dir: PathBuf,
    file_name: String,
    max_size_bytes: u64,
    max_files: usize,
}

impl FileInfo {
    /// Creates a new `FileInfo` instance.
    pub const fn new(
        dir: PathBuf,
        file_name: String,
        max_size_bytes: u64,
        max_files: usize,
    ) -> Self {
        Self { dir, file_name, max_size_bytes, max_files }
    }

    /// Creates the log directory if it doesn't exist.
    fn create_log_dir(&self) -> eyre::Result<&Path> {
        let log_dir: &Path = self.dir.as_ref();
        if !log_dir.exists() {
            std::fs::create_dir_all(log_dir)
                .map_err(|err| eyre::eyre!("Could not create log directory {log_dir:?}: {err}"))?;
        }
        Ok(log_dir)
    }

    /// Creates a non-blocking writer for the log file.
    fn create_log_writer(
        &self,
    ) -> eyre::Result<(tracing_appender::non_blocking::NonBlocking, WorkerGuard)> {
        let log_dir = self.create_log_dir()?;
        let (writer, guard) = tracing_appender::non_blocking(
            RollingFileAppender::new(
                log_dir.join(&self.file_name),
                RollingConditionBasic::new().max_size(self.max_size_bytes),
                self.max_files,
            )
            .map_err(|err| eyre::eyre!("Could not initialize file logging: {err}"))?,
        );
        Ok((writer, guard))
    }
}

/// Builds an environment filter for logging.
///
/// The events are filtered by `default_directive`, unless overridden by `RUST_LOG`.
///
/// # Arguments
/// * `default_directive` - An optional `Directive` that sets the default directive.
/// * `directives` - Additional directives as a comma-separated string.
///
/// # Returns
/// An `eyre::Result<EnvFilter>` that can be used to configure a tracing subscriber.
fn build_env_filter(
    default_directive: Option<Directive>,
    directives: &str,
) -> eyre::Result<EnvFilter> {
    let env_filter = if let Some(default_directive) = default_directive {
        EnvFilter::builder().with_default_directive(default_directive).from_env_lossy()
    } else {
        EnvFilter::builder().from_env_lossy()
    };

    DEFAULT_ENV_FILTER_DIRECTIVES
        .into_iter()
        .chain(directives.split(',').filter(|d| !d.is_empty()))
        .try_fold(env_filter, |env_filter, directive| {
            Ok(env_filter.add_directive(directive.parse()?))
        })
}
