//! Logging subsystem
//!
//! Structured logging via tracing with support for JSON (production) and
//! plaintext (development) output formats.
//!
//! # Log Targets
//!
//! Use these consistent target names across the codebase:
//! - `gateway` - main gateway operations
//! - `ws` - WebSocket server
//! - `http` - HTTP server
//! - `plugins` - plugin system
//! - `auth` - authentication
//! - `config` - configuration loading
//!
//! # Environment Variables
//!
//! - `CARAPACE_LOG` - Primary log level/filter (takes precedence)
//! - `RUST_LOG` - Fallback log level/filter
//!
//! # Examples
//!
//! ```no_run
//! use carapace::logging::{init_logging, LogConfig, LogFormat, LogOutput};
//!
//! // Development setup (plaintext to stdout)
//! init_logging(LogConfig::development()).unwrap();
//!
//! // Production setup (JSON to stdout)
//! init_logging(LogConfig::production()).unwrap();
//!
//! // Custom setup with file output
//! init_logging(LogConfig {
//!     format: LogFormat::Json,
//!     output: LogOutput::File("/var/log/carapace.log".into()),
//!     default_level: tracing::Level::INFO,
//! }).unwrap();
//! ```

pub mod audit;
pub mod buffer;
pub mod redact;
pub mod throttle;

use std::fs::File;
use std::io;
use std::path::PathBuf;
use std::sync::OnceLock;

use crate::logging::redact::RedactingMakeWriter;
use tracing::Level;
use tracing_subscriber::fmt::time::UtcTime;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, Layer};

/// Guard to track if logging has been initialized
static INIT_GUARD: OnceLock<()> = OnceLock::new();

/// Log output format
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LogFormat {
    /// JSON format for production (structured logs)
    Json,
    /// Human-readable plaintext for development
    #[default]
    Plaintext,
}

/// Log output destination
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum LogOutput {
    /// Write to stdout
    #[default]
    Stdout,
    /// Write to stderr
    Stderr,
    /// Write to a file at the given path
    File(PathBuf),
}

/// Configuration for the logging subsystem
#[derive(Debug, Clone)]
pub struct LogConfig {
    /// Output format (JSON or plaintext)
    pub format: LogFormat,
    /// Output destination (stdout, stderr, or file)
    pub output: LogOutput,
    /// Default log level when no env filter is set
    pub default_level: Level,
}

impl Default for LogConfig {
    fn default() -> Self {
        Self {
            format: LogFormat::Plaintext,
            output: LogOutput::Stdout,
            default_level: Level::INFO,
        }
    }
}

impl LogConfig {
    /// Create a development configuration (plaintext to stdout, debug level)
    pub fn development() -> Self {
        Self {
            format: LogFormat::Plaintext,
            output: LogOutput::Stdout,
            default_level: Level::DEBUG,
        }
    }

    /// Create a production configuration (JSON to stdout, info level)
    pub fn production() -> Self {
        Self {
            format: LogFormat::Json,
            output: LogOutput::Stdout,
            default_level: Level::INFO,
        }
    }
}

