//! ACP Protocol Logging
//!
//! This module provides file-based logging of all ACP protocol messages (stdin/stdout)
//! for debugging and analysis. When enabled via configuration, all JSON-RPC messages
//! are logged to JSONL files in the specified directory.
//!
//! # Log Format
//!
//! Logs are written in JSONL (newline-delimited JSON) format:
//! ```json
//! {"ts":"2026-01-10T12:34:56.789Z","dir":"out","msg":{...json-rpc message...}}
//! {"ts":"2026-01-10T12:34:57.001Z","dir":"in","msg":{...json-rpc message...}}
//! {"ts":"2026-01-10T12:34:58.123Z","dir":"in","raw":"not valid json","parse_error":"..."}
//! ```
//!
//! # File Naming
//!
//! Log files are named `{identifier}_{timestamp}.jsonl`
//! - identifier: Command name or connector identifier
//! - timestamp: ISO 8601 timestamp when logging started

use chrono::Utc;
use serde_json::Value;
use std::fs::{self, File};
use std::io::{BufWriter, Write};
use std::path::PathBuf;
use tracing::{debug, error, info, warn};

/// ACP Protocol Logger
///
/// Logs all JSON-RPC messages to files for debugging.
/// Owned by the transport - no Arc/Mutex needed.
pub struct AcpProtocolLogger {
    /// Current log file handle
    file: BufWriter<File>,
    /// Path to the log file (for logging)
    path: PathBuf,
}

impl AcpProtocolLogger {
    /// Create a new logger and open the log file immediately
    ///
    /// # Arguments
    /// * `log_dir` - Directory to store log files (will be created if it doesn't exist)
    /// * `identifier` - Identifier for file naming (e.g., command name)
    ///
    /// # Returns
    /// `Some(logger)` if file was created successfully, `None` otherwise
    pub fn new(log_dir: PathBuf, identifier: &str) -> Option<Self> {
        // Create directory if it doesn't exist
        if let Err(e) = fs::create_dir_all(&log_dir) {
            warn!(
                "Failed to create ACP log directory {:?}: {}",
                log_dir, e
            );
            return None;
        }

        Self::cleanup_old_logs(&log_dir, 100);

        let timestamp = Utc::now().format("%Y%m%d_%H%M%S%.3f").to_string();
        let safe_identifier = identifier
            .replace('/', "_")
            .replace('\\', "_")
            .replace(':', "_");
        let filename = format!("{}_{}.jsonl", safe_identifier, timestamp);
        let path = log_dir.join(filename);

        match File::create(&path) {
            Ok(file) => {
                info!("ACP protocol logging to {:?}", path);
                Some(Self {
                    file: BufWriter::new(file),
                    path,
                })
            }
            Err(e) => {
                error!("Failed to create ACP log file {:?}: {}", path, e);
                None
            }
        }
    }

    /// Log an outgoing message (to the agent)
    pub fn log_outgoing(&mut self, message: &Value) {
        let entry = serde_json::json!({
            "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
            "dir": "out",
            "msg": message
        });
        self.write_entry(&entry);
    }

    /// Log an incoming raw line
    ///
    /// Tries to parse as JSON. If successful, logs the parsed message.
    /// If parsing fails, logs the raw line with the parse error.
    pub fn log_incoming_raw(&mut self, raw_line: &str) {
        match serde_json::from_str::<Value>(raw_line) {
            Ok(message) => {
                let entry = serde_json::json!({
                    "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
                    "dir": "in",
                    "msg": message
                });
                self.write_entry(&entry);
            }
            Err(e) => {
                let entry = serde_json::json!({
                    "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
                    "dir": "in",
                    "raw": raw_line,
                    "parse_error": e.to_string()
                });
                self.write_entry(&entry);
            }
        }
    }

    /// Log an incoming parsed message (when already parsed)
    pub fn log_incoming(&mut self, message: &Value) {
        let entry = serde_json::json!({
            "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
            "dir": "in",
            "msg": message
        });
        self.write_entry(&entry);
    }

