use crate::APP_NAME;
use crate::sandbox::Sandbox;
use crate::tools::edit::{Edit, EditResult};
use crate::tools::info::InfoResult;
use crate::tools::read::{ReadMultipleResults, ReadResult};

use crate::tools::search::SearchResult;
use crate::tools::write::WriteResult;
use crate::tools::{edit, info, read, search, write};
use rmcp::ErrorData as McpError;
use rmcp::{
    ServerHandler,
    handler::server::{tool::ToolRouter, wrapper::Parameters},
    model::{
        CallToolResult, Content, Implementation, InitializeResult, ProtocolVersion,
        ServerCapabilities, ServerInfo,
    },
    schemars, tool, tool_handler, tool_router,
};
use serde::Deserialize;
use std::sync::Arc;

#[derive(Clone)]
pub struct FilesystemServer {
    tool_router: ToolRouter<Self>,
    ctx: Arc<AppContext>,
}

#[derive(Clone)]
pub struct AppContext {
    pub sandbox: Arc<Sandbox>,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct ReadFolderParameters {
    #[schemars(description = "Directory path whose immediate files and subdirectories are listed")]
    path: String,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SearchFolderParameters {
    #[schemars(description = "Root directory to search recursively")]
    path: String,
    #[schemars(description = "Glob pattern used to include matching files")]
    pattern: String,
    #[schemars(
        description = "List of glob patterns used to exclude files or directories from the search"
    )]
    exclude_pattern: Vec<String>,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct ReadFileParameters {
    #[schemars(description = "Filepath for reading a file")]
    path: String,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct ReadMultipleFileParameters {
    #[schemars(description = "Arrays of filenames to be read")]
    paths: Vec<String>,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct GetFileInfoParameters {
    #[schemars(description = "Filepath for get file info of")]
    path: String,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct CreateFileParameters {
    #[schemars(description = "Path for the new file")]
    path: String,
    #[schemars(description = "content for the new file")]
    content: String,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct CreateDirectoryParameter {
    #[schemars(description = "Path of new directory")]
    path: String,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct MoveFileParameters {
    #[schemars(description = "Source file path")]
    source: String,
    #[schemars(description = "Destination file path")]
    destination: String,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct EditFileParameters {
    #[schemars(description = "Source file path")]
    path: String,
    #[schemars(description = "Edits with old and new edits")]
    edits: Vec<Edit>,
    #[schemars(description = "Dry-run edit returns diff")]
    dry_run: bool,
}

// SERVER ROUTER
#[tool_router]
impl FilesystemServer {
    pub fn new(ctx: Arc<AppContext>) -> Self {
        Self {
            tool_router: Self::tool_router(),
            ctx,
        }
    }

    #[tool(description = "List files and subdirectories in a directory")]
    async fn list_directory(
        &self,
        Parameters(ReadFolderParameters { path }): Parameters<ReadFolderParameters>,
    ) -> Result<CallToolResult, McpError> {
        let result = match search::list_directory(&self.ctx.sandbox, &path).await {
            Ok(entries) => SearchResult {
                message: format!("Successfully listed directory: {}", path),
                entries,
                success: true,
            },
            Err(_) => SearchResult {
                message: format!("Error listing directories: {}", path),
                entries: vec![],
                success: false,
            },
        };

        let content = Content::json(&result).map_err(|e| {
            McpError::internal_error(
                format!("Error converting directory listing to JSON: {}", e),
                None,
            )
        })?;

        if result.success {
            Ok(CallToolResult::success(vec![content]))
        } else {
            Ok(CallToolResult::error(vec![content]))
        }
    }

    #[tool(description = "Recursively search for files under a directory matching glob patterns")]
    async fn search_files(
        &self,
        Parameters(SearchFolderParameters {
            path,
            pattern,
            exclude_pattern,
        }): Parameters<SearchFolderParameters>,
    ) -> Result<CallToolResult, McpError> {
        let result: SearchResult =
            match search::search_files(&self.ctx.sandbox, &path, &pattern, exclude_pattern).await {
                Ok(entries) => SearchResult {
                    message: format!("Search for path {}", path),
                    entries,
                    success: true,
                },
                Err(message) => SearchResult {
                    message: format!("Error creating path: {}", message),
                    entries: vec![],
                    success: false,
                },
            };

        let content = Content::json(&result).map_err(|e| {
            McpError::internal_error(
                format!("Error converting search results to JSON: {}", e),
                None,
            )
        })?;

        if result.success {
            Ok(CallToolResult::success(vec![content]))
        } else {
            Ok(CallToolResult::error(vec![content]))
        }
    }

    #[tool(description = "Read a file from a given filepath")]
    async fn read_file(
        &self,
        Parameters(ReadFileParameters { path }): Parameters<ReadFileParameters>,
    ) -> Result<CallToolResult, McpError> {
        let result: ReadResult = match read::read_file(&self.ctx.sandbox, &path).await {
            Ok(message) => ReadResult {
                message,
                success: true,
            },
            Err(message) => ReadResult {
                message: format!("Error reading file: {}", message),
                success: false,
            },
        };

        let content = Content::json(&result).map_err(|e| {
            McpError::internal_error(
                format!("Error converting file content to JSON: {}", e),
                None,
            )
        })?;

        if result.success {
            Ok(CallToolResult::success(vec![content]))
        } else {
            Ok(CallToolResult::error(vec![content]))
        }
    }

    #[tool(description = "Create or overwrite a file")]
    async fn write_file(
        &self,
        Parameters(CreateFileParameters { path, content }): Parameters<CreateFileParameters>,
    ) -> Result<CallToolResult, McpError> {
        let result: WriteResult = match write::write_file(&self.ctx.sandbox, &path, content).await {
            Ok(message) => WriteResult {
                message,
                success: true,
            },
            Err(message) => WriteResult {
                message: format!("Error writing file: {}", message),
                success: false,
            },
        };

        let content = Content::json(&result).map_err(|e| {
            McpError::internal_error(
                format!("Error converting file content to JSON: {}", e),
                None,
            )
        })?;

        if result.success {
            Ok(CallToolResult::success(vec![content]))
        } else {
            Ok(CallToolResult::error(vec![content]))
        }
    }

    #[tool(description = "Edit file with dry run")]
    async fn edit_file(
        &self,
        Parameters(EditFileParameters {
            path,
            edits,
            dry_run,
        }): Parameters<EditFileParameters>,
    ) -> Result<CallToolResult, McpError> {
        let result = match edit::edit_file(&self.ctx.sandbox, &path, edits, dry_run).await {
            Ok(edits) => EditResult {
                message: format!("Edits run successfully, dry run: {}", dry_run),
                edits: Some(edits),
                success: true,
            },
            Err(_edits) => EditResult {
                message: "Error applying edits".to_string(),
                edits: None,
                success: false,
            },
        };

        let content = Content::json(&result).map_err(|e| {
            McpError::internal_error(
                format!("Error converting file content to JSON: {}", e),
                None,
            )
        })?;

        if result.success {
            Ok(CallToolResult::success(vec![content]))
        } else {
            Ok(CallToolResult::error(vec![content]))
        }
    }

    #[tool(description = "Move a file from a source path to destination path")]
    async fn move_file(
        &self,
        Parameters(MoveFileParameters {
            source,
            destination,
        }): Parameters<MoveFileParameters>,
    ) -> Result<CallToolResult, McpError> {
        let result: WriteResult =
            match write::move_file(&self.ctx.sandbox, &source, &destination).await {
                Ok(message) => WriteResult {
                    message,
                    success: true,
                },
                Err(message) => WriteResult {
                    message: format!("Error moving file: {}", message),
                    success: false,
                },
            };
        let content = Content::json(&result).map_err(|e| {
            McpError::internal_error(
                format!("Error converting file content to JSON: {}", e),
                None,
            )
        })?;

        if result.success {
            Ok(CallToolResult::success(vec![content]))
        } else {
            Ok(CallToolResult::error(vec![content]))
        }
    }

    #[tool(description = "Create new directory")]
    async fn create_directory(
        &self,
        Parameters(CreateDirectoryParameter { path }): Parameters<CreateDirectoryParameter>,
    ) -> Result<CallToolResult, McpError> {
        let result: WriteResult = match write::create_directory(&self.ctx.sandbox, &path).await {
            Ok(message) => WriteResult {
                message,
                success: true,
            },
            Err(message) => WriteResult {
                message: format!("Error creating path: {}", message),
                success: false,
            },
        };

        let content = Content::json(&result).map_err(|e| {
            McpError::internal_error(
                format!("Error converting file content to JSON: {}", e),
                None,
            )
        })?;

        if result.success {
            Ok(CallToolResult::success(vec![content]))
        } else {
            Ok(CallToolResult::error(vec![content]))
        }
    }

    #[tool(description = "Read several files from a list of filepaths")]
    async fn read_multiple_files(
        &self,
        Parameters(ReadMultipleFileParameters { paths }): Parameters<ReadMultipleFileParameters>,
    ) -> Result<CallToolResult, McpError> {
        let result: ReadMultipleResults =
            match read::read_multiple_files(&self.ctx.sandbox, paths).await {
                Ok(entries) => ReadMultipleResults {
                    message: "Read files successfully.".to_string(),
                    entries,
                    success: true,
                },
                Err(message) => ReadMultipleResults {
                    message: format!("Error reading files: {}", message),
                    entries: vec![],
                    success: false,
                },
            };

        let content = Content::json(&result).map_err(|e| {
            McpError::internal_error(
                format!("Error converting multiple file contents to JSON: {}", e),
                None,
            )
        })?;

        if result.success {
            Ok(CallToolResult::success(vec![content]))
        } else {
            Ok(CallToolResult::error(vec![content]))
        }
    }

    #[tool(
        description = "Return metadata for a given file path, including size, permissions, creation time, and last modified time"
    )]
    async fn get_file_info(
        &self,
        Parameters(GetFileInfoParameters { path }): Parameters<GetFileInfoParameters>,
    ) -> Result<CallToolResult, McpError> {
        let result = match info::get_file_info(&self.ctx.sandbox, &path).await {
            Ok(metadata) => InfoResult {
                message: "Retrieved file info successfully.".to_string(),
                metadata: Some(metadata),
                success: true,
            },
            Err(err) => InfoResult {
                message: format!("Error getting file info: {}", err),
                metadata: None,
                success: false,
            },
        };

        let content = Content::json(&result).map_err(|e| {
            McpError::internal_error(
                format!("Error converting file metadata to JSON: {}", e),
                None,
            )
        })?;

        if result.success {
            Ok(CallToolResult::success(vec![content]))
        } else {
            Ok(CallToolResult::error(vec![content]))
        }
    }

    #[tool(description = "Reveal sandbox roots")]
    async fn list_allowed_directories(&self) -> Result<CallToolResult, McpError> {
        tracing::info!("List allowed directories");
        let roots = self.ctx.sandbox.get_roots();
        let content = Content::json(&roots).map_err(|e| {
            McpError::internal_error(format!("Error converting roots to JSON: {}", e), None)
        })?;
        tracing::info!("Success: Allowed directories {:?}", roots);
        Ok(CallToolResult::success(vec![content]))
    }
}

#[tool_handler]
impl ServerHandler for FilesystemServer {
    fn get_info(&self) -> ServerInfo {
        ServerInfo {
            protocol_version: ProtocolVersion::V_2025_06_18,
            capabilities: ServerCapabilities::builder().enable_tools().build(),
            server_info: Implementation::from_build_env(),
            instructions: Some(
                "I manage a filesystem sandbox. Available actions:
                - list_directory
                - search_files
                - read_file
                - move_file
                - read_multiple_files
                - get_file_info
                - write_file
                - edit_file
                - create_directory
                - list_allowed_directories"
                    .to_string(),
            ),
        }
    }

    async fn initialize(
        &self,
        _request: rmcp::model::InitializeRequestParams,
        _context: rmcp::service::RequestContext<rmcp::RoleServer>,
    ) -> Result<InitializeResult, McpError> {
        tracing::info!("Client connected to {}", APP_NAME);
        Ok(self.get_info())
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;
    use tempfile::TempDir;

    async fn setup_sandbox(temp_dir: &TempDir) -> Arc<Sandbox> {
        Arc::new(
            Sandbox::new(vec![temp_dir.path().to_string_lossy().to_string()])
                .await
                .expect("sandbox init failed"),
        )
    }

    fn create_app_context(sandbox: Arc<Sandbox>) -> Arc<AppContext> {
        Arc::new(AppContext { sandbox })
    }

    async fn create_test_server(temp_dir: &TempDir) -> FilesystemServer {
        let sandbox = setup_sandbox(temp_dir).await;
        let ctx = create_app_context(sandbox);
        FilesystemServer::new(ctx)
    }

    #[tokio::test]
    async fn test_server_info() {
        let temp_dir = tempfile::tempdir().unwrap();
        let server = create_test_server(&temp_dir).await;

        let info = server.get_info();
        assert!(info.instructions.is_some());
        let instr = info.instructions.unwrap();
        assert!(instr.contains("list_directory"));
        assert!(instr.contains("read_file"));
        assert!(instr.contains("write_file"));
    }

    #[tokio::test]
    async fn test_list_directory_and_allowed_dirs() {
        let temp_dir = tempfile::tempdir().unwrap();
        let server = create_test_server(&temp_dir).await;

        // Create file & subdir
        let file = temp_dir.path().join("file.txt");
        let subdir = temp_dir.path().join("subdir");
        std::fs::write(&file, "content").unwrap();
        std::fs::create_dir(&subdir).unwrap();

        // List directory
        let result = server
            .list_directory(Parameters(ReadFolderParameters {
                path: temp_dir.path().to_string_lossy().to_string(),
            }))
            .await;
        assert!(result.is_ok());

        // List sandbox roots
        let roots = server.list_allowed_directories().await;
        assert!(roots.is_ok());
    }

    #[tokio::test]
    async fn test_write_read_and_edit_file() {
        let temp_dir = tempfile::tempdir().unwrap();
        let server = create_test_server(&temp_dir).await;

        let file_path = temp_dir.path().join("file.txt");
        let path_str = file_path.to_string_lossy().to_string();

        // Write file
        server
            .write_file(Parameters(CreateFileParameters {
                path: path_str.clone(),
                content: "line1\nline2".to_string(),
            }))
            .await
            .unwrap();

        // Read file
        let read_result = server
            .read_file(Parameters(ReadFileParameters {
                path: path_str.clone(),
            }))
            .await;
        assert!(read_result.is_ok());

        // Apply edit
        let edits = vec![Edit {
            old: "line2".to_string(),
            new: "modified_line2".to_string(),
        }];
        server
            .edit_file(Parameters(EditFileParameters {
                path: path_str.clone(),
                edits: edits.clone(),
                dry_run: false,
            }))
            .await
            .unwrap();

        let content = std::fs::read_to_string(&file_path).unwrap();
        assert!(content.contains("modified_line2"));

        // Dry-run edit
        server
            .edit_file(Parameters(EditFileParameters {
                path: path_str.clone(),
                edits,
                dry_run: true,
            }))
            .await
            .unwrap();
    }

    #[tokio::test]
    async fn test_create_move_directory_and_file() {
        let temp_dir = tempfile::tempdir().unwrap();
        let server = create_test_server(&temp_dir).await;

        let nested_dir = temp_dir.path().join("a/b/c");
        server
            .create_directory(Parameters(CreateDirectoryParameter {
                path: nested_dir.to_string_lossy().to_string(),
            }))
            .await
            .unwrap();
        assert!(nested_dir.exists());

        let source = nested_dir.join("source.txt");
        let dest = temp_dir.path().join("dest.txt");
        std::fs::write(&source, "content").unwrap();

        server
            .move_file(Parameters(MoveFileParameters {
                source: source.to_string_lossy().to_string(),
                destination: dest.to_string_lossy().to_string(),
            }))
            .await
            .unwrap();

        assert!(!source.exists());
        assert!(dest.exists());
    }

    #[tokio::test]
    async fn test_get_file_info_and_search() {
        let temp_dir = tempfile::tempdir().unwrap();
        let server = create_test_server(&temp_dir).await;

        // File for info
        let file_path = temp_dir.path().join("file.txt");
        std::fs::write(&file_path, "content").unwrap();

        let info_res = server
            .get_file_info(Parameters(GetFileInfoParameters {
                path: file_path.to_string_lossy().to_string(),
            }))
            .await;
        assert!(info_res.is_ok());

        // Search files
        let nested = temp_dir.path().join("nested");
        std::fs::create_dir(&nested).unwrap();
        std::fs::write(nested.join("b.txt"), "2").unwrap();

        let search_res = server
            .search_files(Parameters(SearchFolderParameters {
                path: temp_dir.path().to_string_lossy().to_string(),
                pattern: "*.txt".to_string(),
                exclude_pattern: vec![],
            }))
            .await;
        assert!(search_res.is_ok());
    }

    #[tokio::test]
    async fn test_read_multiple_files() {
        let temp_dir = tempfile::tempdir().unwrap();
        let server = create_test_server(&temp_dir).await;

        let file1 = temp_dir.path().join("file1.txt");
        let file2 = temp_dir.path().join("file2.txt");
        std::fs::write(&file1, "1").unwrap();
        std::fs::write(&file2, "2").unwrap();

        let paths = vec![
            file1.to_string_lossy().to_string(),
            file2.to_string_lossy().to_string(),
        ];
        let res = server
            .read_multiple_files(Parameters(ReadMultipleFileParameters { paths }))
            .await;
        assert!(res.is_ok());
    }

    // ----------------------------
    // ERROR PATH TESTS
    // ----------------------------

    #[tokio::test]
    async fn test_invalid_paths() {
        let temp_dir = tempfile::tempdir().unwrap();
        let server = create_test_server(&temp_dir).await;

        // Read non-existent
        let r = server
            .read_file(Parameters(ReadFileParameters {
                path: "/nonexistent/file.txt".to_string(),
            }))
            .await
            .unwrap();
        assert!(r.is_error.unwrap());

        // Move non-existent
        let m = server
            .move_file(Parameters(MoveFileParameters {
                source: "/nonexistent/source.txt".to_string(),
                destination: "/nonexistent/dest.txt".to_string(),
            }))
            .await
            .unwrap();
        assert!(m.is_error.unwrap());

        // Edit non-existent
        let e = server
            .edit_file(Parameters(EditFileParameters {
                path: "/nonexistent.txt".to_string(),
                edits: vec![Edit {
                    old: "a".to_string(),
                    new: "b".to_string(),
                }],
                dry_run: false,
            }))
            .await
            .unwrap();
        assert!(e.is_error.unwrap());

        // Search invalid
        let s = server
            .search_files(Parameters(SearchFolderParameters {
                path: "/invalid/path".to_string(),
                pattern: "*.txt".to_string(),
                exclude_pattern: vec![],
            }))
            .await
            .unwrap();
        assert!(s.is_error.unwrap());
    }
}
