use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_core::Subscriber;
use tracing_subscriber::{
    field::RecordFields,
    fmt::{
        self,
        format::{DefaultFields, Writer},
        time::FormatTime,
        FormatEvent, FormatFields, FormattedFields,
    },
    layer::SubscriberExt,
    registry::LookupSpan,
    util::SubscriberInitExt,
    EnvFilter, Layer,
};

use crate::tracing::LogLevel;

pub fn log_panics() {
    std::env::set_var("RUST_BACKTRACE", "1");

    log_panics::init();
}

fn text_layers<S>(config: TracingConfiguration) -> impl Layer<S>
where
    S: Subscriber + for<'a> LookupSpan<'a>,
{
    // Adjusted version of tracing_subscriber::fmt::Format
    struct EventFormatter {
        display_timestamp: bool,
        display_level: bool,
    }

    impl EventFormatter {
        fn new() -> Self {
            Self { display_timestamp: true, display_level: true }
        }

        #[cfg(target_os = "android")]
        fn for_logcat() -> Self {
            // Level and time are already captured by logcat separately
            Self { display_timestamp: false, display_level: false }
        }

        fn format_timestamp(&self, writer: &mut fmt::format::Writer<'_>) -> std::fmt::Result {
            if fmt::time::SystemTime.format_time(writer).is_err() {
                writer.write_str("<unknown time>")?;
            }
            Ok(())
        }

        fn write_filename(
            &self,
            writer: &mut fmt::format::Writer<'_>,
            filename: &str,
        ) -> std::fmt::Result {
            const CRATES_IO_PATH_MATCHER: &str = ".cargo/registry/src/index.crates.io";
            let crates_io_filename = filename
                .split_once(CRATES_IO_PATH_MATCHER)
                .and_then(|(_, rest)| rest.split_once('/').map(|(_, rest)| rest));

            if let Some(filename) = crates_io_filename {
                writer.write_str("<crates.io>/")?;
                writer.write_str(filename)
            } else {
                writer.write_str(filename)
            }
        }
    }

    impl<S, N> FormatEvent<S, N> for EventFormatter
    where
        S: Subscriber + for<'a> LookupSpan<'a>,
        N: for<'a> FormatFields<'a> + 'static,
    {
        fn format_event(
            &self,
            ctx: &fmt::FmtContext<'_, S, N>,
            mut writer: fmt::format::Writer<'_>,
            event: &tracing_core::Event<'_>,
        ) -> std::fmt::Result {
            let meta = event.metadata();

            if self.display_timestamp {
                self.format_timestamp(&mut writer)?;
                writer.write_char(' ')?;
            }

            if self.display_level {
                // For info and warn, add a padding space to the left
                write!(writer, "{:>5} ", meta.level())?;
            }

            write!(writer, "{}: ", meta.target())?;

            ctx.format_fields(writer.by_ref(), event)?;

            if let Some(filename) = meta.file() {
                writer.write_str(" | ")?;
                self.write_filename(&mut writer, filename)?;
                if let Some(line_number) = meta.line() {
                    write!(writer, ":{line_number}")?;
                }
            }

            if let Some(scope) = ctx.event_scope() {
                writer.write_str(" | spans: ")?;

                let mut first = true;

                for span in scope.from_root() {
                    if !first {
                        writer.write_str(" > ")?;
                    }

                    first = false;

                    write!(writer, "{}", span.name())?;

                    if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
                        if !fields.is_empty() {
                            write!(writer, "{{{fields}}}")?;
                        }
                    }
                }
            }

            writeln!(writer)
        }
    }

    let file_layer = config.write_to_files.map(|c| {
        let mut builder = RollingFileAppender::builder()
            .rotation(Rotation::HOURLY)
            .filename_prefix(&c.file_prefix);

        if let Some(max_files) = c.max_files {
            builder = builder.max_log_files(max_files as usize)
        };
        if let Some(file_suffix) = c.file_suffix {
            builder = builder.filename_suffix(file_suffix)
        }

        let writer = builder.build(&c.path).expect("Failed to create a rolling file appender.");

        // Another fields formatter is necessary because of this bug
        // https://github.com/tokio-rs/tracing/issues/1372. Using a new
        // formatter for the fields forces to record them in different span
        // extensions, and thus remove the duplicated fields in the span.
        #[derive(Default)]
        struct FieldsFormatterForFiles(DefaultFields);

        impl<'writer> FormatFields<'writer> for FieldsFormatterForFiles {
            fn format_fields<R: RecordFields>(
                &self,
                writer: Writer<'writer>,
                fields: R,
            ) -> std::fmt::Result {
                self.0.format_fields(writer, fields)
            }
        }

        fmt::layer()
            .fmt_fields(FieldsFormatterForFiles::default())
            .event_format(EventFormatter::new())
            // EventFormatter doesn't support ANSI colors anyways, but the
            // default field formatter does, which is unhelpful for iOS +
            // Android logs, but enabled by default.
            .with_ansi(false)
            .with_writer(writer)
    });

    Layer::and_then(
        file_layer,
        config.write_to_stdout_or_system.then(|| {
            // Another fields formatter is necessary because of this bug
            // https://github.com/tokio-rs/tracing/issues/1372. Using a new
            // formatter for the fields forces to record them in different span
            // extensions, and thus remove the duplicated fields in the span.
            #[derive(Default)]
            struct FieldsFormatterFormStdoutOrSystem(DefaultFields);

            impl<'writer> FormatFields<'writer> for FieldsFormatterFormStdoutOrSystem {
                fn format_fields<R: RecordFields>(
                    &self,
                    writer: Writer<'writer>,
                    fields: R,
                ) -> std::fmt::Result {
                    self.0.format_fields(writer, fields)
                }
            }

            #[cfg(not(target_os = "android"))]
            return fmt::layer()
                .fmt_fields(FieldsFormatterFormStdoutOrSystem::default())
                .event_format(EventFormatter::new())
                // See comment above.
                .with_ansi(false)
                .with_writer(std::io::stderr);

            #[cfg(target_os = "android")]
            return fmt::layer()
                .fmt_fields(FieldsFormatterFormStdoutOrSystem::default())
                .event_format(EventFormatter::for_logcat())
                // See comment above.
                .with_ansi(false)
                .with_writer(paranoid_android::AndroidLogMakeWriter::new(
                    "org.matrix.rust.sdk".to_owned(),
                ));
        }),
    )
}

