//! SSH session logging utilities
//!
//! Provides structured logging for SSH sessions with:
//! - Session output logging
//! - Secret redaction based on patterns
//! - ANSI escape sequence filtering
//! - Per-session log files organized by date

use super::{LogError, formatter::LogFormatter, sanitize_session_name};
use chrono::Local;
use once_cell::sync::Lazy;
use regex::Regex;
use std::{
    borrow::Cow,
    fs::File,
    io::{BufWriter, Write},
    path::PathBuf,
    sync::{
        Arc, Mutex,
        mpsc::{self, Receiver, RecvTimeoutError, SyncSender},
    },
    thread,
    time::{Duration, Instant},
};

const SSH_LOG_FLUSH_BYTES: usize = 64 * 1024;
const SSH_LOG_FLUSH_INTERVAL: Duration = Duration::from_millis(100);
// 1024 * ~8KiB chunks ~= ~8MiB bounded backlog.
const SSH_LOG_QUEUE_CAPACITY: usize = 1024;
const PRIVATE_LOG_DIR_MODE: u32 = 0o700;
const PRIVATE_LOG_FILE_MODE: u32 = 0o600;

enum SshLogCommand {
    Chunk(Arc<String>),
    Flush(SyncSender<Result<(), String>>),
}

type LogFileFactory = Arc<dyn Fn() -> Result<File, LogError> + Send + Sync>;

#[derive(Clone)]
enum SecretPatternSource {
    DynamicConfig,
    Fixed(Vec<Regex>),
}

struct SshLogWorkerState {
    line_buffer: String,
    writer: Option<BufWriter<File>>,
    pending_bytes: usize,
    last_flush: Instant,
    cached_secret_patterns: Vec<Regex>,
    cached_config_version: Option<u64>,
    last_error: Option<String>,
    cached_timestamp_millis: Option<i64>,
    cached_prefix: String,
}

impl SshLogWorkerState {
    // Worker state construction.
    fn new() -> Self {
        Self {
            line_buffer: String::new(),
            writer: None,
            pending_bytes: 0,
            last_flush: Instant::now(),
            cached_secret_patterns: Vec::new(),
            cached_config_version: None,
            last_error: None,
            cached_timestamp_millis: None,
            cached_prefix: String::new(),
        }
    }
}

// Compiled regex for removing ANSI escape sequences
static ANSI_ESCAPE_REGEX: Lazy<Option<Regex>> = Lazy::new(|| {
    Regex::new(
        r"(?x)
        \x1B\[[\x30-\x3F]*[\x20-\x2F]*[\x40-\x7E]    # CSI: ESC [ params intermediates final
        |\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)           # OSC: ESC ] ... (BEL or ESC \)
        |\x1B[PX^_].*?\x1B\\                         # DCS/SOS/PM/APC: ESC P/X/^/_ ... ESC \
        |\x1B.                                       # Other ESC sequences (2 bytes)
        |\x1B                                        # Stray ESC character
    ",
    )
    .ok()
});

#[derive(Clone)]
pub(super) struct SshLogger {
    formatter: LogFormatter,
    worker_tx: Arc<Mutex<Option<SyncSender<SshLogCommand>>>>,
    file_factory: LogFileFactory,
    secret_pattern_source: SecretPatternSource,
}

impl Default for SshLogger {
    fn default() -> Self {
        Self::new()
    }
}

impl SshLogger {
    // Construction.
    pub(super) fn new() -> Self {
        let mut formatter = LogFormatter::new();
        formatter.set_include_timestamp(true);
        formatter.set_include_break(true);

        Self {
            formatter,
            worker_tx: Arc::new(Mutex::new(None)),
            file_factory: Arc::new(SshLogger::create_log_file),
            secret_pattern_source: SecretPatternSource::DynamicConfig,
        }
    }

    pub(super) fn with_session_name_and_secret_patterns(session_name: impl Into<String>, secret_patterns: Vec<Regex>) -> Self {
        let mut formatter = LogFormatter::new();
        formatter.set_include_timestamp(true);
        formatter.set_include_break(true);

        let session_name = session_name.into();
        let file_factory: LogFileFactory = Arc::new(move || SshLogger::create_log_file_for_session_name(&session_name));

        Self {
            formatter,
            worker_tx: Arc::new(Mutex::new(None)),
            file_factory,
            secret_pattern_source: SecretPatternSource::Fixed(secret_patterns),
        }
    }

