// file: kb_lib/src/tracing.rs

//! Tracing initialization helpers for `kb_lib`.
//!
//! This module initializes `tracing_subscriber` using the shared JSON
//! configuration loaded by `kb_lib::Config`.
//!
//! The implementation intentionally avoids dynamic boxed layers because they
//! become awkward once additional layers are stacked on the registry.
//! Instead, it follows an explicit branch-per-output strategy similar to the
//! reference implementation you provided.

use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;

/// Guard keeping non-blocking tracing workers alive.
///
/// The guard must remain alive during the whole application lifetime so that
/// buffered log records are flushed correctly.
#[derive(Debug)]
pub struct TracingGuard {
    guards: std::vec::Vec<tracing_appender::non_blocking::WorkerGuard>,
}

impl TracingGuard {
    /// Returns the number of active tracing worker guards.
    pub fn guard_count(&self) -> usize {
        return self.guards.len();
    }
}

/// Initializes the global tracing subscriber.
///
/// This function is expected to be called once at application startup.
pub fn init_tracing(config: &crate::LoggingConfig) -> Result<TracingGuard, crate::Error> {
    let filter_expression = build_filter_expression(config);
    let timer_format = resolve_time_format(&config.time_format);

    match (config.console_enabled, config.file_enabled) {
        (true, false) => {
            let (stdout_non_blocking, stdout_guard) =
                tracing_appender::non_blocking(std::io::stdout());
            let init_result = init_single_layer_subscriber(
                &filter_expression,
                &timer_format,
                &config.message_format,
                config.console_ansi,
                stdout_non_blocking,
            );
            match init_result {
                Ok(()) => return Ok(TracingGuard { guards: vec![stdout_guard] }),
                Err(error) => return Err(error),
            }
        },
        (false, true) => {
            let file_writer_result = build_file_writer(config);
            let (file_non_blocking, file_guard) = match file_writer_result {
                Ok(parts) => parts,
                Err(error) => return Err(error),
            };
            let init_result = init_single_layer_subscriber(
                &filter_expression,
                &timer_format,
                &config.message_format,
                false,
                file_non_blocking,
            );
            match init_result {
                Ok(()) => return Ok(TracingGuard { guards: vec![file_guard] }),
                Err(error) => return Err(error),
            }
        },
        (true, true) => {
            let (stdout_non_blocking, stdout_guard) =
                tracing_appender::non_blocking(std::io::stdout());

            let file_writer_result = build_file_writer(config);
            let (file_non_blocking, file_guard) = match file_writer_result {
                Ok(parts) => parts,
                Err(error) => return Err(error),
            };
            let init_result = init_dual_layer_subscriber(
                &filter_expression,
                &timer_format,
                &config.message_format,
                config.console_ansi,
                stdout_non_blocking,
                file_non_blocking,
            );
            match init_result {
                Ok(()) => return Ok(TracingGuard { guards: vec![stdout_guard, file_guard] }),
                Err(error) => return Err(error),
            }
        },
        (false, false) => {
            let env_filter_result = build_env_filter(&filter_expression);
            let env_filter = match env_filter_result {
                Ok(env_filter) => env_filter,
                Err(error) => return Err(error),
            };
            let init_result = tracing_subscriber::registry().with(env_filter).try_init();
            match init_result {
                Ok(()) => return Ok(TracingGuard { guards: vec![] }),
                Err(error) => {
                    return Err(crate::Error::Tracing(format!(
                        "cannot initialize tracing subscriber: {error}"
                    )));
                },
            }
        },
    }
}

fn build_filter_expression(config: &crate::LoggingConfig) -> std::string::String {
    let mut directives = std::vec::Vec::<std::string::String>::new();
    directives.push(config.level.clone());
    for (target, level) in &config.target_filters {
        directives.push(format!("{target}={level}"));
    }
    return directives.join(",");
}

fn build_env_filter(
    filter_expression: &str,
) -> Result<tracing_subscriber::EnvFilter, crate::Error> {
    let from_env_result = tracing_subscriber::EnvFilter::try_from_default_env();
    match from_env_result {
        Ok(env_filter) => return Ok(env_filter),
        Err(_) => {
            let new_result = tracing_subscriber::EnvFilter::try_new(filter_expression);
            match new_result {
                Ok(env_filter) => return Ok(env_filter),
                Err(error) => {
                    return Err(crate::Error::Tracing(format!(
                        "cannot build env filter from '{filter_expression}': {error}"
                    )));
                },
            }
        },
    }
}

fn resolve_time_format(value: &str) -> std::string::String {
    if value == "rfc3339" {
        return "%+".to_string();
    }
    if value == "rfc3339_millis" {
        return "%Y-%m-%dT%H:%M:%S%.3f%:z".to_string();
    }
    if value == "none" {
        return "%+".to_string();
    }
    return "%+".to_string();
}

fn build_file_writer(
    config: &crate::LoggingConfig,
) -> Result<
    (
        StripAnsiMakeWriter<tracing_appender::non_blocking::NonBlocking>,
        tracing_appender::non_blocking::WorkerGuard,
    ),
    crate::Error,
