//! Consistent JSON response envelopes for all MCP tools.
//!
//! Every tool response is wrapped in either [`ResponseEnvelope<T>`] (success)
//! or [`ErrorEnvelope`] (failure). This gives AI agents a single schema to
//! parse regardless of which tool they called.

use serde::{Deserialize, Serialize};

// ── Error codes ──────────────────────────────────────────────

/// Structured error codes for MCP tool failures.
///
/// These are serialized as SCREAMING_SNAKE_CASE strings in JSON.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
    /// No scanned project database found.
    RepoNotScanned,
    /// The `topic` parameter was empty or whitespace-only.
    EmptyTopic,
    /// A required parameter was missing or had an invalid value.
    InvalidInput,
    /// The requested knowledge node does not exist (used by graph tools
    /// that operate on the `nodes` table — query_dependencies,
    /// validate_approach, etc.).
    NodeNotFound,
    /// The requested decision row does not exist in the V12 `decisions`
    /// table (returned by update_decision / remove_decision when no row
    /// matches the supplied `description_hash`).
    DecisionNotFound,
    /// Attempted to modify an auto-detected convention (only user decisions
    /// can be updated/removed).
    NotUserDecision,
    /// The requested scope (submodule) does not exist.
    UnknownScope,
    /// The requested repository name does not match the loaded project.
    RepoNotFound,
    /// Auto-scan on first serve failed.
    AutoScanFailed,
    /// An unexpected internal error.
    InternalError,
}

impl ErrorCode {
    /// Canonical string representation (matches serde output).
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::RepoNotScanned => "REPO_NOT_SCANNED",
            Self::EmptyTopic => "EMPTY_TOPIC",
            Self::InvalidInput => "INVALID_INPUT",
            Self::NodeNotFound => "NODE_NOT_FOUND",
            Self::DecisionNotFound => "DECISION_NOT_FOUND",
            Self::NotUserDecision => "NOT_USER_DECISION",
            Self::UnknownScope => "UNKNOWN_SCOPE",
            Self::RepoNotFound => "REPO_NOT_FOUND",
            Self::AutoScanFailed => "AUTO_SCAN_FAILED",
            Self::InternalError => "INTERNAL_ERROR",
        }
    }
}

impl std::fmt::Display for ErrorCode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

// ── Metadata ─────────────────────────────────────────────────

/// Optional metadata attached to every success response.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseMetadata {
    /// Context-aware hints suggesting what the agent should do next.
    pub next_steps: Vec<String>,

    /// Arbitrary extra metadata fields (search_type, results_count, etc.).
    #[serde(flatten)]
    pub extra: serde_json::Map<String, serde_json::Value>,
}

impl ResponseMetadata {
    /// Create metadata with the given next-step suggestions.
    pub fn new(next_steps: Vec<String>) -> Self {
        Self {
            next_steps,
            extra: serde_json::Map::new(),
        }
    }

    /// Add an arbitrary key-value pair to the metadata.
    pub fn with_extra(
        mut self,
        key: impl Into<String>,
        value: impl Into<serde_json::Value>,
    ) -> Self {
        self.extra.insert(key.into(), value.into());
        self
    }
}

// ── Success envelope ─────────────────────────────────────────

/// Uniform success envelope wrapping tool-specific data.
///
/// ```json
/// {
///   "status": "success",
///   "tool": "query_convention",
///   "repo": "seshat",
///   "data": { ... },
///   "metadata": { "next_steps": ["..."] }
/// }
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseEnvelope<T: Serialize> {
    /// Always `"success"`.
    pub status: String,
    /// Name of the tool that produced the response.
    pub tool: String,
    /// Repository name.
    pub repo: String,
    /// Tool-specific payload.
    pub data: T,
    /// Context-aware hints and extra metadata.
    pub metadata: ResponseMetadata,
}

impl<T: Serialize> ResponseEnvelope<T> {
    /// Build a success envelope.
    pub fn success(
        tool: impl Into<String>,
        repo: impl Into<String>,
        data: T,
        metadata: ResponseMetadata,
    ) -> Self {
        Self {
            status: "success".to_owned(),
            tool: tool.into(),
            repo: repo.into(),
            data,
            metadata,
        }
    }
}

// ── Error detail ─────────────────────────────────────────────

/// Structured error detail inside an [`ErrorEnvelope`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorDetail {
    /// Machine-readable error code.
    pub code: ErrorCode,
    /// Human-readable error description.
    pub message: String,
    /// Actionable suggestion for the caller.
    pub suggestion: String,
}

// ── Error envelope ───────────────────────────────────────────

