use crate::http::context::{AppContext, OpenApiResult, bad_request};
use crate::store::StoreOps;
use poem_openapi::{Object, OpenApi, Tags, param::Query, payload::Json};
use serde::Serialize;
use serde_json::json;
use std::sync::Arc;

const EDITOR_COMMAND_KEY: &str = "editor_command";

#[derive(Tags)]
enum SettingsTags {
    Settings,
}

#[derive(Object)]
struct EditorCommandPayload {
    editor_command: Option<String>,
}

#[derive(Object, Serialize)]
struct EditorCommandResponse {
    editor_command: Option<String>,
}

#[derive(Debug, Object, Serialize)]
struct OpenFileResponse {
    success: bool,
}

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

#[OpenApi(tag = "SettingsTags::Settings")]
impl SettingsApi {
    /// Get the configured editor command (e.g. "zed", "code", "cursor"), or null if not set.
    #[oai(path = "/settings/editor", method = "get")]
    async fn get_editor(&self) -> OpenApiResult<Json<EditorCommandResponse>> {
        let editor_command = self
            .ctx
            .store
            .get(EDITOR_COMMAND_KEY)
            .and_then(|v| v.as_str().map(|s| s.to_string()));
        Ok(Json(EditorCommandResponse { editor_command }))
    }

    /// Open a file in the configured editor at the given line number.
    ///
    /// Invokes the stored editor CLI (e.g. "zed", "code") with `path:line` as the argument.
    /// Returns an error if no editor is configured or if the process fails to spawn.
    #[oai(path = "/settings/open-file", method = "post")]
    async fn open_file(
        &self,
        path: Query<String>,
        line: Query<Option<u32>>,
    ) -> OpenApiResult<Json<OpenFileResponse>> {
        let editor_command = get_editor_command_from_store(&self.ctx)
            .ok_or_else(|| bad_request("No editor configured".to_string()))?;

        let file_ref = match line.0 {
            Some(l) => format!("{}:{}", path.0, l),
            None => path.0.clone(),
        };

        std::process::Command::new(&editor_command)
            .arg(&file_ref)
            .spawn()
            .map_err(|e| bad_request(format!("Failed to open editor: {e}")))?;

        Ok(Json(OpenFileResponse { success: true }))
    }

    /// Set the preferred editor command (e.g. "zed", "code", "cursor").
    /// Pass null or empty string to clear the preference.
    #[oai(path = "/settings/editor", method = "put")]
    async fn set_editor(
        &self,
        payload: Json<EditorCommandPayload>,
    ) -> OpenApiResult<Json<EditorCommandResponse>> {
        let editor_command = payload.0.editor_command;
        let trimmed = editor_command.as_deref().map(str::trim).filter(|s| !s.is_empty());
        match trimmed {
            Some(cmd) => {
                self.ctx.store.set(EDITOR_COMMAND_KEY, json!(cmd));
                self.ctx.store.save().map_err(bad_request)?;
                Ok(Json(EditorCommandResponse {
                    editor_command: Some(cmd.to_string()),
                }))
            }
            None => {
                self.ctx.store.delete(EDITOR_COMMAND_KEY);
                self.ctx.store.save().map_err(bad_request)?;
                Ok(Json(EditorCommandResponse {
                    editor_command: None,
                }))
            }
        }
    }
}

