use std::collections::BTreeMap;
use retiretui_engine::params::Inflation;
use retiretui_engine::plan::Dollars;
use retiretui_engine::project::{ClassTotals, Summary, YearRow, project};
use rmcp::handler::server::wrapper::{Json, Parameters};
use rmcp::{tool, tool_router};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::PlanServer;
use crate::commands::actions::{self, ActionsReply};
#[derive(Deserialize, JsonSchema)]
pub struct ProjectPlanArgs {
pub path: String,
pub from_year: Option<i16>,
pub to_year: Option<i16>,
#[serde(default)]
pub detail: Detail,
}
#[derive(Deserialize, JsonSchema, Clone, Copy, Default)]
#[serde(rename_all = "lowercase")]
pub enum Detail {
#[default]
Summary,
Full,
}
#[derive(Serialize, JsonSchema)]
pub struct ProjectionReply {
pub years: Vec<serde_json::Value>,
}
#[derive(Serialize)]
struct SummaryRow<'a> {
year: i16,
ages: &'a BTreeMap<String, u8>,
total_income: Dollars,
expenses: Dollars,
taxes: Dollars,
withdrawals: Dollars,
class_totals: ClassTotals,
net_worth: Dollars,
surplus: Dollars,
unfunded: Dollars,
deflator: f64,
}
#[derive(Deserialize, JsonSchema)]
pub struct ComparePlansArgs {
pub paths: Vec<String>,
#[serde(default)]
pub nominal: bool,
}
#[derive(Serialize, JsonSchema)]
pub struct CompareReply {
pub plans: Vec<ComparedPlan>,
}
#[derive(Serialize, JsonSchema)]
pub struct ComparedPlan {
pub path: String,
pub name: Option<String>,
pub summary: Summary,
}
#[derive(Deserialize, JsonSchema)]
pub struct PlanActionsArgs {
pub path: String,
pub year: Option<i16>,
}
#[derive(Deserialize, JsonSchema)]
pub struct TaxParametersArgs {
pub year: i16,
pub inflation: Option<f64>,
}
#[tool_router(router = query_router, vis = "pub(super)")]
impl PlanServer {
#[tool]
fn project_plan(
&self,
Parameters(args): Parameters<ProjectPlanArgs>,
) -> Result<Json<ProjectionReply>, String> {
let plan = self.load_valid_plan(&args.path)?;
let projection = project(&plan, &self.tables);
let years = projection
.years
.iter()
.filter(|row| {
args.from_year.is_none_or(|from| row.year >= from)
&& args.to_year.is_none_or(|to| row.year <= to)
})
.map(|row| row_value(row, args.detail))
.collect();
Ok(Json(ProjectionReply { years }))
}
#[tool]
fn compare_plans(
&self,
Parameters(ComparePlansArgs { paths, nominal }): Parameters<ComparePlansArgs>,
) -> Result<Json<CompareReply>, String> {
if paths.len() < 2 {
return Err("compare_plans needs at least two paths".to_owned());
}
let plans = paths
.into_iter()
.map(|path| {
let plan = self.load_valid_plan(&path)?;
let summary = project(&plan, &self.tables).summary(!nominal);
Ok(ComparedPlan {
path,
name: plan.plan.name,
summary,
})
})
.collect::<Result<Vec<_>, String>>()?;
Ok(Json(CompareReply { plans }))
}
#[tool]
fn plan_actions(
&self,
Parameters(args): Parameters<PlanActionsArgs>,
) -> Result<Json<ActionsReply>, String> {
let plan = self.load_valid_plan(&args.path)?;
let projection = project(&plan, &self.tables);
let year = args.year.unwrap_or_else(actions::current_year);
let row = actions::year_row(&projection, year)?;
let warnings = actions::collect_warnings(&plan, &self.tables, row);
Ok(Json(ActionsReply::new(row, warnings)))
}
#[tool(name = "tax_parameters")]
fn resolve_tax_parameters(
&self,
Parameters(TaxParametersArgs { year, inflation }): Parameters<TaxParametersArgs>,
) -> Result<Json<serde_json::Value>, String> {
let latest = self.tables.latest_known_year().unwrap_or(year);
let inflation = match inflation {
Some(rate) => rate,
None if year <= latest => 0.0,
None => {
return Err(format!(
"{year} is past the last known table ({latest}); \
pass `inflation` to extend it"
));
}
};
serde_json::to_value(
self.tables
.params_for(year, &Inflation::constant(inflation)),
)
.map(Json)
.map_err(|err| err.to_string())
}
#[tool]
fn describe_schema() -> String {
include_str!("schema.md").to_owned()
}
}
fn row_value(row: &YearRow, detail: Detail) -> serde_json::Value {
let value = match detail {
Detail::Full => serde_json::to_value(row),
Detail::Summary => serde_json::to_value(SummaryRow {
year: row.year,
ages: &row.ages,
total_income: row.total_income,
expenses: row.expenses,
taxes: row.taxes.total,
withdrawals: row.total_withdrawals(),
class_totals: row.class_totals,
net_worth: row.net_worth,
surplus: row.surplus,
unfunded: row.unfunded,
deflator: row.deflator,
}),
};
value.expect("plain data serializes")
}