/// Error type for logging initialization
#[derive(Debug, thiserror::Error)]
pub enum LoggingError {
    #[error("failed to create log file: {0}")]
    FileCreation(#[from] io::Error),
    #[error("failed to parse log filter: {0}")]
    FilterParse(#[from] tracing_subscriber::filter::ParseError),
    #[error("logging already initialized")]
    AlreadyInitialized,
    #[error("failed to set global subscriber: {0}")]
    SetGlobalDefault(#[from] tracing::subscriber::SetGlobalDefaultError),
    #[error("failed to initialize subscriber: {0}")]
    TryInit(#[from] tracing_subscriber::util::TryInitError),
}

/// Build an EnvFilter from environment variables or default level.
///
/// Checks CARAPACE_LOG first, then RUST_LOG, falling back to the default level.
fn build_env_filter(default_level: Level) -> Result<EnvFilter, LoggingError> {
    // Check CARAPACE_LOG first, then RUST_LOG
    if let Some(filter) = crate::config::read_process_env("CARAPACE_LOG") {
        return Ok(EnvFilter::try_new(filter)?);
    }
    if let Some(filter) = crate::config::read_process_env("RUST_LOG") {
        return Ok(EnvFilter::try_new(filter)?);
    }

    // Default filter with standard targets
    let default_filter = format!(
        "{level},gateway={level},ws={level},http={level},plugins={level},auth={level},config={level}",
        level = default_level.as_str().to_lowercase()
    );
    Ok(EnvFilter::try_new(default_filter)?)
}

/// Open the configured log-output file with O_NOFOLLOW + O_NONBLOCK
/// + 0o600 + post-open `is_file()` refusal.
///
/// SECURITY: bare `File::create(path)` followed symlinks and used
/// the default umask (typically 0o644). On hosts where the
/// operator-chosen log path sits in a directory another local user
/// can write, a same-uid attacker can pre-plant either:
/// - a symlink at the log path → daemon's `create+truncate`
///   redirects to the symlink target (O_NOFOLLOW closes this); OR
/// - a FIFO at the log path → daemon's open(2) blocks waiting for
///   a reader, hanging startup before the audit log even
///   initializes (O_NONBLOCK + post-open is_file() closes this).
///
/// O_NOFOLLOW refuses symlinks; O_NONBLOCK makes the open(2)
/// itself non-blocking so a FIFO returns immediately; the post-
/// open metadata check then refuses anything that isn't a regular
/// file. Mode 0o600 keeps log content (which may carry redacted
/// PII or partial tokens via tracing field emission) owner-only.
/// After the regular-file check, `fchmod(file.as_fd(), 0o600)`
/// is also applied so a pre-existing log file at the same path
/// gets mode-ratcheted even on truncate-of-existing (the `mode()`
/// option only applies at file CREATION).
///
/// Note: preserves the existing `O_CREAT | O_WRONLY | O_TRUNC`
/// semantics of `File::create` (every daemon start truncates the
/// file). Operator log retention should rely on external log
/// rotation rather than in-process append.
fn open_log_file_no_follow_owner_only(path: &std::path::Path) -> io::Result<File> {
    let mut options = std::fs::OpenOptions::new();
    options.write(true).create(true).truncate(true);
    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt;
        options
            .mode(0o600)
            .custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
    }
    let file = options.open(path)?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let metadata = file.metadata()?;
        if !metadata.file_type().is_file() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "log path is not a regular file (refusing FIFO/socket/device)",
            ));
        }
        // Mode 0o600 was set on creation; if the dirent already
        // existed (pre-planted at 0o666 by a same-uid attacker
        // before first daemon start), `OpenOptions::mode` was
        // ignored. Re-apply 0o600 via the open fd so the log
        // content is owner-only regardless of pre-existing mode.
        // `set_permissions` on `File` uses fchmod internally on
        // Linux/macOS, which operates on the fd (symlink-safe);
        // log a warn but don't fail if the platform doesn't
        // support chmod-via-fd.
        if let Err(err) = file.set_permissions(std::fs::Permissions::from_mode(0o600)) {
            tracing::warn!(
                path = %path.display(),
                error = %err,
                "failed to fchmod log file to 0o600 after open; \
                 content protection relies on the creation-time mode"
            );
        }
    }
    Ok(file)
}

/// Initialize the logging subsystem with the given configuration.
///
/// This function should be called once at application startup. Subsequent calls
/// will return an error.
///
/// # Errors
///
/// Returns an error if:
/// - Logging has already been initialized
/// - The log file cannot be created (for file output)
/// - The environment filter is invalid
pub fn init_logging(config: LogConfig) -> Result<(), LoggingError> {
    // Prevent double initialization
    if INIT_GUARD.set(()).is_err() {
        return Err(LoggingError::AlreadyInitialized);
    }

    let filter = build_env_filter(config.default_level)?;

    // RFC 3339 timestamp format
    let timer = UtcTime::rfc_3339();

    // Buffer layer captures logs for the logs.tail WebSocket endpoint
    let buffer_layer = buffer::LogBufferLayer::new();

    match (&config.format, &config.output) {
        (LogFormat::Json, LogOutput::Stdout) => {
            let writer = RedactingMakeWriter::new(io::stdout);
            let layer = tracing_subscriber::fmt::layer()
                .json()
                .with_timer(timer)
                .with_target(true)
                .with_current_span(true)
                .with_span_list(true)
                .with_writer(writer)
                .with_filter(filter);

            tracing_subscriber::registry()
                .with(layer)
                .with(buffer_layer)
                .init();
        }
        (LogFormat::Json, LogOutput::Stderr) => {
            let writer = RedactingMakeWriter::new(io::stderr);
            let layer = tracing_subscriber::fmt::layer()
                .json()
                .with_timer(timer)
                .with_target(true)
                .with_current_span(true)
                .with_span_list(true)
                .with_writer(writer)
                .with_filter(filter);

            tracing_subscriber::registry()
                .with(layer)
                .with(buffer_layer)
                .init();
        }
        (LogFormat::Json, LogOutput::File(path)) => {
            let file = open_log_file_no_follow_owner_only(path)?;
            let writer = RedactingMakeWriter::new(file);
            let layer = tracing_subscriber::fmt::layer()
                .json()
                .with_timer(timer)
                .with_target(true)
                .with_current_span(true)
                .with_span_list(true)
                .with_writer(writer)
                .with_filter(filter);

            tracing_subscriber::registry()
                .with(layer)
                .with(buffer_layer)
                .init();
        }
        (LogFormat::Plaintext, LogOutput::Stdout) => {
            let writer = RedactingMakeWriter::new(io::stdout);
            let layer = tracing_subscriber::fmt::layer()
                .with_timer(timer)
                .with_target(true)
                .with_thread_ids(false)
                .with_thread_names(false)
                .with_file(false)
                .with_line_number(false)
                .with_writer(writer)
                .with_filter(filter);

            tracing_subscriber::registry()
                .with(layer)
                .with(buffer_layer)
                .init();
        }
        (LogFormat::Plaintext, LogOutput::Stderr) => {
            let writer = RedactingMakeWriter::new(io::stderr);
            let layer = tracing_subscriber::fmt::layer()
                .with_timer(timer)
                .with_target(true)
                .with_thread_ids(false)
                .with_thread_names(false)
                .with_file(false)
                .with_line_number(false)
                .with_writer(writer)
                .with_filter(filter);

            tracing_subscriber::registry()
                .with(layer)
                .with(buffer_layer)
                .init();
        }
        (LogFormat::Plaintext, LogOutput::File(path)) => {
            let file = open_log_file_no_follow_owner_only(path)?;
            let writer = RedactingMakeWriter::new(file);
            let layer = tracing_subscriber::fmt::layer()
                .with_timer(timer)
                .with_target(true)
                .with_thread_ids(false)
                .with_thread_names(false)
                .with_file(false)
                .with_line_number(false)
                .with_writer(writer)
                .with_filter(filter);

            tracing_subscriber::registry()
                .with(layer)
                .with(buffer_layer)
                .init();
        }
    }

    Ok(())
}

