// RLM API endpoints - Phase 8
// Recursive Language Models integration for distributed reasoning

use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
use ontoref_derive::onto_api;
use serde::{Deserialize, Serialize};
use vapora_shared::VaporaError;

use crate::api::error::ApiError;
use crate::api::state::AppState;
use crate::api::ApiResult;

/// Map a prompt-injection or SSRF scanner error to a 400 Bad Request.
fn security_rejection(e: impl std::fmt::Display) -> ApiError {
    ApiError(VaporaError::InvalidInput(format!(
        "Input rejected by security scanner: {}",
        e
    )))
}

/// Request payload for RLM document loading
#[derive(Debug, Deserialize)]
pub struct LoadDocumentRequest {
    /// Unique document ID
    pub doc_id: String,
    /// Document content to chunk and index
    pub content: String,
    /// Optional chunking strategy: "fixed", "semantic", "code"
    #[serde(default = "default_strategy")]
    pub strategy: String,
}

fn default_strategy() -> String {
    "semantic".to_string()
}

/// Response for document loading
#[derive(Debug, Serialize)]
pub struct LoadDocumentResponse {
    /// Number of chunks created
    pub chunk_count: usize,
    /// Document ID
    pub doc_id: String,
    /// Strategy used
    pub strategy: String,
}

/// Request payload for RLM query
#[derive(Debug, Deserialize)]
pub struct QueryRequest {
    /// Document ID to query
    pub doc_id: String,
    /// Query text
    pub query: String,
    /// Number of chunks to retrieve (default: 5)
    #[serde(default = "default_limit")]
    pub limit: usize,
}

fn default_limit() -> usize {
    5
}

/// Response for RLM query
#[derive(Debug, Serialize)]
pub struct QueryResponse {
    /// Query text
    pub query: String,
    /// Retrieved chunks
    pub chunks: Vec<ChunkInfo>,
    /// Number of results
    pub result_count: usize,
}

/// Chunk information in response
#[derive(Debug, Serialize)]
pub struct ChunkInfo {
    /// Chunk ID
    pub chunk_id: String,
    /// Chunk content
    pub content: String,
    /// Combined score
    pub score: f64,
    /// BM25 score (if available)
    pub bm25_score: Option<f64>,
    /// Semantic score (if available)
    pub semantic_score: Option<f64>,
}

/// Request payload for RLM analyze (dispatch to LLM)
#[derive(Debug, Deserialize)]
pub struct AnalyzeRequest {
    /// Document ID to analyze
    pub doc_id: String,
    /// Analysis query/task description
    pub query: String,
    /// Number of chunks to use (default: 5)
    #[serde(default = "default_limit")]
    pub limit: usize,
}

/// Response for RLM analyze
#[derive(Debug, Serialize)]
pub struct AnalyzeResponse {
    /// Query text
    pub query: String,
    /// LLM response text
    pub result: String,
    /// Number of chunks used
    pub chunks_used: usize,
    /// Total input tokens
    pub input_tokens: u64,
    /// Total output tokens
    pub output_tokens: u64,
    /// Number of LLM calls made
    pub num_calls: usize,
    /// Total duration in milliseconds
    pub duration_ms: u64,
}

/// Load and chunk a document
///
/// POST /api/v1/rlm/documents
#[onto_api(
    method = "POST",
    path = "/api/v1/rlm/documents",
    description = "Loads, scans, and chunks a document into the RLM index for subsequent queries.",
    auth = "bearer",
    actors = "developer, agent, admin",
    tags = "rlm, write"
)]
pub async fn load_document(
    State(state): State<AppState>,
    Json(request): Json<LoadDocumentRequest>,
) -> ApiResult<impl IntoResponse> {
    // Scan and sanitize content before indexing — stored chunks are injected
    // verbatim into LLM prompts at query time, making upload the critical
    // injection point.
    crate::security::prompt_injection::scan(&request.content).map_err(security_rejection)?;
    let content = crate::security::prompt_injection::sanitize(
        &request.content,
        crate::security::prompt_injection::MAX_PROMPT_CHARS,
    );

    let rlm_engine = state
        .rlm_engine
        .as_ref()
        .ok_or_else(|| anyhow::anyhow!("RLM engine not configured"))?;

    // Load document with specified strategy
    let chunk_count = rlm_engine
        .load_document(&request.doc_id, &content, None)
        .await?;

    Ok((
        StatusCode::CREATED,
        Json(LoadDocumentResponse {
            chunk_count,
            doc_id: request.doc_id,
            strategy: request.strategy,
        }),
    ))
}

