//! ActionResultModel — unified result type for all Action executions.
//!
//! Rust struct exposed to Python via PyO3.

#[cfg(feature = "python-bindings")]
use pyo3::prelude::*;
#[cfg(feature = "python-bindings")]
use pyo3::types::PyDict;

#[cfg(feature = "python-bindings")]
use dcc_mcp_utils::py_json::{json_value_to_bound_py, py_any_to_json_value, py_dict_to_json_map};

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Internal Rust data representation (serde-friendly).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct ActionResultModelData {
    /// Whether the action completed successfully.
    pub success: bool,
    /// Human-readable result or error summary.
    pub message: String,
    /// Optional prompt/hint for the next user action.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub prompt: Option<String>,
    /// Optional error message when `success` is `false`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    /// Arbitrary key-value context data (e.g. traceback, error_type).
    #[serde(default)]
    pub context: HashMap<String, serde_json::Value>,
}

// Manual impl: `success` defaults to `true` (unlike `bool::default()` which is `false`),
// matching the Python `ActionResultModel.__new__` signature.
impl Default for ActionResultModelData {
    fn default() -> Self {
        Self {
            success: true,
            message: String::new(),
            prompt: None,
            error: None,
            context: HashMap::new(),
        }
    }
}

impl ActionResultModelData {
    /// Create a success result with context.
    #[must_use]
    pub fn success(
        message: String,
        prompt: Option<String>,
        context: HashMap<String, serde_json::Value>,
    ) -> Self {
        Self {
            success: true,
            message,
            prompt,
            error: None,
            context,
        }
    }

    /// Create a failure result with context.
    #[must_use]
    pub fn failure(
        message: String,
        error: Option<String>,
        prompt: Option<String>,
        context: HashMap<String, serde_json::Value>,
    ) -> Self {
        Self {
            success: false,
            message,
            prompt,
            error,
            context,
        }
    }
}

/// Python-facing ActionResultModel.
#[cfg_attr(
    feature = "python-bindings",
    pyclass(name = "ActionResultModel", eq, from_py_object)
)]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ActionResultModel {
    pub(crate) inner: ActionResultModelData,
}

#[cfg(feature = "python-bindings")]
#[pymethods]
impl ActionResultModel {
    #[new]
    #[pyo3(signature = (success=true, message="".to_string(), prompt=None, error=None, context=None))]
    fn new(
        success: bool,
        message: String,
        prompt: Option<String>,
        error: Option<String>,
        context: Option<&Bound<'_, PyDict>>,
    ) -> PyResult<Self> {
        let ctx = if let Some(dict) = context {
            py_dict_to_json_map(dict)?
        } else {
            HashMap::new()
        };
        Ok(Self {
            inner: ActionResultModelData {
                success,
                message,
                prompt,
                error,
                context: ctx,
            },
        })
    }

    #[getter]
    fn success(&self) -> bool {
        self.inner.success
    }

    #[getter]
    fn message(&self) -> &str {
        &self.inner.message
    }

    #[setter]
    fn set_message(&mut self, value: String) {
        self.inner.message = value;
    }

    #[getter]
    fn prompt(&self) -> Option<&str> {
        self.inner.prompt.as_deref()
    }

    #[getter]
    fn error(&self) -> Option<&str> {
        self.inner.error.as_deref()
    }

    #[getter]
    fn context<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
        let dict = PyDict::new(py);
        for (k, v) in &self.inner.context {
            dict.set_item(k, json_value_to_bound_py(py, v)?)?;
        }
        Ok(dict)
    }

    /// Create a new instance with error information.
    #[allow(clippy::double_must_use)]
    #[must_use]
    fn with_error(&self, error: String) -> Self {
        let mut data = self.inner.clone();
        data.success = false;
        data.error = Some(error);
        Self { inner: data }
    }

    /// Create a new instance with updated context.
    #[allow(clippy::double_must_use)]
    #[must_use]
    #[pyo3(signature = (**kwargs))]
    fn with_context(&self, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<Self> {
        let mut data = self.inner.clone();
        if let Some(kw) = kwargs {
            for (k, v) in kw.iter() {
                let key: String = k.extract()?;
                let val = py_any_to_json_value(&v)?;
                data.context.insert(key, val);
            }
        }
        Ok(Self { inner: data })
    }

    /// Convert to dictionary.
    fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
        let dict = PyDict::new(py);
        dict.set_item("success", self.inner.success)?;
        dict.set_item("message", &self.inner.message)?;
        dict.set_item("prompt", self.inner.prompt.as_deref())?;
        dict.set_item("error", self.inner.error.as_deref())?;
        dict.set_item("context", self.context(py)?)?;
        Ok(dict)
    }

    fn __repr__(&self) -> String {
        format!(
            "ActionResultModel(success={}, message={:?})",
            self.inner.success, self.inner.message
        )
    }

    fn __str__(&self) -> String {
        self.to_string()
    }
}

