//! CLI command for viewing and managing gateway logs.
//!
//! Provides access to gateway logs through three mechanisms:
//! - Reading the gateway log file (`~/.ironclaw/gateway.log`)
//! - Streaming live logs via the gateway's SSE endpoint (`/api/logs/events`)
//! - Getting/setting the runtime log level via `/api/logs/level`

use std::io::{Seek, SeekFrom};
use std::path::Path;

use clap::Args;

/// View and manage gateway logs.
#[derive(Args, Debug, Clone)]
#[command(
    about = "View and manage gateway logs",
    long_about = "Tail gateway logs, stream live output, or adjust log level.\nExamples:\n  ironclaw logs                          # Show last 200 lines\n  ironclaw logs --follow                 # Stream live logs via SSE\n  ironclaw logs --limit 50 --json        # Last 50 lines as JSON\n  ironclaw logs --level                  # Show current log level\n  ironclaw logs --level debug            # Set log level to debug"
)]
pub struct LogsCommand {
    /// Stream live logs from the running gateway via SSE.
    /// Replays recent history then streams new entries in real time.
    #[arg(short, long)]
    pub follow: bool,

    /// Maximum number of lines to show (default: 200)
    #[arg(short, long, default_value = "200")]
    pub limit: usize,

    /// Output log entries as JSON (one object per line)
    #[arg(long)]
    pub json: bool,

    /// Display timestamps in local timezone
    #[arg(long)]
    pub local_time: bool,

    /// Plain text output (no ANSI styling)
    #[arg(long)]
    pub plain: bool,

    /// Gateway URL (default: http://{GATEWAY_HOST}:{GATEWAY_PORT})
    #[arg(long)]
    pub url: Option<String>,

    /// Gateway auth token (reads GATEWAY_AUTH_TOKEN env if not set)
    #[arg(long)]
    pub token: Option<String>,

    /// Connection timeout in milliseconds (default: 5000)
    #[arg(long, default_value = "5000")]
    pub timeout: u64,

    /// Get or set runtime log level. Without a value, shows current level.
    /// With a value (trace|debug|info|warn|error), sets the level.
    #[arg(long, num_args = 0..=1, default_missing_value = "")]
    pub level: Option<String>,
}

/// Resolved gateway connection parameters.
struct GatewayParams {
    base_url: String,
    token: String,
}

/// Run the logs CLI command.
pub async fn run_logs_command(cmd: LogsCommand, config_path: Option<&Path>) -> anyhow::Result<()> {
    // --level takes priority: it's a control-plane operation, not log viewing.
    if let Some(level_arg) = &cmd.level {
        let params = resolve_gateway_params(&cmd, config_path).await?;
        if level_arg.is_empty() {
            return cmd_get_level(&cmd, &params).await;
        } else {
            return cmd_set_level(&cmd, level_arg, &params).await;
        }
    }

    if cmd.follow {
        let params = resolve_gateway_params(&cmd, config_path).await?;
        cmd_follow(&cmd, &params).await
    } else {
        cmd_show(&cmd)
    }
}

// ── Show log file ────────────────────────────────────────────────────────

/// Read the last N lines from `~/.ironclaw/gateway.log`.
///
/// Uses a reverse-scan strategy: seeks to the end of the file and reads
/// backwards in chunks to find the last `limit` newlines, so memory usage
/// is proportional to the output size, not the file size.
fn cmd_show(cmd: &LogsCommand) -> anyhow::Result<()> {
    let log_path = crate::bootstrap::ironclaw_base_dir().join("gateway.log");
    if !log_path.exists() {
        anyhow::bail!(
            "No gateway log file found at {}.\n\
             The log file is created when the gateway runs in background mode \
             (e.g. `ironclaw gateway start`).",
            log_path.display()
        );
    }

    let lines = tail_file(&log_path, cmd.limit)?;

    if lines.is_empty() {
        println!("(log file is empty)");
        return Ok(());
    }

    if cmd.json {
        for line in &lines {
            let obj = serde_json::json!({ "line": line });
            println!("{}", obj);
        }
    } else {
        for line in &lines {
            println!("{}", line);
        }
    }

    Ok(())
}