/// Configuration to save logs to (rotated) log-files.
#[derive(uniffi::Record)]
pub struct TracingFileConfiguration {
    /// Base location for all the log files.
    path: String,

    /// Prefix for the log files' names.
    file_prefix: String,

    /// Optional suffix for the log file's names.
    file_suffix: Option<String>,

    /// Maximum number of rotated files.
    ///
    /// If not set, there's no max limit, i.e. the number of log files is
    /// unlimited.
    max_files: Option<u64>,
}

#[derive(PartialEq, PartialOrd)]
enum LogTarget {
    Hyper,
    MatrixSdkFfi,
    MatrixSdk,
    MatrixSdkClient,
    MatrixSdkCrypto,
    MatrixSdkCryptoAccount,
    MatrixSdkOidc,
    MatrixSdkHttpClient,
    MatrixSdkSlidingSync,
    MatrixSdkBaseSlidingSync,
    MatrixSdkUiTimeline,
    MatrixSdkEventCache,
    MatrixSdkBaseEventCache,
    MatrixSdkEventCacheStore,
}

impl LogTarget {
    fn as_str(&self) -> &'static str {
        match self {
            LogTarget::Hyper => "hyper",
            LogTarget::MatrixSdkFfi => "matrix_sdk_ffi",
            LogTarget::MatrixSdk => "matrix_sdk",
            LogTarget::MatrixSdkClient => "matrix_sdk::client",
            LogTarget::MatrixSdkCrypto => "matrix_sdk_crypto",
            LogTarget::MatrixSdkCryptoAccount => "matrix_sdk_crypto::olm::account",
            LogTarget::MatrixSdkOidc => "matrix_sdk::oidc",
            LogTarget::MatrixSdkHttpClient => "matrix_sdk::http_client",
            LogTarget::MatrixSdkSlidingSync => "matrix_sdk::sliding_sync",
            LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
            LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
            LogTarget::MatrixSdkEventCache => "matrix_sdk::event_cache",
            LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
            LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
        }
    }
}

const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
    (LogTarget::Hyper, LogLevel::Warn),
    (LogTarget::MatrixSdkFfi, LogLevel::Info),
    (LogTarget::MatrixSdk, LogLevel::Info),
    (LogTarget::MatrixSdkClient, LogLevel::Trace),
    (LogTarget::MatrixSdkCrypto, LogLevel::Debug),
    (LogTarget::MatrixSdkCryptoAccount, LogLevel::Trace),
    (LogTarget::MatrixSdkOidc, LogLevel::Trace),
    (LogTarget::MatrixSdkHttpClient, LogLevel::Debug),
    (LogTarget::MatrixSdkSlidingSync, LogLevel::Info),
    (LogTarget::MatrixSdkBaseSlidingSync, LogLevel::Info),
    (LogTarget::MatrixSdkUiTimeline, LogLevel::Info),
    (LogTarget::MatrixSdkEventCache, LogLevel::Info),
    (LogTarget::MatrixSdkBaseEventCache, LogLevel::Info),
    (LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
];

