use roder_api::context::{
    ContextBlock, ContextBlockKind, ContextPlan, ContextPlanner, ContextPlannerId, ContextQuery,
};
use roder_api::retrieval::{
    RetrievalAvoidance, RetrievalConfidence, RetrievalIntent, RetrievalMode,
    RetrievalRecommendation, RetrievalRoutePlan,
};
use serde::Serialize;
use serde_json::json;
use time::OffsetDateTime;

#[derive(Debug, Clone, Default)]
pub struct RetrievalRouterPlanner;

#[async_trait::async_trait]
impl ContextPlanner for RetrievalRouterPlanner {
    fn id(&self) -> ContextPlannerId {
        "retrieval-router".to_string()
    }

    async fn plan(
        &self,
        query: &ContextQuery,
        mut provider_blocks: Vec<ContextBlock>,
    ) -> anyhow::Result<ContextPlan> {
        let plan = route_retrieval(query, &provider_blocks);
        if !plan.recommended.is_empty() || !plan.avoid.is_empty() {
            provider_blocks.push(render_retrieval_block(&plan));
        }
        provider_blocks.sort_by_key(|block| std::cmp::Reverse(block.priority));
        Ok(ContextPlan {
            blocks: provider_blocks,
        })
    }
}

pub fn route_retrieval(
    query: &ContextQuery,
    provider_blocks: &[ContextBlock],
) -> RetrievalRoutePlan {
    let prompt = query.prompt.to_ascii_lowercase();
    let mut recommended = Vec::new();
    let mut avoid = Vec::new();
    let intent = classify_intent(&prompt);
    let semantic_ready = provider_blocks.iter().any(|block| {
        block
            .metadata
            .get("source")
            .and_then(serde_json::Value::as_str)
            == Some("indexed_semantic_code_search")
    });

    if looks_like_command_failure(&prompt) {
        recommended.push(recommend(
            RetrievalMode::Artifact,
            "grep_artifact",
            extract_query(&query.prompt),
            "command or terminal output failure should start with saved artifact search",
            RetrievalConfidence::High,
        ));
    }
    if looks_like_capability_lookup(&prompt) {
        if let Some(item_id) = matching_promoted_capability(provider_blocks, &prompt) {
            let mut rec = recommend(
                RetrievalMode::Promotion,
                "discovery.read",
                extract_query(&query.prompt),
                "matching capability is already promoted or warm-cached for this session",
                RetrievalConfidence::High,
            );
            rec.item_id = Some(item_id);
            recommended.push(rec);
        }
        recommended.push(recommend(
            RetrievalMode::Discovery,
            "discovery.search",
            extract_query(&query.prompt),
            "tool, MCP, skill, command, or plugin capability lookup",
            RetrievalConfidence::High,
        ));
    }
    if looks_like_capability_execution(&prompt) {
        recommended.push(recommend(
            RetrievalMode::Promotion,
            "discovery.read",
            extract_query(&query.prompt),
            "full schema or instructions are needed before capability use",
            RetrievalConfidence::High,
        ));
    }
    if looks_like_file_name(&query.prompt) {
        recommended.push(recommend(
            RetrievalMode::FileName,
            "glob",
            extract_query(&query.prompt),
            "path or filename-shaped prompt",
            RetrievalConfidence::High,
        ));
    }
    if looks_like_exact_search(&query.prompt) {
        recommended.push(recommend(
            RetrievalMode::ExactText,
            "grep",
            extract_query(&query.prompt),
            "exact symbol, path, regex, or error string",
            RetrievalConfidence::High,
        ));
    }
    if matches!(intent, RetrievalIntent::BroadConcept) {
        if semantic_ready {
            recommended.push(recommend(
                RetrievalMode::SemanticCode,
                "code_index.search",
                extract_query(&query.prompt),
                "conceptual code search with ready semantic index",
                RetrievalConfidence::Medium,
            ));
        } else {
            recommended.push(recommend(
                RetrievalMode::ExactText,
                "grep",
                extract_query(&query.prompt),
                "semantic index not observed; start with exact local search fallback",
                RetrievalConfidence::Medium,
            ));
        }
    }
    if prompt.contains("history") || prompt.contains("previous turn") || prompt.contains("resume") {
        recommended.push(recommend(
            RetrievalMode::History,
            "history.search",
            extract_query(&query.prompt),
            "prior conversation or session recovery",
            RetrievalConfidence::Medium,
        ));
    }
    if prompt.contains("code") || prompt.contains("repo") || prompt.contains("workspace") {
        avoid.push(RetrievalAvoidance {
            mode: RetrievalMode::Web,
            reason: "local workspace retrieval should be tried before web search".to_string(),
        });
    }

    dedupe_recommendations(&mut recommended);
    RetrievalRoutePlan {
        route_id: format!("route:{}:{}", query.thread_id, query.turn_id),
        thread_id: query.thread_id.clone(),
        turn_id: query.turn_id.clone(),
        intent,
        recommended,
        avoid,
        timestamp: OffsetDateTime::now_utc(),
    }
}