pub fn get_editor_command_from_store(ctx: &AppContext) -> Option<String> {
    ctx.store
        .get(EDITOR_COMMAND_KEY)
        .and_then(|v| v.as_str().map(|s| s.to_string()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::http::context::AppContext;
    use std::sync::Arc;
    use tempfile::TempDir;

    fn test_ctx(dir: &TempDir) -> AppContext {
        AppContext::new_test(dir.path().to_path_buf())
    }

    fn make_api(dir: &TempDir) -> SettingsApi {
        SettingsApi {
            ctx: Arc::new(AppContext::new_test(dir.path().to_path_buf())),
        }
    }

    #[tokio::test]
    async fn get_editor_returns_none_when_unset() {
        let dir = TempDir::new().unwrap();
        let api = make_api(&dir);
        let result = api.get_editor().await.unwrap();
        assert!(result.0.editor_command.is_none());
    }

    #[tokio::test]
    async fn set_editor_stores_command() {
        let dir = TempDir::new().unwrap();
        let api = make_api(&dir);
        let payload = Json(EditorCommandPayload {
            editor_command: Some("zed".to_string()),
        });
        let result = api.set_editor(payload).await.unwrap();
        assert_eq!(result.0.editor_command, Some("zed".to_string()));
    }

    #[tokio::test]
    async fn set_editor_clears_command_on_null() {
        let dir = TempDir::new().unwrap();
        let api = make_api(&dir);
        api.set_editor(Json(EditorCommandPayload {
            editor_command: Some("zed".to_string()),
        }))
        .await
        .unwrap();
        let result = api
            .set_editor(Json(EditorCommandPayload {
                editor_command: None,
            }))
            .await
            .unwrap();
        assert!(result.0.editor_command.is_none());
    }

    #[tokio::test]
    async fn set_editor_clears_command_on_empty_string() {
        let dir = TempDir::new().unwrap();
        let api = make_api(&dir);
        let result = api
            .set_editor(Json(EditorCommandPayload {
                editor_command: Some(String::new()),
            }))
            .await
            .unwrap();
        assert!(result.0.editor_command.is_none());
    }

    #[tokio::test]
    async fn set_editor_trims_whitespace_only() {
        let dir = TempDir::new().unwrap();
        let api = make_api(&dir);
        let result = api
            .set_editor(Json(EditorCommandPayload {
                editor_command: Some("   ".to_string()),
            }))
            .await
            .unwrap();
        assert!(result.0.editor_command.is_none());
    }

    #[tokio::test]
    async fn get_editor_returns_value_after_set() {
        let dir = TempDir::new().unwrap();
        let api = make_api(&dir);
        api.set_editor(Json(EditorCommandPayload {
            editor_command: Some("cursor".to_string()),
        }))
        .await
        .unwrap();
        let result = api.get_editor().await.unwrap();
        assert_eq!(result.0.editor_command, Some("cursor".to_string()));
    }

    #[test]
    fn editor_command_defaults_to_null() {
        let dir = TempDir::new().unwrap();
        let ctx = test_ctx(&dir);
        let result = get_editor_command_from_store(&ctx);
        assert!(result.is_none());
    }

    #[test]
    fn set_editor_command_persists_in_store() {
        let dir = TempDir::new().unwrap();
        let ctx = test_ctx(&dir);

        ctx.store.set(EDITOR_COMMAND_KEY, json!("zed"));
        ctx.store.save().unwrap();

        let result = get_editor_command_from_store(&ctx);
        assert_eq!(result, Some("zed".to_string()));
    }

    #[test]
    fn get_editor_command_from_store_returns_value() {
        let dir = TempDir::new().unwrap();
        let ctx = test_ctx(&dir);

        ctx.store.set(EDITOR_COMMAND_KEY, json!("code"));
        let result = get_editor_command_from_store(&ctx);
        assert_eq!(result, Some("code".to_string()));
    }

    #[test]
    fn delete_editor_command_returns_none() {
        let dir = TempDir::new().unwrap();
        let ctx = test_ctx(&dir);

        ctx.store.set(EDITOR_COMMAND_KEY, json!("cursor"));
        ctx.store.delete(EDITOR_COMMAND_KEY);
        let result = get_editor_command_from_store(&ctx);
        assert!(result.is_none());
    }

    #[test]
    fn editor_command_survives_reload() {
        let dir = TempDir::new().unwrap();
        let store_path = dir.path().join(".storkit_store.json");

        {
            let ctx = AppContext::new_test(dir.path().to_path_buf());
            ctx.store.set(EDITOR_COMMAND_KEY, json!("zed"));
            ctx.store.save().unwrap();
        }

        // Reload from disk
        let store2 = crate::store::JsonFileStore::new(store_path).unwrap();
        let val = store2.get(EDITOR_COMMAND_KEY);
        assert_eq!(val, Some(json!("zed")));
    }

    #[tokio::test]
    async fn get_editor_http_handler_returns_null_when_not_set() {
        let dir = TempDir::new().unwrap();
        let ctx = test_ctx(&dir);
        let api = SettingsApi {
            ctx: Arc::new(ctx),
        };
        let result = api.get_editor().await.unwrap().0;
        assert!(result.editor_command.is_none());
    }

    #[tokio::test]
    async fn set_editor_http_handler_stores_value() {
        let dir = TempDir::new().unwrap();
        let ctx = test_ctx(&dir);
        let api = SettingsApi {
            ctx: Arc::new(ctx),
        };
        let result = api
            .set_editor(Json(EditorCommandPayload {
                editor_command: Some("zed".to_string()),
            }))
            .await
            .unwrap()
            .0;
        assert_eq!(result.editor_command, Some("zed".to_string()));
    }

    #[tokio::test]
    async fn set_editor_http_handler_clears_value_when_null() {
        let dir = TempDir::new().unwrap();
        let ctx = test_ctx(&dir);
        let api = SettingsApi {
            ctx: Arc::new(ctx),
        };
        // First set a value
        api.set_editor(Json(EditorCommandPayload {
            editor_command: Some("code".to_string()),
        }))
        .await
        .unwrap();
        // Now clear it
        let result = api
            .set_editor(Json(EditorCommandPayload {
                editor_command: None,
            }))
            .await
            .unwrap()
            .0;
        assert!(result.editor_command.is_none());
    }

    #[tokio::test]
    async fn open_file_returns_error_when_no_editor_configured() {
        let dir = TempDir::new().unwrap();
        let api = make_api(&dir);
        let result = api
            .open_file(Query("src/main.rs".to_string()), Query(Some(42)))
            .await;
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(err.status(), poem::http::StatusCode::BAD_REQUEST);
    }

    #[tokio::test]
    async fn open_file_spawns_editor_with_path_and_line() {
        let dir = TempDir::new().unwrap();
        let api = make_api(&dir);
        // Configure the editor to "echo" which is a safe no-op command
        api.set_editor(Json(EditorCommandPayload {
            editor_command: Some("echo".to_string()),
        }))
        .await
        .unwrap();
        let result = api
            .open_file(Query("src/main.rs".to_string()), Query(Some(42)))
            .await
            .unwrap();
        assert!(result.0.success);
    }

    #[tokio::test]
    async fn open_file_spawns_editor_with_path_only_when_no_line() {
        let dir = TempDir::new().unwrap();
        let api = make_api(&dir);
        api.set_editor(Json(EditorCommandPayload {
            editor_command: Some("echo".to_string()),
        }))
        .await
        .unwrap();
        let result = api
            .open_file(Query("src/lib.rs".to_string()), Query(None))
            .await
            .unwrap();
        assert!(result.0.success);
    }

    #[tokio::test]
    async fn open_file_returns_error_for_nonexistent_editor() {
        let dir = TempDir::new().unwrap();
        let api = make_api(&dir);
        api.set_editor(Json(EditorCommandPayload {
            editor_command: Some("this_editor_does_not_exist_xyz_abc".to_string()),
        }))
        .await
        .unwrap();
        let result = api
            .open_file(Query("src/main.rs".to_string()), Query(Some(1)))
            .await;
        assert!(result.is_err());
    }
}
