use axum::Json;
use axum::extract::State;
use axum::extract::rejection::JsonRejection;
use serde::Serialize;
use serde_json::Value;
use uuid::Uuid;
use crw_core::error::CrwError;
use crw_core::types::{ImageResult, SearchData, SearchRequest, SearchResult};
use crate::error::AppError;
use crate::routes::search::search_inner;
use crate::state::AppState;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct V2SearchResponse {
pub success: bool,
pub data: V2SearchData,
pub credits_used: u32,
pub id: String,
}
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct V2SearchData {
#[serde(skip_serializing_if = "Option::is_none")]
pub web: Option<Vec<SearchResult>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub news: Option<Vec<SearchResult>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub images: Option<Vec<ImageResult>>,
}
fn normalize_search_body(mut v: Value) -> Value {
if let Some(opts) = v.get_mut("scrapeOptions").and_then(Value::as_object_mut)
&& let Some(Value::Array(arr)) = opts.get("formats").cloned()
{
let mut strs = Vec::new();
let mut schema: Option<Value> = None;
for f in arr {
match f {
Value::String(s) => strs.push(Value::String(s)),
Value::Object(m) => {
if let Some(t) = m.get("type").and_then(Value::as_str) {
strs.push(Value::String(t.to_string()));
if t == "json"
&& let Some(s) = m.get("schema")
{
schema = Some(s.clone());
}
}
}
_ => {}
}
}
opts.insert("formats".to_string(), Value::Array(strs));
if let Some(s) = schema {
opts.entry("jsonSchema".to_string()).or_insert(s);
}
}
v
}
fn shape(results: SearchData) -> V2SearchData {
match results {
SearchData::Flat(v) => V2SearchData {
web: Some(v),
..Default::default()
},
SearchData::Grouped(g) => V2SearchData {
web: g.web,
news: g.news,
images: g.images,
},
}
}
pub async fn search(
State(state): State<AppState>,
body: Result<Json<Value>, JsonRejection>,
) -> Result<Json<V2SearchResponse>, AppError> {
let Json(raw) = body.map_err(AppError::from)?;
let normalized = normalize_search_body(raw);
let req: SearchRequest = serde_json::from_value(normalized)
.map_err(|e| CrwError::InvalidRequest(format!("Invalid search request: {e}")))?;
let resp = search_inner(&state, req).await?;
let data = resp.data.map(|d| shape(d.results)).unwrap_or_default();
Ok(Json(V2SearchResponse {
success: true,
data,
credits_used: 0,
id: Uuid::new_v4().to_string(),
}))
}