    fn cleanup_old_logs(log_dir: &PathBuf, max_files: usize) {
        let entries: Vec<_> = match fs::read_dir(log_dir) {
            Ok(rd) => rd
                .filter_map(|e| e.ok())
                .filter(|e| {
                    e.path()
                        .extension()
                        .map(|ext| ext == "jsonl")
                        .unwrap_or(false)
                })
                .collect(),
            Err(_) => return,
        };

        if entries.len() <= max_files {
            return;
        }

        let mut by_modified: Vec<_> = entries
            .into_iter()
            .filter_map(|e| e.metadata().ok().and_then(|m| m.modified().ok()).map(|t| (e, t)))
            .collect();
        by_modified.sort_by_key(|(_, t)| *t);

        let to_remove = by_modified.len().saturating_sub(max_files);
        let mut removed = 0;
        for (entry, _) in by_modified.into_iter().take(to_remove) {
            if fs::remove_file(entry.path()).is_ok() {
                removed += 1;
            }
        }
        if removed > 0 {
            info!("Cleaned up {} old ACP log files from {:?}", removed, log_dir);
        }
    }

    fn write_entry(&mut self, entry: &Value) {
        if let Err(e) = writeln!(self.file, "{}", entry.to_string()) {
            error!("Failed to write ACP log entry: {}", e);
        } else if let Err(e) = self.file.flush() {
            debug!("Failed to flush ACP log: {}", e);
        }
    }

    /// Get the log file path
    pub fn path(&self) -> &PathBuf {
        &self.path
    }
}

impl Drop for AcpProtocolLogger {
    fn drop(&mut self) {
        if let Err(e) = self.file.flush() {
            warn!("Failed to flush ACP log on close: {}", e);
        }
        info!("Closed ACP protocol log: {:?}", self.path);
    }
}

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

    #[test]
    fn test_logger_creates_directory_and_file() {
        let temp = tempdir().unwrap();
        let log_dir = temp.path().join("acp_logs");

        let logger = AcpProtocolLogger::new(log_dir.clone(), "test-command");
        assert!(logger.is_some());
        assert!(log_dir.exists());

        // Should have created a file
        let entries: Vec<_> = fs::read_dir(&log_dir).unwrap().collect();
        assert_eq!(entries.len(), 1);
    }

    #[test]
    fn test_logger_writes_messages() {
        let temp = tempdir().unwrap();
        let log_dir = temp.path().join("acp_logs");

        let mut logger = AcpProtocolLogger::new(log_dir.clone(), "test-command").unwrap();

        let msg = serde_json::json!({"jsonrpc": "2.0", "method": "test", "id": 1});
        logger.log_outgoing(&msg);
        logger.log_incoming(&serde_json::json!({"jsonrpc": "2.0", "result": "ok", "id": 1}));
        drop(logger); // Flush on drop

        // Read the file
        let entries: Vec<_> = fs::read_dir(&log_dir).unwrap().collect();
        let file_path = entries[0].as_ref().unwrap().path();
        let content = fs::read_to_string(file_path).unwrap();
        let lines: Vec<&str> = content.lines().collect();

        assert_eq!(lines.len(), 2);
        assert!(lines[0].contains("\"dir\":\"out\""));
        assert!(lines[1].contains("\"dir\":\"in\""));
    }

    #[test]
    fn test_logger_handles_raw_parse_error() {
        let temp = tempdir().unwrap();
        let log_dir = temp.path().join("acp_logs");

        let mut logger = AcpProtocolLogger::new(log_dir.clone(), "test-command").unwrap();

        // Log a valid JSON
        logger.log_incoming_raw(r#"{"jsonrpc":"2.0","result":"ok","id":1}"#);
        // Log invalid JSON (raw output from crashed process)
        logger.log_incoming_raw("Error: process crashed with SIGSEGV");
        drop(logger);

        // Read the file
        let entries: Vec<_> = fs::read_dir(&log_dir).unwrap().collect();
        let file_path = entries[0].as_ref().unwrap().path();
        let content = fs::read_to_string(file_path).unwrap();
        let lines: Vec<&str> = content.lines().collect();

        assert_eq!(lines.len(), 2);
        // First line should be parsed JSON
        assert!(lines[0].contains("\"msg\":"));
        assert!(!lines[0].contains("\"parse_error\""));
        // Second line should have raw + parse_error
        assert!(lines[1].contains("\"raw\":"));
        assert!(lines[1].contains("\"parse_error\":"));
        assert!(lines[1].contains("SIGSEGV"));
    }
}
