//! Agent-optimized response types and API functions.
//!
//! This module provides a structured, agent-friendly API layer on top of the
//! core altimate-core validation, explanation, fix, and policy engines. Every response
//! follows the [`AgentResponse`] structure so agents (LLMs, MCP clients, CI bots)
//! can reliably parse results without guessing at formats.
//!
//! # Quick Start
//!
//! ```rust,no_run
//! use altimate_core::agent::{full_check, batch_validate, BatchRequest};
//! use altimate_core::SchemaDefinition;
//!
//! # async fn example() {
//! let schema = SchemaDefinition::from_yaml_file("schema.yaml").unwrap();
//! let response = full_check("SELECT id FROM users", &schema, None).await;
//! println!("Status: {:?}, Code: {}", response.status, response.code);
//! # }
//! ```

use serde::{Deserialize, Serialize};
use std::time::Instant;

use crate::errors::types::{ErrorCode, ErrorKind, ValidationResult};
use crate::explainer::types::ExplanationResult;
use crate::fixer::types::FixResult;
use crate::guardrails::policy::PolicyConfig;
use crate::guardrails::types::PolicyResult;
use crate::polyglot::types::{CompareResult, ExtractResult, FormatResult, TranspileResult};
use crate::schema::types::SchemaDefinition;

/// Agent-friendly response wrapper. Every response from altimate-core
/// follows this structure so agents can reliably parse it.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentResponse {
    /// "success" | "error" | "warning"
    pub status: ResponseStatus,
    /// Machine-readable code: "VALID", "TABLE_NOT_FOUND", "COLUMN_NOT_FOUND", etc.
    pub code: String,
    /// Human-readable summary (1 line, suitable for agent to show user)
    pub summary: String,
    /// The main result payload
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result: Option<serde_json::Value>,
    /// Structured errors with fix suggestions
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub errors: Vec<AgentError>,
    /// Actionable next steps the agent can take
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub suggested_actions: Vec<SuggestedAction>,
    /// Timing metadata
    pub metadata: AgentMetadata,
}

/// Response status for agent consumption.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ResponseStatus {
    /// Query is valid / operation succeeded
    Success,
    /// Query has errors / operation failed
    Error,
    /// Query is valid but has warnings (e.g., policy warnings)
    Warning,
}

/// A structured error with fix suggestions for agent consumption.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentError {
    /// Machine-readable: "TABLE_NOT_FOUND", "COLUMN_NOT_FOUND", "AMBIGUOUS_COLUMN",
    /// "SYNTAX_ERROR", "TYPE_MISMATCH", "POLICY_VIOLATION"
    pub code: String,
    /// Human-readable message
    pub message: String,
    /// Location in SQL (line, column, offset) if available
    #[serde(skip_serializing_if = "Option::is_none")]
    pub location: Option<ErrorLocation>,
    /// The problematic SQL fragment
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sql_fragment: Option<String>,
    /// Suggested fix — the corrected version of the fragment
    #[serde(skip_serializing_if = "Option::is_none")]
    pub suggested_fix: Option<String>,
    /// Confidence of the suggestion (0.0-1.0)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub fix_confidence: Option<f64>,
}

/// An actionable next step the agent can take.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SuggestedAction {
    /// What the agent should do: "apply_fix", "explain_query", "check_policy",
    /// "review_schema", "add_where_clause", "add_limit"
    pub action: String,
    /// Description for the agent
    pub description: String,
    /// Pre-built parameters the agent can pass directly
    #[serde(skip_serializing_if = "Option::is_none")]
    pub params: Option<serde_json::Value>,
}

/// Location within the SQL source text.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorLocation {
    /// Line number (1-indexed)
    pub line: usize,
    /// Column number (1-indexed)
    pub column: usize,
    /// Byte offset from start of SQL string
    pub offset: usize,
}

/// Metadata about the altimate-core processing context.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentMetadata {
    /// SQL dialect used for parsing
    pub dialect: String,
    /// Processing time in milliseconds
    pub processing_time_ms: u64,
    /// altimate-core version
    pub altimate_version: String,
    /// Tables available in the schema
    pub schema_tables: Vec<String>,
}

/// A single SQL query in a batch validation request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchRequest {
    /// Identifier for this query (e.g., filename, model name)
    pub id: String,
    /// The SQL to validate
    pub sql: String,
}

/// Result of validating multiple SQL queries at once.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchResponse {
    /// Individual results
    pub results: Vec<BatchItem>,
    /// Total number of queries processed
    pub total: usize,
    /// Number of queries that passed validation
    pub passed: usize,
    /// Number of queries that failed validation
    pub failed: usize,
    /// Timing metadata
    pub metadata: AgentMetadata,
}

/// A single item in a batch validation response.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchItem {
    /// The identifier from the request
    pub id: String,
    /// The validation response for this query
    pub response: AgentResponse,
}

// ---------------------------------------------------------------------------
// Helper: build AgentMetadata
// ---------------------------------------------------------------------------