/// Read the last `n` lines from a file by scanning backwards from EOF.
///
/// Reads in 8 KiB chunks from the end, counting newlines until enough
/// are found or the beginning of the file is reached.
fn tail_file(path: &Path, n: usize) -> anyhow::Result<Vec<String>> {
    let mut file = std::fs::File::open(path)
        .map_err(|e| anyhow::anyhow!("Failed to open {}: {}", path.display(), e))?;

    let file_len = file
        .seek(SeekFrom::End(0))
        .map_err(|e| anyhow::anyhow!("Failed to seek {}: {}", path.display(), e))?;

    if file_len == 0 {
        return Ok(Vec::new());
    }

    // Read backwards in chunks to find enough newlines.
    const CHUNK_SIZE: u64 = 8192;
    let mut tail_bytes = Vec::new();
    let mut newline_count = 0;
    let mut remaining = file_len;

    while remaining > 0 && newline_count <= n {
        let read_size = std::cmp::min(CHUNK_SIZE, remaining);
        remaining -= read_size;

        file.seek(SeekFrom::Start(remaining))
            .map_err(|e| anyhow::anyhow!("Seek failed: {e}"))?;

        let mut chunk = vec![0u8; read_size as usize];
        std::io::Read::read_exact(&mut file, &mut chunk)
            .map_err(|e| anyhow::anyhow!("Read failed: {e}"))?;

        // Count newlines in this chunk (backwards).
        for &byte in chunk.iter().rev() {
            if byte == b'\n' {
                newline_count += 1;
            }
        }

        // Prepend chunk to collected bytes.
        chunk.append(&mut tail_bytes);
        tail_bytes = chunk;
    }

    // Convert to string and take last N lines.
    let text = String::from_utf8_lossy(&tail_bytes);
    let all_lines: Vec<&str> = text.lines().collect();
    let start = all_lines.len().saturating_sub(n);

    Ok(all_lines[start..].iter().map(|s| s.to_string()).collect())
}

// ── Follow (live SSE stream) ─────────────────────────────────────────────

/// Connect to the gateway's `/api/logs/events` SSE endpoint and stream logs.
async fn cmd_follow(cmd: &LogsCommand, params: &GatewayParams) -> anyhow::Result<()> {
    let timeout_dur = std::time::Duration::from_millis(cmd.timeout);

    let client = reqwest::Client::builder()
        .connect_timeout(timeout_dur)
        .build()
        .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {e}"))?;

    let url = format!("{}/api/logs/events", params.base_url);
    let resp = client
        .get(&url)
        .header("Authorization", format!("Bearer {}", params.token))
        .header("Accept", "text/event-stream")
        // No per-request timeout: SSE streams are long-lived.
        .timeout(std::time::Duration::from_secs(u64::MAX / 2))
        .send()
        .await
        .map_err(|e| {
            anyhow::anyhow!(
                "Failed to connect to gateway at {url}: {e}\n\
                 Is the gateway running? Try `ironclaw gateway status`."
            )
        })?;

    if !resp.status().is_success() {
        anyhow::bail!(
            "Gateway returned HTTP {}: {}",
            resp.status(),
            resp.text().await.unwrap_or_default()
        );
    }

    eprintln!("Connected to {} — streaming logs (Ctrl-C to stop)", url);

    // Parse SSE stream line by line.
    let mut bytes_stream = resp.bytes_stream();
    let mut buffer = String::new();
    let mut lines_shown: usize = 0;

    use futures::StreamExt;
    while let Some(chunk) = bytes_stream.next().await {
        let chunk = chunk.map_err(|e| anyhow::anyhow!("Stream error: {e}"))?;
        buffer.push_str(&String::from_utf8_lossy(&chunk));

        // Process complete lines from the buffer.
        while let Some(newline_pos) = buffer.find('\n') {
            let line = buffer[..newline_pos].to_string();
            buffer = buffer[newline_pos + 1..].to_string();

            // SSE format: "data: {...}" lines carry the payload.
            if let Some(data) = line.strip_prefix("data: ")
                && let Ok(entry) = serde_json::from_str::<serde_json::Value>(data)
            {
                print_log_entry(&entry, cmd);
                lines_shown += 1;
            }
            // Skip "event:", "id:", "retry:", and empty keepalive lines.
        }
    }

    if lines_shown == 0 {
        eprintln!("(no log entries received)");
    }

    Ok(())
}

// ── Log level get/set ────────────────────────────────────────────────────

/// GET /api/logs/level — show the current log level.
async fn cmd_get_level(cmd: &LogsCommand, params: &GatewayParams) -> anyhow::Result<()> {
    let timeout_dur = std::time::Duration::from_millis(cmd.timeout);

    let client = reqwest::Client::builder()
        .timeout(timeout_dur)
        .build()
        .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {e}"))?;

    let url = format!("{}/api/logs/level", params.base_url);
    let resp = client
        .get(&url)
        .header("Authorization", format!("Bearer {}", params.token))
        .send()
        .await
        .map_err(|e| {
            anyhow::anyhow!(
                "Failed to connect to gateway at {url}: {e}\n\
                 Is the gateway running? Try `ironclaw gateway status`."
            )
        })?;

    if !resp.status().is_success() {
        anyhow::bail!(
            "Gateway returned HTTP {}: {}",
            resp.status(),
            resp.text().await.unwrap_or_default()
        );
    }

    let body: serde_json::Value = resp
        .json()
        .await
        .map_err(|e| anyhow::anyhow!("Invalid response: {e}"))?;

    if cmd.json {
        println!(
            "{}",
            serde_json::to_string_pretty(&body).unwrap_or_default()
        );
    } else {
        let level = body
            .get("level")
            .and_then(|v| v.as_str())
            .unwrap_or("unknown");
        println!("Current log level: {}", level);
    }

    Ok(())
}