> {
    let directory_path = config.directory_path();
    let create_dir_result = std::fs::create_dir_all(&directory_path);
    if let Err(error) = create_dir_result {
        return Err(crate::Error::Tracing(format!(
            "cannot create log directory '{}': {error}",
            directory_path.display()
        )));
    }
    let rotation = if config.rotation == "hourly" {
        tracing_appender::rolling::Rotation::HOURLY
    } else if config.rotation == "never" {
        tracing_appender::rolling::Rotation::NEVER
    } else {
        tracing_appender::rolling::Rotation::DAILY
    };
    let appender_result = tracing_appender::rolling::Builder::default()
        .rotation(rotation)
        .filename_prefix(config.file_prefix.clone())
        .filename_suffix("log")
        .build(&directory_path);
    let file_appender = match appender_result {
        Ok(file_appender) => file_appender,
        Err(error) => {
            return Err(crate::Error::Tracing(format!(
                "cannot build rolling file appender in '{}': {error}",
                directory_path.display()
            )));
        },
    };
    let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
    return Ok((StripAnsiMakeWriter::new(non_blocking), guard));
}

fn init_single_layer_subscriber<W>(
    filter_expression: &str,
    timer_format: &str,
    message_format: &str,
    ansi: bool,
    writer: W,
) -> Result<(), crate::Error>
where
    W: for<'writer> tracing_subscriber::fmt::MakeWriter<'writer>
        + std::marker::Send
        + std::marker::Sync
        + 'static,
{
    let env_filter_result = build_env_filter(filter_expression);
    let env_filter = match env_filter_result {
        Ok(env_filter) => env_filter,
        Err(error) => return Err(error),
    };
    let timer = tracing_subscriber::fmt::time::ChronoLocal::new(timer_format.to_string());
    let init_result = if message_format == "compact" {
        tracing_subscriber::registry()
            .with(env_filter)
            .with(
                tracing_subscriber::fmt::layer()
                    .compact()
                    .with_timer(timer)
                    .with_ansi(ansi)
                    .with_target(true)
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .with_writer(writer),
            )
            .try_init()
    } else if message_format == "pretty" {
        tracing_subscriber::registry()
            .with(env_filter)
            .with(
                tracing_subscriber::fmt::layer()
                    .pretty()
                    .with_timer(timer)
                    .with_ansi(ansi)
                    .with_target(true)
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .with_writer(writer),
            )
            .try_init()
    } else if message_format == "json" {
        tracing_subscriber::registry()
            .with(env_filter)
            .with(
                tracing_subscriber::fmt::layer()
                    .json()
                    .with_timer(timer)
                    .with_ansi(false)
                    .with_target(true)
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .with_writer(writer),
            )
            .try_init()
    } else {
        tracing_subscriber::registry()
            .with(env_filter)
            .with(
                tracing_subscriber::fmt::layer()
                    .with_timer(timer)
                    .with_ansi(ansi)
                    .with_target(true)
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .with_writer(writer),
            )
            .try_init()
    };
    match init_result {
        Ok(()) => return Ok(()),
        Err(error) => {
            return Err(crate::Error::Tracing(format!(
                "cannot initialize tracing subscriber: {error}"
            )));
        },
    }
}

fn init_dual_layer_subscriber<W1, W2>(
    filter_expression: &str,
    timer_format: &str,
    message_format: &str,
    console_ansi: bool,
    stdout_writer: W1,
    file_writer: W2,
) -> Result<(), crate::Error>
where
    W1: for<'writer> tracing_subscriber::fmt::MakeWriter<'writer>
        + std::marker::Send
        + std::marker::Sync
        + 'static,
    W2: for<'writer> tracing_subscriber::fmt::MakeWriter<'writer>
        + std::marker::Send
        + std::marker::Sync
        + 'static,
{
    let env_filter_result = build_env_filter(filter_expression);
    let env_filter = match env_filter_result {
        Ok(env_filter) => env_filter,
        Err(error) => return Err(error),
    };
    let timer_stdout = tracing_subscriber::fmt::time::ChronoLocal::new(timer_format.to_string());
    let timer_file = tracing_subscriber::fmt::time::ChronoLocal::new(timer_format.to_string());
    let init_result = if message_format == "compact" {
        tracing_subscriber::registry()
            .with(env_filter)
            .with(
                tracing_subscriber::fmt::layer()
                    .compact()
                    .with_timer(timer_stdout)
                    .with_ansi(console_ansi)
                    .with_target(true)
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .with_writer(stdout_writer),
            )
            .with(
                tracing_subscriber::fmt::layer()
                    .compact()
                    .with_timer(timer_file)
                    .with_ansi(false)
                    .with_target(true)
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .with_writer(file_writer),
            )
            .try_init()
    } else if message_format == "pretty" {
        tracing_subscriber::registry()
            .with(env_filter)
            .with(
                tracing_subscriber::fmt::layer()
                    .pretty()
                    .with_timer(timer_stdout)
                    .with_ansi(console_ansi)
                    .with_target(true)
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .with_writer(stdout_writer),
            )
            .with(
                tracing_subscriber::fmt::layer()
                    .pretty()
                    .with_timer(timer_file)
                    .with_ansi(false)
                    .with_target(true)
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .with_writer(file_writer),
            )
            .try_init()
    } else if message_format == "json" {
        tracing_subscriber::registry()
            .with(env_filter)
            .with(
                tracing_subscriber::fmt::layer()
                    .json()
                    .with_timer(timer_stdout)
                    .with_ansi(false)
                    .with_target(true)
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .with_writer(stdout_writer),
            )
            .with(
                tracing_subscriber::fmt::layer()
                    .json()
                    .with_timer(timer_file)
                    .with_ansi(false)
                    .with_target(true)
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .with_writer(file_writer),
            )
            .try_init()
    } else {
        tracing_subscriber::registry()
            .with(env_filter)
            .with(
                tracing_subscriber::fmt::layer()
                    .with_timer(timer_stdout)
                    .with_ansi(console_ansi)
                    .with_target(true)
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .with_writer(stdout_writer),
            )
            .with(
                tracing_subscriber::fmt::layer()
                    .with_timer(timer_file)
                    .with_ansi(false)
                    .with_target(true)
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .with_writer(file_writer),
            )
            .try_init()
    };
    match init_result {
        Ok(()) => return Ok(()),
        Err(error) => {
            return Err(crate::Error::Tracing(format!(
                "cannot initialize tracing subscriber: {error}"
            )));
        },
    }
}