/// Build an [`AgentMetadata`] from schema and timing information.
fn build_metadata(schema: &SchemaDefinition, time_ms: u64) -> AgentMetadata {
    let mut tables: Vec<String> = schema.tables.keys().cloned().collect();
    tables.sort();
    AgentMetadata {
        dialect: schema.dialect.to_string(),
        processing_time_ms: time_ms,
        altimate_version: env!("CARGO_PKG_VERSION").to_string(),
        schema_tables: tables,
    }
}

// ---------------------------------------------------------------------------
// Helper: map ErrorCode to agent code string
// ---------------------------------------------------------------------------

/// Map an [`ErrorCode`] to a machine-readable agent code string.
fn error_code_to_agent_code(code: &ErrorCode) -> &'static str {
    match code {
        ErrorCode::E000 => "SYNTAX_ERROR",
        ErrorCode::E001 => "TABLE_NOT_FOUND",
        ErrorCode::E002 => "COLUMN_NOT_FOUND",
        ErrorCode::E003 => "TYPE_MISMATCH",
        ErrorCode::E004 => "AMBIGUOUS_COLUMN",
        ErrorCode::E005 => "INVALID_JOIN",
        ErrorCode::E006 => "DIALECT_UNSUPPORTED",
        ErrorCode::E099 => "SEMANTIC_ERROR",
    }
}

// ---------------------------------------------------------------------------
// Helper: extract SQL fragment from ErrorKind
// ---------------------------------------------------------------------------

/// Extract the problematic SQL fragment from an [`ErrorKind`].
fn extract_sql_fragment(kind: &ErrorKind) -> Option<String> {
    match kind {
        ErrorKind::TableNotFound { table } => Some(table.clone()),
        ErrorKind::ColumnNotFound { column, table } => {
            if let Some(t) = table {
                Some(format!("{t}.{column}"))
            } else {
                Some(column.clone())
            }
        }
        ErrorKind::AmbiguousColumn { column, .. } => Some(column.clone()),
        ErrorKind::TypeMismatch { expected, found } => {
            Some(format!("expected {expected}, found {found}"))
        }
        ErrorKind::InvalidJoin { reason } => Some(reason.clone()),
        ErrorKind::UnsupportedDialectFeature { feature, .. } => Some(feature.clone()),
        ErrorKind::SyntaxError | ErrorKind::Other { .. } => None,
    }
}

// ---------------------------------------------------------------------------
// Conversion: ValidationResult -> AgentResponse
// ---------------------------------------------------------------------------

/// Convert a [`ValidationResult`] to an [`AgentResponse`].
pub fn from_validation(
    result: &ValidationResult,
    schema: &SchemaDefinition,
    time_ms: u64,
) -> AgentResponse {
    let metadata = build_metadata(schema, time_ms);

    if result.valid {
        let mut actions = vec![SuggestedAction {
            action: "explain_query".to_string(),
            description: "Get a detailed explanation of the query plan".to_string(),
            params: None,
        }];
        actions.push(SuggestedAction {
            action: "check_policy".to_string(),
            description: "Check the query against guardrail policies".to_string(),
            params: None,
        });

        return AgentResponse {
            status: ResponseStatus::Success,
            code: "VALID".to_string(),
            summary: "Query is valid".to_string(),
            result: None,
            errors: vec![],
            suggested_actions: actions,
            metadata,
        };
    }

    // Invalid query — build agent errors
    let agent_errors: Vec<AgentError> = result
        .errors
        .iter()
        .map(|e| {
            let suggested_fix = e.suggestions.first().map(|s| s.message.clone());
            let fix_confidence = e.suggestions.first().map(|s| s.confidence);

            AgentError {
                code: error_code_to_agent_code(&e.code).to_string(),
                message: e.message.clone(),
                location: e.location.as_ref().map(|loc| ErrorLocation {
                    line: loc.line,
                    column: loc.column,
                    offset: 0,
                }),
                sql_fragment: extract_sql_fragment(&e.kind),
                suggested_fix,
                fix_confidence,
            }
        })
        .collect();

    // Determine the top-level code from the first error
    let code = result
        .errors
        .first()
        .map(|e| error_code_to_agent_code(&e.code).to_string())
        .unwrap_or_else(|| "SEMANTIC_ERROR".to_string());

    let error_count = result.errors.len();
    let summary = if error_count == 1 {
        format!("Query has 1 error: {}", result.errors[0].message)
    } else {
        format!("Query has {error_count} errors")
    };

    // Build suggested actions
    let has_fixable = agent_errors.iter().any(|e| e.suggested_fix.is_some());
    let mut actions = Vec::new();
    if has_fixable {
        actions.push(SuggestedAction {
            action: "apply_fix".to_string(),
            description: "Automatically fix the detected errors".to_string(),
            params: None,
        });
    }
    actions.push(SuggestedAction {
        action: "explain_query".to_string(),
        description: "Get more details about why this query fails".to_string(),
        params: None,
    });

    AgentResponse {
        status: ResponseStatus::Error,
        code,
        summary,
        result: None,
        errors: agent_errors,
        suggested_actions: actions,
        metadata,
    }
}

// ---------------------------------------------------------------------------
// Conversion: ExplanationResult -> AgentResponse
// ---------------------------------------------------------------------------