/// PUT /api/logs/level — change the runtime log level.
async fn cmd_set_level(
    cmd: &LogsCommand,
    level: &str,
    params: &GatewayParams,
) -> anyhow::Result<()> {
    const VALID: &[&str] = &["trace", "debug", "info", "warn", "error"];
    let level_lower = level.to_lowercase();
    if !VALID.contains(&level_lower.as_str()) {
        anyhow::bail!(
            "Invalid log level '{}'. Must be one of: {}",
            level,
            VALID.join(", ")
        );
    }

    let timeout_dur = std::time::Duration::from_millis(cmd.timeout);

    let client = reqwest::Client::builder()
        .timeout(timeout_dur)
        .build()
        .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {e}"))?;

    let url = format!("{}/api/logs/level", params.base_url);
    let resp = client
        .put(&url)
        .header("Authorization", format!("Bearer {}", params.token))
        .json(&serde_json::json!({ "level": level_lower }))
        .send()
        .await
        .map_err(|e| {
            anyhow::anyhow!(
                "Failed to connect to gateway at {url}: {e}\n\
                 Is the gateway running? Try `ironclaw gateway status`."
            )
        })?;

    if !resp.status().is_success() {
        anyhow::bail!(
            "Gateway returned HTTP {}: {}",
            resp.status(),
            resp.text().await.unwrap_or_default()
        );
    }

    let body: serde_json::Value = resp
        .json()
        .await
        .map_err(|e| anyhow::anyhow!("Invalid response: {e}"))?;

    if cmd.json {
        println!(
            "{}",
            serde_json::to_string_pretty(&body).unwrap_or_default()
        );
    } else {
        let new_level = body
            .get("level")
            .and_then(|v| v.as_str())
            .unwrap_or(&level_lower);
        println!("Log level set to: {}", new_level);
    }

    Ok(())
}

// ── Helpers ──────────────────────────────────────────────────────────────