/// Initialize logging for tests.
///
/// This is a convenience function that initializes logging with test-friendly
/// defaults (plaintext, debug level). It silently ignores errors if logging
/// is already initialized, making it safe to call from multiple tests.
pub fn init_test_logging() {
    let _ = init_logging_internal(LogConfig {
        format: LogFormat::Plaintext,
        output: LogOutput::Stdout,
        default_level: Level::DEBUG,
    });
}

/// Internal initialization that doesn't check the guard (for testing)
fn init_logging_internal(config: LogConfig) -> Result<(), LoggingError> {
    let filter = build_env_filter(config.default_level)?;
    let timer = UtcTime::rfc_3339();

    let subscriber = tracing_subscriber::registry();

    match (&config.format, &config.output) {
        (LogFormat::Json, LogOutput::Stdout) => {
            let writer = RedactingMakeWriter::new(io::stdout);
            let layer = tracing_subscriber::fmt::layer()
                .json()
                .with_timer(timer)
                .with_target(true)
                .with_current_span(true)
                .with_span_list(true)
                .with_writer(writer)
                .with_filter(filter);

            subscriber.with(layer).try_init()?;
        }
        (LogFormat::Plaintext, LogOutput::Stdout) => {
            let writer = RedactingMakeWriter::new(io::stdout);
            let layer = tracing_subscriber::fmt::layer()
                .with_timer(timer)
                .with_target(true)
                .with_writer(writer)
                .with_filter(filter);

            subscriber.with(layer).try_init()?;
        }
        _ => {
            // For testing, just use stdout plaintext
            let writer = RedactingMakeWriter::new(io::stdout);
            let layer = tracing_subscriber::fmt::layer()
                .with_timer(timer)
                .with_target(true)
                .with_writer(writer)
                .with_filter(filter);

            subscriber.with(layer).try_init()?;
        }
    }

    Ok(())
}

/// Log target constants for consistent naming across the codebase
pub mod targets {
    /// Main gateway operations
    pub const GATEWAY: &str = "gateway";
    /// WebSocket server
    pub const WS: &str = "ws";
    /// HTTP server
    pub const HTTP: &str = "http";
    /// Plugin system
    pub const PLUGINS: &str = "plugins";
    /// Authentication
    pub const AUTH: &str = "auth";
    /// Configuration loading
    pub const CONFIG: &str = "config";
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_support::env::ScopedEnv;
    use tempfile::NamedTempFile;

    #[test]
    fn test_log_config_defaults() {
        let config = LogConfig::default();
        assert_eq!(config.format, LogFormat::Plaintext);
        assert_eq!(config.output, LogOutput::Stdout);
        assert_eq!(config.default_level, Level::INFO);
    }

