//! Mad Hatter logging — sealed-aware, format-driven, file-rotatable.
//!
//! # Core design
//!
//! `LogParam` trait dispatch: `Sealed<T>` is auto-unsealed via `value()` (pub(crate)),
//! while all other `Display` types pass through unchanged. The log macros wrap each
//! argument with `LogParam::as_log_display()` so Sealed values are transparently
//! rendered without giving Sealed a `Display` impl.
//!
//! # Usage
//!
//! ```ignore
//! use mad_hatter::log;
//!
//! log::init(log::LogConfig {
//!     level: log::LogLevel::Info,
//!     output: log::LogOutput::Stdout,
//!     format: log::LogFormat::Pretty,
//!     rotation: None,
//! });
//!
//! log::warn!("[AUTH] login failed: user={}", user_id);
//! log::info!("[HUB] connected: {}", sealed_addr);  // Sealed auto-unsealed
//! ```

use std::fmt;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Mutex;

use chrono::Local;


// ===== LogParam trait =====

/// Trait for log argument dispatch.
///
/// All Display types (including Sealed<T: Display>) pass through.
pub trait LogParam {
    fn as_log_display(&self) -> &dyn fmt::Display;
}

/// Blanket impl for all Display types (Sealed<T: Display> included via its Display impl).
impl<T: fmt::Display> LogParam for T {
    fn as_log_display(&self) -> &dyn fmt::Display {
        self
    }
}

// ===== Log Level =====

#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
    Error = 1,
    Warn = 2,
    Info = 3,
    Debug = 4,
    Trace = 5,
}

impl LogLevel {
    fn from_u8(v: u8) -> Self {
        match v {
            1 => LogLevel::Error,
            2 => LogLevel::Warn,
            3 => LogLevel::Info,
            4 => LogLevel::Debug,
            5 => LogLevel::Trace,
            _ => LogLevel::Info,
        }
    }

    /// Fixed-width label for pretty format alignment.
    pub fn label(&self) -> &'static str {
        match self {
            LogLevel::Error => "ERROR",
            LogLevel::Warn => "WARN ",
            LogLevel::Info => "INFO ",
            LogLevel::Debug => "DEBUG",
            LogLevel::Trace => "TRACE",
        }
    }

    /// Trimmed label for JSON format.
    pub fn name(&self) -> &'static str {
        match self {
            LogLevel::Error => "ERROR",
            LogLevel::Warn => "WARN",
            LogLevel::Info => "INFO",
            LogLevel::Debug => "DEBUG",
            LogLevel::Trace => "TRACE",
        }
    }
}

impl fmt::Display for LogLevel {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str(self.label())
    }
}

// ===== Config =====

/// Where log output goes.
pub enum LogOutput {
    /// stdout only (journalctl manages persistence).
    Stdout,
    /// File only.
    File(String),
    /// Both stdout and file.
    Both(String),
}

/// Log line format.
pub enum LogFormat {
    /// `2026-03-20 08:03:17.234 WARN  [INSTANCE] msg`
    Pretty,
    /// `{"ts":"...","level":"WARN","tag":"INSTANCE","msg":"..."}`
    Json,
}

/// File rotation policy.
pub struct RotationConfig {
    /// Max single file size in MB before rotation.
    pub max_size_mb: u64,
    /// Number of rotated files to keep.
    pub max_files: u32,
}

/// Logger configuration passed to `init()`.
pub struct LogConfig {
    pub level: LogLevel,
    pub output: LogOutput,
    pub format: LogFormat,
    pub rotation: Option<RotationConfig>,
}

// ===== Internal state =====

static LEVEL: AtomicU8 = AtomicU8::new(3); // Info default
static LOGGER: Mutex<Option<LoggerInner>> = Mutex::new(None);

struct LoggerInner {
    format: LogFormat,
    output: OutputInner,
}

enum OutputInner {
    Stdout,
    File(FileWriter),
    Both(FileWriter),
}

struct FileWriter {
    path: PathBuf,
    file: std::fs::File,
    bytes_written: u64,
    max_size_bytes: u64,
    max_files: u32,
}

impl FileWriter {
    fn new(path: &str, rotation: Option<&RotationConfig>) -> std::io::Result<Self> {
        let path = PathBuf::from(path);
        if let Some(parent) = path.parent() {
            if !parent.as_os_str().is_empty() {
                std::fs::create_dir_all(parent)?;
            }
        }
        let file = std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(&path)?;
        let bytes_written = file.metadata()?.len();
        let (max_size_bytes, max_files) = match rotation {
            Some(r) => (r.max_size_mb * 1024 * 1024, r.max_files),
            None => (u64::MAX, 0),
        };
        Ok(FileWriter {
            path,
            file,
            bytes_written,
            max_size_bytes,
            max_files,
        })
    }

    fn write(&mut self, buf: &[u8]) -> std::io::Result<()> {
        self.file.write_all(buf)?;
        self.bytes_written += buf.len() as u64;
        if self.bytes_written >= self.max_size_bytes && self.max_files > 0 {
            self.rotate()?;
        }
        Ok(())
    }

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