/// Convert an [`ExplanationResult`] to an [`AgentResponse`].
pub fn from_explanation(
    result: &ExplanationResult,
    schema: &SchemaDefinition,
    time_ms: u64,
) -> AgentResponse {
    let metadata = build_metadata(schema, time_ms);

    if !result.valid || result.explanation.is_none() {
        // Invalid query — delegate to validation errors
        let agent_errors: Vec<AgentError> = result
            .validation
            .errors
            .iter()
            .map(|e| {
                let suggested_fix = e.suggestions.first().map(|s| s.message.clone());
                let fix_confidence = e.suggestions.first().map(|s| s.confidence);
                AgentError {
                    code: error_code_to_agent_code(&e.code).to_string(),
                    message: e.message.clone(),
                    location: e.location.as_ref().map(|loc| ErrorLocation {
                        line: loc.line,
                        column: loc.column,
                        offset: 0,
                    }),
                    sql_fragment: extract_sql_fragment(&e.kind),
                    suggested_fix,
                    fix_confidence,
                }
            })
            .collect();

        let code = result
            .validation
            .errors
            .first()
            .map(|e| error_code_to_agent_code(&e.code).to_string())
            .unwrap_or_else(|| "SEMANTIC_ERROR".to_string());

        return AgentResponse {
            status: ResponseStatus::Error,
            code,
            summary: "Query is invalid — cannot explain".to_string(),
            result: None,
            errors: agent_errors,
            suggested_actions: vec![SuggestedAction {
                action: "apply_fix".to_string(),
                description: "Fix the query before explaining".to_string(),
                params: None,
            }],
            metadata,
        };
    }

    let explanation = result.explanation.as_ref().expect("checked above");

    // Build the result payload with explanation details
    let result_payload = serde_json::json!({
        "summary": explanation.summary,
        "plan_steps": explanation.plan_steps,
        "lineage": {
            "tables": explanation.lineage.tables,
            "columns_read": explanation.lineage.columns_read,
            "columns_output": explanation.lineage.columns_output,
            "join_graph": explanation.lineage.join_graph,
        },
        "cost_signals": {
            "complexity": explanation.cost_signals.complexity,
            "complexity_score": explanation.cost_signals.complexity_score,
            "join_count": explanation.cost_signals.join_count,
            "has_aggregation": explanation.cost_signals.has_aggregation,
            "has_subquery": explanation.cost_signals.has_subquery,
            "has_limit": explanation.cost_signals.has_limit,
            "table_count": explanation.cost_signals.table_count,
            "risk_flags": explanation.cost_signals.risk_flags,
        },
        "suggestions": explanation.suggestions,
    });

    let mut actions = vec![SuggestedAction {
        action: "check_policy".to_string(),
        description: "Check the query against guardrail policies".to_string(),
        params: None,
    }];

    if !explanation.cost_signals.risk_flags.is_empty() {
        actions.push(SuggestedAction {
            action: "review_schema".to_string(),
            description: "Review the schema to understand risk flags".to_string(),
            params: None,
        });
    }

    if !explanation.cost_signals.has_limit {
        actions.push(SuggestedAction {
            action: "add_limit".to_string(),
            description: "Consider adding a LIMIT clause to bound result size".to_string(),
            params: None,
        });
    }

    AgentResponse {
        status: ResponseStatus::Success,
        code: "VALID".to_string(),
        summary: explanation.summary.clone(),
        result: Some(result_payload),
        errors: vec![],
        suggested_actions: actions,
        metadata,
    }
}

// ---------------------------------------------------------------------------
// Conversion: FixResult -> AgentResponse
// ---------------------------------------------------------------------------

/// Convert a [`FixResult`] to an [`AgentResponse`].
pub fn from_fix(
    result: &FixResult,
    original_sql: &str,
    schema: &SchemaDefinition,
    time_ms: u64,
) -> AgentResponse {
    let metadata = build_metadata(schema, time_ms);

    if result.fixed && result.post_fix_valid {
        let result_payload = serde_json::json!({
            "original_sql": original_sql,
            "fixed_sql": result.fixed_sql,
            "fixes_applied": result.fixes_applied,
            "iterations": result.iterations,
        });

        return AgentResponse {
            status: ResponseStatus::Success,
            code: "FIXED".to_string(),
            summary: format!(
                "Applied {} fix{} — query is now valid",
                result.fixes_applied.len(),
                if result.fixes_applied.len() == 1 {
                    ""
                } else {
                    "es"
                }
            ),
            result: Some(result_payload),
            errors: vec![],
            suggested_actions: vec![SuggestedAction {
                action: "explain_query".to_string(),
                description: "Explain the fixed query".to_string(),
                params: Some(serde_json::json!({
                    "sql": result.fixed_sql,
                })),
            }],
            metadata,
        };
    }

    // Fix was not fully successful
    let agent_errors: Vec<AgentError> = result
        .unfixable_errors
        .iter()
        .map(|ue| {
            let suggested_fix = ue.error.suggestions.first().map(|s| s.message.clone());
            let fix_confidence = ue.error.suggestions.first().map(|s| s.confidence);
            AgentError {
                code: error_code_to_agent_code(&ue.error.code).to_string(),
                message: ue.error.message.clone(),
                location: ue.error.location.as_ref().map(|loc| ErrorLocation {
                    line: loc.line,
                    column: loc.column,
                    offset: 0,
                }),
                sql_fragment: extract_sql_fragment(&ue.error.kind),
                suggested_fix,
                fix_confidence,
            }
        })
        .collect();

    let result_payload = if result.fixed {
        // Some fixes applied but still not valid
        Some(serde_json::json!({
            "original_sql": original_sql,
            "partially_fixed_sql": result.fixed_sql,
            "fixes_applied": result.fixes_applied,
            "iterations": result.iterations,
        }))
    } else {
        None
    };

    AgentResponse {
        status: ResponseStatus::Error,
        code: "FIX_FAILED".to_string(),
        summary: format!(
            "Could not fully fix the query ({} unfixable error{})",
            result.unfixable_errors.len(),
            if result.unfixable_errors.len() == 1 {
                ""
            } else {
                "s"
            }
        ),
        result: result_payload,
        errors: agent_errors,
        suggested_actions: vec![SuggestedAction {
            action: "review_schema".to_string(),
            description: "Review the schema to manually fix remaining errors".to_string(),
            params: None,
        }],
        metadata,
    }
}