    // Public write/flush entry points.
    pub(super) fn log(&self, message: &str) -> Result<(), LogError> {
        self.log_raw(message)
    }

    pub(super) fn log_raw(&self, message: &str) -> Result<(), LogError> {
        self.log_raw_shared(Arc::new(message.to_string()))
    }

    pub(super) fn log_raw_shared(&self, message: Arc<String>) -> Result<(), LogError> {
        let tx = self.ensure_worker()?;
        tx.send(SshLogCommand::Chunk(message))
            .map_err(|err| LogError::FormattingError(format!("failed to enqueue ssh log chunk: {}", err)))
    }

    pub(super) fn flush(&self) -> Result<(), LogError> {
        let tx = self.ensure_worker()?;
        let (ack_tx, ack_rx) = mpsc::sync_channel(0);

        tx.send(SshLogCommand::Flush(ack_tx))
            .map_err(|err| LogError::FormattingError(format!("failed to enqueue ssh log flush: {}", err)))?;

        match ack_rx.recv() {
            Ok(Ok(())) => Ok(()),
            Ok(Err(err_msg)) => Err(LogError::FormattingError(err_msg)),
            Err(err) => Err(LogError::FormattingError(format!("failed waiting for ssh log flush ack: {}", err))),
        }
    }

    // Worker lifecycle.
    fn ensure_worker(&self) -> Result<SyncSender<SshLogCommand>, LogError> {
        let mut worker_tx_guard = match self.worker_tx.lock() {
            Ok(worker_tx_guard) => worker_tx_guard,
            Err(poisoned) => {
                eprintln!("SSH log worker lock poisoned; continuing with recovered state");
                poisoned.into_inner()
            }
        };
        if let Some(existing_tx) = worker_tx_guard.as_ref() {
            return Ok(existing_tx.clone());
        }

        let (tx, rx) = mpsc::sync_channel(SSH_LOG_QUEUE_CAPACITY);
        let formatter = self.formatter.clone();
        let file_factory = self.file_factory.clone();
        let secret_pattern_source = self.secret_pattern_source.clone();

        thread::Builder::new()
            .name("ssh-log-writer".to_string())
            .spawn(move || run_worker(rx, formatter, file_factory, secret_pattern_source))
            .map_err(|err| LogError::FormattingError(format!("failed to spawn ssh log worker: {}", err)))?;

        *worker_tx_guard = Some(tx.clone());
        Ok(tx)
    }

    // File creation helper.
    fn create_log_file() -> Result<File, LogError> {
        let log_path = get_ssh_log_path()?;
        Ok(crate::platform::open_private_append_file(&log_path, PRIVATE_LOG_FILE_MODE)?)
    }

    fn create_log_file_for_session_name(session_name: &str) -> Result<File, LogError> {
        let log_path = get_ssh_log_path_for_session_name(session_name)?;
        Ok(crate::platform::open_private_append_file(&log_path, PRIVATE_LOG_FILE_MODE)?)
    }
}

fn run_worker(receiver: Receiver<SshLogCommand>, formatter: LogFormatter, file_factory: LogFileFactory, secret_pattern_source: SecretPatternSource) {
    let mut state = SshLogWorkerState::new();

    loop {
        match receiver.recv_timeout(SSH_LOG_FLUSH_INTERVAL) {
            Ok(SshLogCommand::Chunk(message)) => {
                if let Err(err) = process_chunk_message(&mut state, &formatter, message.as_ref(), file_factory.as_ref(), &secret_pattern_source) {
                    state.last_error = Some(err.to_string());
                }
            }
            Ok(SshLogCommand::Flush(ack_tx)) => {
                let flush_result = flush_worker(&mut state, &formatter, file_factory.as_ref(), &secret_pattern_source).map_err(|err| err.to_string());
                let _ = ack_tx.send(flush_result);
            }
            Err(RecvTimeoutError::Timeout) => {
                if let Err(err) = flush_if_due(&mut state) {
                    state.last_error = Some(err.to_string());
                }
            }
            Err(RecvTimeoutError::Disconnected) => {
                let _ = flush_worker(&mut state, &formatter, file_factory.as_ref(), &secret_pattern_source);
                break;
            }
        }
    }
}