    #[test]
    fn test_log_config_development() {
        let config = LogConfig::development();
        assert_eq!(config.format, LogFormat::Plaintext);
        assert_eq!(config.output, LogOutput::Stdout);
        assert_eq!(config.default_level, Level::DEBUG);
    }

    #[test]
    fn test_log_config_production() {
        let config = LogConfig::production();
        assert_eq!(config.format, LogFormat::Json);
        assert_eq!(config.output, LogOutput::Stdout);
        assert_eq!(config.default_level, Level::INFO);
    }

    #[test]
    fn test_env_filter_default() {
        let mut env = ScopedEnv::new();
        env.unset("CARAPACE_LOG");
        env.unset("RUST_LOG");

        // Filter should be created successfully with default level
        let filter = build_env_filter(Level::INFO);
        assert!(
            filter.is_ok(),
            "Should create filter with default INFO level"
        );
    }

    #[test]
    fn test_env_filter_carapace_log() {
        let mut env = ScopedEnv::new();
        env.set("CARAPACE_LOG", "debug");
        let filter = build_env_filter(Level::INFO);
        assert!(filter.is_ok(), "Should create filter from CARAPACE_LOG");
    }

    #[test]
    fn test_env_filter_rust_log_fallback() {
        let mut env = ScopedEnv::new();
        env.unset("CARAPACE_LOG");
        env.set("RUST_LOG", "warn");
        let filter = build_env_filter(Level::INFO);
        assert!(
            filter.is_ok(),
            "Should create filter from RUST_LOG fallback"
        );
    }

    #[test]
    fn test_env_filter_carapace_takes_precedence() {
        let mut env = ScopedEnv::new();
        env.set("CARAPACE_LOG", "error");
        env.set("RUST_LOG", "debug");
        // CARAPACE_LOG should take precedence (both are valid, so just verify creation)
        let filter = build_env_filter(Level::INFO);
        assert!(
            filter.is_ok(),
            "Should create filter with CARAPACE_LOG taking precedence"
        );
    }

    #[test]
    fn test_env_filter_complex_directive() {
        let mut env = ScopedEnv::new();
        env.set("CARAPACE_LOG", "gateway=debug,ws=info,http=warn");
        let filter = build_env_filter(Level::INFO);
        assert!(
            filter.is_ok(),
            "Should parse complex directive from CARAPACE_LOG"
        );
    }

    #[test]
    fn test_env_filter_matrix_target_directive() {
        let mut env = ScopedEnv::new();
        env.set("CARAPACE_LOG", "matrix=debug,matrix_sdk=warn");
        let filter = build_env_filter(Level::INFO);
        assert!(
            filter.is_ok(),
            "Should parse Matrix target directives from CARAPACE_LOG"
        );
    }

    #[test]
    fn test_log_output_file() {
        let temp_file = NamedTempFile::new().unwrap();
        let path = temp_file.path().to_path_buf();

        let config = LogConfig {
            format: LogFormat::Plaintext,
            output: LogOutput::File(path.clone()),
            default_level: Level::INFO,
        };

        assert_eq!(config.output, LogOutput::File(path));
    }

    #[test]
    fn test_targets_constants() {
        assert_eq!(targets::GATEWAY, "gateway");
        assert_eq!(targets::WS, "ws");
        assert_eq!(targets::HTTP, "http");
        assert_eq!(targets::PLUGINS, "plugins");
        assert_eq!(targets::AUTH, "auth");
        assert_eq!(targets::CONFIG, "config");
    }

    #[test]
    fn test_logging_error_display() {
        let err = LoggingError::AlreadyInitialized;
        assert_eq!(err.to_string(), "logging already initialized");

        let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
        let err = LoggingError::FileCreation(io_err);
        assert!(err.to_string().contains("failed to create log file"));
    }

    // Integration test for JSON format output
    #[test]
    fn test_json_format_structure() {
        // We can't fully test the global subscriber in unit tests,
        // but we can verify the JSON layer builds correctly
        let filter = EnvFilter::try_new("info").unwrap();
        let timer = UtcTime::rfc_3339();

        // This should not panic
        let _layer = tracing_subscriber::fmt::layer::<tracing_subscriber::Registry>()
            .json()
            .with_timer(timer)
            .with_target(true)
            .with_current_span(true)
            .with_span_list(true)
            .with_filter(filter);
    }

    // Verify file output creates the file
    #[test]
    fn test_file_output_creation() {
        let temp_file = NamedTempFile::new().unwrap();
        let path = temp_file.path().to_path_buf();

        // Create a file and write to it (simulating what the logger does)
        let file = File::create(&path).unwrap();
        drop(file);

        assert!(path.exists());
    }
}
