#[cfg(feature = "file-logger")]
use chrono::Local;
#[cfg(feature = "file-logger")]
use log::{Level, LevelFilter, Log, Metadata, Record};
#[cfg(feature = "file-logger")]
use std::boxed::Box;
#[cfg(feature = "file-logger")]
use std::fs::{self, File, OpenOptions};
#[cfg(feature = "file-logger")]
use std::io::{BufWriter, Write};
#[cfg(feature = "file-logger")]
use std::path::PathBuf;
#[cfg(feature = "file-logger")]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(feature = "file-logger")]
use std::sync::{Mutex, OnceLock};

#[cfg(feature = "file-logger")]
static LOGGER: OnceLock<&'static SimpleFileLogger> = OnceLock::new();

/// File-backed logger implementation used by this crate.
#[cfg(feature = "file-logger")]
pub struct SimpleFileLogger {
    inner: Mutex<Option<BufWriter<File>>>,
    file_path: PathBuf,
    level: LevelFilter,
    bytes_written: AtomicU64,
}

// Keep log files reasonably sized so editors (and VS Code) can open them
// without trying to load hundreds of MB into memory.
#[cfg(feature = "file-logger")]
const MAX_LOG_BYTES: u64 = 10 * 1024 * 1024; // 10 MiB

#[cfg(feature = "file-logger")]
impl SimpleFileLogger {
    /// Initialize the global file logger.
    pub fn init(enabled: bool, level: LevelFilter) -> Result<(), Box<dyn std::error::Error>> {
        if !enabled {
            log::set_max_level(LevelFilter::Off);
            return Ok(());
        }

        // Use platform-appropriate cache directories for logs.
        // **Windows Portable Mode**: {exe_dir}\logs\
        // **Windows Installed Mode**: %LOCALAPPDATA%\<editor>\logs
        // **Linux**: ~/.cache/<editor>/logs

        let mut log_root: Option<PathBuf> = {
            // Windows: portable mode + Windows-specific fallbacks.
            #[cfg(target_os = "windows")]
            {
                let mut root = if let Some(portable_root) = detect_portable_mode_windows() {
                    Some(portable_root.join("logs"))
                } else {
                    None
                };

                if root.is_none() {
                    root = std::env::var_os("LOCALAPPDATA")
                        .map(|p| PathBuf::from(p).join("Marco").join("logs"));
                }

                if root.is_none() {
                    root = std::env::var_os("TEMP")
                        .map(|p| PathBuf::from(p).join("marco").join("logs"));
                }

                root
            }

            // Linux: prefer XDG cache location.
            #[cfg(target_os = "linux")]
            {
                let mut root = std::env::var_os("XDG_CACHE_HOME")
                    .map(|p| PathBuf::from(p).join("marco").join("logs"));

                if root.is_none() {
                    root = dirs::home_dir().map(|h| h.join(".cache").join("marco").join("logs"));
                }

                root
            }

            // Other OSes: start with no platform-specific preference.
            #[cfg(not(any(target_os = "windows", target_os = "linux")))]
            {
                None
            }
        };

        // Generic (no cfg): if the OS provides a cache dir via `dirs`, use it.
        if log_root.is_none() {
            log_root = dirs::cache_dir().map(|c| c.join("marco").join("logs"));
        }

        let log_root = log_root.unwrap_or_else(|| PathBuf::from("/tmp/marco/log"));
        fs::create_dir_all(&log_root)
            .map_err(|e| -> Box<dyn std::error::Error> { e.to_string().into() })?;

        // YYYYMM folder
        let month_folder = Local::now().format("%Y%m").to_string();
        let month_dir = log_root.join(month_folder);
        fs::create_dir_all(&month_dir)
            .map_err(|e| -> Box<dyn std::error::Error> { e.to_string().into() })?;
        // File name: YYMMDD.log
        let file_name = Local::now().format("%y%m%d.log").to_string();
        let file_path = month_dir.join(file_name);

        let file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&file_path)
            .map_err(|e| -> Box<dyn std::error::Error> { e.to_string().into() })?;

        let initial_size = file.metadata().map(|m| m.len()).unwrap_or(0);

        let writer = BufWriter::new(file);

        let boxed = Box::new(SimpleFileLogger {
            inner: Mutex::new(Some(writer)),
            file_path,
            level,
            bytes_written: AtomicU64::new(initial_size),
        });

        // If we already initialized our logger earlier in this process, behave idempotently
        if LOGGER.get().is_some() {
            // Update global max level and return success
            log::set_max_level(level);
            return Ok(());
        }