/// Resolve gateway connection params from CLI flags, config file, or env.
///
/// Priority: --url/--token flags > config TOML > env vars > defaults.
async fn resolve_gateway_params(
    cmd: &LogsCommand,
    config_path: Option<&Path>,
) -> anyhow::Result<GatewayParams> {
    // Load gateway config. Errors propagate when --config is explicit.
    let gw_config = load_gateway_config(config_path).await?;

    // URL: --url flag > config TOML > env vars > defaults.
    let base_url = if let Some(url) = &cmd.url {
        url.trim_end_matches('/').to_string()
    } else if let Some(cfg) = &gw_config {
        format!("http://{}:{}", cfg.host, cfg.port)
    } else {
        let host = std::env::var("GATEWAY_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
        let port: u16 = std::env::var("GATEWAY_PORT")
            .ok()
            .and_then(|p| p.parse().ok())
            .unwrap_or(3000);
        format!("http://{}:{}", host, port)
    };

    // Token: --token flag > config TOML > env var.
    let token = if let Some(token) = &cmd.token {
        token.clone()
    } else if let Some(t) = gw_config.as_ref().and_then(|c| c.auth_token.clone()) {
        t
    } else {
        std::env::var("GATEWAY_AUTH_TOKEN").map_err(|_| {
            anyhow::anyhow!(
                "No auth token provided. Use --token <TOKEN> or set GATEWAY_AUTH_TOKEN.\n\
                 The token is printed when the gateway starts."
            )
        })?
    };

    Ok(GatewayParams { base_url, token })
}

/// Try to load gateway config from the TOML config file.
///
/// If `config_path` was explicitly provided (via `--config`), errors are
/// propagated — the user asked for a specific file and deserves a clear
/// failure when it is missing, unreadable, or malformed.  When no path
/// was given we fall back to env-only resolution and silently return
/// `None` on failure so that `ironclaw logs` works without any config.
async fn load_gateway_config(
    config_path: Option<&Path>,
) -> anyhow::Result<Option<crate::config::GatewayConfig>> {
    if config_path.is_some() {
        // Explicit --config: propagate errors.
        let config = crate::config::Config::from_env_with_toml(config_path)
            .await
            .map_err(|e| anyhow::anyhow!("{e:#}"))?;
        Ok(config.channels.gateway)
    } else {
        // No explicit config: best-effort, swallow errors.
        let config = crate::config::Config::from_env_with_toml(None).await.ok();
        Ok(config.and_then(|c| c.channels.gateway))
    }
}

/// Print a single log entry to stdout.
fn print_log_entry(entry: &serde_json::Value, cmd: &LogsCommand) {
    if cmd.json {
        println!("{}", serde_json::to_string(entry).unwrap_or_default());
        return;
    }

    let level = entry.get("level").and_then(|v| v.as_str()).unwrap_or("?");
    let target = entry.get("target").and_then(|v| v.as_str()).unwrap_or("");
    let message = entry.get("message").and_then(|v| v.as_str()).unwrap_or("");
    let timestamp = entry
        .get("timestamp")
        .and_then(|v| v.as_str())
        .unwrap_or("");

    let display_ts = if cmd.local_time {
        convert_to_local_time(timestamp)
    } else {
        timestamp.to_string()
    };

    if cmd.plain {
        println!("{} {} [{}] {}", display_ts, level, target, message);
    } else {
        let level_colored = colorize_level(level);
        println!("{} {} [{}] {}", display_ts, level_colored, target, message);
    }
}

/// Convert an RFC 3339 timestamp to local time display.
fn convert_to_local_time(ts: &str) -> String {
    chrono::DateTime::parse_from_rfc3339(ts)
        .map(|dt| {
            dt.with_timezone(&chrono::Local)
                .format("%Y-%m-%dT%H:%M:%S%.3f")
                .to_string()
        })
        .unwrap_or_else(|_| ts.to_string())
}

/// Apply ANSI color to log level for terminal display.
fn colorize_level(level: &str) -> String {
    match level {
        "ERROR" => format!("\x1b[31m{}\x1b[0m", level), // red
        "WARN" => format!("\x1b[33m{}\x1b[0m", level),  // yellow
        "INFO" => format!("\x1b[32m{}\x1b[0m", level),  // green
        "DEBUG" => format!("\x1b[36m{}\x1b[0m", level), // cyan
        "TRACE" => format!("\x1b[90m{}\x1b[0m", level), // gray
        _ => level.to_string(),
    }
}

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

    #[test]
    fn test_colorize_level() {
        assert!(colorize_level("ERROR").contains("\x1b[31m"));
        assert!(colorize_level("WARN").contains("\x1b[33m"));
        assert!(colorize_level("INFO").contains("\x1b[32m"));
        assert!(colorize_level("DEBUG").contains("\x1b[36m"));
        assert!(colorize_level("TRACE").contains("\x1b[90m"));
        assert_eq!(colorize_level("UNKNOWN"), "UNKNOWN");
    }

    #[test]
    fn test_convert_to_local_time_valid() {
        let ts = "2024-01-15T10:30:00.000Z";
        let result = convert_to_local_time(ts);
        assert!(result.contains("2024-01-15"));
    }

    #[test]
    fn test_convert_to_local_time_invalid() {
        let ts = "not-a-timestamp";
        assert_eq!(convert_to_local_time(ts), "not-a-timestamp");
    }

    #[test]
    fn test_print_log_entry_json() {
        let entry = serde_json::json!({
            "level": "INFO",
            "target": "ironclaw::agent",
            "message": "test message",
            "timestamp": "2024-01-15T10:30:00.000Z"
        });
        let cmd = LogsCommand {
            follow: false,
            limit: 200,
            json: true,
            local_time: false,
            plain: false,
            url: None,
            token: None,
            timeout: 5000,
            level: None,
        };
        // Should not panic
        print_log_entry(&entry, &cmd);
    }

    #[test]
    fn test_tail_file_small() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("test.log");
        std::fs::write(&path, "line1\nline2\nline3\nline4\nline5\n").unwrap();

        let result = tail_file(&path, 3).unwrap();
        assert_eq!(result, vec!["line3", "line4", "line5"]);
    }

    #[test]
    fn test_tail_file_fewer_lines_than_limit() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("test.log");
        std::fs::write(&path, "a\nb\n").unwrap();

        let result = tail_file(&path, 200).unwrap();
        assert_eq!(result, vec!["a", "b"]);
    }

    #[test]
    fn test_tail_file_empty() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("test.log");
        std::fs::write(&path, "").unwrap();

        let result = tail_file(&path, 10).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_tail_file_large() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("big.log");
        // Write 10000 lines to test chunked reading.
        let content: String = (0..10000).map(|i| format!("line {}\n", i)).collect();
        std::fs::write(&path, &content).unwrap();

        let result = tail_file(&path, 5).unwrap();
        assert_eq!(result.len(), 5);
        assert_eq!(result[0], "line 9995");
        assert_eq!(result[4], "line 9999");
    }

    #[test]
    fn test_tail_file_no_trailing_newline() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("test.log");
        std::fs::write(&path, "line1\nline2\nline3").unwrap();

        let result = tail_file(&path, 2).unwrap();
        assert_eq!(result, vec!["line2", "line3"]);
    }
}