/// Uniform error envelope for any MCP tool failure.
///
/// ```json
/// {
///   "status": "error",
///   "tool": "query_convention",
///   "repo": "seshat",
///   "error": {
///     "code": "EMPTY_TOPIC",
///     "message": "The topic parameter must not be empty",
///     "suggestion": "Provide a topic like 'error handling' or 'logging'"
///   }
/// }
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorEnvelope {
    /// Always `"error"`.
    pub status: String,
    /// Name of the tool that produced the error.
    pub tool: String,
    /// Repository name (may be empty if unknown).
    pub repo: String,
    /// Structured error detail.
    pub error: ErrorDetail,
}

impl ErrorEnvelope {
    /// Build an error envelope.
    pub fn new(
        tool: impl Into<String>,
        repo: impl Into<String>,
        code: ErrorCode,
        message: impl Into<String>,
        suggestion: impl Into<String>,
    ) -> Self {
        Self {
            status: "error".to_owned(),
            tool: tool.into(),
            repo: repo.into(),
            error: ErrorDetail {
                code,
                message: message.into(),
                suggestion: suggestion.into(),
            },
        }
    }
}

// ── Helpers ──────────────────────────────────────────────────

/// Serialize a response envelope to JSON, falling back to an error envelope
/// if serialization fails.
pub fn serialize_response(tool: &str, repo: &str, envelope: &impl serde::Serialize) -> String {
    serde_json::to_string(envelope).unwrap_or_else(|e| {
        let err = ErrorEnvelope::new(
            tool,
            repo,
            ErrorCode::InternalError,
            format!("Failed to serialize response: {e}"),
            "Please report this issue",
        );
        serde_json::to_string(&err).unwrap_or_else(|_| {
            r#"{"status":"error","tool":"unknown","repo":"","error":{"code":"INTERNAL_ERROR","message":"Failed to serialize error","suggestion":"Report this issue"}}"#.to_owned()
        })
    })
}

/// Format an internal error as a JSON error envelope string.
pub fn internal_error(tool: &str, repo: &str, err: impl std::fmt::Display) -> String {
    let envelope = ErrorEnvelope::new(
        tool,
        repo,
        ErrorCode::InternalError,
        format!("{err}"),
        "Check database and retry",
    );
    serde_json::to_string(&envelope).unwrap_or_else(|_| {
        r#"{"status":"error","tool":"unknown","repo":"","error":{"code":"INTERNAL_ERROR","message":"Failed to serialize error","suggestion":"Report this issue"}}"#.to_owned()
    })
}

/// Map a [`seshat_graph::GraphError`] to a JSON error envelope string,
/// handling common error variants with appropriate codes.
pub fn map_graph_error(tool: &str, repo: &str, err: seshat_graph::GraphError) -> String {
    use seshat_graph::GraphError;

    let fallback = || {
        r#"{"status":"error","tool":"unknown","repo":"","error":{"code":"INTERNAL_ERROR","message":"Failed to serialize error","suggestion":"Report this issue"}}"#.to_owned()
    };

    match err {
        GraphError::NodeNotFound(msg) => {
            let envelope = ErrorEnvelope::new(
                tool,
                repo,
                ErrorCode::NodeNotFound,
                msg,
                "Verify the node ID from a previous query_convention response",
            );
            serde_json::to_string(&envelope).unwrap_or_else(|_| fallback())
        }
        GraphError::DecisionNotFound(msg) => {
            let envelope = ErrorEnvelope::new(
                tool,
                repo,
                ErrorCode::DecisionNotFound,
                msg,
                "Verify the description_hash from a prior record_decision \
                 response or DecisionEntry in validate_approach results",
            );
            serde_json::to_string(&envelope).unwrap_or_else(|_| fallback())
        }
        GraphError::NotUserDecision(msg) => {
            let envelope = ErrorEnvelope::new(
                tool,
                repo,
                ErrorCode::NotUserDecision,
                msg,
                "Only user-recorded decisions can be modified. Auto-detected conventions are managed by re-scanning.",
            );
            serde_json::to_string(&envelope).unwrap_or_else(|_| fallback())
        }
        GraphError::InvalidInput(msg) => {
            let envelope = ErrorEnvelope::new(
                tool,
                repo,
                ErrorCode::InvalidInput,
                msg,
                "Check the input parameter values against the tool's parameter descriptions",
            );
            serde_json::to_string(&envelope).unwrap_or_else(|_| fallback())
        }
        GraphError::RepoNotScanned { path } => {
            let envelope = ErrorEnvelope::new(
                tool,
                repo,
                ErrorCode::RepoNotScanned,
                format!("Repository not scanned: {path}"),
                "Run a scan on the repository first",
            );
            serde_json::to_string(&envelope).unwrap_or_else(|_| fallback())
        }
        GraphError::EmptyResult(msg) => {
            let envelope = ErrorEnvelope::new(
                tool,
                repo,
                ErrorCode::NodeNotFound,
                msg,
                "Try a broader search or verify the target exists in the scanned data",
            );
            serde_json::to_string(&envelope).unwrap_or_else(|_| fallback())
        }
        GraphError::Storage(e) => internal_error(tool, repo, e),
        GraphError::CacheError(msg) => internal_error(tool, repo, msg),
    }
}

