//! HTTP I/O endpoints — thin adapters over `service::file_io`.
use crate::http::context::{AppContext, OpenApiResult, bad_request};
use crate::service::file_io::{self as svc, FileEntry};
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
use serde::Deserialize;
use std::sync::Arc;

#[derive(Tags)]
enum IoTags {
    Io,
}

#[derive(Deserialize, Object)]
struct FilePathPayload {
    pub path: String,
}

#[derive(Deserialize, Object)]
struct WriteFilePayload {
    pub path: String,
    pub content: String,
}

#[derive(Deserialize, Object)]
struct SearchPayload {
    query: String,
}

#[derive(Deserialize, Object)]
struct CreateDirectoryPayload {
    pub path: String,
}

#[derive(Deserialize, Object)]
struct ExecShellPayload {
    pub command: String,
    pub args: Vec<String>,
}

pub struct IoApi {
    pub ctx: Arc<AppContext>,
}

#[OpenApi(tag = "IoTags::Io")]
impl IoApi {
    /// Read a file from the currently open project and return its contents.
    #[oai(path = "/io/fs/read", method = "post")]
    async fn read_file(&self, payload: Json<FilePathPayload>) -> OpenApiResult<Json<String>> {
        let content = svc::read_file(payload.0.path, &self.ctx.state)
            .await
            .map_err(|e| bad_request(e.to_string()))?;
        Ok(Json(content))
    }

    /// Write a file to the currently open project, creating parent directories if needed.
    #[oai(path = "/io/fs/write", method = "post")]
    async fn write_file(&self, payload: Json<WriteFilePayload>) -> OpenApiResult<Json<bool>> {
        svc::write_file(payload.0.path, payload.0.content, &self.ctx.state)
            .await
            .map_err(|e| bad_request(e.to_string()))?;
        Ok(Json(true))
    }

    /// List files and folders in a directory within the currently open project.
    #[oai(path = "/io/fs/list", method = "post")]
    async fn list_directory(
        &self,
        payload: Json<FilePathPayload>,
    ) -> OpenApiResult<Json<Vec<FileEntry>>> {
        let entries = svc::list_directory(payload.0.path, &self.ctx.state)
            .await
            .map_err(|e| bad_request(e.to_string()))?;
        Ok(Json(entries))
    }

    /// List files and folders at an absolute path (not scoped to the project root).
    #[oai(path = "/io/fs/list/absolute", method = "post")]
    async fn list_directory_absolute(
        &self,
        payload: Json<FilePathPayload>,
    ) -> OpenApiResult<Json<Vec<FileEntry>>> {
        let entries = svc::list_directory_absolute(payload.0.path)
            .await
            .map_err(|e| bad_request(e.to_string()))?;
        Ok(Json(entries))
    }

    /// Create a directory at an absolute path.
    #[oai(path = "/io/fs/create/absolute", method = "post")]
    async fn create_directory_absolute(
        &self,
        payload: Json<CreateDirectoryPayload>,
    ) -> OpenApiResult<Json<bool>> {
        svc::create_directory_absolute(payload.0.path)
            .await
            .map_err(|e| bad_request(e.to_string()))?;
        Ok(Json(true))
    }

    /// Get the user's home directory.
    #[oai(path = "/io/fs/home", method = "get")]
    async fn get_home_directory(&self) -> OpenApiResult<Json<String>> {
        let home = svc::get_home_directory().map_err(|e| bad_request(e.to_string()))?;
        Ok(Json(home))
    }

    /// List all files in the project recursively, respecting .gitignore.
    #[oai(path = "/io/fs/files", method = "get")]
    async fn list_project_files(&self) -> OpenApiResult<Json<Vec<String>>> {
        let files = svc::list_project_files(&self.ctx.state)
            .await
            .map_err(|e| bad_request(e.to_string()))?;
        Ok(Json(files))
    }

    /// Search the currently open project for files containing the provided query string.
    #[oai(path = "/io/search", method = "post")]
    async fn search_files(
        &self,
        payload: Json<SearchPayload>,
    ) -> OpenApiResult<Json<Vec<crate::service::file_io::SearchResult>>> {
        let results = svc::search_files(payload.0.query, &self.ctx.state)
            .await
            .map_err(|e| bad_request(e.to_string()))?;
        Ok(Json(results))
    }

    /// Execute an allowlisted shell command in the currently open project.
    #[oai(path = "/io/shell/exec", method = "post")]
    async fn exec_shell(
        &self,
        payload: Json<ExecShellPayload>,
    ) -> OpenApiResult<Json<crate::service::file_io::CommandOutput>> {
        let output = svc::exec_shell(payload.0.command, payload.0.args, &self.ctx.state)
            .await
            .map_err(|e| bad_request(e.to_string()))?;
        Ok(Json(output))
    }
}