// ---------------------------------------------------------------------------
// Conversion: PolicyResult -> AgentResponse
// ---------------------------------------------------------------------------

/// Convert a [`PolicyResult`] to an [`AgentResponse`].
pub fn from_policy(
    result: &PolicyResult,
    schema: &SchemaDefinition,
    time_ms: u64,
) -> AgentResponse {
    let metadata = build_metadata(schema, time_ms);

    if result.allowed && result.warnings.is_empty() {
        return AgentResponse {
            status: ResponseStatus::Success,
            code: "POLICY_PASS".to_string(),
            summary: format!(
                "Query passes all {} policy rules",
                result.policies_evaluated
            ),
            result: Some(serde_json::json!({
                "policies_evaluated": result.policies_evaluated,
                "audit": result.audit,
            })),
            errors: vec![],
            suggested_actions: vec![],
            metadata,
        };
    }

    if result.allowed {
        // Allowed but has warnings
        let agent_errors: Vec<AgentError> = result
            .warnings
            .iter()
            .map(|w| AgentError {
                code: "POLICY_WARNING".to_string(),
                message: w.message.clone(),
                location: None,
                sql_fragment: None,
                suggested_fix: None,
                fix_confidence: None,
            })
            .collect();

        return AgentResponse {
            status: ResponseStatus::Warning,
            code: "POLICY_WARN".to_string(),
            summary: format!(
                "Query allowed with {} warning{}",
                result.warnings.len(),
                if result.warnings.len() == 1 { "" } else { "s" }
            ),
            result: Some(serde_json::json!({
                "policies_evaluated": result.policies_evaluated,
                "audit": result.audit,
            })),
            errors: agent_errors,
            suggested_actions: vec![SuggestedAction {
                action: "review_schema".to_string(),
                description: "Review policy warnings and consider adjusting the query".to_string(),
                params: None,
            }],
            metadata,
        };
    }

    // Not allowed — has violations
    let agent_errors: Vec<AgentError> = result
        .violations
        .iter()
        .map(|v| AgentError {
            code: "POLICY_VIOLATION".to_string(),
            message: v.message.clone(),
            location: None,
            sql_fragment: None,
            suggested_fix: v.remediation.clone(),
            fix_confidence: None,
        })
        .collect();

    let mut actions = Vec::new();
    // If there are remediations, suggest applying them
    if result.violations.iter().any(|v| v.remediation.is_some()) {
        actions.push(SuggestedAction {
            action: "apply_fix".to_string(),
            description: "Apply the suggested remediations to fix policy violations".to_string(),
            params: None,
        });
    }
    actions.push(SuggestedAction {
        action: "review_schema".to_string(),
        description: "Review the policy configuration and schema".to_string(),
        params: None,
    });

    AgentResponse {
        status: ResponseStatus::Error,
        code: "POLICY_VIOLATION".to_string(),
        summary: format!(
            "Query blocked by {} policy violation{}",
            result.violations.len(),
            if result.violations.len() == 1 {
                ""
            } else {
                "s"
            }
        ),
        result: Some(serde_json::json!({
            "policies_evaluated": result.policies_evaluated,
            "audit": result.audit,
        })),
        errors: agent_errors,
        suggested_actions: actions,
        metadata,
    }
}

// ---------------------------------------------------------------------------
// API: full_check
// ---------------------------------------------------------------------------