        // Leak the box temporarily to obtain a &'static reference required by log::set_logger.
        // If another logger is already registered, gracefully abort and drop our boxed logger.
        let leaked: &'static SimpleFileLogger = Box::leak(boxed);

        // Attempt to register; if it fails, drop the leaked box and return Ok with a warning.
        match log::set_logger(leaked) {
            Ok(()) => {
                // Successfully set our logger; record the static reference and apply level.
                // OnceLock::set returns Err if already set, but we checked above, so this should always succeed
                let _ = LOGGER.set(leaked);
                log::set_max_level(level);
                Ok(())
            }
            Err(e) => {
                // Another logger is already present (e.g., env_logger). Drop our leaked box to avoid leaking memory.
                unsafe {
                    let _ =
                        Box::from_raw(leaked as *const SimpleFileLogger as *mut SimpleFileLogger);
                }
                // Return an error to the caller so the application can decide how to surface it.
                Err(format!("Failed to set global logger: {}", e).into())
            }
        }
    }

    fn rotate_if_needed_locked(&self, guard: &mut Option<BufWriter<File>>) {
        let current = self.bytes_written.load(Ordering::Relaxed);
        if current <= MAX_LOG_BYTES {
            return;
        }

        // Best-effort rotation: flush current writer, rename the file, start a new one.
        if let Some(writer) = guard.as_mut() {
            let _ = writer.flush();
        }

        // Drop writer so the underlying file handle is released before rename on Windows.
        *guard = None;

        let ts = Local::now().format("%y%m%d-%H%M%S").to_string();
        let rotated_path =
            self.file_path
                .with_file_name(format!("{}.rotated.{}.log", ts, std::process::id()));

        if let Err(e) = fs::rename(&self.file_path, &rotated_path) {
            // If rename fails (e.g. file missing), just continue with a new file.
            eprintln!(
                "[logger] rotation rename failed ({} -> {}): {}",
                self.file_path.display(),
                rotated_path.display(),
                e
            );
        }

        match OpenOptions::new()
            .create(true)
            .write(true)
            .truncate(true)
            .open(&self.file_path)
        {
            Ok(file) => {
                *guard = Some(BufWriter::new(file));
                self.bytes_written.store(0, Ordering::Relaxed);
            }
            Err(e) => {
                eprintln!(
                    "[logger] failed to open new log file {}: {}",
                    self.file_path.display(),
                    e
                );
            }
        }
    }
}

#[cfg(feature = "file-logger")]
impl Log for SimpleFileLogger {
    fn enabled(&self, metadata: &Metadata) -> bool {
        // Always accept logs at the configured level or higher
        metadata.level() <= self.level.to_level().unwrap_or(Level::Trace)
    }

    fn log(&self, record: &Record) {
        if !self.enabled(record.metadata()) {
            return;
        }
        let ts = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();

        // Format the log message
        let message = format!("{}", record.args());

        // Sanitize UTF-8 in log message to prevent panics from invalid slicing
        // This protects against debug logs that slice strings at non-char boundaries
        let sanitized_message = crate::logic::utf8::sanitize_input(
            message.as_bytes(),
            crate::logic::utf8::InputSource::Unknown,
        );

        let line = format!(
            "{} [{}] {}: {}\n",
            ts,
            record.level(),
            record.target(),
            sanitized_message
        );

        // Track size and rotate early if needed.
        // Note: this is approximate (UTF-8 bytes). Good enough for keeping files small.
        let line_len = line.len() as u64;
        self.bytes_written.fetch_add(line_len, Ordering::Relaxed);

        if let Ok(mut guard) = self.inner.lock() {
            self.rotate_if_needed_locked(&mut guard);
            if let Some(ref mut writer) = *guard {
                let _ = writer.write_all(line.as_bytes());

                // Avoid flushing on every line (can stall UI).
                // Flush eagerly only for high-severity events.
                if record.level() <= Level::Error {
                    let _ = writer.flush();
                }
            }
        }
    }

    fn flush(&self) {
        if let Ok(mut guard) = self.inner.lock() {
            if let Some(ref mut writer) = *guard {
                let _ = writer.flush();
            }
        }
    }
}

/// Initialize file logging with enable flag and maximum log level.
#[cfg(feature = "file-logger")]
pub fn init_file_logger(
    enabled: bool,
    level: LevelFilter,
) -> Result<(), Box<dyn std::error::Error>> {
    SimpleFileLogger::init(enabled, level).map_err(|e| format!("{}", e).into())
}