/// Query a document using hybrid search
///
/// POST /api/v1/rlm/query
#[onto_api(
    method = "POST",
    path = "/api/v1/rlm/query",
    description = "Retrieves relevant document chunks using BM25 and semantic hybrid search.",
    auth = "bearer",
    actors = "developer, agent, admin",
    tags = "rlm, read"
)]
pub async fn query_document(
    State(state): State<AppState>,
    Json(request): Json<QueryRequest>,
) -> ApiResult<impl IntoResponse> {
    crate::security::prompt_injection::scan(&request.query).map_err(security_rejection)?;

    let rlm_engine = state
        .rlm_engine
        .as_ref()
        .ok_or_else(|| anyhow::anyhow!("RLM engine not configured"))?;

    // Query document with hybrid search
    let results = rlm_engine
        .query(&request.doc_id, &request.query, None, request.limit)
        .await?;

    // Convert to API response format
    let chunks: Vec<ChunkInfo> = results
        .iter()
        .map(|scored_chunk| ChunkInfo {
            chunk_id: scored_chunk.chunk.chunk_id.clone(),
            content: scored_chunk.chunk.content.clone(),
            score: scored_chunk.score as f64,
            bm25_score: scored_chunk.bm25_score.map(|s| s as f64),
            semantic_score: scored_chunk.semantic_score.map(|s| s as f64),
        })
        .collect();

    Ok(Json(QueryResponse {
        query: request.query,
        result_count: chunks.len(),
        chunks,
    }))
}

/// Analyze a document with LLM dispatch
///
/// POST /api/v1/rlm/analyze
#[onto_api(
    method = "POST",
    path = "/api/v1/rlm/analyze",
    description = "Dispatches a query against retrieved chunks to an LLM and returns the \
                   synthesized response.",
    auth = "bearer",
    actors = "developer, agent, admin",
    tags = "rlm, read"
)]
pub async fn analyze_document(
    State(state): State<AppState>,
    Json(request): Json<AnalyzeRequest>,
) -> ApiResult<impl IntoResponse> {
    // query goes directly to the LLM — scan for injection and sanitize.
    crate::security::prompt_injection::scan(&request.query).map_err(security_rejection)?;
    let query = crate::security::prompt_injection::sanitize(
        &request.query,
        crate::security::prompt_injection::MAX_PROMPT_CHARS,
    );

    let rlm_engine = state
        .rlm_engine
        .as_ref()
        .ok_or_else(|| anyhow::anyhow!("RLM engine not configured"))?;

    // Dispatch subtask to LLM
    let result = rlm_engine
        .dispatch_subtask(&request.doc_id, &query, None, request.limit)
        .await?;

    Ok(Json(AnalyzeResponse {
        query,
        result: result.text,
        chunks_used: request.limit,
        input_tokens: result.total_input_tokens,
        output_tokens: result.total_output_tokens,
        num_calls: result.num_calls,
        duration_ms: result.total_duration_ms,
    }))
}

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

    #[test]
    fn test_load_document_request_deserialization() {
        let json = r#"{"doc_id": "doc-1", "content": "test content"}"#;
        let request: LoadDocumentRequest = serde_json::from_str(json).unwrap();
        assert_eq!(request.doc_id, "doc-1");
        assert_eq!(request.content, "test content");
        assert_eq!(request.strategy, "semantic"); // default
    }

    #[test]
    fn test_query_request_deserialization() {
        let json = r#"{"doc_id": "doc-1", "query": "test query"}"#;
        let request: QueryRequest = serde_json::from_str(json).unwrap();
        assert_eq!(request.doc_id, "doc-1");
        assert_eq!(request.query, "test query");
        assert_eq!(request.limit, 5); // default
    }

    #[test]
    fn test_analyze_request_deserialization() {
        let json = r#"{"doc_id": "doc-1", "query": "analyze this", "limit": 10}"#;
        let request: AnalyzeRequest = serde_json::from_str(json).unwrap();
        assert_eq!(request.doc_id, "doc-1");
        assert_eq!(request.query, "analyze this");
        assert_eq!(request.limit, 10);
    }

    #[test]
    fn test_load_document_response_serialization() {
        let response = LoadDocumentResponse {
            chunk_count: 42,
            doc_id: "doc-1".to_string(),
            strategy: "semantic".to_string(),
        };
        let json = serde_json::to_string(&response).unwrap();
        assert!(json.contains("42"));
        assert!(json.contains("doc-1"));
        assert!(json.contains("semantic"));
    }

    #[test]
    fn test_query_response_serialization() {
        let response = QueryResponse {
            query: "test query".to_string(),
            result_count: 2,
            chunks: vec![
                ChunkInfo {
                    chunk_id: "chunk-1".to_string(),
                    content: "content 1".to_string(),
                    score: 0.8,
                    bm25_score: Some(0.7),
                    semantic_score: Some(0.9),
                },
                ChunkInfo {
                    chunk_id: "chunk-2".to_string(),
                    content: "content 2".to_string(),
                    score: 0.6,
                    bm25_score: Some(0.5),
                    semantic_score: None,
                },
            ],
        };
        let json = serde_json::to_string(&response).unwrap();
        assert!(json.contains("test query"));
        assert!(json.contains("chunk-1"));
        assert!(json.contains("chunk-2"));
    }

    #[test]
    fn test_analyze_response_serialization() {
        let response = AnalyzeResponse {
            query: "analyze query".to_string(),
            result: "analysis result".to_string(),
            chunks_used: 5,
            input_tokens: 1000,
            output_tokens: 500,
            num_calls: 2,
            duration_ms: 3000,
        };
        let json = serde_json::to_string(&response).unwrap();
        assert!(json.contains("analyze query"));
        assert!(json.contains("analysis result"));
        assert!(json.contains("1000"));
        assert!(json.contains("500"));
    }
}