fn process_chunk_message(
    state: &mut SshLogWorkerState,
    formatter: &LogFormatter,
    message: &str,
    create_log_file: &dyn Fn() -> Result<File, LogError>,
    secret_pattern_source: &SecretPatternSource,
) -> Result<(), LogError> {
    state.line_buffer.push_str(message);
    let lines = extract_complete_lines(&mut state.line_buffer);

    if lines.is_empty() {
        return Ok(());
    }

    ensure_secret_patterns_loaded(&mut state.cached_config_version, &mut state.cached_secret_patterns, secret_pattern_source);

    if state.writer.is_none() {
        state.writer = Some(BufWriter::new(create_log_file()?));
    }

    for line in lines {
        if line.is_empty() {
            continue;
        }

        let sanitized = sanitize_line(&line, &state.cached_secret_patterns);
        if sanitized.is_empty() {
            continue;
        }

        let formatted = format_log_line(state, formatter, sanitized.as_ref());
        if let Some(writer) = state.writer.as_mut() {
            writer.write_all(formatted.as_bytes())?;
            writer.write_all(b"\n")?;
        }
        state.pending_bytes = state.pending_bytes.saturating_add(formatted.len() + 1);
    }

    flush_if_due(state)
}

fn format_log_line(state: &mut SshLogWorkerState, formatter: &LogFormatter, message: &str) -> String {
    if formatter.uses_cached_timestamp_prefix_without_level() {
        let now = Local::now();
        let now_millis = now.timestamp_millis();
        if state.cached_timestamp_millis != Some(now_millis) {
            state.cached_prefix.clear();
            state.cached_prefix.push_str(&now.format("%Y-%m-%d %H:%M:%S%.3f").to_string());
            state.cached_prefix.push(' ');
            state.cached_prefix.push('█');
            state.cached_prefix.push(' ');
            state.cached_timestamp_millis = Some(now_millis);
        }

        let mut formatted = String::with_capacity(state.cached_prefix.len().saturating_add(message.len()));
        formatted.push_str(&state.cached_prefix);
        formatted.push_str(message);
        formatted
    } else {
        formatter.format(None, message)
    }
}

fn flush_if_due(state: &mut SshLogWorkerState) -> Result<(), LogError> {
    if should_flush(state.pending_bytes, state.last_flush.elapsed()) {
        flush_writer(state)?;
    }
    Ok(())
}

fn flush_writer(state: &mut SshLogWorkerState) -> Result<(), LogError> {
    if let Some(writer) = state.writer.as_mut() {
        writer.flush()?;
        state.pending_bytes = 0;
        state.last_flush = Instant::now();
    }
    Ok(())
}

fn flush_worker(
    state: &mut SshLogWorkerState,
    formatter: &LogFormatter,
    create_log_file: &dyn Fn() -> Result<File, LogError>,
    secret_pattern_source: &SecretPatternSource,
) -> Result<(), LogError> {
    flush_partial_line(state, formatter, create_log_file, secret_pattern_source)?;
    flush_writer(state)?;
    if let Some(last_error) = state.last_error.take() {
        return Err(LogError::FormattingError(last_error));
    }
    Ok(())
}

fn flush_partial_line(
    state: &mut SshLogWorkerState,
    formatter: &LogFormatter,
    create_log_file: &dyn Fn() -> Result<File, LogError>,
    secret_pattern_source: &SecretPatternSource,
) -> Result<(), LogError> {
    if state.line_buffer.is_empty() {
        return Ok(());
    }

    ensure_secret_patterns_loaded(&mut state.cached_config_version, &mut state.cached_secret_patterns, secret_pattern_source);

    let tail_line = std::mem::take(&mut state.line_buffer);
    let sanitized = sanitize_line(tail_line.trim_end_matches('\r'), &state.cached_secret_patterns);
    if sanitized.is_empty() {
        return Ok(());
    }

    if state.writer.is_none() {
        state.writer = Some(BufWriter::new(create_log_file()?));
    }

    let formatted = format_log_line(state, formatter, sanitized.as_ref());
    if let Some(writer) = state.writer.as_mut() {
        writer.write_all(formatted.as_bytes())?;
        writer.write_all(b"\n")?;
    }
    state.pending_bytes = state.pending_bytes.saturating_add(formatted.len() + 1);

    Ok(())
}

