use super::{ToolError, arg_optional_object, arg_optional_u64};
use serde_json::{Map, Value, json};

pub(crate) const ANALYZE_ENDPOINT: &str = "/v4/agent/analyze/run";

pub(super) const ANALYZE_WAIT_TIMEOUT_MS_MIN: u64 = 250;
pub(super) const ANALYZE_WAIT_TIMEOUT_MS_MAX: u64 = 60_000;
pub(super) const ANALYZE_AGENT_DEFAULT_WAIT_TIMEOUT_MS: u64 = ANALYZE_WAIT_TIMEOUT_MS_MAX;

pub(super) fn build_analyze_body(args: &Map<String, Value>) -> Result<Value, ToolError> {
    let allowed_fields = [
        "schema_version",
        "mode",
        "canonical_request",
        "semantic_request",
        "analysis_spec",
        "audit_context",
        "wait_timeout_ms",
    ];
    let unexpected_fields = args
        .keys()
        .filter(|field| !allowed_fields.contains(&field.as_str()))
        .cloned()
        .collect::<Vec<_>>();
    if !unexpected_fields.is_empty() {
        return Err(ToolError::new(
            "validation_failed",
            format!(
                "Unknown analyze fields: {}",
                unexpected_fields.join(", ")
            ),
        )
        .with_field(unexpected_fields[0].clone())
        .with_docs_hint(
            "Send schema_version=analyze_envelope.v1, mode=analysis_spec|resolve_and_run|run, and exactly one of analysis_spec, semantic_request, or canonical_request.",
        ));
    }

    let mut body = Map::new();
    body.insert(
        "schema_version".to_string(),
        args.get("schema_version")
            .cloned()
            .unwrap_or_else(|| json!("analyze_envelope.v1")),
    );
    let inferred_mode = if args.get("analysis_spec").is_some() {
        "analysis_spec"
    } else {
        "resolve_and_run"
    };
    let mode = args
        .get("mode")
        .and_then(Value::as_str)
        .unwrap_or(inferred_mode)
        .trim()
        .to_string();
    if !matches!(mode.as_str(), "run" | "resolve_and_run" | "analysis_spec") {
        return Err(ToolError::new(
            "validation_failed",
            "mode must be run, resolve_and_run, or analysis_spec",
        )
        .with_field("mode")
        .with_docs_hint("Use mode=analysis_spec for role-scoped variables. Legacy mode=resolve_and_run and mode=run are compatibility-only."));
    }
    body.insert("mode".to_string(), json!(mode));

    let canonical_request = arg_optional_object(args, "canonical_request")?;
    let semantic_request = arg_optional_object(args, "semantic_request")?;
    let analysis_spec = arg_optional_object(args, "analysis_spec")?;
    let present_count = [
        canonical_request.is_some(),
        semantic_request.is_some(),
        analysis_spec.is_some(),
    ]
    .into_iter()
    .filter(|present| *present)
    .count();
    if present_count != 1 {
        return Err(ToolError::new(
            "validation_failed",
            "send exactly one of analysis_spec, canonical_request, or semantic_request",
        )
        .with_field("analysis_spec")
        .with_docs_hint(
            "mode=analysis_spec requires analysis_spec; legacy mode=run requires canonical_request; legacy mode=resolve_and_run requires semantic_request.",
        ));
    }
    if mode == "analysis_spec" && analysis_spec.is_none() {
        return Err(ToolError::new(
            "validation_failed",
            "mode=analysis_spec requires analysis_spec",
        )
        .with_field("mode")
        .with_docs_hint(
            "Send analysis_spec={question_type, variables:[...]} with role-scoped variables.",
        ));
    }
    if mode == "run" && canonical_request.is_none() {
        return Err(
            ToolError::new("validation_failed", "mode=run requires canonical_request")
                .with_field("mode")
                .with_docs_hint("Use mode=analysis_spec for normal role-scoped requests."),
        );
    }
    if mode == "resolve_and_run" && semantic_request.is_none() {
        return Err(ToolError::new(
            "validation_failed",
            "mode=resolve_and_run requires semantic_request",
        )
        .with_field("mode")
        .with_docs_hint("Use mode=analysis_spec for normal role-scoped requests."));
    }
    if let Some(spec) = analysis_spec {
        body.insert("analysis_spec".to_string(), Value::Object(spec));
    }
    if let Some(canonical) = canonical_request {
        body.insert("canonical_request".to_string(), Value::Object(canonical));
    }
    if let Some(semantic) = semantic_request {
        body.insert("semantic_request".to_string(), Value::Object(semantic));
    }
    if let Some(audit) = arg_optional_object(args, "audit_context")? {
        body.insert("audit_context".to_string(), Value::Object(audit));
    }

    let wait_timeout_ms = arg_optional_u64(args, "wait_timeout_ms")?;
    if let Some(ms) = wait_timeout_ms {
        if !(ANALYZE_WAIT_TIMEOUT_MS_MIN..=ANALYZE_WAIT_TIMEOUT_MS_MAX).contains(&ms) {
            return Err(ToolError::new(
                "validation_failed",
                format!(
                    "wait_timeout_ms={ms} must be between {} and {}",
                    ANALYZE_WAIT_TIMEOUT_MS_MIN, ANALYZE_WAIT_TIMEOUT_MS_MAX
                ),
            )
            .with_field("wait_timeout_ms")
            .with_docs_hint(format!(
                "Use an integer in [{}, {}] milliseconds or omit to use the server default.",
                ANALYZE_WAIT_TIMEOUT_MS_MIN, ANALYZE_WAIT_TIMEOUT_MS_MAX
            )));
        }
    }

    if let Some(ms) = wait_timeout_ms {
        body.insert("wait_timeout_ms".to_string(), Value::Number(ms.into()));
    }
    Ok(Value::Object(body))
}

