use crate::client::TrendsClient;
use crate::models::{
ExploreRequest, GeoResolution, InterestByRegion, InterestOverTime, RelatedQueries,
};
use crate::Result;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct AgentTrendReport {
pub keyword_trends: String,
pub regional_hotspots: String,
pub related_queries: String,
}
impl AgentTrendReport {
pub fn to_llm_markdown(&self) -> String {
format!(
"## Google Trends Market Report\n\n### Interest Over Time\n{}\n\n### Regional Hotspots\n{}\n\n### Related Queries\n{}",
self.keyword_trends, self.regional_hotspots, self.related_queries
)
}
}
pub struct GwelleAgent {
client: TrendsClient,
}
impl GwelleAgent {
pub async fn new() -> Result<Self> {
let cookies = crate::session::bootstrap_session().await?;
let client = TrendsClient::new(&cookies, "en-US", -60)?;
Ok(Self { client })
}
pub async fn fetch_report(
&self,
keywords: Vec<String>,
geo: &str,
timeframe: &str,
) -> Result<AgentTrendReport> {
let req = ExploreRequest {
keywords: keywords.clone(),
geo: geo.to_string(),
timeframe: timeframe.to_string(),
category: 0,
property: "".to_string(),
};
let explore = self.client.explore(&req).await?;
let keyword_trends = if let Some(token) = &explore.interest_over_time {
let data = self.client.interest_over_time(token).await?;
format_interest_over_time(&data, &keywords)
} else {
return Err(crate::TrendsError::TokenNotFound(
"No timeline capability available.".into(),
));
};
let regional_hotspots = if let Some(token) = &explore.interest_by_region {
let res = if geo.is_empty() {
GeoResolution::Country
} else {
GeoResolution::Region
};
let geo_data = self.client.interest_by_region(token, res).await?;
format_geo_map(&geo_data)
} else {
return Err(crate::TrendsError::TokenNotFound(
"No regional capability available.".into(),
));
};
let related_queries = if let Some(token) = &explore.related_queries {
let data = self.client.related_queries(token).await?;
format_related_queries(&data)
} else {
return Err(crate::TrendsError::TokenNotFound(
"No related queries capability available.".into(),
));
};
Ok(AgentTrendReport {
keyword_trends,
regional_hotspots,
related_queries,
})
}
}
fn format_interest_over_time(data: &InterestOverTime, keywords: &[String]) -> String {
if data.timeline_data.is_empty() {
return "No sufficient trend volume data available.".to_string();
}
let mut out = String::new();
let n = data.timeline_data.len();
for (idx, keyword) in keywords.iter().enumerate() {
let mut max_val: Option<u32> = None;
let mut min_val: Option<u32> = None;
let mut max_date = String::new();
let mut min_date = String::new();
for point in &data.timeline_data {
if let Some(&val) = point.values.get(idx) {
if max_val.is_none_or(|current| val > current) {
max_val = Some(val);
max_date = point.formatted_time.clone();
}
if min_val.is_none_or(|current| val < current) {
min_val = Some(val);
min_date = point.formatted_time.clone();
}
}
}
let (max_val, min_val) = match (max_val, min_val) {
(Some(max_val), Some(min_val)) => (max_val, min_val),
_ => {
out.push_str(&format!("**Keyword:** `{}`\n", keyword));
out.push_str("- No timeline values available.\n\n");
continue;
}
};
let current_val = data
.timeline_data
.last()
.and_then(|pt| pt.values.get(idx).copied())
.unwrap_or(0);
out.push_str(&format!("**Keyword:** `{}`\n", keyword));
out.push_str(&format!("- Current Status (Latest): {}/100\n", current_val));
out.push_str(&format!(
"- Peak Volatility: {}/100 on {}\n",
max_val, max_date
));
out.push_str(&format!(
"- Lowest Point: {}/100 on {}\n",
min_val, min_date
));
if n >= 4 {
let start = data.timeline_data[0].values.get(idx).unwrap_or(&0);
let q1 = data.timeline_data[n / 4].values.get(idx).unwrap_or(&0);
let q2 = data.timeline_data[n / 2].values.get(idx).unwrap_or(&0);
let q3 = data.timeline_data[3 * n / 4].values.get(idx).unwrap_or(&0);
out.push_str(&format!(
"- Macro Curve (Start -> Q1 -> Q2 -> Q3 -> End): {} -> {} -> {} -> {} -> {}\n",
start, q1, q2, q3, current_val
));
}
out.push('\n');
}
out
}
fn format_geo_map(geo_data: &InterestByRegion) -> String {
let mut out = String::new();
let mut sorted_geo = geo_data.geo_map_data.clone();
sorted_geo.sort_by(|a, b| {
b.values
.first()
.unwrap_or(&0)
.cmp(a.values.first().unwrap_or(&0))
});
out.push_str("Top 5 Penetration Regions:\n");
for region in sorted_geo.iter().take(5) {
if let Some(v) = region.values.first() {
if *v > 0 {
out.push_str(&format!(
"- {} ({}): {}/100\n",
region.geo_name, region.geo_code, v
));
}
}
}
out
}
fn format_related_queries(data: &RelatedQueries) -> String {
let mut out = String::new();
out.push_str("**Top Search Correlates:**\n");
for q in data.top.iter().take(5) {
out.push_str(&format!("- {} (Relevance: {})\n", q.query, q.value));
}
out.push_str("\n**Rising / Breakout Topics:**\n");
for q in data.rising.iter().take(5) {
out.push_str(&format!("- {} (Growth: {})\n", q.query, q.value));
}
out
}