impl ActionResultModel {
    /// Create an `ActionResultModel` from raw data.
    #[must_use]
    pub fn from_data(data: ActionResultModelData) -> Self {
        Self { inner: data }
    }

    /// Access the underlying data.
    #[must_use]
    pub fn data(&self) -> &ActionResultModelData {
        &self.inner
    }
}

impl std::fmt::Display for ActionResultModel {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.inner.success {
            write!(f, "Success: {}", self.inner.message)
        } else {
            write!(
                f,
                "Error: {}",
                self.inner.error.as_deref().unwrap_or(&self.inner.message)
            )
        }
    }
}

// ── Factory functions & helpers (Python-only) ──

#[cfg(feature = "python-bindings")]
mod py_factories {
    use super::*;
    use pyo3::types::PyDict;

    use dcc_mcp_utils::constants::{
        ACTION_RESULT_KNOWN_KEYS, CTX_KEY_ERROR_TYPE, CTX_KEY_POSSIBLE_SOLUTIONS,
        CTX_KEY_TRACEBACK, CTX_KEY_VALUE, DEFAULT_ERROR_PROMPT, DEFAULT_ERROR_TYPE,
        DEFAULT_SUCCESS_MESSAGE,
    };
    use dcc_mcp_utils::py_json::{py_any_to_json_value, py_dict_to_json_map};

    pub(super) fn extract_context(
        context: Option<&Bound<'_, PyDict>>,
    ) -> PyResult<HashMap<String, serde_json::Value>> {
        match context {
            Some(dict) => py_dict_to_json_map(dict),
            None => Ok(HashMap::new()),
        }
    }

    fn insert_possible_solutions(
        ctx: &mut HashMap<String, serde_json::Value>,
        solutions: Option<Vec<String>>,
    ) {
        if let Some(solutions) = solutions {
            ctx.insert(
                CTX_KEY_POSSIBLE_SOLUTIONS.to_string(),
                serde_json::Value::Array(
                    solutions
                        .into_iter()
                        .map(serde_json::Value::String)
                        .collect(),
                ),
            );
        }
    }

    /// Extract a required-type field from a Python dict with a type error on mismatch.
    fn extract_bool_field(dict: &Bound<'_, PyDict>, key: &str, default: bool) -> PyResult<bool> {
        dict.get_item(key)?
            .map(|v| {
                v.extract::<bool>().map_err(|_| {
                    pyo3::exceptions::PyTypeError::new_err(format!("'{key}' field must be a bool"))
                })
            })
            .transpose()
            .map(|opt| opt.unwrap_or(default))
    }

    /// Extract a string field from a Python dict, returning the default on absence.
    fn extract_string_field(dict: &Bound<'_, PyDict>, key: &str) -> PyResult<String> {
        dict.get_item(key)?
            .map(|v| {
                v.extract::<String>().map_err(|_| {
                    pyo3::exceptions::PyTypeError::new_err(format!(
                        "'{key}' field must be a string"
                    ))
                })
            })
            .transpose()
            .map(|opt| opt.unwrap_or_default())
    }

    /// Extract an optional nullable string field from a Python dict.
    fn extract_optional_string_field(
        dict: &Bound<'_, PyDict>,
        key: &str,
    ) -> PyResult<Option<String>> {
        dict.get_item(key)?
            .map(|v| {
                if v.is_none() {
                    Ok(None)
                } else {
                    v.extract::<String>().map(Some).map_err(|_| {
                        pyo3::exceptions::PyTypeError::new_err(format!(
                            "'{key}' field must be a string"
                        ))
                    })
                }
            })
            .transpose()
            .map(|opt| opt.flatten())
    }

    /// Extract dict fields into an `ActionResultModel`, filtering out standard keys.
    fn validate_from_dict(dict: &Bound<'_, PyDict>) -> PyResult<ActionResultModel> {
        let success = extract_bool_field(dict, "success", true)?;
        let message = extract_string_field(dict, "message")?;
        let prompt = extract_optional_string_field(dict, "prompt")?;
        let error = extract_optional_string_field(dict, "error")?;

        // Build context directly as a Rust HashMap, skipping the intermediate PyDict.
        let mut ctx = HashMap::new();
        for (k, v) in dict.iter() {
            if let Ok(key) = k.extract::<String>() {
                if !ACTION_RESULT_KNOWN_KEYS.contains(&key.as_str()) {
                    ctx.insert(key, py_any_to_json_value(&v)?);
                }
            }
        }

        Ok(ActionResultModel {
            inner: ActionResultModelData {
                success,
                message,
                prompt,
                error,
                context: ctx,
            },
        })
    }

