//! Handler for the `logs <number>` command — shows agent log tail for a story.
//!
//! Reads from the CRDT pipeline state (to resolve story numbers) and from the
//! agent log directory on disk.  No LLM invocation — handled entirely at the
//! bot level.

use super::CommandContext;
use std::path::{Path, PathBuf};

/// Handle `{bot_name} logs <number>`.
///
/// Finds the story by its numeric prefix in the CRDT, then tails the most
/// recently modified agent log file for that story.
pub(super) fn handle_logs(ctx: &CommandContext) -> Option<String> {
    let num_str = ctx.args.trim();
    if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
        return None;
    }

    let story_id = match find_story_id_by_number(num_str) {
        Some(id) => id,
        None => return Some(format!("Story **{num_str}** not found in the pipeline.")),
    };

    let log_dir = ctx
        .project_root
        .join(".huskies")
        .join("logs")
        .join(&story_id);

    match latest_log_file(&log_dir) {
        Some(log_path) => {
            let tail = read_log_tail(&log_path, 30);
            let filename = log_path
                .file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("agent.log");
            if tail.is_empty() {
                Some(format!("**Agent log** (`{filename}`): *(empty)*"))
            } else {
                Some(format!(
                    "**Agent log tail** (`{filename}`):\n```\n{tail}\n```"
                ))
            }
        }
        None => Some(format!("**Story {num_str}:** *(no agent log found)*")),
    }
}

/// Find a story's full ID by its numeric prefix using the CRDT pipeline state.
fn find_story_id_by_number(num_str: &str) -> Option<String> {
    let items = crate::pipeline_state::read_all_typed();
    items.into_iter().find_map(|item| {
        let file_num = item
            .story_id
            .0
            .split('_')
            .next()
            .filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()))
            .unwrap_or("");
        if file_num == num_str {
            Some(item.story_id.0)
        } else {
            None
        }
    })
}

/// Find the most recently modified `.log` file in the given directory.
fn latest_log_file(log_dir: &Path) -> Option<PathBuf> {
    if !log_dir.is_dir() {
        return None;
    }
    let mut best: Option<(PathBuf, std::time::SystemTime)> = None;
    for entry in std::fs::read_dir(log_dir).ok()?.flatten() {
        let path = entry.path();
        if path.extension().and_then(|e| e.to_str()) != Some("log") {
            continue;
        }
        let modified = match entry.metadata().and_then(|m| m.modified()) {
            Ok(t) => t,
            Err(_) => continue,
        };
        if best.as_ref().is_none_or(|(_, t)| modified > *t) {
            best = Some((path, modified));
        }
    }
    best.map(|(p, _)| p)
}