    fn rotate(&mut self) -> std::io::Result<()> {
        self.file.flush()?;

        // Delete oldest rotated file if it exists
        let oldest = rotated_path(&self.path, self.max_files);
        if oldest.exists() {
            std::fs::remove_file(&oldest)?;
        }

        // Shift existing: .{n-1} → .{n}
        for i in (1..self.max_files).rev() {
            let from = rotated_path(&self.path, i);
            let to = rotated_path(&self.path, i + 1);
            if from.exists() {
                std::fs::rename(&from, &to)?;
            }
        }

        // Current → .1
        let first = rotated_path(&self.path, 1);
        std::fs::rename(&self.path, &first)?;

        // Open fresh file
        self.file = std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(&self.path)?;
        self.bytes_written = 0;

        Ok(())
    }
}

fn rotated_path(base: &Path, n: u32) -> PathBuf {
    let name = base.file_name().unwrap().to_string_lossy();
    base.with_file_name(format!("{}.{}", name, n))
}

// ===== Public API =====

/// Initialize the global logger. Can be called multiple times (reconfigures).
pub fn init(config: LogConfig) {
    LEVEL.store(config.level as u8, Ordering::Relaxed);
    let rotation = config.rotation.as_ref();
    let output = match &config.output {
        LogOutput::Stdout => OutputInner::Stdout,
        LogOutput::File(path) => {
            OutputInner::File(
                FileWriter::new(path, rotation).expect("[MAD-LOG] Failed to open log file"),
            )
        }
        LogOutput::Both(path) => {
            OutputInner::Both(
                FileWriter::new(path, rotation).expect("[MAD-LOG] Failed to open log file"),
            )
        }
    };
    let inner = LoggerInner {
        format: config.format,
        output,
    };
    let mut guard = LOGGER.lock().unwrap();
    *guard = Some(inner);
}

/// Change the log level at runtime.
pub fn set_level(level: LogLevel) {
    LEVEL.store(level as u8, Ordering::Relaxed);
}

/// Get the current log level.
pub fn current_level() -> LogLevel {
    LogLevel::from_u8(LEVEL.load(Ordering::Relaxed))
}

/// Core write function. Called by the log macros — not for direct use.
#[doc(hidden)]
pub fn __log_write(level: LogLevel, args: fmt::Arguments<'_>) {
    if (level as u8) > LEVEL.load(Ordering::Relaxed) {
        return;
    }

    let message = fmt::format(args);

    let mut guard = LOGGER.lock().unwrap();
    match guard.as_mut() {
        Some(inner) => {
            let formatted = format_message(&inner.format, level, &message);
            let buf = formatted.as_bytes();
            match &mut inner.output {
                OutputInner::Stdout => {
                    let _ = std::io::stdout().write_all(buf);
                }
                OutputInner::File(writer) => {
                    let _ = writer.write(buf);
                }
                OutputInner::Both(writer) => {
                    let _ = std::io::stdout().write_all(buf);
                    let _ = writer.write(buf);
                }
            }
        }
        None => {
            // Logger not initialized — fallback to stderr with pretty format
            drop(guard); // release lock before stderr write
            let formatted = format_pretty(level, &message);
            let _ = std::io::stderr().write_all(formatted.as_bytes());
        }
    }
}

/// Flush the logger (useful in tests). No-op if logger not initialized.
pub fn flush() {
    let mut guard = LOGGER.lock().unwrap();
    if let Some(inner) = guard.as_mut() {
        match &mut inner.output {
            OutputInner::File(w) | OutputInner::Both(w) => {
                let _ = w.flush();
            }
            OutputInner::Stdout => {
                let _ = std::io::stdout().flush();
            }
        }
    }
}

// ===== Formatting =====

fn format_message(format: &LogFormat, level: LogLevel, message: &str) -> String {
    match format {
        LogFormat::Pretty => format_pretty(level, message),
        LogFormat::Json => format_json(level, message),
    }
}

/// Pretty format: `2026-03-20 08:03:17.234 WARN  [INSTANCE] msg\n`
pub fn format_pretty(level: LogLevel, message: &str) -> String {
    let now = Local::now();
    format!(
        "{} {} {}\n",
        now.format("%Y-%m-%d %H:%M:%S%.3f"),
        level.label(),
        message
    )
}

/// JSON format: `{"ts":"...","level":"WARN","tag":"INSTANCE","msg":"..."}\n`
pub fn format_json(level: LogLevel, message: &str) -> String {
    let now = Local::now();
    let ts = now.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
    let (tag, msg) = extract_tag(message);
    let mut map = serde_json::Map::new();
    map.insert("ts".into(), serde_json::Value::String(ts));
    map.insert(
        "level".into(),
        serde_json::Value::String(level.name().into()),
    );
    if let Some(t) = tag {
        map.insert("tag".into(), serde_json::Value::String(t.into()));
    }
    map.insert("msg".into(), serde_json::Value::String(msg.into()));
    let mut out = serde_json::to_string(&map).unwrap_or_default();
    out.push('\n');
    out
}

/// Extract `[TAG]` prefix from log message.
///
/// `"[AUTH] login failed"` → `(Some("AUTH"), "login failed")`
/// `"no tag here"` → `(None, "no tag here")`
#[allow(clippy::string_slice)]
pub fn extract_tag(message: &str) -> (Option<&str>, &str) {
    if message.starts_with('[') {
        if let Some(end) = message.find(']') {
            let tag = &message[1..end];
            let rest = message[end + 1..].trim_start();
            return (Some(tag), rest);
        }
    }
    (None, message)
}

// ===== Re-exports for macros =====

pub use crate::__mad_error as error;
pub use crate::__mad_warn as warn;
pub use crate::__mad_info as info;
pub use crate::__mad_debug as debug;
pub use crate::__mad_trace as trace;