    #[pyfunction]
    #[pyo3(name = "success_result")]
    #[pyo3(signature = (message, prompt=None, **context))]
    pub fn py_success_result(
        message: String,
        prompt: Option<String>,
        context: Option<&Bound<'_, PyDict>>,
    ) -> PyResult<ActionResultModel> {
        let ctx = extract_context(context)?;
        Ok(ActionResultModel {
            inner: ActionResultModelData::success(message, prompt, ctx),
        })
    }

    #[pyfunction]
    #[pyo3(name = "error_result")]
    #[pyo3(signature = (message, error, prompt=None, possible_solutions=None, **context))]
    pub fn py_error_result(
        message: String,
        error: String,
        prompt: Option<String>,
        possible_solutions: Option<Vec<String>>,
        context: Option<&Bound<'_, PyDict>>,
    ) -> PyResult<ActionResultModel> {
        let mut ctx = extract_context(context)?;
        insert_possible_solutions(&mut ctx, possible_solutions);
        Ok(ActionResultModel {
            inner: ActionResultModelData::failure(message, Some(error), prompt, ctx),
        })
    }

    #[pyfunction]
    #[pyo3(name = "from_exception")]
    #[pyo3(signature = (error_message, message=None, prompt=None, include_traceback=true, possible_solutions=None, **context))]
    pub fn py_from_exception(
        error_message: String,
        message: Option<String>,
        prompt: Option<String>,
        include_traceback: bool,
        possible_solutions: Option<Vec<String>>,
        context: Option<&Bound<'_, PyDict>>,
    ) -> PyResult<ActionResultModel> {
        let mut ctx = extract_context(context)?;
        // Extract error type from the error_message pattern "ErrorType: details"
        let error_type = error_message
            .split_once(':')
            .map(|(t, _)| t.trim().to_string())
            .unwrap_or_else(|| DEFAULT_ERROR_TYPE.to_string());
        ctx.insert(
            CTX_KEY_ERROR_TYPE.to_string(),
            serde_json::Value::String(error_type),
        );
        // Build the user-facing message before moving error_message.
        let msg = message.unwrap_or_else(|| format!("Error: {error_message}"));
        if include_traceback {
            // Store error_message under the traceback key. Callers that need the real
            // Python traceback should pass it explicitly via the **context kwarg.
            ctx.insert(
                CTX_KEY_TRACEBACK.to_string(),
                serde_json::Value::String(error_message.clone()),
            );
        }
        insert_possible_solutions(&mut ctx, possible_solutions);
        Ok(ActionResultModel {
            inner: ActionResultModelData::failure(
                msg,
                Some(error_message),
                Some(prompt.unwrap_or_else(|| DEFAULT_ERROR_PROMPT.to_string())),
                ctx,
            ),
        })
    }

    #[pyfunction]
    #[pyo3(name = "validate_action_result")]
    pub fn py_validate_action_result(result: &Bound<'_, PyAny>) -> PyResult<ActionResultModel> {
        // If already ActionResultModel, clone it
        if let Ok(arm) = result.extract::<ActionResultModel>() {
            return Ok(arm);
        }
        // If dict, try to convert
        if let Ok(dict) = result.cast::<PyDict>() {
            return validate_from_dict(dict);
        }
        // Wrap as success
        let msg = result.to_string();
        Ok(ActionResultModel {
            inner: ActionResultModelData::success(
                DEFAULT_SUCCESS_MESSAGE.to_string(),
                None,
                HashMap::from([(CTX_KEY_VALUE.to_string(), serde_json::Value::String(msg))]),
            ),
        })
    }
}

#[cfg(feature = "python-bindings")]
pub use py_factories::{
    py_error_result, py_from_exception, py_success_result, py_validate_action_result,
};

#[cfg(test)]
mod tests {
    use super::*;

    // ── Default ─────────────────────────────────────────────────────────────────

    #[test]
    fn test_action_result_default() {
        let data = ActionResultModelData::default();
        assert!(data.success); // Consistent with Python __new__ default (success=true)
        assert!(data.message.is_empty());
        assert!(data.prompt.is_none());
        assert!(data.error.is_none());
        assert!(data.context.is_empty());
    }

    #[test]
    fn test_action_result_model_default_success_true() {
        let model = ActionResultModel::default();
        assert!(model.data().success);
    }

    // ── Factory methods ─────────────────────────────────────────────────────────

    #[test]
    fn test_success_factory() {
        let data = ActionResultModelData::success(
            "All done".to_string(),
            Some("Next: verify mesh".to_string()),
            HashMap::new(),
        );
        assert!(data.success);
        assert_eq!(data.message, "All done");
        assert_eq!(data.prompt.as_deref(), Some("Next: verify mesh"));
        assert!(data.error.is_none());
    }