fn render_retrieval_block(plan: &RetrievalRoutePlan) -> ContextBlock {
    let mut text = format!("Retrieval route intent: {:?}", plan.intent);
    for (index, rec) in plan.recommended.iter().take(5).enumerate() {
        text.push_str(&format!(
            "\n{}. {:?} via `{}` query `{}` - {}",
            index + 1,
            rec.mode,
            rec.tool,
            truncate(&rec.query, 80),
            rec.reason
        ));
    }
    if !plan.avoid.is_empty() {
        let avoid = plan
            .avoid
            .iter()
            .map(|avoid| format!("{:?}: {}", avoid.mode, avoid.reason))
            .collect::<Vec<_>>()
            .join("; ");
        text.push_str(&format!("\nAvoid: {avoid}"));
    }

    ContextBlock {
        id: "retrieval-router".to_string(),
        kind: ContextBlockKind::RetrievalHint,
        text,
        priority: 88,
        token_estimate: None,
        metadata: json!({
            "planner": "retrieval-router",
            "route_id": plan.route_id,
            "intent": format!("{:?}", plan.intent),
            "retrievalPlan": serializable(plan),
            "recommended": serializable(&plan.recommended),
            "avoid": serializable(&plan.avoid),
        }),
    }
}

fn classify_intent(prompt: &str) -> RetrievalIntent {
    if prompt.contains("tool")
        || prompt.contains("mcp")
        || prompt.contains("skill")
        || prompt.contains("plugin")
    {
        return RetrievalIntent::InspectTool;
    }
    if looks_like_command_failure(prompt) {
        return RetrievalIntent::DebugFailure;
    }
    if prompt.contains("usage") || prompt.contains("call sites") || prompt.contains("where used") {
        return RetrievalIntent::TraceUsage;
    }
    if prompt.contains("history") || prompt.contains("previous turn") || prompt.contains("resume") {
        return RetrievalIntent::RecoverHistory;
    }
    if prompt.contains("file") || prompt.contains("path") || prompt.contains("filename") {
        return RetrievalIntent::FileLookup;
    }
    if looks_like_exact_search(prompt) {
        return RetrievalIntent::FindDefinition;
    }
    RetrievalIntent::BroadConcept
}

fn recommend(
    mode: RetrievalMode,
    tool: &str,
    query: String,
    reason: &str,
    confidence: RetrievalConfidence,
) -> RetrievalRecommendation {
    RetrievalRecommendation {
        mode,
        tool: tool.to_string(),
        query,
        reason: reason.to_string(),
        confidence,
        item_id: None,
    }
}

fn dedupe_recommendations(recommended: &mut Vec<RetrievalRecommendation>) {
    let mut seen = std::collections::BTreeSet::new();
    recommended.retain(|rec| seen.insert((rec.mode.clone(), rec.tool.clone())));
    recommended.truncate(5);
}

fn looks_like_capability_lookup(prompt: &str) -> bool {
    prompt.contains("tool")
        || prompt.contains("mcp")
        || prompt.contains("skill")
        || prompt.contains("command")
        || prompt.contains("plugin")
}

fn looks_like_capability_execution(prompt: &str) -> bool {
    looks_like_capability_lookup(prompt)
        && (prompt.contains("run")
            || prompt.contains("use")
            || prompt.contains("execute")
            || prompt.contains("call")
            || prompt.contains("invoke"))
}

fn looks_like_command_failure(prompt: &str) -> bool {
    prompt.contains("stderr")
        || prompt.contains("stdout")
        || prompt.contains("exit code")
        || prompt.contains("terminal")
        || prompt.contains("command failed")
        || prompt.contains("panic")
        || prompt.contains("stack trace")
}

fn looks_like_file_name(prompt: &str) -> bool {
    prompt.contains('/')
        || prompt.contains(".rs")
        || prompt.contains(".ts")
        || prompt.contains(".tsx")
        || prompt.contains(".json")
        || prompt.contains(".toml")
        || prompt.contains(".md")
}

fn looks_like_exact_search(prompt: &str) -> bool {
    prompt.contains("::")
        || prompt.contains("->")
        || prompt.contains("fn ")
        || prompt.contains("struct ")
        || prompt.contains("enum ")
        || prompt.split_whitespace().any(|token| {
            token.len() >= 4
                && token
                    .chars()
                    .any(|ch| ch == '_' || ch.is_ascii_uppercase() || ch.is_ascii_digit())
        })
}