pub(super) fn prepare_agent_analyze_body(body: &mut Value) {
    let Some(obj) = body.as_object_mut() else {
        return;
    };
    obj.entry("wait_timeout_ms".to_string())
        .or_insert_with(|| Value::Number(ANALYZE_AGENT_DEFAULT_WAIT_TIMEOUT_MS.into()));
}

pub(super) fn analyze_tool_description() -> &'static str {
    "Dispatch one typed analyze envelope to POST /v4/agent/analyze/run. Use this first for trend, explanation, comparison, association, forecast, and decision questions when the exact server-language variables are known. If the signal/source/scope name is unknown, use read_query source_id=analyze_catalog subject=vocabulary_search as bounded vocabulary discovery, then build analysis_spec from returned analyze_fill/candidate handles. Do not prefetch raw sources with read_query; do not prefetch sources with read_query as a routine analysis setup step. Normal new analysis uses mode=analysis_spec with analysis_spec.v1 role-scoped variables: each variable has variable_id, role, signal, source, scope, and time_alignment. For group comparisons, set analysis_spec.comparison_axis=\"group\" and include a role=\"stratifier\" variable such as prior-day activity presence. For broad lower_body_strength trend questions, use signal=estimated_1rm, source=strength_inference, and scope={strength_family:lower_body_strength}; the server keeps comparison_streams separate and returns any global summary as a relative index, not a shared 1RM. Do not put global entity, subject, subjects, outcome, or drivers at analysis_spec root. Use time_alignment.auto_reconcile=\"coarsest\" only as explicit no-lookahead opt-in for sparse exploratory driver scans; temporal_direction refuses coarsened auto reconcile. Use method_hint=\"temporal_direction\" when checking whether a driver tends to precede an outcome or appears to be reactive. If method_hint falls back, the payload includes method_hint_resolution like fell_back_to_X due to Y. Use response_shape=\"rank_summary\" for broad scans that need a compact paged ranking. Legacy mode=resolve_and_run with semantic_request and mode=run with canonical_request are compatibility-only. Raw user text is allowed only in audit_context.raw_user_text; never use raw user-language strings as resolver fields. Broad ranked results put top items first and include driver_results.continuation when more pages exist; continue by sending that next_request through analyze. Timeout is terminal type=timeout; do not sleep, resume, poll, call read_query, or use analyze_catalog to continue the same analysis. If type=answer is returned, stop tool use and write the final user answer from the payload."
}

