//! Reasoning session — orchestrates adaptive action execution.
//!
//! A session takes a problem, classifies it, selects actions from the graph,
//! executes them in DAG order threading results forward, reports outcomes
//! for learning, and assembles the structured result.

use std::sync::{Arc, Mutex};
use std::time::Instant;

use crate::classifier;
use crate::executor::{self, AccumulatedContext};
use crate::handle::ReasoningInferenceHandle;
use crate::selector;
use crate::types::*;
use crate::verifier;
use crate::ReasonError;
use car_memgine::graph::SkillOutcome;
use car_memgine::MemgineEngine;
use chrono::Utc;

/// Progress event emitted during streaming reasoning.
#[derive(Debug, Clone)]
pub enum ReasoningEvent {
    /// Problem classified.
    Classified { class: ProblemClass },
    /// Action plan selected.
    PlanSelected { actions: Vec<ActionKind> },
    /// An action started executing.
    ActionStarted { action: ActionKind },
    /// An action completed.
    ActionCompleted { outcome: ActionOutcome },
    /// Full session complete.
    Complete { result: ReasoningResult },
}

/// A reasoning session that orchestrates the full reasoning flow.
pub struct ReasoningSession {
    memgine: Arc<Mutex<MemgineEngine>>,
    inference: Arc<dyn ReasoningInferenceHandle>,
}

impl ReasoningSession {
    pub fn new(
        memgine: Arc<Mutex<MemgineEngine>>,
        inference: Arc<dyn ReasoningInferenceHandle>,
    ) -> Self {
        Self { memgine, inference }
    }

    /// Run reasoning with streaming progress events.
    /// The callback fires after each step so the CLI can print live.
    pub async fn reason_streaming<F>(
        &self,
        problem: &str,
        mut on_event: F,
    ) -> Result<ReasoningResult, ReasonError>
    where
        F: FnMut(&ReasoningEvent),
    {
        let session_start = Instant::now();
        let session_id = uuid::Uuid::new_v4().to_string();

        // 1. Build context from graph memory
        let memory_context = {
            let mut engine = self
                .memgine
                .lock()
                .map_err(|_| ReasonError::SessionError("lock poisoned".into()))?;
            engine.build_context(problem)
        };

        // 2. Classify
        let problem_class = classifier::classify_problem(problem).await?;
        on_event(&ReasoningEvent::Classified {
            class: problem_class,
        });

        // 3. Select action plan
        let actions = {
            let engine = self
                .memgine
                .lock()
                .map_err(|_| ReasonError::SessionError("lock poisoned".into()))?;
            selector::select_actions(&engine, problem_class, problem)
        };
        on_event(&ReasoningEvent::PlanSelected {
            actions: actions.iter().map(|(k, _)| *k).collect(),
        });

        // 4. Execute actions, emitting events as each completes
        let mut outcomes: Vec<ActionOutcome> = Vec::new();
        let mut ctx = AccumulatedContext::new(problem, &memory_context, problem_class);

        for (action_kind, action_config) in &actions {
            on_event(&ReasoningEvent::ActionStarted {
                action: *action_kind,
            });

            let outcome = executor::execute_action(
                self.inference.as_ref(),
                *action_kind,
                action_config,
                &ctx,
            )
            .await?;

            on_event(&ReasoningEvent::ActionCompleted {
                outcome: outcome.clone(),
            });

            // Special handling for locate: extract source code into context
            if outcome.action == ActionKind::Locate {
                if let Some(split) = outcome.output.find("---SOURCE_CODE_START---") {
                    ctx.locations = outcome.output[..split].trim().to_string();
                    ctx.source_code = outcome.output[split + 23..].trim().to_string();
                } else {
                    ctx.locations = outcome.output.clone();
                }
            } else {
                ctx.integrate(&outcome);
            }
            outcomes.push(outcome);
        }

        // 5. Report outcomes for learning
        {
            let mut engine = self
                .memgine
                .lock()
                .map_err(|_| ReasonError::SessionError("lock poisoned".into()))?;
            for outcome in &outcomes {
                let skill_name = outcome.action.skill_name();
                let skill_outcome = if outcome.success {
                    SkillOutcome::Success
                } else {
                    SkillOutcome::Fail
                };
                engine.report_outcome(&skill_name, skill_outcome);
            }
        }

        // 6. Infer conversation-signal outcomes — best-effort. In
        // daemon mode this no-ops with a debug log (no outcome.* WS
        // method yet, tracked as a follow-up); in-process behavior
        // matches the pre-#189 path exactly.
        {
            let action_results: Vec<(String, bool, f64, String)> = outcomes
                .iter()
                .map(|o| {
                    (
                        o.trace_id.clone(),
                        o.success,
                        o.confidence,
                        o.output.clone(),
                    )
                })
                .collect();
            let _ = self
                .inference
                .record_inferred_outcomes(action_results)
                .await;
        }

        // 7. Assemble result
        let total_latency = session_start.elapsed().as_millis() as u64;

        let mut suggestions = extract_suggestions(&ctx);
        for s in &mut suggestions {
            verifier::verify_suggestion(s);
        }
        let overall_confidence = if outcomes.is_empty() {
            0.0
        } else {
            outcomes.iter().map(|o| o.confidence).sum::<f64>() / outcomes.len() as f64
        };

        let result = ReasoningResult {
            session_id,
            problem_class,
            diagnosis: ctx.diagnosis.clone(),
            suggestions,
            explanation: ctx.explanation.clone(),
            actions_taken: outcomes,
            overall_confidence,
            total_latency_ms: total_latency,
        };

        on_event(&ReasoningEvent::Complete {
            result: result.clone(),
        });

        // 8. Store this session as a learnable fact for future retrieval
        self.store_session_fact(problem, &result);

        Ok(result)
    }