/// Full pipeline check — the most common agent operation.
///
/// Validates, explains, and optionally checks policy in one call.
/// Returns a single [`AgentResponse`] with combined results.
///
/// # Behavior
///
/// 1. Validates the SQL against the schema
/// 2. If invalid, returns error response with suggested fixes
/// 3. If valid, explains the query (plan steps, lineage, cost signals)
/// 4. If a policy is provided, checks it and merges results
///
/// # Example
///
/// ```rust,no_run
/// use altimate_core::agent::full_check;
/// use altimate_core::SchemaDefinition;
///
/// # async fn example() {
/// let schema = SchemaDefinition::from_yaml_file("schema.yaml").unwrap();
/// let response = full_check("SELECT id FROM users", &schema, None).await;
/// assert_eq!(response.code, "VALID");
/// # }
/// ```
pub async fn full_check(
    sql: &str,
    schema: &SchemaDefinition,
    policy: Option<&PolicyConfig>,
) -> AgentResponse {
    let start = Instant::now();

    // Step 1: Validate
    let validation_result = crate::validator::engine::validate(sql, schema).await;

    if !validation_result.valid {
        let elapsed = start.elapsed().as_millis() as u64;
        let mut response = from_validation(&validation_result, schema, elapsed);
        // Ensure apply_fix is suggested for invalid queries
        if !response
            .suggested_actions
            .iter()
            .any(|a| a.action == "apply_fix")
        {
            response.suggested_actions.insert(
                0,
                SuggestedAction {
                    action: "apply_fix".to_string(),
                    description: "Automatically fix the detected errors".to_string(),
                    params: None,
                },
            );
        }
        return response;
    }

    // Step 2: Explain (query is valid)
    let explanation_result = crate::explainer::engine::explain(sql, schema).await;
    let mut response = from_explanation(&explanation_result, schema, 0);

    // Step 3: Policy check (optional)
    if let Some(policy_config) = policy {
        let policy_result =
            crate::guardrails::engine::check_policy(sql, schema, policy_config).await;

        if !policy_result.allowed {
            // Merge policy violations into the response
            let policy_response = from_policy(&policy_result, schema, 0);
            response.status = policy_response.status;
            response.code = policy_response.code;
            response.summary = policy_response.summary;
            response.errors.extend(policy_response.errors);
            response.suggested_actions = policy_response.suggested_actions;
        } else if !policy_result.warnings.is_empty() {
            // Add policy warnings
            response.status = ResponseStatus::Warning;
            for w in &policy_result.warnings {
                response.errors.push(AgentError {
                    code: "POLICY_WARNING".to_string(),
                    message: w.message.clone(),
                    location: None,
                    sql_fragment: None,
                    suggested_fix: None,
                    fix_confidence: None,
                });
            }
        }
    }

    // Update timing to include the full pipeline
    let elapsed = start.elapsed().as_millis() as u64;
    response.metadata.processing_time_ms = elapsed;

    response
}

// ---------------------------------------------------------------------------
// API: batch_validate
// ---------------------------------------------------------------------------

/// Validate multiple SQL queries at once.
///
/// Runs each request through the validator and converts results into
/// [`AgentResponse`] objects, collected into a [`BatchResponse`] with
/// summary statistics.
///
/// # Example
///
/// ```rust,no_run
/// use altimate_core::agent::{batch_validate, BatchRequest};
/// use altimate_core::SchemaDefinition;
///
/// # async fn example() {
/// let schema = SchemaDefinition::from_yaml_file("schema.yaml").unwrap();
/// let requests = vec![
///     BatchRequest { id: "q1".into(), sql: "SELECT id FROM users".into() },
///     BatchRequest { id: "q2".into(), sql: "SELECT * FROM nonexistent".into() },
/// ];
/// let batch = batch_validate(requests, &schema).await;
/// println!("Passed: {}/{}", batch.passed, batch.total);
/// # }
/// ```
pub async fn batch_validate(
    requests: Vec<BatchRequest>,
    schema: &SchemaDefinition,
) -> BatchResponse {
    let start = Instant::now();
    let total = requests.len();
    let mut results = Vec::with_capacity(total);
    let mut passed = 0usize;
    let mut failed = 0usize;

    for req in requests {
        let item_start = Instant::now();
        let validation_result = crate::validator::engine::validate(&req.sql, schema).await;
        let item_elapsed = item_start.elapsed().as_millis() as u64;

        if validation_result.valid {
            passed += 1;
        } else {
            failed += 1;
        }

        let response = from_validation(&validation_result, schema, item_elapsed);
        results.push(BatchItem {
            id: req.id,
            response,
        });
    }

    let elapsed = start.elapsed().as_millis() as u64;
    let metadata = build_metadata(schema, elapsed);

    BatchResponse {
        results,
        total,
        passed,
        failed,
        metadata,
    }
}

// ---------------------------------------------------------------------------
// Conversion: TranspileResult -> AgentResponse
// ---------------------------------------------------------------------------

