//! `ReadTool`: built-in trait wrapper around `crate::fs::read_text_file`.

use crate::fs::read::{read_text_file, ReadTextFileRequest, ReadTextFileResponse};
use crate::tool::{
    Tool, ToolContext, ToolEvent, ToolEventSink, ToolInput, ToolKind, ToolLocation,
    ToolResultContent,
};
use async_trait::async_trait;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;

#[derive(Serialize, Deserialize, JsonSchema, Clone)]
pub struct ReadInput {
    /// Absolute path to the file to read.
    pub path: String,
    /// Optional 1-indexed start line.
    #[serde(default)] pub line: Option<usize>,
    /// Optional max number of lines to return.
    #[serde(default)] pub limit: Option<usize>,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ReadOutput {
    Ok { content: String },
    Err { error: String },
}

impl ReadOutput {
    fn err(msg: impl Into<String>) -> Self { ReadOutput::Err { error: msg.into() } }
}

#[derive(Default)]
pub struct ReadTool;

#[async_trait]
impl Tool for ReadTool {
    type Input = ReadInput;
    type Output = ReadOutput;
    const NAME: &'static str = "read";

    fn kind() -> ToolKind { ToolKind::Read }

    async fn run(
        self: Arc<Self>,
        input: ToolInput<Self::Input>,
        events: ToolEventSink,
        ctx: &ToolContext,
    ) -> Result<Self::Output, Self::Output> {
        let i = match input {
            ToolInput::Final(i) => i,
            _ => return Err(ReadOutput::err("streaming inputs not supported by read")),
        };

        events.emit(ToolEvent::Started {
            title: format!("Read {}", i.path).into(),
            kind: ToolKind::Read,
            location: Some(ToolLocation { path: i.path.clone(), line: i.line.map(|n| n as u32) }),
        });

        let req = ReadTextFileRequest { path: i.path.clone(), line: i.line, limit: i.limit };
        let res: ReadTextFileResponse = read_text_file(req, ctx.sandbox.as_ref()).await
            .map_err(|e| ReadOutput::err(e.to_string()))?;

        events.emit(ToolEvent::PartialOutput {
            content: ToolResultContent::text(res.content.clone()),
        });
        events.emit(ToolEvent::Completed);
        Ok(ReadOutput::Ok { content: res.content })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::{PermissionConfig, SandboxConfig, WhitelistConfig};
    use crate::permission::check::PermissionContext;
    use crate::permission::whitelist::CompiledWhitelist;
    use crate::tool::{ClientKind, ProtocolKind};
    use std::io::Write;
    use tempfile::TempDir;

    fn ctx_for(root: &std::path::Path) -> ToolContext {
        let wl = CompiledWhitelist::compile(&WhitelistConfig::default()).unwrap();
        let pc = PermissionContext::new("c".to_string(), None, wl);
        let mut sandbox = SandboxConfig::default();
        sandbox.allowed_roots = vec![root.to_path_buf()];
        ToolContext::for_test(
            "c", ClientKind::claude(), ProtocolKind::acp(),
            root.to_path_buf(), sandbox, PermissionConfig::default(), pc,
        )
    }

    #[tokio::test]
    async fn reads_file_through_trait() {
        let dir = TempDir::new().unwrap();
        let path = dir.path().join("hello.txt");
        std::fs::File::create(&path).unwrap().write_all(b"hi\nthere\n").unwrap();

        let tool = Arc::new(ReadTool);
        let (sink, mut rx) = ToolEventSink::new();
        let result = Tool::run(
            tool,
            ToolInput::Final(ReadInput {
                path: path.to_string_lossy().into_owned(),
                line: None, limit: None,
            }),
            sink,
            &ctx_for(dir.path()),
        ).await.unwrap();

        match result {
            ReadOutput::Ok { content } => {
                assert!(content.contains("hi"));
                assert!(content.contains("there"));
            }
            ReadOutput::Err { error } => panic!("expected ok, got err: {error}"),
        }

        // At least Started + PartialOutput + Completed should have fired.
        let mut count = 0;
        while let Ok(_ev) = rx.try_recv() { count += 1; }
        assert!(count >= 3, "expected >=3 events, got {count}");
    }

    #[tokio::test]
    async fn returns_structured_error_on_sandbox_violation() {
        let dir = TempDir::new().unwrap();
        let other = TempDir::new().unwrap();
        let outside = other.path().join("nope.txt");
        std::fs::File::create(&outside).unwrap().write_all(b"x").unwrap();

        let tool = Arc::new(ReadTool);
        let (sink, _rx) = ToolEventSink::new();
        let result = Tool::run(
            tool,
            ToolInput::Final(ReadInput {
                path: outside.to_string_lossy().into_owned(),
                line: None, limit: None,
            }),
            sink,
            &ctx_for(dir.path()),
        ).await.unwrap_err();

        match result {
            ReadOutput::Err { error } => assert!(!error.is_empty()),
            ReadOutput::Ok { .. } => panic!("expected sandbox error"),
        }
    }
}