const IMMUTABLE_TARGET_LOG_LEVELS: &[LogTarget] = &[
    LogTarget::Hyper,        // Too verbose
    LogTarget::MatrixSdk,    // Too generic
    LogTarget::MatrixSdkFfi, // Too verbose
];

#[derive(uniffi::Record)]
pub struct TracingConfiguration {
    /// The desired log level
    log_level: LogLevel,

    /// Additional targets that the FFI client would like to use e.g.
    /// the target names for created [`crate::tracing::Span`]
    extra_targets: Option<Vec<String>>,

    /// Whether to log to stdout, or in the logcat on Android.
    write_to_stdout_or_system: bool,

    /// If set, configures rotated log files where to write additional logs.
    write_to_files: Option<TracingFileConfiguration>,
}

fn build_tracing_filter(config: &TracingConfiguration) -> String {
    // We are intentionally not setting a global log level because we don't want to
    // risk third party crates logging sensitive information.
    // As such we need to make sure that panics will be properly logged.
    // On 2025-01-08, `log_panics` uses the `panic` target, at the error log level.
    let mut filters = vec!["panic=error".to_owned()];

    DEFAULT_TARGET_LOG_LEVELS.iter().for_each(|(target, level)| {
        // Use the default if the log level shouldn't be changed for this target or
        // if it's already logging more than requested
        let level = if IMMUTABLE_TARGET_LOG_LEVELS.contains(target) || level > &config.log_level {
            level.as_str()
        } else {
            config.log_level.as_str()
        };

        filters.push(format!("{}={}", target.as_str(), level));
    });

    // Finally append the extra targets requested by the client
    if let Some(extra_targets) = &config.extra_targets {
        for target in extra_targets {
            filters.push(format!("{}={}", target, config.log_level.as_str()));
        }
    }

    filters.join(",")
}

#[matrix_sdk_ffi_macros::export]
pub fn setup_tracing(config: TracingConfiguration) {
    log_panics();

    tracing_subscriber::registry()
        .with(EnvFilter::new(build_tracing_filter(&config)))
        .with(text_layers(config))
        .init();
}

#[cfg(test)]
mod tests {
    use super::build_tracing_filter;

    #[test]
    fn test_default_tracing_filter() {
        let config = super::TracingConfiguration {
            log_level: super::LogLevel::Error,
            extra_targets: Some(vec!["super_duper_app".to_owned()]),
            write_to_stdout_or_system: true,
            write_to_files: None,
        };

        let filter = build_tracing_filter(&config);

        assert_eq!(
            filter,
            "panic=error,\
            hyper=warn,\
            matrix_sdk_ffi=info,\
            matrix_sdk=info,\
            matrix_sdk::client=trace,\
            matrix_sdk_crypto=debug,\
            matrix_sdk_crypto::olm::account=trace,\
            matrix_sdk::oidc=trace,\
            matrix_sdk::http_client=debug,\
            matrix_sdk::sliding_sync=info,\
            matrix_sdk_base::sliding_sync=info,\
            matrix_sdk_ui::timeline=info,\
            matrix_sdk::event_cache=info,\
            matrix_sdk_base::event_cache=info,\
            matrix_sdk_sqlite::event_cache_store=info,\
            super_duper_app=error"
        );
    }

    #[test]
    fn test_trace_tracing_filter() {
        let config = super::TracingConfiguration {
            log_level: super::LogLevel::Trace,
            extra_targets: Some(vec!["super_duper_app".to_owned(), "some_other_span".to_owned()]),
            write_to_stdout_or_system: true,
            write_to_files: None,
        };

        let filter = build_tracing_filter(&config);

        assert_eq!(
            filter,
            "panic=error,\
            hyper=warn,\
            matrix_sdk_ffi=info,\
            matrix_sdk=info,\
            matrix_sdk::client=trace,\
            matrix_sdk_crypto=trace,\
            matrix_sdk_crypto::olm::account=trace,\
            matrix_sdk::oidc=trace,\
            matrix_sdk::http_client=trace,\
            matrix_sdk::sliding_sync=trace,\
            matrix_sdk_base::sliding_sync=trace,\
            matrix_sdk_ui::timeline=trace,\
            matrix_sdk::event_cache=trace,\
            matrix_sdk_base::event_cache=trace,\
            matrix_sdk_sqlite::event_cache_store=trace,\
            super_duper_app=trace,\
            some_other_span=trace"
        );
    }
}