/// Convert a [`TranspileResult`] to an [`AgentResponse`].
pub fn from_transpile(result: &TranspileResult, time_ms: u64) -> AgentResponse {
    let metadata = AgentMetadata {
        dialect: result.source_dialect.clone(),
        processing_time_ms: time_ms,
        altimate_version: env!("CARGO_PKG_VERSION").to_string(),
        schema_tables: vec![],
    };

    if result.success {
        AgentResponse {
            status: ResponseStatus::Success,
            code: "TRANSPILED".to_string(),
            summary: format!(
                "Transpiled from {} to {} ({} statement{})",
                result.source_dialect,
                result.target_dialect,
                result.transpiled_sql.len(),
                if result.transpiled_sql.len() == 1 {
                    ""
                } else {
                    "s"
                }
            ),
            result: Some(serde_json::json!({
                "original_sql": result.original_sql,
                "transpiled_sql": result.transpiled_sql,
                "source_dialect": result.source_dialect,
                "target_dialect": result.target_dialect,
            })),
            errors: vec![],
            suggested_actions: vec![],
            metadata,
        }
    } else {
        AgentResponse {
            status: ResponseStatus::Error,
            code: "TRANSPILE_FAILED".to_string(),
            summary: format!(
                "Transpilation failed: {}",
                result.error.as_deref().unwrap_or("unknown error")
            ),
            result: None,
            errors: vec![AgentError {
                code: "TRANSPILE_ERROR".to_string(),
                message: result.error.clone().unwrap_or_default(),
                location: None,
                sql_fragment: None,
                suggested_fix: None,
                fix_confidence: None,
            }],
            suggested_actions: vec![],
            metadata,
        }
    }
}

// ---------------------------------------------------------------------------
// Conversion: ExtractResult -> AgentResponse
// ---------------------------------------------------------------------------

/// Convert an [`ExtractResult`] to an [`AgentResponse`].
pub fn from_extract(result: &ExtractResult, time_ms: u64) -> AgentResponse {
    let metadata = AgentMetadata {
        dialect: String::new(),
        processing_time_ms: time_ms,
        altimate_version: env!("CARGO_PKG_VERSION").to_string(),
        schema_tables: vec![],
    };

    AgentResponse {
        status: ResponseStatus::Success,
        code: "EXTRACTED".to_string(),
        summary: format!(
            "Extracted {} table{}, {} column{}",
            result.tables.len(),
            if result.tables.len() == 1 { "" } else { "s" },
            result.columns.len(),
            if result.columns.len() == 1 { "" } else { "s" }
        ),
        result: Some(serde_json::to_value(result).unwrap_or(serde_json::Value::Null)),
        errors: vec![],
        suggested_actions: vec![],
        metadata,
    }
}

// ---------------------------------------------------------------------------
// Conversion: CompareResult -> AgentResponse
// ---------------------------------------------------------------------------

/// Convert a [`CompareResult`] to an [`AgentResponse`].
pub fn from_compare(result: &CompareResult, time_ms: u64) -> AgentResponse {
    let metadata = AgentMetadata {
        dialect: String::new(),
        processing_time_ms: time_ms,
        altimate_version: env!("CARGO_PKG_VERSION").to_string(),
        schema_tables: vec![],
    };

    let (status, code, summary) = if result.identical {
        (
            ResponseStatus::Success,
            "IDENTICAL".to_string(),
            "Queries are structurally identical".to_string(),
        )
    } else {
        (
            ResponseStatus::Warning,
            "DIFFERENT".to_string(),
            format!(
                "Queries differ: {} change{}",
                result.diff_count,
                if result.diff_count == 1 { "" } else { "s" }
            ),
        )
    };

    AgentResponse {
        status,
        code,
        summary,
        result: Some(serde_json::to_value(result).unwrap_or(serde_json::Value::Null)),
        errors: vec![],
        suggested_actions: vec![],
        metadata,
    }
}

// ---------------------------------------------------------------------------
// Conversion: FormatResult -> AgentResponse
// ---------------------------------------------------------------------------