/// Returns true if the file logger was successfully initialized by this library.
#[cfg(feature = "file-logger")]
pub fn is_file_logger_initialized() -> bool {
    LOGGER.get().is_some()
}

/// Return the resolved root logs directory (no month folder). This is a
/// non-negotiable platform-specific location using the system cache dir and
/// the folder name `logs` per project policy.
#[cfg(feature = "file-logger")]
pub fn current_log_root_dir() -> std::path::PathBuf {
    // Prefer OS cache dir when available, else fall back to a platform temp path
    if let Some(cache_dir) = dirs::cache_dir() {
        return cache_dir.join("marco").join("logs");
    }

    // Platform fallback (should be rare)
    #[cfg(target_os = "windows")]
    {
        std::path::PathBuf::from("C:\\Temp\\marco\\logs")
    }
    #[cfg(target_os = "linux")]
    {
        std::path::PathBuf::from("/tmp/marco/logs")
    }
}

/// Return the resolved log directory for the current month (YYYYMM folder).
#[cfg(feature = "file-logger")]
pub fn current_log_dir() -> std::path::PathBuf {
    use chrono::Local;
    let mut root = current_log_root_dir();
    let month_folder = Local::now().format("%Y%m").to_string();
    root.push(month_folder);
    root
}

/// Convenience: return the current log file path for today (YYMMDD.log) inside
/// the resolved log directory.
#[cfg(feature = "file-logger")]
pub fn current_log_file_for_today() -> std::path::PathBuf {
    use chrono::Local;
    let dir = current_log_dir();
    let file_name = Local::now().format("%y%m%d.log").to_string();
    dir.join(file_name)
}

