//! HTTP project endpoints — thin adapters over `service::project`.
use crate::http::context::{AppContext, OpenApiResult, bad_request, not_found};
use crate::service::project::{self as svc, Error as ProjectError};
use poem::http::StatusCode;
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
use serde::Deserialize;
use std::sync::Arc;

#[derive(Tags)]
enum ProjectTags {
    Project,
}

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

/// Map a typed [`ProjectError`] to a `poem::Error` with the appropriate HTTP status.
fn map_project_error(e: ProjectError) -> poem::Error {
    match e {
        ProjectError::PathNotFound(msg) => not_found(msg),
        ProjectError::NotADirectory(msg) => bad_request(msg),
        ProjectError::Internal(msg) => {
            poem::Error::from_string(msg, StatusCode::INTERNAL_SERVER_ERROR)
        }
    }
}

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

#[OpenApi(tag = "ProjectTags::Project")]
impl ProjectApi {
    /// Get the currently open project path (if any).
    ///
    /// Returns null when no project is open.
    #[oai(path = "/project", method = "get")]
    async fn get_current_project(&self) -> OpenApiResult<Json<Option<String>>> {
        let result = svc::get_current_project(&self.ctx.state, self.ctx.store.as_ref())
            .map_err(map_project_error)?;
        Ok(Json(result))
    }

    /// Open a project and set it as the current project.
    ///
    /// Persists the selected path for later sessions.
    #[oai(path = "/project", method = "post")]
    async fn open_project(&self, payload: Json<PathPayload>) -> OpenApiResult<Json<String>> {
        let confirmed = svc::open_project(
            payload.0.path,
            &self.ctx.state,
            self.ctx.store.as_ref(),
            self.ctx.services.agents.port(),
        )
        .await
        .map_err(map_project_error)?;
        Ok(Json(confirmed))
    }

    /// Close the current project and clear the stored selection.
    #[oai(path = "/project", method = "delete")]
    async fn close_project(&self) -> OpenApiResult<Json<bool>> {
        // TRACE:MERGE-DEBUG — remove once root cause is found
        crate::slog_error!(
            "[MERGE-DEBUG] DELETE /project called! \
             Backtrace: this is the only code path that clears project_root."
        );
        svc::close_project(&self.ctx.state, self.ctx.store.as_ref()).map_err(map_project_error)?;
        Ok(Json(true))
    }

    /// List known projects from the store.
    #[oai(path = "/projects", method = "get")]
    async fn list_known_projects(&self) -> OpenApiResult<Json<Vec<String>>> {
        let projects =
            svc::get_known_projects(self.ctx.store.as_ref()).map_err(map_project_error)?;
        Ok(Json(projects))
    }

    /// Forget a known project path.
    #[oai(path = "/projects/forget", method = "post")]
    async fn forget_known_project(&self, payload: Json<PathPayload>) -> OpenApiResult<Json<bool>> {
        svc::forget_known_project(payload.0.path, self.ctx.store.as_ref())
            .map_err(map_project_error)?;
        Ok(Json(true))
    }
}

#[cfg(test)]
impl From<std::sync::Arc<AppContext>> for ProjectApi {
    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;

    #[tokio::test]
    async fn get_current_project_returns_none_when_unset() {
        let dir = TempDir::new().unwrap();
        let api = make_api::<ProjectApi>(&dir);
        // Clear the project root that new_test sets
        api.close_project().await.unwrap();
        let result = api.get_current_project().await.unwrap();
        assert!(result.0.is_none());
    }

    #[tokio::test]
    async fn get_current_project_returns_path_from_state() {
        let dir = TempDir::new().unwrap();
        let api = make_api::<ProjectApi>(&dir);
        let result = api.get_current_project().await.unwrap();
        assert_eq!(result.0, Some(dir.path().to_string_lossy().to_string()));
    }

    #[tokio::test]
    async fn open_project_succeeds_with_valid_directory() {
        let dir = TempDir::new().unwrap();
        let api = make_api::<ProjectApi>(&dir);
        let path = dir.path().to_string_lossy().to_string();
        let payload = Json(PathPayload { path: path.clone() });
        let result = api.open_project(payload).await.unwrap();
        assert_eq!(result.0, path);
    }

    #[tokio::test]
    async fn open_project_fails_with_nonexistent_file_path() {
        let dir = TempDir::new().unwrap();
        let api = make_api::<ProjectApi>(&dir);
        // Create a file (not a directory) to trigger validation error
        let file_path = dir.path().join("not_a_dir.txt");
        std::fs::write(&file_path, "content").unwrap();
        let payload = Json(PathPayload {
            path: file_path.to_string_lossy().to_string(),
        });
        let result = api.open_project(payload).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn close_project_returns_true() {
        let dir = TempDir::new().unwrap();
        let api = make_api::<ProjectApi>(&dir);
        let result = api.close_project().await.unwrap();
        assert!(result.0);
    }

    #[tokio::test]
    async fn close_project_clears_current_project() {
        let dir = TempDir::new().unwrap();
        let api = make_api::<ProjectApi>(&dir);

        // Verify project is set initially
        let before = api.get_current_project().await.unwrap();
        assert!(before.0.is_some());

        // Close the project
        api.close_project().await.unwrap();

        // Verify project is now None
        let after = api.get_current_project().await.unwrap();
        assert!(after.0.is_none());
    }

    #[tokio::test]
    async fn list_known_projects_returns_empty_initially() {
        let dir = TempDir::new().unwrap();
        let api = make_api::<ProjectApi>(&dir);
        // Close the project so the store has no known projects
        api.close_project().await.unwrap();
        let result = api.list_known_projects().await.unwrap();
        assert!(result.0.is_empty());
    }

    #[tokio::test]
    async fn list_known_projects_returns_project_after_open() {
        let dir = TempDir::new().unwrap();
        let api = make_api::<ProjectApi>(&dir);
        let path = dir.path().to_string_lossy().to_string();

        api.open_project(Json(PathPayload { path: path.clone() }))
            .await
            .unwrap();

        let result = api.list_known_projects().await.unwrap();
        assert!(result.0.contains(&path));
    }

    #[tokio::test]
    async fn forget_known_project_removes_project() {
        let dir = TempDir::new().unwrap();
        let api = make_api::<ProjectApi>(&dir);
        let path = dir.path().to_string_lossy().to_string();

        api.open_project(Json(PathPayload { path: path.clone() }))
            .await
            .unwrap();

        let before = api.list_known_projects().await.unwrap();
        assert!(before.0.contains(&path));

        let result = api
            .forget_known_project(Json(PathPayload { path: path.clone() }))
            .await
            .unwrap();
        assert!(result.0);

        let after = api.list_known_projects().await.unwrap();
        assert!(!after.0.contains(&path));
    }

    #[tokio::test]
    async fn forget_known_project_returns_true_for_nonexistent_path() {
        let dir = TempDir::new().unwrap();
        let api = make_api::<ProjectApi>(&dir);
        let result = api
            .forget_known_project(Json(PathPayload {
                path: "/some/unknown/path".to_string(),
            }))
            .await
            .unwrap();
        assert!(result.0);
    }
}