/// Read the last `n` non-empty lines from a file as a single string.
fn read_log_tail(path: &Path, n: usize) -> String {
    let contents = match std::fs::read_to_string(path) {
        Ok(c) => c,
        Err(_) => return String::new(),
    };
    let lines: Vec<&str> = contents.lines().filter(|l| !l.trim().is_empty()).collect();
    let start = lines.len().saturating_sub(n);
    lines[start..].join("\n")
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    use super::super::{CommandDispatch, try_handle_command};

    fn logs_cmd(root: &Path, args: &str) -> Option<String> {
        let services = crate::services::Services::new_test(root.to_path_buf(), "Timmy".to_string());
        let room_id = "!test:example.com".to_string();
        let dispatch = CommandDispatch {
            services: &services,
            project_root: &services.project_root,
            bot_user_id: "@timmy:homeserver.local",
            room_id: &room_id,
        };
        try_handle_command(&dispatch, &format!("@timmy logs {args}"))
    }

    // -- registration -------------------------------------------------------

    #[test]
    fn logs_command_is_registered() {
        let found = super::super::commands().iter().any(|c| c.name == "logs");
        assert!(found, "logs command must be in the registry");
    }

    #[test]
    fn logs_command_appears_in_help() {
        let result = super::super::tests::try_cmd_addressed(
            "Timmy",
            "@timmy:homeserver.local",
            "@timmy help",
        );
        let output = result.unwrap();
        assert!(
            output.contains("logs"),
            "help should list logs command: {output}"
        );
    }

    // -- input validation ---------------------------------------------------

    #[test]
    fn logs_no_args_routes_to_timmy() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = logs_cmd(tmp.path(), "");
        assert!(
            result.is_none(),
            "no args should route to LLM (None): {result:?}"
        );
    }

    #[test]
    fn logs_non_numeric_routes_to_timmy() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = logs_cmd(tmp.path(), "for the login story");
        assert!(
            result.is_none(),
            "natural-language args should route to LLM (None): {result:?}"
        );
    }

    #[test]
    fn logs_well_formed_runs_handler() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = logs_cmd(tmp.path(), "99999");
        assert!(
            result.is_some(),
            "well-formed numeric arg should run the command handler: {result:?}"
        );
    }

    // -- not found ----------------------------------------------------------

    #[test]
    fn logs_story_not_in_pipeline_returns_friendly_message() {
        crate::db::ensure_content_store();
        let tmp = tempfile::TempDir::new().unwrap();
        let output = logs_cmd(tmp.path(), "99998").unwrap();
        assert!(
            output.contains("99998"),
            "message should include story number: {output}"
        );
        assert!(
            output.contains("not found") || output.contains("Not found"),
            "message should say not found: {output}"
        );
    }

    // -- no log file --------------------------------------------------------

    #[test]
    fn logs_no_log_shows_no_log_message() {
        use crate::chat::test_helpers::write_story_file;
        let tmp = tempfile::TempDir::new().unwrap();
        write_story_file(
            tmp.path(),
            "2_current",
            "77_story_no_log.md",
            "---\nname: No Log\n---\n",
            Some("No Log"),
        );
        let output = logs_cmd(tmp.path(), "77").unwrap();
        assert!(
            output.contains("no agent log") || output.contains("No agent log"),
            "should indicate no log exists: {output}"
        );
    }

    // -- log file present ---------------------------------------------------

    #[test]
    fn logs_shows_log_tail_when_log_exists() {
        use crate::chat::test_helpers::write_story_file;
        let tmp = tempfile::TempDir::new().unwrap();
        write_story_file(
            tmp.path(),
            "2_current",
            "88_story_has_log.md",
            "---\nname: Has Log\n---\n",
            Some("Has Log"),
        );
        // Write a log file in the expected location.
        let log_dir = tmp
            .path()
            .join(".huskies")
            .join("logs")
            .join("88_story_has_log");
        std::fs::create_dir_all(&log_dir).unwrap();
        let log_path = log_dir.join("coder-1-session.log");
        std::fs::write(&log_path, "line one\nline two\nline three\n").unwrap();

        let output = logs_cmd(tmp.path(), "88").unwrap();
        assert!(
            output.contains("line one") || output.contains("line three"),
            "should show log content: {output}"
        );
    }

    // -- read_log_tail -------------------------------------------------------

    #[test]
    fn read_log_tail_returns_last_n_lines() {
        let tmp = tempfile::TempDir::new().unwrap();
        let path = tmp.path().join("test.log");
        let content = (1..=30)
            .map(|i| format!("line {i}"))
            .collect::<Vec<_>>()
            .join("\n");
        std::fs::write(&path, &content).unwrap();
        let tail = read_log_tail(&path, 5);
        let lines: Vec<&str> = tail.lines().collect();
        assert_eq!(lines.len(), 5);
        assert_eq!(lines[0], "line 26");
        assert_eq!(lines[4], "line 30");
    }

    #[test]
    fn read_log_tail_fewer_lines_than_n() {
        let tmp = tempfile::TempDir::new().unwrap();
        let path = tmp.path().join("short.log");
        std::fs::write(&path, "line A\nline B\n").unwrap();
        let tail = read_log_tail(&path, 20);
        assert!(tail.contains("line A"));
        assert!(tail.contains("line B"));
    }

    // -- latest_log_file ----------------------------------------------------

    #[test]
    fn latest_log_file_returns_none_for_missing_dir() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = latest_log_file(&tmp.path().join("nonexistent"));
        assert!(result.is_none());
    }

    #[test]
    fn latest_log_file_finds_log() {
        let tmp = tempfile::TempDir::new().unwrap();
        let log_path = tmp.path().join("coder-1-sess-abc.log");
        std::fs::write(&log_path, "some log content\n").unwrap();
        let result = latest_log_file(tmp.path());
        assert!(result.is_some());
        assert_eq!(result.unwrap(), log_path);
    }
}