fn matching_promoted_capability(blocks: &[ContextBlock], prompt: &str) -> Option<String> {
    let prompt_tokens = prompt
        .split(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '-')
        .map(str::to_ascii_lowercase)
        .filter(|token| token.len() >= 3)
        .collect::<Vec<_>>();
    blocks.iter().find_map(|block| {
        let source = block
            .metadata
            .get("source")
            .and_then(serde_json::Value::as_str)
            .unwrap_or_default();
        if !matches!(
            source,
            "promoted_capabilities" | "discovery_promotions" | "warm_cached_capabilities"
        ) {
            return None;
        }
        let item_id = block
            .metadata
            .get("item_id")
            .and_then(serde_json::Value::as_str)
            .or_else(|| {
                block
                    .metadata
                    .get("itemId")
                    .and_then(serde_json::Value::as_str)
            })?;
        let haystack = format!("{} {}", item_id, block.text).to_ascii_lowercase();
        prompt_tokens
            .iter()
            .any(|token| haystack.contains(token))
            .then(|| item_id.to_string())
    })
}

fn extract_query(prompt: &str) -> String {
    truncate(prompt.trim(), 120).to_string()
}

fn truncate(text: &str, max: usize) -> &str {
    if text.len() <= max {
        return text;
    }
    let mut end = max;
    while !text.is_char_boundary(end) {
        end -= 1;
    }
    &text[..end]
}

fn serializable<T: Serialize>(value: &T) -> serde_json::Value {
    serde_json::to_value(value).unwrap_or_else(|_| serde_json::Value::Null)
}

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

    #[tokio::test]
    async fn retrieval_router_routes_exact_symbols_to_grep() {
        let planner = RetrievalRouterPlanner;
        let plan = planner
            .plan(&query("Find ToolExecutionContext in the repo"), Vec::new())
            .await
            .unwrap();

        let block = plan
            .blocks
            .iter()
            .find(|block| block.kind == ContextBlockKind::RetrievalHint)
            .unwrap();
        assert!(block.text.contains("ExactText"));
        assert!(block.text.contains("`grep`"));
    }

    #[tokio::test]
    async fn retrieval_router_routes_concepts_to_semantic_when_index_ready() {
        let planner = RetrievalRouterPlanner;
        let semantic_block = ContextBlock {
            id: "code-index".to_string(),
            kind: ContextBlockKind::RetrievedDocument,
            text: "Indexed context".to_string(),
            priority: 10,
            token_estimate: None,
            metadata: json!({ "source": "indexed_semantic_code_search" }),
        };

        let plan = planner
            .plan(
                &query("How does the policy gate choose approvals?"),
                vec![semantic_block],
            )
            .await
            .unwrap();

        let block = plan.blocks.first().unwrap();
        assert_eq!(block.kind, ContextBlockKind::RetrievalHint);
        assert!(block.text.contains("SemanticCode"));
        assert!(block.text.contains("code_index.search"));
    }

    #[tokio::test]
    async fn retrieval_router_routes_capability_execution_to_discovery_and_promotion() {
        let planner = RetrievalRouterPlanner;
        let plan = planner
            .plan(
                &query("Use the GitHub MCP issue search tool to find blockers"),
                Vec::new(),
            )
            .await
            .unwrap();
        let block = plan.blocks.first().unwrap();

        assert!(block.text.contains("Discovery"));
        assert!(block.text.contains("Promotion"));
        assert!(block.text.contains("discovery.search"));
        assert!(block.text.contains("discovery.read"));
    }

    #[tokio::test]
    async fn retrieval_router_prefers_promoted_capability_state() {
        let planner = RetrievalRouterPlanner;
        let promoted = ContextBlock {
            id: "promoted-github".to_string(),
            kind: ContextBlockKind::ToolAvailability,
            text: "GitHub issue search is promoted".to_string(),
            priority: 20,
            token_estimate: None,
            metadata: json!({
                "source": "promoted_capabilities",
                "item_id": "mcp:github/issues.search",
            }),
        };

        let plan = planner
            .plan(
                &query("Use the GitHub MCP issue search tool"),
                vec![promoted],
            )
            .await
            .unwrap();
        let block = plan.blocks.first().unwrap();

        assert!(block.text.contains("already promoted or warm-cached"));
        assert_eq!(
            block.metadata["recommended"][0]["itemId"],
            "mcp:github/issues.search"
        );
    }

    #[tokio::test]
    async fn retrieval_router_routes_command_failures_to_artifacts() {
        let planner = RetrievalRouterPlanner;
        let plan = planner
            .plan(
                &query("A terminal command failed with stderr; inspect the log"),
                Vec::new(),
            )
            .await
            .unwrap();
        let block = plan.blocks.first().unwrap();

        assert!(block.text.contains("Artifact"));
        assert!(block.text.contains("grep_artifact"));
    }

    fn query(prompt: &str) -> ContextQuery {
        ContextQuery {
            thread_id: "thread-a".to_string(),
            turn_id: "turn-a".to_string(),
            prompt: prompt.to_string(),
            workspace: None,
            token_budget: None,
        }
    }
}