fn ensure_secret_patterns_loaded(cached_version: &mut Option<u64>, cached_patterns: &mut Vec<Regex>, secret_pattern_source: &SecretPatternSource) {
    match secret_pattern_source {
        SecretPatternSource::DynamicConfig => {
            let current_version = crate::config::current_config_version();
            if cached_version.is_some_and(|version| version == current_version) {
                return;
            }

            *cached_patterns = current_secret_patterns();
            *cached_version = Some(current_version);
        }
        SecretPatternSource::Fixed(patterns) => {
            if cached_version.is_some() {
                return;
            }

            *cached_patterns = patterns.clone();
            *cached_version = Some(u64::MAX);
        }
    }
}

fn get_ssh_log_path() -> Result<PathBuf, LogError> {
    let session_name = crate::config::with_current_config("reading SSH session name", |cfg| cfg.metadata.session_name.clone());
    get_ssh_log_path_for_session_name(&session_name)
}

fn get_ssh_log_path_for_session_name(session_name: &str) -> Result<PathBuf, LogError> {
    let log_dir = ssh_log_directory()?;
    let sanitized = sanitize_session_name(session_name);
    Ok(log_dir.join(format!("{sanitized}.log")))
}

fn ssh_log_directory() -> Result<PathBuf, LogError> {
    let home_dir = dirs::home_dir().ok_or_else(|| LogError::DirectoryCreationError("Home directory not found".to_string()))?;

    let date = Local::now().format("%Y-%m-%d");
    let log_dir = home_dir.join(".color-ssh").join("logs").join("ssh_sessions").join(date.to_string());

    crate::platform::create_private_directory(&log_dir, PRIVATE_LOG_DIR_MODE)?;
    Ok(log_dir)
}

fn current_secret_patterns() -> Vec<Regex> {
    crate::config::with_current_config("reading SSH secret patterns", |cfg| cfg.metadata.compiled_secret_patterns.clone())
}

fn sanitize_line<'a>(line: &'a str, secret_patterns: &[Regex]) -> Cow<'a, str> {
    let cleaned = if line.as_bytes().contains(&0x1b) {
        if let Some(ansi_escape_regex) = ANSI_ESCAPE_REGEX.as_ref() {
            ansi_escape_regex.replace_all(line, "")
        } else {
            Cow::Borrowed(line)
        }
    } else {
        Cow::Borrowed(line)
    };

    let needs_filter = cleaned
        .chars()
        .any(|ch| !(ch.is_alphanumeric() || ch.is_ascii_punctuation() || ch.is_whitespace()) || ch == '\n' || ch == '\r');

    if !needs_filter && secret_patterns.iter().all(|regex| !regex.is_match(cleaned.as_ref())) {
        return cleaned;
    }

    let mut sanitized = if needs_filter {
        cleaned
            .chars()
            .filter(|ch| (ch.is_alphanumeric() || ch.is_ascii_punctuation() || ch.is_whitespace()) && *ch != '\n' && *ch != '\r')
            .collect::<String>()
    } else {
        cleaned.into_owned()
    };

    for regex in secret_patterns {
        if regex.is_match(&sanitized) {
            sanitized = regex.replace_all(&sanitized, "[REDACTED]").into_owned();
        }
    }

    Cow::Owned(sanitized)
}

fn extract_complete_lines(buffer: &mut String) -> Vec<String> {
    let mut lines = Vec::new();
    let mut start = 0usize;

    while let Some(relative_newline) = buffer[start..].find('\n') {
        let end = start + relative_newline;
        lines.push(buffer[start..end].trim_end_matches('\r').to_string());
        start = end + 1;
    }

    if start > 0 {
        buffer.drain(..start);
    }

    lines
}

fn should_flush(pending_bytes: usize, elapsed_since_flush: Duration) -> bool {
    pending_bytes >= SSH_LOG_FLUSH_BYTES || elapsed_since_flush >= SSH_LOG_FLUSH_INTERVAL
}

#[cfg(test)]
#[path = "../test/log/ssh.rs"]
mod tests;