/// Compute total size in bytes of all log files under the root logs directory.
#[cfg(feature = "file-logger")]
pub fn total_log_size_bytes() -> u64 {
    use std::fs;
    let root = current_log_root_dir();
    let mut total: u64 = 0;
    if root.exists() {
        // Walk month folders and files
        if let Ok(entries) = fs::read_dir(&root) {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.is_file() {
                    if let Ok(md) = entry.metadata() {
                        total += md.len();
                    }
                } else if path.is_dir() {
                    if let Ok(subs) = fs::read_dir(&path) {
                        for s in subs.flatten() {
                            if let Ok(md) = s.metadata() {
                                if md.is_file() {
                                    total += md.len();
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    total
}

/// Delete all logs under the root logs directory.
/// Best-effort: removes files and month folders, returns error on I/O failures.
#[cfg(feature = "file-logger")]
pub fn delete_all_logs() -> Result<(), Box<dyn std::error::Error>> {
    use std::fs;
    let root = current_log_root_dir();
    if !root.exists() {
        return Ok(());
    }

    for entry in fs::read_dir(&root)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_file() {
            let _ = fs::remove_file(&path);
        } else if path.is_dir() {
            for sub in fs::read_dir(&path)? {
                let sub = sub?;
                let subpath = sub.path();
                if subpath.is_file() {
                    let _ = fs::remove_file(&subpath);
                }
            }
            // Try to remove the month folder if empty
            let _ = fs::remove_dir(&path);
        }
    }

    // Remove root if empty
    if root.read_dir()?.next().is_none() {
        let _ = fs::remove_dir(&root);
    }

    Ok(())
}

#[cfg(feature = "file-logger")]
impl SimpleFileLogger {
    /// Flush and close the inner file. After shutdown, the global LOGGER will be cleared.
    pub fn shutdown(&self) {
        if let Ok(mut guard) = self.inner.lock() {
            if let Some(ref mut writer) = *guard {
                let _ = writer.flush();
            }
            // Drop the file by taking it out
            *guard = None;
        }
    }
}

/// Public shutdown hook to safely flush and drop the global logger.
#[cfg(feature = "file-logger")]
pub fn shutdown_file_logger() {
    if let Some(logger) = LOGGER.get() {
        logger.shutdown();
        // Note: OnceLock doesn't support clearing after initialization.
        // The logger remains set but is shut down (file handle closed).
        // This is acceptable for program shutdown.
    }
}

/// Safe string preview for logging - truncates by character count, not bytes
///
/// This function safely truncates strings for debug logging without causing
/// UTF-8 boundary panics. Use this instead of byte slicing in log statements.
///
/// # Examples
/// ```
/// use marco_core::logic::logger::safe_preview;
///
/// let text = "Hello 😀 World — test";
/// let preview = safe_preview(text, 10); // Takes first 10 characters safely
/// log::debug!("Parsing: {}", preview);
/// ```
#[inline]
pub fn safe_preview(s: &str, max_chars: usize) -> String {
    s.chars().take(max_chars).collect()
}

/// Macro for safe debug logging with automatic string truncation
///
/// Use this instead of `log::debug!()` when logging string slices that might
/// contain multi-byte UTF-8 characters. It automatically truncates safely.
///
/// # Examples
/// ```
/// use marco_core::safe_debug;
///
/// let input = "Text with emoji 😀 and em dash —";
/// safe_debug!("Parsing paragraph from: {:?}", input, 40);
/// safe_debug!("Short preview: {:?}", input, 20);
/// ```
#[macro_export]
macro_rules! safe_debug {
    ($fmt:expr, $text:expr, $max:expr) => {
        log::debug!($fmt, $crate::logic::logger::safe_preview($text, $max))
    };
    ($fmt:expr, $text:expr, $max:expr, $($arg:tt)*) => {
        log::debug!($fmt, $crate::logic::logger::safe_preview($text, $max), $($arg)*)
    };
}

// ---------------------------------------------------------------------------
// Windows-only portable mode detection (inlined to avoid a `paths` dependency)
// ---------------------------------------------------------------------------

/// Detect Windows portable mode by checking for a writable `config/` directory
/// next to the executable. Returns the exe directory if portable mode is active.
#[cfg(target_os = "windows")]
fn detect_portable_mode_windows() -> Option<PathBuf> {
    let exe_path = std::env::current_exe().ok()?;
    let exe_dir = exe_path.parent()?;

    let portable_config = exe_dir.join("config");
    if is_dir_writable(&portable_config) {
        return Some(exe_dir.to_path_buf());
    }
    if is_dir_writable(exe_dir) {
        return Some(exe_dir.to_path_buf());
    }
    None
}

#[cfg(target_os = "windows")]
fn is_dir_writable(dir: &std::path::Path) -> bool {
    use std::io::Write;
    if !dir.exists() {
        return false;
    }
    let test_file = dir.join(".marco_write_test");
    std::fs::File::create(&test_file)
        .and_then(|mut f| {
            f.write_all(b"test")?;
            f.sync_all()?;
            std::fs::remove_file(&test_file)
        })
        .is_ok()
}

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

    #[test]
    fn smoke_test_safe_preview_ascii() {
        assert_eq!(safe_preview("Hello, world!", 5), "Hello");
        assert_eq!(safe_preview("Hi", 100), "Hi");
        assert_eq!(safe_preview("", 10), "");
    }

    #[test]
    fn smoke_test_safe_preview_multibyte_emoji() {
        // "😀" is a 4-byte character; safe_preview counts chars, not bytes
        let s = "😀 café";
        assert_eq!(safe_preview(s, 1), "😀");
        assert_eq!(safe_preview(s, 3), "😀 c");
    }

    #[test]
    fn smoke_test_safe_preview_zero_limit() {
        assert_eq!(safe_preview("anything", 0), "");
    }

    #[cfg(feature = "file-logger")]
    #[test]
    fn smoke_test_is_file_logger_initialized_returns_bool() {
        // Before any init call in this test process the result is either
        // true (another test initialised it) or false — it must not panic.
        let _ = is_file_logger_initialized();
    }

    #[cfg(feature = "file-logger")]
    #[test]
    fn smoke_test_log_path_helpers_return_non_empty_paths() {
        // These functions compute deterministic paths and must not panic.
        let root = current_log_root_dir();
        assert!(
            !root.as_os_str().is_empty(),
            "log root dir must not be empty"
        );

        let dir = current_log_dir();
        assert!(!dir.as_os_str().is_empty(), "log dir must not be empty");

        let file = current_log_file_for_today();
        assert!(
            !file.as_os_str().is_empty(),
            "log file path must not be empty"
        );

        // log dir should be a subdirectory of root
        assert!(
            dir.starts_with(&root),
            "current_log_dir() should be nested inside current_log_root_dir()"
        );

        // today's file should be inside the log dir
        assert!(
            file.starts_with(&dir),
            "current_log_file_for_today() should be inside current_log_dir()"
        );
    }

    #[cfg(feature = "file-logger")]
    #[test]
    fn smoke_test_total_log_size_bytes_does_not_panic() {
        // May return 0 if no log files exist; must not panic regardless.
        let _size = total_log_size_bytes();
    }
}