// ── Tests ────────────────────────────────────────────────────

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

    #[test]
    fn success_envelope_serializes_correctly() {
        let data = serde_json::json!({"languages": ["rust", "python"]});
        let meta =
            ResponseMetadata::new(vec!["Run query_convention to explore conventions".into()]);

        let envelope = ResponseEnvelope::success("query_project_context", "seshat", data, meta);

        let json = serde_json::to_value(&envelope).unwrap();

        assert_eq!(json["status"], "success");
        assert_eq!(json["tool"], "query_project_context");
        assert_eq!(json["repo"], "seshat");
        assert!(json["branch"].is_null());
        assert!(json["scope"].is_null());
        assert!(json["duration_ms"].is_null());
        assert_eq!(json["data"]["languages"][0], "rust");
        assert_eq!(
            json["metadata"]["next_steps"][0],
            "Run query_convention to explore conventions"
        );
    }

    #[test]
    fn success_envelope_deserializes_roundtrip() {
        let data = serde_json::json!({"count": 42});
        let meta = ResponseMetadata::new(vec![]);

        let envelope = ResponseEnvelope::success("test_tool", "repo", data, meta);

        let json_str = serde_json::to_string(&envelope).unwrap();
        let parsed: ResponseEnvelope<serde_json::Value> = serde_json::from_str(&json_str).unwrap();

        assert_eq!(parsed.status, "success");
        assert_eq!(parsed.tool, "test_tool");
        assert_eq!(parsed.data["count"], 42);
    }

    #[test]
    fn error_envelope_serializes_correctly() {
        let envelope = ErrorEnvelope::new(
            "query_convention",
            "seshat",
            ErrorCode::EmptyTopic,
            "The topic parameter must not be empty",
            "Provide a topic like 'error handling' or 'logging'",
        );

        let json = serde_json::to_value(&envelope).unwrap();

        assert_eq!(json["status"], "error");
        assert_eq!(json["tool"], "query_convention");
        assert_eq!(json["repo"], "seshat");
        assert_eq!(json["error"]["code"], "EMPTY_TOPIC");
        assert_eq!(
            json["error"]["message"],
            "The topic parameter must not be empty"
        );
        assert_eq!(
            json["error"]["suggestion"],
            "Provide a topic like 'error handling' or 'logging'"
        );
    }

    #[test]
    fn error_envelope_deserializes_roundtrip() {
        let envelope = ErrorEnvelope::new(
            "record_decision",
            "seshat",
            ErrorCode::InvalidInput,
            "description is required",
            "Provide a non-empty description string",
        );

        let json_str = serde_json::to_string(&envelope).unwrap();
        let parsed: ErrorEnvelope = serde_json::from_str(&json_str).unwrap();

        assert_eq!(parsed.status, "error");
        assert_eq!(parsed.error.code, ErrorCode::InvalidInput);
    }

    #[test]
    fn all_error_codes_serialize_screaming_snake() {
        let codes = [
            (ErrorCode::RepoNotScanned, "REPO_NOT_SCANNED"),
            (ErrorCode::EmptyTopic, "EMPTY_TOPIC"),
            (ErrorCode::InvalidInput, "INVALID_INPUT"),
            (ErrorCode::NodeNotFound, "NODE_NOT_FOUND"),
            (ErrorCode::NotUserDecision, "NOT_USER_DECISION"),
            (ErrorCode::UnknownScope, "UNKNOWN_SCOPE"),
            (ErrorCode::RepoNotFound, "REPO_NOT_FOUND"),
            (ErrorCode::AutoScanFailed, "AUTO_SCAN_FAILED"),
            (ErrorCode::InternalError, "INTERNAL_ERROR"),
        ];

        for (code, expected) in &codes {
            let json = serde_json::to_value(code).unwrap();
            assert_eq!(json.as_str().unwrap(), *expected, "code: {:?}", code);
        }
    }

    #[test]
    fn error_code_as_str_matches_serde() {
        let codes = [
            ErrorCode::RepoNotScanned,
            ErrorCode::EmptyTopic,
            ErrorCode::InvalidInput,
            ErrorCode::NodeNotFound,
            ErrorCode::NotUserDecision,
            ErrorCode::UnknownScope,
            ErrorCode::RepoNotFound,
            ErrorCode::AutoScanFailed,
            ErrorCode::InternalError,
        ];

        for code in &codes {
            let serde_str = serde_json::to_value(code)
                .unwrap()
                .as_str()
                .unwrap()
                .to_owned();
            assert_eq!(code.as_str(), serde_str);
        }
    }

    #[test]
    fn error_code_display() {
        assert_eq!(ErrorCode::EmptyTopic.to_string(), "EMPTY_TOPIC");
        assert_eq!(ErrorCode::InternalError.to_string(), "INTERNAL_ERROR");
        assert_eq!(ErrorCode::AutoScanFailed.to_string(), "AUTO_SCAN_FAILED");
    }

    #[test]
    fn auto_scan_failed_error_code_serializes() {
        let envelope = ErrorEnvelope::new(
            "query_project_context",
            "my-project",
            ErrorCode::AutoScanFailed,
            "Auto-scan failed: disk full",
            "Run: seshat scan --verbose /path/to/project",
        );
        let json = serde_json::to_value(&envelope).unwrap();
        assert_eq!(json["status"], "error");
        assert_eq!(json["error"]["code"], "AUTO_SCAN_FAILED");
        assert_eq!(json["error"]["message"], "Auto-scan failed: disk full");
        assert_eq!(
            json["error"]["suggestion"],
            "Run: seshat scan --verbose /path/to/project"
        );
    }

    #[test]
    fn metadata_with_extra_fields() {
        let meta = ResponseMetadata::new(vec!["next".into()])
            .with_extra("test_key", "some_value")
            .with_extra("results_count", 7);

        let json = serde_json::to_value(&meta).unwrap();

        assert_eq!(json["next_steps"][0], "next");
        assert_eq!(json["test_key"], "some_value");
        assert_eq!(json["results_count"], 7);
    }

    #[test]
    fn serialize_response_success_roundtrip() {
        let data = serde_json::json!({"foo": 1});
        let meta = ResponseMetadata::new(vec![]);
        let env = ResponseEnvelope::success("test_tool", "test_repo", data, meta);
        let json = serialize_response("test_tool", "test_repo", &env);
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["status"], "success");
        assert_eq!(parsed["tool"], "test_tool");
        assert_eq!(parsed["data"]["foo"], 1);
    }

    #[test]
    fn internal_error_serializes_correct_json() {
        let json = internal_error("test_tool", "test_repo", "something broke");
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["status"], "error");
        assert_eq!(parsed["tool"], "test_tool");
        assert_eq!(parsed["repo"], "test_repo");
        assert_eq!(parsed["error"]["code"], "INTERNAL_ERROR");
        assert!(
            parsed["error"]["message"]
                .as_str()
                .unwrap()
                .contains("something broke")
        );
    }

    #[test]
    fn map_graph_error_node_not_found() {
        let json = map_graph_error(
            "qcp",
            "repo",
            seshat_graph::GraphError::NodeNotFound("nope".to_owned()),
        );
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["error"]["code"], "NODE_NOT_FOUND");
    }

    #[test]
    fn map_graph_error_not_user_decision() {
        let json = map_graph_error(
            "ud",
            "repo",
            seshat_graph::GraphError::NotUserDecision("auto detected".to_owned()),
        );
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["error"]["code"], "NOT_USER_DECISION");
    }

    #[test]
    fn map_graph_error_invalid_input() {
        let json = map_graph_error(
            "rd",
            "repo",
            seshat_graph::GraphError::InvalidInput("bad nature".to_owned()),
        );
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["error"]["code"], "INVALID_INPUT");
    }

    #[test]
    fn map_graph_error_repo_not_scanned() {
        let json = map_graph_error(
            "qpc",
            "repo",
            seshat_graph::GraphError::RepoNotScanned {
                path: "/tmp".to_owned(),
            },
        );
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["error"]["code"], "REPO_NOT_SCANNED");
    }

    #[test]
    fn map_graph_error_empty_result() {
        let json = map_graph_error(
            "qcp",
            "repo",
            seshat_graph::GraphError::EmptyResult("no data".to_owned()),
        );
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["error"]["code"], "NODE_NOT_FOUND");
    }

    #[test]
    fn map_graph_error_storage_and_cache_fallback_to_internal() {
        let json = map_graph_error(
            "t",
            "r",
            seshat_graph::GraphError::Storage(seshat_storage::StorageError::QueryError(
                "sql failed".to_owned(),
            )),
        );
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["error"]["code"], "INTERNAL_ERROR");
    }
}