    #[test]
    fn test_failure_factory() {
        let data = ActionResultModelData::failure(
            "Operation failed".to_string(),
            Some("FileNotFoundError: missing.py".to_string()),
            Some("Check file path".to_string()),
            HashMap::new(),
        );
        assert!(!data.success);
        assert_eq!(data.message, "Operation failed");
        assert_eq!(data.error.as_deref(), Some("FileNotFoundError: missing.py"));
        assert_eq!(data.prompt.as_deref(), Some("Check file path"));
    }

    #[test]
    fn test_failure_factory_no_error() {
        let data =
            ActionResultModelData::failure("Cancelled".to_string(), None, None, HashMap::new());
        assert!(!data.success);
        assert!(data.error.is_none());
        assert!(data.prompt.is_none());
    }

    // ── from_data / data accessor ───────────────────────────────────────────────

    #[test]
    fn test_from_data_round_trip() {
        let data = ActionResultModelData {
            success: false,
            message: "bad".to_string(),
            prompt: None,
            error: Some("oops".to_string()),
            context: HashMap::from([("key".to_string(), serde_json::Value::Bool(true))]),
        };
        let model = ActionResultModel::from_data(data.clone());
        assert_eq!(model.data(), &data);
    }

    // ── Display ─────────────────────────────────────────────────────────────────

    #[test]
    fn test_display_success() {
        let model = ActionResultModel::from_data(ActionResultModelData {
            success: true,
            message: "render complete".to_string(),
            ..Default::default()
        });
        let s = model.to_string();
        assert!(s.contains("Success"));
        assert!(s.contains("render complete"));
    }

    #[test]
    fn test_display_failure_with_error() {
        let model = ActionResultModel::from_data(ActionResultModelData {
            success: false,
            message: "operation failed".to_string(),
            error: Some("TypeError: bad arg".to_string()),
            ..Default::default()
        });
        let s = model.to_string();
        assert!(s.contains("Error"));
        assert!(s.contains("TypeError: bad arg"));
    }

    #[test]
    fn test_display_failure_uses_message_when_no_error() {
        let model = ActionResultModel::from_data(ActionResultModelData {
            success: false,
            message: "something went wrong".to_string(),
            error: None,
            ..Default::default()
        });
        let s = model.to_string();
        assert!(s.contains("something went wrong"));
    }

    // ── Serialization ───────────────────────────────────────────────────────────

    #[test]
    fn test_action_result_serialization() {
        let data = ActionResultModelData {
            success: true,
            message: "test".to_string(),
            prompt: Some("next step".to_string()),
            error: None,
            context: HashMap::new(),
        };
        let json = serde_json::to_string(&data).unwrap();
        assert!(json.contains("\"success\":true"));
        assert!(json.contains("\"message\":\"test\""));
        assert!(json.contains("\"prompt\":\"next step\""));
        // Optional None fields are skipped
        assert!(!json.contains("\"error\""));
    }

    #[test]
    fn test_action_result_deserialization_round_trip() {
        let data = ActionResultModelData {
            success: false,
            message: "failed operation".to_string(),
            prompt: None,
            error: Some("IOError: file not found".to_string()),
            context: HashMap::from([("retry".to_string(), serde_json::json!(true))]),
        };
        let json = serde_json::to_string(&data).unwrap();
        let back: ActionResultModelData = serde_json::from_str(&json).unwrap();
        assert_eq!(data, back);
    }

    #[test]
    fn test_action_result_deserialize_minimal_json() {
        // Only 'message' provided, everything else defaults
        let json = r#"{"message": "hello"}"#;
        let data: ActionResultModelData = serde_json::from_str(json).unwrap();
        assert!(data.success); // default = true
        assert_eq!(data.message, "hello");
        assert!(data.error.is_none());
    }

    // ── Context ─────────────────────────────────────────────────────────────────

    #[test]
    fn test_context_with_nested_values() {
        let mut ctx = HashMap::new();
        ctx.insert("count".to_string(), serde_json::json!(42));
        ctx.insert("nested".to_string(), serde_json::json!({"a": 1}));
        ctx.insert("list".to_string(), serde_json::json!([1, 2, 3]));

        let data = ActionResultModelData::success("ctx test".to_string(), None, ctx.clone());
        assert_eq!(data.context["count"], serde_json::json!(42));
        assert_eq!(data.context["list"], serde_json::json!([1, 2, 3]));
    }

    // ── PartialEq / Clone ────────────────────────────────────────────────────────

    #[test]
    fn test_action_result_clone_eq() {
        let data = ActionResultModelData {
            success: true,
            message: "ok".to_string(),
            ..Default::default()
        };
        let cloned = data.clone();
        assert_eq!(data, cloned);
    }

    #[test]
    fn test_action_result_model_clone_eq() {
        let model = ActionResultModel::from_data(ActionResultModelData {
            success: false,
            message: "err".to_string(),
            ..Default::default()
        });
        let cloned = model.clone();
        assert_eq!(model, cloned);
    }
}