pub(super) fn analyze_input_schema() -> Value {
    json!({
        "type": "object",
        "examples": [
            {
                "label": "run a role-scoped analysis_spec request",
                "payload": {
                    "schema_version": "analyze_envelope.v1",
                    "mode": "analysis_spec",
                    "analysis_spec": {
                        "schema_version": "analysis_spec.v1",
                        "question_type": "explanation",
                        "variables": [
                            {
                                "variable_id": "jump_height",
                                "role": "outcome",
                                "signal": "jump_height",
                                "source": "performance_tests",
                                "scope": {"protocol_variant": "free_arms"},
                                "time_alignment": {"anchor": "session_date"}
                            },
                            {
                                "variable_id": "power_activity",
                                "role": "driver",
                                "signal": "presence",
                                "source": "activity",
                                "scope": {"group": {"metadata_field": "capability_targets", "value": "power"}},
                                "time_alignment": {"lookback_days": 14}
                            }
                        ]
                    },
                    "wait_timeout_ms": 60000
                }
            },
            {
                "label": "run a role-scoped readiness-style association",
                "payload": {
                    "schema_version": "analyze_envelope.v1",
                    "mode": "analysis_spec",
                    "analysis_spec": {
                        "schema_version": "analysis_spec.v1",
                        "question_type": "association",
                        "variables": [
                            {
                                "variable_id": "e1rm",
                                "role": "outcome",
                                "signal": "estimated_1rm",
                                "source": "strength_inference",
                                "scope": {"comparison_stream": "barbell_back_squat"},
                                "time_alignment": {"strategy": "same_date"}
                            },
                            {
                                "variable_id": "sleep_quality",
                                "role": "driver",
                                "signal": "sleep_quality",
                                "source": "recovery",
                                "scope": {},
                                "time_alignment": {"lookback_days": 1}
                            },
                            {
                                "variable_id": "soreness",
                                "role": "driver",
                                "signal": "soreness",
                                "source": "recovery",
                                "scope": {},
                                "time_alignment": {"lookback_days": 1}
                            }
                        ]
                    },
                    "wait_timeout_ms": 60000
                }
            },
            {
                "label": "run a lower_body_strength bundle trend",
                "payload": {
                    "schema_version": "analyze_envelope.v1",
                    "mode": "analysis_spec",
                    "analysis_spec": {
                        "schema_version": "analysis_spec.v1",
                        "question_type": "trend",
                        "variables": [
                            {
                                "variable_id": "lower_body_strength",
                                "role": "outcome",
                                "signal": "estimated_1rm",
                                "source": "strength_inference",
                                "scope": {"strength_family": "lower_body_strength"},
                                "time_alignment": {"strategy": "same_date"}
                            }
                        ]
                    },
                    "wait_timeout_ms": 60000
                }
            },
            {
                "label": "run a role-scoped outcome against an activity metadata group",
                "payload": {
                    "schema_version": "analyze_envelope.v1",
                    "mode": "analysis_spec",
                    "analysis_spec": {
                        "schema_version": "analysis_spec.v1",
                        "question_type": "explanation",
                        "variables": [
                            {
                                "variable_id": "jump_height",
                                "role": "outcome",
                                "signal": "jump_height",
                                "source": "performance_tests",
                                "scope": {"protocol_variant": "free_arms"},
                                "time_alignment": {"strategy": "same_date"}
                            },
                            {
                                "variable_id": "power_activity",
                                "role": "driver",
                                "signal": "presence",
                                "source": "activity",
                                "scope": {"group": {"metadata_field": "capability_targets", "value": "power"}},
                                "time_alignment": {"lookback_days": 14}
                            }
                        ]
                    },
                    "wait_timeout_ms": 60000
                }
            }
        ],
        "properties": {
            "schema_version": {
                "type": "string",
                "enum": ["analyze_envelope.v1"],
                "description": "Typed analyze envelope schema."
            },
            "mode": {
                "type": "string",
                "enum": ["analysis_spec", "resolve_and_run", "run"],
                "description": "Use analysis_spec for normal role-scoped variables; resolve_and_run and run are compatibility-only."
            },
            "analysis_spec": {
                "type": "object",
                "description": "Role-scoped analysis_spec.v1 plan. Root must not include global entity, subject, subjects, outcome, drivers, explanatory_space, or candidate_space.",
                "properties": {
                    "schema_version": {"type": "string", "enum": ["analysis_spec.v1"]},
                    "question_type": {
                        "type": "string",
                        "enum": ["trend", "explanation", "comparison", "association", "forecast", "decision"]
                    },
                    "variables": {
                        "type": "array",
                        "description": "Role-scoped variables. Each variable has variable_id, role, signal, source, scope, and time_alignment.",
                        "items": {
                            "type": "object",
                            "properties": {
                                "variable_id": {"type": "string"},
                                "role": {"type": "string", "enum": ["outcome", "driver", "control", "comparison_member", "stratifier"]},
                                "signal": {"type": "string"},
                                "source": {"type": "string"},
                                "scope": {"type": "object"},
                                "time_alignment": {
                                    "description": "Explicit time alignment object. Use auto_reconcile:\"coarsest\" with lookahead_policy:\"forbid\" only when sparse exploratory driver scans should fall back to a no-lookahead coarser anchor."
                                },
                                "transform": {},
                                "window": {},
                                "candidate_handle": {"type": "string"}
                            },
                            "required": ["variable_id", "role", "signal", "source", "scope", "time_alignment"],
                            "additionalProperties": false
                        }
                    },
                    "comparison": {"type": "object"},
                    "comparison_axis": {
                        "type": "string",
                        "enum": ["entity", "time_window", "group", "threshold"]
                    },
                    "time_scope_mode": {
                        "type": "string",
                        "enum": ["last_n_days", "date_range", "all_time", "a_vs_b"]
                    },
                    "method_hint": {},
                    "response_shape": {"type": "string", "enum": ["page", "rank_summary"]},
                    "time_window": {"type": "object"},
                    "result_page": {
                        "type": "object",
                        "properties": {
                            "cursor": {"type": "string"},
                            "limit": {"type": "integer", "minimum": 1, "maximum": 50},
                            "offset": {"type": "integer", "minimum": 0}
                        },
                        "additionalProperties": false
                    },
                    "budget_class": {"type": "string", "enum": ["fast", "standard", "broad"]}
                },
                "required": ["question_type", "variables"],
                "additionalProperties": false
            },
            "semantic_request": {
                "type": "object",
                "description": "Legacy compatibility typed server-language semantic request. Normal new analysis should use analysis_spec. Do not include raw user-language strings here.",
                "properties": {
                    "question_type": {
                        "type": "string",
                        "enum": ["trend", "explanation", "comparison", "association", "forecast", "decision"]
                    },
                    "intent_kind": {
                        "type": "string",
                        "enum": [
                            "association_recovery_to_performance",
                            "cross_domain_pain_performance_explanation",
                            "exercise_strength_comparison",
                            "performance_trend",
                            "recovery_sleep_summary",
                            "normalized_progress_comparison",
                            "cross_domain_enjoyment_explanation",
                            "strength_trend",
                            "training_decision_readiness"
                        ]
                    },
                    "outcome": {
                        "type": "object",
                        "description": "Canonical outcome, e.g. {signal:\"jump_height\"} or {signal:\"estimated_1rm\"}."
                    },
                    "subjects": {
                        "type": "array",
                        "description": "Canonical subjects, e.g. {entity_id:\"barbell_back_squat\"} or {canonical_label:\"front_squat\"}."
                    },
                    "drivers": {
                        "type": "array",
                        "description": "Canonical driver signal ids, e.g. {signal:\"sleep_quality\"}."
                    },
                    "candidate_space": {
                        "type": "object",
                        "description": "Source-bound candidate universe to enumerate before analysis, e.g. {space_kind:\"exercise_identity\", source:\"activity\", signals:[\"presence\",\"volume_load_kg\"], window_days:14} or {space_kind:\"exercise_group\", source:\"activity\"}. Supported space_kind values include exercise_identity, exercise_group, and comparison_stream."
                    },
                    "result_page": {
                        "type": "object",
                        "description": "Same-analysis cursor continuation for broad ranked results. Use only the server-issued driver_results.continuation.next_request or {cursor, limit}; do not invent cursors.",
                        "properties": {
                            "cursor": {"type": "string"},
                            "limit": {"type": "integer", "minimum": 1, "maximum": 5},
                            "offset": {"type": "integer", "minimum": 0}
                        },
                        "additionalProperties": false
                    },
                    "comparison_axis": {
                        "type": "string",
                        "enum": ["entity", "time_window", "group", "threshold"]
                    },
                    "time_scope_mode": {
                        "type": "string",
                        "enum": ["last_n_days", "date_range", "all_time", "a_vs_b"]
                    },
                    "time_window": {
                        "type": "object",
                        "description": "Canonical time window, e.g. {days:30} or {date_from,date_to}."
                    },
                    "budget_class": {
                        "type": "string",
                        "enum": ["fast", "standard", "broad"]
                    }
                },
                "additionalProperties": false
            },
            "canonical_request": {
                "type": "object",
                "description": "Canonical request for mode=run: question_type, scope_level, outcome, optional entity/explanatory_space/candidate_space/result_page/comparison_axis/time_scope_mode/budget_class."
            },
            "audit_context": {
                "type": "object",
                "description": "Optional audit-only raw text context. Raw user text belongs here, never in semantic_request or canonical_request."
            },
            "wait_timeout_ms": {
                "type": "integer",
                "minimum": ANALYZE_WAIT_TIMEOUT_MS_MIN,
                "maximum": ANALYZE_WAIT_TIMEOUT_MS_MAX,
                "description": "Optional server-side wait window. Timeout returns terminal type=timeout, not a resume handle."
            }
        },
        "additionalProperties": false
    })
}