    /// Non-streaming version (backward compat).
    pub async fn reason(&self, problem: &str) -> Result<ReasoningResult, ReasonError> {
        self.reason_streaming(problem, |_| {}).await
    }

    /// Reason with pre-provided source code context (for benchmarks).
    ///
    /// Instead of grepping the filesystem, the source code is provided directly.
    /// The Locate action is skipped — its output is pre-populated from `source_code`.
    /// AST parsing still runs on the provided code. All other actions (Classify,
    /// Diagnose, GenerateFix, Verify, Explain) run normally.
    pub async fn reason_with_context(
        &self,
        problem: &str,
        source_code: &str,
    ) -> Result<ReasoningResult, ReasonError> {
        let session_start = Instant::now();
        let session_id = uuid::Uuid::new_v4().to_string();

        // 1. For benchmark mode, use minimal memory context (not full build_context
        // which can produce 260K+ chars from seeded skills). Only query for
        // directly relevant facts, capped at 2K chars.
        let memory_context = {
            let engine = self
                .memgine
                .lock()
                .map_err(|_| ReasonError::SessionError("lock poisoned".into()))?;
            // Just get fact count for minimal grounding — skip the full context
            // assembly which is designed for interactive sessions, not benchmarks
            let fact_count = engine.valid_fact_count();
            if fact_count > 0 {
                format!("[{} prior facts available]", fact_count)
            } else {
                String::new()
            }
        };

        // 2. Classify
        let problem_class = classifier::classify_problem(problem).await?;

        // 3. Select action plan
        let actions = {
            let engine = self
                .memgine
                .lock()
                .map_err(|_| ReasonError::SessionError("lock poisoned".into()))?;
            selector::select_actions(&engine, problem_class, problem)
        };

        // 4. Execute actions, skipping Locate (pre-populated)
        let mut outcomes: Vec<ActionOutcome> = Vec::new();
        let mut ctx = AccumulatedContext::new(problem, &memory_context, problem_class);

        // Pre-populate source code (what Locate would have produced)
        ctx.source_code = source_code.to_string();
        ctx.locations = format!("Pre-provided source context ({} bytes)", source_code.len());

        for (action_kind, action_config) in &actions {
            // Skip Locate — we already have the source code
            if *action_kind == ActionKind::Locate {
                outcomes.push(ActionOutcome {
                    action: ActionKind::Locate,
                    model_used: "pre-provided".into(),
                    trace_id: String::new(),
                    latency_ms: 0,
                    output: ctx.locations.clone(),
                    confidence: 1.0,
                    success: true,
                });
                continue;
            }

            let outcome = executor::execute_action(
                self.inference.as_ref(),
                *action_kind,
                action_config,
                &ctx,
            )
            .await?;

            ctx.integrate(&outcome);
            outcomes.push(outcome);
        }

        // 5. Report outcomes
        {
            let mut engine = self
                .memgine
                .lock()
                .map_err(|_| ReasonError::SessionError("lock poisoned".into()))?;
            for outcome in &outcomes {
                let skill_name = outcome.action.skill_name();
                let skill_outcome = if outcome.success {
                    SkillOutcome::Success
                } else {
                    SkillOutcome::Fail
                };
                engine.report_outcome(&skill_name, skill_outcome);
            }
        }

        // 6. Infer outcomes — daemon-safe path (see `reason_streaming`).
        {
            let action_results: Vec<(String, bool, f64, String)> = outcomes
                .iter()
                .map(|o| {
                    (
                        o.trace_id.clone(),
                        o.success,
                        o.confidence,
                        o.output.clone(),
                    )
                })
                .collect();
            let _ = self
                .inference
                .record_inferred_outcomes(action_results)
                .await;
        }

        // 7. Assemble result
        let total_latency = session_start.elapsed().as_millis() as u64;
        let mut suggestions = extract_suggestions(&ctx);
        for s in &mut suggestions {
            verifier::verify_suggestion(s);
        }
        let overall_confidence = if outcomes.is_empty() {
            0.0
        } else {
            outcomes.iter().map(|o| o.confidence).sum::<f64>() / outcomes.len() as f64
        };

        let result = ReasoningResult {
            session_id,
            problem_class,
            diagnosis: ctx.diagnosis.clone(),
            suggestions,
            explanation: ctx.explanation.clone(),
            actions_taken: outcomes,
            overall_confidence,
            total_latency_ms: total_latency,
        };

        self.store_session_fact(problem, &result);

        Ok(result)
    }