struct StripAnsiMakeWriter<W> {
    inner: W,
}

impl<W> StripAnsiMakeWriter<W> {
    fn new(inner: W) -> Self {
        return Self { inner };
    }
}

struct StripAnsiWriter<W> {
    inner: W,
}

impl<'a, W> tracing_subscriber::fmt::MakeWriter<'a> for StripAnsiMakeWriter<W>
where
    W: tracing_subscriber::fmt::MakeWriter<'a>,
{
    type Writer = StripAnsiWriter<W::Writer>;

    fn make_writer(&'a self) -> Self::Writer {
        return StripAnsiWriter { inner: self.inner.make_writer() };
    }
}

impl<W> std::io::Write for StripAnsiWriter<W>
where
    W: std::io::Write,
{
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        let stripped = strip_ansi_bytes(buf);
        let write_result = self.inner.write_all(&stripped);
        match write_result {
            Ok(()) => return Ok(buf.len()),
            Err(error) => return Err(error),
        }
    }

    fn flush(&mut self) -> std::io::Result<()> {
        return self.inner.flush();
    }
}

fn strip_ansi_bytes(input: &[u8]) -> std::vec::Vec<u8> {
    let mut output = std::vec::Vec::with_capacity(input.len());
    let mut index = 0usize;
    while index < input.len() {
        let byte = input[index];
        if byte == 0x1B && index + 1 < input.len() && input[index + 1] == b'[' {
            index += 2;
            while index < input.len() {
                let current = input[index];
                if (0x40..=0x7E).contains(&current) {
                    index += 1;
                    break;
                }
                index += 1;
            }
            continue;
        }
        output.push(byte);
        index += 1;
    }
    return output;
}

#[cfg(test)]
mod tests {
    #[test]
    fn resolve_time_format_maps_rfc3339() {
        let value = super::resolve_time_format("rfc3339");
        assert_eq!(value, "%+");
    }

    #[test]
    fn resolve_time_format_maps_rfc3339_millis() {
        let value = super::resolve_time_format("rfc3339_millis");
        assert_eq!(value, "%Y-%m-%dT%H:%M:%S%.3f%:z");
    }

    #[test]
    fn build_filter_expression_includes_default_level() {
        let config = crate::LoggingConfig {
            level: "debug".to_string(),
            console_enabled: true,
            console_ansi: true,
            file_enabled: false,
            directory: "./logs".to_string(),
            file_prefix: "app".to_string(),
            rotation: "daily".to_string(),
            message_format: "compact".to_string(),
            time_format: "rfc3339_millis".to_string(),
            target_filters: std::collections::BTreeMap::new(),
        };
        let value = super::build_filter_expression(&config);
        assert_eq!(value, "debug");
    }

    #[test]
    fn build_filter_expression_includes_target_overrides() {
        let mut target_filters = std::collections::BTreeMap::new();
        target_filters.insert("hyper".to_string(), "warn".to_string());
        target_filters.insert("reqwest".to_string(), "error".to_string());
        let config = crate::LoggingConfig {
            level: "info".to_string(),
            console_enabled: true,
            console_ansi: true,
            file_enabled: true,
            directory: "./logs".to_string(),
            file_prefix: "app".to_string(),
            rotation: "daily".to_string(),
            message_format: "compact".to_string(),
            time_format: "rfc3339".to_string(),
            target_filters,
        };
        let value = super::build_filter_expression(&config);
        assert!(value.contains("info"));
        assert!(value.contains("hyper=warn"));
        assert!(value.contains("reqwest=error"));
    }
}