#[cfg(test)]
impl From<std::sync::Arc<AppContext>> for IoApi {
    fn from(ctx: std::sync::Arc<AppContext>) -> Self {
        Self { ctx }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::http::test_helpers::make_api;
    use tempfile::TempDir;

    // --- list_directory_absolute ---

    #[tokio::test]
    async fn list_directory_absolute_returns_entries_for_valid_path() {
        let dir = TempDir::new().unwrap();
        std::fs::create_dir(dir.path().join("subdir")).unwrap();
        std::fs::write(dir.path().join("file.txt"), "content").unwrap();

        let api = make_api::<IoApi>(&dir);
        let payload = Json(FilePathPayload {
            path: dir.path().to_string_lossy().to_string(),
        });
        let result = api.list_directory_absolute(payload).await.unwrap();
        let entries = &result.0;

        assert!(entries.len() >= 2);
        assert!(
            entries
                .iter()
                .any(|e| e.name == "subdir" && e.kind == "dir")
        );
        assert!(
            entries
                .iter()
                .any(|e| e.name == "file.txt" && e.kind == "file")
        );
    }

    #[tokio::test]
    async fn list_directory_absolute_returns_empty_for_empty_dir() {
        let dir = TempDir::new().unwrap();
        let empty = dir.path().join("empty");
        std::fs::create_dir(&empty).unwrap();

        let api = make_api::<IoApi>(&dir);
        let payload = Json(FilePathPayload {
            path: empty.to_string_lossy().to_string(),
        });
        let result = api.list_directory_absolute(payload).await.unwrap();
        assert!(result.0.is_empty());
    }

    #[tokio::test]
    async fn list_directory_absolute_errors_on_nonexistent_path() {
        let dir = TempDir::new().unwrap();
        let api = make_api::<IoApi>(&dir);
        let payload = Json(FilePathPayload {
            path: dir.path().join("nonexistent").to_string_lossy().to_string(),
        });
        let result = api.list_directory_absolute(payload).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn list_directory_absolute_errors_on_file_path() {
        let dir = TempDir::new().unwrap();
        let file = dir.path().join("not_a_dir.txt");
        std::fs::write(&file, "content").unwrap();

        let api = make_api::<IoApi>(&dir);
        let payload = Json(FilePathPayload {
            path: file.to_string_lossy().to_string(),
        });
        let result = api.list_directory_absolute(payload).await;
        assert!(result.is_err());
    }

    // --- create_directory_absolute ---

    #[tokio::test]
    async fn create_directory_absolute_creates_new_dir() {
        let dir = TempDir::new().unwrap();
        let new_dir = dir.path().join("new_dir");

        let api = make_api::<IoApi>(&dir);
        let payload = Json(CreateDirectoryPayload {
            path: new_dir.to_string_lossy().to_string(),
        });
        let result = api.create_directory_absolute(payload).await.unwrap();
        assert!(result.0);
        assert!(new_dir.is_dir());
    }

    #[tokio::test]
    async fn create_directory_absolute_succeeds_for_existing_dir() {
        let dir = TempDir::new().unwrap();
        let existing = dir.path().join("existing");
        std::fs::create_dir(&existing).unwrap();

        let api = make_api::<IoApi>(&dir);
        let payload = Json(CreateDirectoryPayload {
            path: existing.to_string_lossy().to_string(),
        });
        let result = api.create_directory_absolute(payload).await.unwrap();
        assert!(result.0);
    }

    #[tokio::test]
    async fn create_directory_absolute_creates_nested_dirs() {
        let dir = TempDir::new().unwrap();
        let nested = dir.path().join("a").join("b").join("c");

        let api = make_api::<IoApi>(&dir);
        let payload = Json(CreateDirectoryPayload {
            path: nested.to_string_lossy().to_string(),
        });
        let result = api.create_directory_absolute(payload).await.unwrap();
        assert!(result.0);
        assert!(nested.is_dir());
    }

    // --- get_home_directory ---

    #[tokio::test]
    async fn get_home_directory_returns_a_path() {
        let dir = TempDir::new().unwrap();
        let api = make_api::<IoApi>(&dir);
        let result = api.get_home_directory().await.unwrap();
        let home = &result.0;
        assert!(!home.is_empty());
        assert!(std::path::Path::new(home).is_absolute());
    }

    // --- read_file (project-scoped) ---

    #[tokio::test]
    async fn read_file_returns_content() {
        let dir = TempDir::new().unwrap();
        std::fs::write(dir.path().join("hello.txt"), "hello world").unwrap();

        let api = make_api::<IoApi>(&dir);
        let payload = Json(FilePathPayload {
            path: "hello.txt".to_string(),
        });
        let result = api.read_file(payload).await.unwrap();
        assert_eq!(result.0, "hello world");
    }

    #[tokio::test]
    async fn read_file_errors_on_missing_file() {
        let dir = TempDir::new().unwrap();
        let api = make_api::<IoApi>(&dir);
        let payload = Json(FilePathPayload {
            path: "nonexistent.txt".to_string(),
        });
        let result = api.read_file(payload).await;
        assert!(result.is_err());
    }

    // --- write_file (project-scoped) ---

    #[tokio::test]
    async fn write_file_creates_file() {
        let dir = TempDir::new().unwrap();
        let api = make_api::<IoApi>(&dir);
        let payload = Json(WriteFilePayload {
            path: "output.txt".to_string(),
            content: "written content".to_string(),
        });
        let result = api.write_file(payload).await.unwrap();
        assert!(result.0);
        assert_eq!(
            std::fs::read_to_string(dir.path().join("output.txt")).unwrap(),
            "written content"
        );
    }

    #[tokio::test]
    async fn write_file_creates_parent_dirs() {
        let dir = TempDir::new().unwrap();
        let api = make_api::<IoApi>(&dir);
        let payload = Json(WriteFilePayload {
            path: "sub/dir/file.txt".to_string(),
            content: "nested".to_string(),
        });
        let result = api.write_file(payload).await.unwrap();
        assert!(result.0);
        assert_eq!(
            std::fs::read_to_string(dir.path().join("sub/dir/file.txt")).unwrap(),
            "nested"
        );
    }

    // --- list_project_files ---

    #[tokio::test]
    async fn list_project_files_returns_file_paths() {
        let dir = TempDir::new().unwrap();
        std::fs::create_dir(dir.path().join("src")).unwrap();
        std::fs::write(dir.path().join("src/main.rs"), "fn main() {}").unwrap();
        std::fs::write(dir.path().join("README.md"), "# readme").unwrap();

        let api = make_api::<IoApi>(&dir);
        let result = api.list_project_files().await.unwrap();
        let files = &result.0;

        assert!(files.contains(&"README.md".to_string()));
        assert!(files.contains(&"src/main.rs".to_string()));
    }

    #[tokio::test]
    async fn list_project_files_excludes_directories() {
        let dir = TempDir::new().unwrap();
        std::fs::create_dir(dir.path().join("subdir")).unwrap();
        std::fs::write(dir.path().join("file.txt"), "").unwrap();

        let api = make_api::<IoApi>(&dir);
        let result = api.list_project_files().await.unwrap();
        let files = &result.0;

        assert!(files.contains(&"file.txt".to_string()));
        // Directories should not appear
        assert!(!files.iter().any(|f| f == "subdir"));
    }

    #[tokio::test]
    async fn list_project_files_returns_sorted_paths() {
        let dir = TempDir::new().unwrap();
        std::fs::write(dir.path().join("z_last.txt"), "").unwrap();
        std::fs::write(dir.path().join("a_first.txt"), "").unwrap();

        let api = make_api::<IoApi>(&dir);
        let result = api.list_project_files().await.unwrap();
        let files = &result.0;

        let a_idx = files.iter().position(|f| f == "a_first.txt").unwrap();
        let z_idx = files.iter().position(|f| f == "z_last.txt").unwrap();
        assert!(a_idx < z_idx);
    }

    // --- list_directory (project-scoped) ---

    #[tokio::test]
    async fn list_directory_returns_entries() {
        let dir = TempDir::new().unwrap();
        std::fs::create_dir(dir.path().join("adir")).unwrap();
        std::fs::write(dir.path().join("bfile.txt"), "").unwrap();

        let api = make_api::<IoApi>(&dir);
        let payload = Json(FilePathPayload {
            path: ".".to_string(),
        });
        let result = api.list_directory(payload).await.unwrap();
        let entries = &result.0;

        assert!(entries.iter().any(|e| e.name == "adir" && e.kind == "dir"));
        assert!(
            entries
                .iter()
                .any(|e| e.name == "bfile.txt" && e.kind == "file")
        );
    }

    #[tokio::test]
    async fn list_directory_errors_on_nonexistent() {
        let dir = TempDir::new().unwrap();
        let api = make_api::<IoApi>(&dir);
        let payload = Json(FilePathPayload {
            path: "nonexistent_dir".to_string(),
        });
        let result = api.list_directory(payload).await;
        assert!(result.is_err());
    }
}