    /// Store a successful reasoning session as a fact in the graph for future retrieval.
    fn store_session_fact(&self, problem: &str, result: &ReasoningResult) {
        if result.overall_confidence < 0.3 {
            return; // Don't learn from low-confidence sessions
        }

        let fact_id = format!("session:{}", result.session_id);
        let key = format!("reasoning:{}", result.problem_class);

        let actions_str: Vec<String> = result
            .actions_taken
            .iter()
            .map(|a| format!("{}({})", a.action, a.model_used))
            .collect();

        let value = format!(
            "Problem: {}\nClass: {}\nDiagnosis: {}\nActions: {}\nConfidence: {:.0}%",
            truncate(problem, 200),
            result.problem_class,
            truncate(&result.diagnosis, 300),
            actions_str.join(" → "),
            result.overall_confidence * 100.0,
        );

        let Ok(mut engine) = self.memgine.lock() else {
            tracing::warn!("skipping session fact storage: lock poisoned");
            return;
        };
        engine.ingest_fact(
            &fact_id,
            &key,
            &value,
            "car-reason",
            "system",
            Utc::now(),
            "project",
            None,
            result.problem_class.keywords(),
            false,
        );
    }
}

fn truncate(s: &str, max: usize) -> &str {
    if s.len() <= max {
        s
    } else {
        &s[..s.floor_char_boundary(max)]
    }
}

/// Extract code suggestions from the accumulated context.
fn extract_suggestions(ctx: &AccumulatedContext) -> Vec<CodeSuggestion> {
    if ctx.code.is_empty() {
        return vec![];
    }

    let mut suggestions = Vec::new();
    let mut in_block = false;
    let mut current_block = String::new();

    for line in ctx.code.lines() {
        if line.trim().starts_with("```") {
            if in_block {
                if !current_block.trim().is_empty() {
                    suggestions.push(CodeSuggestion {
                        file_path: None,
                        original: None,
                        suggested: current_block.trim().to_string(),
                        confidence: 0.7,
                        verification: VerificationStatus::NotVerified,
                    });
                }
                current_block.clear();
                in_block = false;
            } else {
                in_block = true;
            }
        } else if in_block {
            current_block.push_str(line);
            current_block.push('\n');
        }
    }

    if suggestions.is_empty() && !ctx.code.trim().is_empty() {
        suggestions.push(CodeSuggestion {
            file_path: None,
            original: None,
            suggested: ctx.code.trim().to_string(),
            confidence: 0.5,
            verification: VerificationStatus::NotVerified,
        });
    }

    suggestions
}

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

    #[test]
    fn extract_code_blocks() {
        let mut ctx = AccumulatedContext::new("test", "", ProblemClass::BugFix);
        ctx.code = "Here's the fix:\n```rust\nfn add(a: i32, b: i32) -> i32 { a + b }\n```\nThis fixes it.".into();
        let suggestions = extract_suggestions(&ctx);
        assert_eq!(suggestions.len(), 1);
        assert!(suggestions[0].suggested.contains("fn add"));
    }

    #[test]
    fn extract_no_code_blocks() {
        let mut ctx = AccumulatedContext::new("test", "", ProblemClass::BugFix);
        ctx.code = "Just change the + to a -".into();
        let suggestions = extract_suggestions(&ctx);
        assert_eq!(suggestions.len(), 1);
        assert!(suggestions[0].suggested.contains("change the + to a -"));
    }
}