/// Convert a [`FormatResult`] to an [`AgentResponse`].
pub fn from_format(result: &FormatResult, time_ms: u64) -> AgentResponse {
    let metadata = AgentMetadata {
        dialect: result.dialect.clone(),
        processing_time_ms: time_ms,
        altimate_version: env!("CARGO_PKG_VERSION").to_string(),
        schema_tables: vec![],
    };

    if result.success {
        AgentResponse {
            status: ResponseStatus::Success,
            code: "FORMATTED".to_string(),
            summary: "SQL formatted successfully".to_string(),
            result: Some(serde_json::json!({
                "formatted_sql": result.formatted_sql,
                "dialect": result.dialect,
            })),
            errors: vec![],
            suggested_actions: vec![],
            metadata,
        }
    } else {
        AgentResponse {
            status: ResponseStatus::Error,
            code: "FORMAT_FAILED".to_string(),
            summary: format!(
                "Formatting failed: {}",
                result.error.as_deref().unwrap_or("unknown error")
            ),
            result: None,
            errors: vec![AgentError {
                code: "FORMAT_ERROR".to_string(),
                message: result.error.clone().unwrap_or_default(),
                location: None,
                sql_fragment: None,
                suggested_fix: None,
                fix_confidence: None,
            }],
            suggested_actions: vec![],
            metadata,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::schema::types::SchemaDefinition;

    fn ecommerce_schema() -> SchemaDefinition {
        SchemaDefinition::from_yaml(include_str!(
            "../../../tests/fixtures/schemas/ecommerce.yaml"
        ))
        .expect("Failed to load ecommerce schema fixture")
    }

    // -----------------------------------------------------------------------
    // from_validation tests
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_from_validation_valid_query() {
        let schema = ecommerce_schema();
        let result =
            crate::validator::engine::validate("SELECT id, email FROM users", &schema).await;
        let response = from_validation(&result, &schema, 42);

        assert_eq!(response.status, ResponseStatus::Success);
        assert_eq!(response.code, "VALID");
        assert_eq!(response.summary, "Query is valid");
        assert!(response.errors.is_empty());
        assert_eq!(response.metadata.processing_time_ms, 42);
        assert!(!response.metadata.schema_tables.is_empty());
    }

    #[tokio::test]
    async fn test_from_validation_table_not_found() {
        let schema = ecommerce_schema();
        let result = crate::validator::engine::validate("SELECT * FROM nonexistent", &schema).await;
        let response = from_validation(&result, &schema, 10);

        assert_eq!(response.status, ResponseStatus::Error);
        assert_eq!(response.code, "TABLE_NOT_FOUND");
        assert!(!response.errors.is_empty());
        assert_eq!(response.errors[0].code, "TABLE_NOT_FOUND");
        assert_eq!(
            response.errors[0].sql_fragment.as_deref(),
            Some("nonexistent")
        );
    }

    #[tokio::test]
    async fn test_from_validation_column_not_found() {
        let schema = ecommerce_schema();
        let result = crate::validator::engine::validate("SELECT foo_bar FROM users", &schema).await;
        let response = from_validation(&result, &schema, 10);

        assert_eq!(response.status, ResponseStatus::Error);
        assert_eq!(response.code, "COLUMN_NOT_FOUND");
        assert!(!response.errors.is_empty());
        assert_eq!(response.errors[0].code, "COLUMN_NOT_FOUND");
    }

    #[tokio::test]
    async fn test_from_validation_syntax_error() {
        let schema = ecommerce_schema();
        let result = crate::validator::engine::validate("SELECTZ * FROM users", &schema).await;
        let response = from_validation(&result, &schema, 5);

        assert_eq!(response.status, ResponseStatus::Error);
        assert_eq!(response.code, "SYNTAX_ERROR");
    }

    #[tokio::test]
    async fn test_from_validation_has_suggestions_for_fixable_errors() {
        let schema = ecommerce_schema();
        // "usrs" is close to "users" — should get a fuzzy suggestion
        let result = crate::validator::engine::validate("SELECT * FROM usrs", &schema).await;
        let response = from_validation(&result, &schema, 10);

        assert_eq!(response.status, ResponseStatus::Error);
        // Should have an apply_fix action since there are suggestions
        let has_apply_fix = response
            .suggested_actions
            .iter()
            .any(|a| a.action == "apply_fix");
        assert!(
            has_apply_fix,
            "Expected apply_fix action for fixable error, got: {:?}",
            response.suggested_actions
        );
    }

    // -----------------------------------------------------------------------
    // from_explanation tests
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_from_explanation_valid_query() {
        let schema = ecommerce_schema();
        let result =
            crate::explainer::engine::explain("SELECT id, email FROM users", &schema).await;
        let response = from_explanation(&result, &schema, 20);

        assert_eq!(response.status, ResponseStatus::Success);
        assert_eq!(response.code, "VALID");
        assert!(response.result.is_some());
        let payload = response.result.unwrap();
        assert!(payload.get("summary").is_some());
        assert!(payload.get("plan_steps").is_some());
        assert!(payload.get("lineage").is_some());
        assert!(payload.get("cost_signals").is_some());
    }

    #[tokio::test]
    async fn test_from_explanation_invalid_query() {
        let schema = ecommerce_schema();
        let result = crate::explainer::engine::explain("SELECT * FROM nonexistent", &schema).await;
        let response = from_explanation(&result, &schema, 10);

        assert_eq!(response.status, ResponseStatus::Error);
        assert!(response.result.is_none());
        assert!(!response.errors.is_empty());
    }

    // -----------------------------------------------------------------------
    // from_fix tests
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_from_fix_successful() {
        let schema = ecommerce_schema();
        let fix_result =
            crate::fixer::engine::fix("SELECT * FROM usrs", &schema, &Default::default()).await;
        let response = from_fix(&fix_result, "SELECT * FROM usrs", &schema, 30);

        assert_eq!(response.status, ResponseStatus::Success);
        assert_eq!(response.code, "FIXED");
        assert!(response.result.is_some());
        let payload = response.result.unwrap();
        assert_eq!(payload["fixed_sql"], "SELECT * FROM users");
    }

    #[tokio::test]
    async fn test_from_fix_unfixable() {
        let schema = ecommerce_schema();
        let fix_result =
            crate::fixer::engine::fix("SELECTZ * FROM users", &schema, &Default::default()).await;
        let response = from_fix(&fix_result, "SELECTZ * FROM users", &schema, 10);

        assert_eq!(response.status, ResponseStatus::Error);
        assert_eq!(response.code, "FIX_FAILED");
    }

    // -----------------------------------------------------------------------
    // from_policy tests
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_from_policy_allowed() {
        let schema = ecommerce_schema();
        let policy = PolicyConfig::default();
        let result =
            crate::guardrails::engine::check_policy("SELECT id FROM users", &schema, &policy).await;
        let response = from_policy(&result, &schema, 15);

        assert_eq!(response.status, ResponseStatus::Success);
        assert_eq!(response.code, "POLICY_PASS");
    }

    // -----------------------------------------------------------------------
    // full_check tests
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_full_check_valid_query() {
        let schema = ecommerce_schema();
        let response = full_check("SELECT id, email FROM users", &schema, None).await;

        assert_eq!(response.status, ResponseStatus::Success);
        assert_eq!(response.code, "VALID");
        assert!(response.errors.is_empty());
        assert!(
            response.metadata.processing_time_ms > 0 || response.metadata.processing_time_ms == 0
        );
    }

    #[tokio::test]
    async fn test_full_check_invalid_query() {
        let schema = ecommerce_schema();
        let response = full_check("SELECT * FROM nonexistent", &schema, None).await;

        assert_eq!(response.status, ResponseStatus::Error);
        assert!(!response.errors.is_empty());
    }

    #[tokio::test]
    async fn test_full_check_with_policy() {
        let schema = ecommerce_schema();
        let policy = PolicyConfig::default();
        let response = full_check("SELECT id FROM users", &schema, Some(&policy)).await;

        // With default (empty) policy, should pass
        assert!(
            response.status == ResponseStatus::Success
                || response.status == ResponseStatus::Warning
        );
    }

    // -----------------------------------------------------------------------
    // batch_validate tests
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_batch_validate_mixed() {
        let schema = ecommerce_schema();
        let requests = vec![
            BatchRequest {
                id: "valid".to_string(),
                sql: "SELECT id FROM users".to_string(),
            },
            BatchRequest {
                id: "invalid".to_string(),
                sql: "SELECT * FROM nonexistent".to_string(),
            },
        ];

        let batch = batch_validate(requests, &schema).await;

        assert_eq!(batch.total, 2);
        assert_eq!(batch.passed, 1);
        assert_eq!(batch.failed, 1);
        assert_eq!(batch.results.len(), 2);
        assert_eq!(batch.results[0].id, "valid");
        assert_eq!(batch.results[0].response.status, ResponseStatus::Success);
        assert_eq!(batch.results[1].id, "invalid");
        assert_eq!(batch.results[1].response.status, ResponseStatus::Error);
    }

    #[tokio::test]
    async fn test_batch_validate_all_valid() {
        let schema = ecommerce_schema();
        let requests = vec![
            BatchRequest {
                id: "q1".to_string(),
                sql: "SELECT id FROM users".to_string(),
            },
            BatchRequest {
                id: "q2".to_string(),
                sql: "SELECT * FROM orders".to_string(),
            },
        ];

        let batch = batch_validate(requests, &schema).await;
        assert_eq!(batch.passed, 2);
        assert_eq!(batch.failed, 0);
    }

    #[tokio::test]
    async fn test_batch_validate_empty() {
        let schema = ecommerce_schema();
        let batch = batch_validate(vec![], &schema).await;
        assert_eq!(batch.total, 0);
        assert_eq!(batch.passed, 0);
        assert_eq!(batch.failed, 0);
        assert!(batch.results.is_empty());
    }

    // -----------------------------------------------------------------------
    // Serialization round-trip tests
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_agent_response_json_roundtrip() {
        let schema = ecommerce_schema();
        let result = crate::validator::engine::validate("SELECT id FROM users", &schema).await;
        let response = from_validation(&result, &schema, 10);

        let json = serde_json::to_string(&response).expect("serialize");
        let deserialized: AgentResponse = serde_json::from_str(&json).expect("deserialize");

        assert_eq!(deserialized.status, response.status);
        assert_eq!(deserialized.code, response.code);
        assert_eq!(deserialized.summary, response.summary);
    }

    #[tokio::test]
    async fn test_batch_response_json_roundtrip() {
        let schema = ecommerce_schema();
        let requests = vec![BatchRequest {
            id: "test".to_string(),
            sql: "SELECT id FROM users".to_string(),
        }];
        let batch = batch_validate(requests, &schema).await;

        let json = serde_json::to_string(&batch).expect("serialize");
        let deserialized: BatchResponse = serde_json::from_str(&json).expect("deserialize");

        assert_eq!(deserialized.total, batch.total);
        assert_eq!(deserialized.passed, batch.passed);
    }

    // -----------------------------------------------------------------------
    // Metadata tests
    // -----------------------------------------------------------------------

    #[test]
    fn test_metadata_has_sorted_tables() {
        let schema = ecommerce_schema();
        let meta = build_metadata(&schema, 0);
        let mut sorted = meta.schema_tables.clone();
        sorted.sort();
        assert_eq!(meta.schema_tables, sorted, "schema_tables should be sorted");
    }

    #[test]
    fn test_metadata_has_version() {
        let schema = ecommerce_schema();
        let meta = build_metadata(&schema, 100);
        assert_eq!(meta.altimate_version, env!("CARGO_PKG_VERSION"));
        assert_eq!(meta.processing_time_ms, 100);
    }
}
