use chrono::Local;
use std::{
    fs::File,
    io::{self, Write as _},
    sync::{Arc, Mutex},
};

pub type LogBuffer = Arc<Mutex<Vec<String>>>;
pub type LogFile = Arc<Mutex<Option<std::io::BufWriter<File>>>>;
type PendingLine = Arc<Mutex<String>>;

#[derive(Clone)]
pub struct GuiLogWriter {
    pub buffer: LogBuffer,
    /// Swapped in at the start of each crawl session so every run gets its own log file.
    log_file: LogFile,
    pending_line: PendingLine,
}

impl GuiLogWriter {
    pub fn new() -> Self {
        Self {
            buffer: Arc::new(Mutex::new(Vec::new())),
            log_file: Arc::new(Mutex::new(None)),
            pending_line: Arc::new(Mutex::new(String::new())),
        }
    }

    pub fn set_log_file(&self, file: File) {
        *self.log_file.lock().unwrap() = Some(std::io::BufWriter::new(file));
    }

    pub fn close_log_file(&self) {
        self.flush_pending_line();
        if let Some(mut f) = self.log_file.lock().unwrap().take() {
            let _ = f.flush();
        }
    }

    fn flush_pending_line(&self) {
        let mut pending = self.pending_line.lock().unwrap();
        if pending.is_empty() {
            return;
        }

        self.push_line(&pending);
        pending.clear();
    }

    fn push_line(&self, line: &str) {
        let trimmed = line.trim();
        if trimmed.is_empty() {
            return;
        }

        if let Some(ref mut file) = *self.log_file.lock().unwrap() {
            let ts = Local::now().format("[%Y-%m-%d %H:%M:%S]");
            let _ = writeln!(file, "{} {}", ts, trimmed);
        }

        let message = strip_log_level(trimmed).to_string();
        self.buffer.lock().unwrap().push(message);
    }
}

impl io::Write for GuiLogWriter {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        let raw = String::from_utf8_lossy(buf);
        let mut pending = self.pending_line.lock().unwrap();
        pending.push_str(&raw);

        while let Some(idx) = pending.find('\n') {
            let line = pending[..idx].to_string();
            pending.drain(..=idx);
            drop(pending);
            self.push_line(&line);
            pending = self.pending_line.lock().unwrap();
        }
        Ok(buf.len())
    }

    fn flush(&mut self) -> io::Result<()> {
        self.flush_pending_line();
        if let Some(ref mut f) = *self.log_file.lock().unwrap() {
            let _ = f.flush();
        }
        Ok(())
    }
}

/// Remove the `[LEVEL] ` prefix that simplelog prepends so the GUI only shows
/// the message text (the icon already communicates severity).
fn strip_log_level(s: &str) -> &str {
    for prefix in &["[ERROR] ", "[WARN] ", "[INFO] ", "[DEBUG] ", "[TRACE] "] {
        if let Some(rest) = s.strip_prefix(prefix) {
            return rest;
        }
    }
    s
}

#[cfg(test)]
mod tests {
    use super::GuiLogWriter;
    use std::io::Write;

    #[test]
    fn write_buffers_partial_lines_until_newline() {
        let mut writer = GuiLogWriter::new();

        writer.write_all(b"[INFO] partial").unwrap();
        assert!(writer.buffer.lock().unwrap().is_empty());

        writer.write_all(b" line\n").unwrap();
        assert_eq!(
            writer.buffer.lock().unwrap().as_slice(),
            &["partial line".to_string()]
        );
    }

    #[test]
    fn flush_emits_unterminated_pending_line() {
        let mut writer = GuiLogWriter::new();

        writer.write_all(b"[WARN] trailing line").unwrap();
        writer.flush().unwrap();

        assert_eq!(
            writer.buffer.lock().unwrap().as_slice(),
            &["trailing line".to_string()]
        );
    }
}
