use super::report_utils::{extract_handle_fields, stable_cache_key, strings_from_array};
use super::{AppState, ToolResult, required_string, success_meta};
use crate::error::CodeLensError;
use crate::protocol::BackendKind;
use crate::resources::{analysis_section_handles, analysis_summary_resource};
use serde_json::{Value, json};
use std::collections::BTreeMap;
use std::path::Path;
use std::time::Duration;

fn run_job_kind(state: &AppState, kind: &str, arguments: &Value) -> ToolResult {
    match kind {
        "impact_report" => super::reports::impact_report(state, arguments),
        "dead_code_report" => super::reports::dead_code_report(state, arguments),
        "refactor_safety_report" => super::reports::refactor_safety_report(state, arguments),
        "module_boundary_report" => super::reports::module_boundary_report(state, arguments),
        "safe_rename_report" => super::reports::safe_rename_report(state, arguments),
        "diff_aware_references" => super::reports::diff_aware_references(state, arguments),
        "semantic_code_review" => super::reports::semantic_code_review(state, arguments),
        "analyze_change_request" => super::reports::analyze_change_request(state, arguments),
        "verify_change_readiness" => super::reports::verify_change_readiness(state, arguments),
        "eval_session_audit" => super::reports::eval_session_audit(state, arguments),
        _ => Err(CodeLensError::Validation(format!(
            "unsupported analysis job kind `{kind}`"
        ))),
    }
}

fn debug_step_delay_ms(arguments: &Value) -> u64 {
    arguments
        .get("debug_step_delay_ms")
        .and_then(|value| value.as_u64())
        .unwrap_or(0)
        .min(250)
}

fn maybe_delay(ms: u64) {
    if ms > 0 {
        std::thread::sleep(Duration::from_millis(ms));
    }
}

fn estimated_sections_for_kind(kind: &str) -> Vec<String> {
    match kind {
        "impact_report" => vec!["impact_rows".to_owned()],
        "dead_code_report" => vec!["candidates".to_owned(), "raw_dead_code".to_owned()],
        "refactor_safety_report" => vec![
            "module_boundary".to_owned(),
            "symbol_impact".to_owned(),
            "change_request".to_owned(),
            "related_tests".to_owned(),
        ],
        "module_boundary_report" => vec!["boundary".to_owned()],
        "safe_rename_report" => vec!["rename_safety".to_owned()],
        "diff_aware_references" => vec!["references".to_owned()],
        "semantic_code_review" => vec!["review_items".to_owned(), "semantic_status".to_owned()],
        "analyze_change_request" => vec!["change_request".to_owned()],
        "verify_change_readiness" => vec!["readiness".to_owned()],
        "eval_session_audit" => vec!["audit_pass_rate".to_owned(), "session_rows".to_owned()],
        _ => Vec::new(),
    }
}

fn job_handle_fields(analysis_id: Option<&str>, sections: &[String]) -> Value {
    match analysis_id {
        Some(analysis_id) => json!({
            "summary_resource": analysis_summary_resource(analysis_id),
            "section_handles": analysis_section_handles(analysis_id, sections),
        }),
        None => json!({
            "summary_resource": Value::Null,
            "section_handles": Vec::<Value>::new(),
        }),
    }
}

fn patch_job_file(
    project_path: &str,
    job_id: &str,
    status: Option<crate::runtime_types::JobLifecycle>,
    progress: Option<u8>,
    current_step: Option<Option<String>>,
    analysis_id: Option<Option<String>>,
    error: Option<Option<String>>,
) {
    let path = Path::new(project_path)
        .join(".codelens")
        .join("analysis-cache")
        .join("jobs")
        .join(format!("{job_id}.json"));
    let Ok(bytes) = std::fs::read(&path) else {
        return;
    };
    let Ok(mut job) = serde_json::from_slice::<crate::state::AnalysisJob>(&bytes) else {
        return;
    };
    if let Some(status) = status {
        job.status = status;
    }
    if let Some(progress) = progress {
        job.progress = progress;
    }
    if let Some(current_step) = current_step {
        job.current_step = current_step;
    }
    if let Some(analysis_id) = analysis_id {
        job.analysis_id = analysis_id;
    }
    if let Some(error) = error {
        job.error = error;
    }
    job.updated_at_ms = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as u64;
    if let Ok(updated) = serde_json::to_vec_pretty(&job) {
        let tmp_path = path.with_extension("json.tmp");
        let _ = std::fs::write(&tmp_path, updated);
        let _ = std::fs::rename(tmp_path, path);
    }
}

fn advance_job_progress(
    state: &AppState,
    scope: &str,
    job_id: &str,
    progress: u8,
    current_step: &str,
    delay_ms: u64,
) -> Result<bool, String> {
    if state
        .get_analysis_job_for_scope(scope, job_id)
        .as_ref()
        .map(|job| job.status)
        == Some(crate::runtime_types::JobLifecycle::Cancelled)
    {
        return Ok(false);
    }
    state
        .update_analysis_job(
            scope,
            job_id,
            Some(crate::runtime_types::JobLifecycle::Running),
            Some(progress),
            Some(Some(current_step.to_owned())),
            None,
            None,
            None,
        )
        .map_err(|error| error.to_string())?;
    maybe_delay(delay_ms);
    Ok(true)
}

fn run_job_kind_with_progress(
    state: &AppState,
    scope: &str,
    job_id: &str,
    kind: &str,
    arguments: &Value,
) -> Result<Value, String> {
    let delay_ms = debug_step_delay_ms(arguments);
    match kind {
        "impact_report" => run_impact_report_job(state, scope, job_id, arguments, delay_ms),
        "dead_code_report" => run_dead_code_report_job(state, scope, job_id, arguments, delay_ms),
        "refactor_safety_report" => {
            run_refactor_safety_report_job(state, scope, job_id, arguments, delay_ms)
        }
        "module_boundary_report" => {
            run_simple_report_job(state, scope, job_id, kind, arguments, delay_ms)
        }
        "safe_rename_report" => {
            run_simple_report_job(state, scope, job_id, kind, arguments, delay_ms)
        }
        "diff_aware_references" => {
            run_simple_report_job(state, scope, job_id, kind, arguments, delay_ms)
        }
        "semantic_code_review" => {
            run_simple_report_job(state, scope, job_id, kind, arguments, delay_ms)
        }
        "analyze_change_request" => {
            run_simple_report_job(state, scope, job_id, kind, arguments, delay_ms)
        }
        "verify_change_readiness" => {
            run_simple_report_job(state, scope, job_id, kind, arguments, delay_ms)
        }
        _ => run_job_kind(state, kind, arguments)
            .map(|(payload, _meta)| payload)
            .map_err(|error| error.to_string()),
    }
}

/// Generic progress wrapper for report kinds that don't need step-level progress tracking.
fn run_simple_report_job(
    state: &AppState,
    scope: &str,
    job_id: &str,
    kind: &str,
    arguments: &Value,
    delay_ms: u64,
) -> Result<Value, String> {
    if !advance_job_progress(
        state,
        scope,
        job_id,
        30,
        &format!("starting {kind}"),
        delay_ms,
    )? {
        return Ok(json!({}));
    }
    let result = run_job_kind(state, kind, arguments)
        .map(|(payload, _meta)| payload)
        .map_err(|error| error.to_string())?;
    if !advance_job_progress(
        state,
        scope,
        job_id,
        90,
        &format!("finalizing {kind}"),
        delay_ms,
    )? {
        return Ok(json!({}));
    }
    Ok(result)
}

#[allow(deprecated)]
fn run_dead_code_report_job(
    state: &AppState,
    scope_key: &str,
    job_id: &str,
    arguments: &Value,
    delay_ms: u64,
) -> Result<Value, String> {
    let scope = arguments
        .get("scope")
        .and_then(|v| v.as_str())
        .unwrap_or(".");
    let max_results = arguments
        .get("max_results")
        .and_then(|v| v.as_u64())
        .unwrap_or(20);
    if !advance_job_progress(
        state,
        scope_key,
        job_id,
        20,
        "scanning dead code candidates",
        delay_ms,
    )? {
        return Ok(json!({}));
    }
    let dead_code =
        super::graph::find_dead_code_v2_tool(state, &json!({"max_results": max_results}))
            .map(|output| output.0)
            .map_err(|error| error.to_string())?;
    if !advance_job_progress(
        state,
        scope_key,
        job_id,
        70,
        "filtering scoped dead code",
        delay_ms,
    )? {
        return Ok(json!({}));
    }
    let candidates = dead_code
        .get("dead_code")
        .and_then(|v| v.as_array())
        .cloned()
        .unwrap_or_default()
        .into_iter()
        .filter(|entry| entry.to_string().contains(scope))
        .take(10)
        .collect::<Vec<_>>();
    let top_findings = strings_from_array(Some(&candidates), "file", 3);
    let mut sections = BTreeMap::new();
    sections.insert(
        "candidates".to_owned(),
        json!({"scope": scope, "dead_code": candidates}),
    );
    sections.insert("raw_dead_code".to_owned(), dead_code);
    if !advance_job_progress(
        state,
        scope_key,
        job_id,
        90,
        "writing dead code analysis",
        delay_ms,
    )? {
        return Ok(json!({}));
    }
    super::report_contract::make_handle_response(
        state,
        "dead_code_report",
        stable_cache_key("dead_code_report", arguments, &["scope", "max_results"]),
        format!("Bounded dead-code audit for scope `{scope}`."),
        top_findings,
        0.84,
        vec!["Validate runtime entry points before deleting candidates".to_owned()],
        sections,
        if scope == "." {
            Vec::new()
        } else {
            vec![scope.to_owned()]
        },
        None,
        Some(arguments),
    )
    .map(|(payload, _meta)| payload)
    .map_err(|error| error.to_string())
}

#[allow(deprecated)]
fn run_impact_report_job(
    state: &AppState,
    scope: &str,
    job_id: &str,
    arguments: &Value,
    delay_ms: u64,
) -> Result<Value, String> {
    if !advance_job_progress(
        state,
        scope,
        job_id,
        20,
        "collecting changed files",
        delay_ms,
    )? {
        return Ok(json!({}));
    }
    let changed_files = strings_from_array(
        arguments
            .get("changed_files")
            .and_then(|value| value.as_array()),
        "file",
        8,
    );
    let target_files = if !changed_files.is_empty() {
        changed_files
    } else if let Some(path) = arguments.get("path").and_then(|value| value.as_str()) {
        vec![path.to_owned()]
    } else {
        let changed =
            super::graph::get_changed_files_tool(state, &json!({"include_untracked": true}))
                .map(|out| out.0)
                .unwrap_or_else(|_| json!({"files": [], "count": 0}));
        strings_from_array(
            changed.get("files").and_then(|value| value.as_array()),
            "file",
            8,
        )
    };
    if !advance_job_progress(
        state,
        scope,
        job_id,
        45,
        "measuring impact surface",
        delay_ms,
    )? {
        return Ok(json!({}));
    }
    let mut impact_rows = Vec::new();
    let mut top_findings = Vec::new();
    let total = target_files.iter().take(5).count().max(1);
    for (idx, path) in target_files.iter().take(5).enumerate() {
        let impact = super::graph::get_impact_analysis(
            state,
            &json!({"file_path": path, "max_depth": 2}),
        )
        .map(|output| output.0)
        .unwrap_or_else(
            |_| json!({"file_path": path, "total_affected_files": 0, "direct_importers": []}),
        );
        let affected = impact
            .get("total_affected_files")
            .and_then(|value| value.as_u64())
            .unwrap_or_default();
        let change_kind = codelens_engine::git::classify_change_kind(&state.project(), path);
        let kind_label = if change_kind == "additive" {
            " (additive)"
        } else {
            ""
        };
        top_findings.push(format!("{path}: {affected} affected file(s){kind_label}"));
        impact_rows.push(json!({
            "path": path,
            "affected_files": affected,
            "change_kind": change_kind,
            "direct_importers": impact.get("direct_importers").cloned().unwrap_or(json!([])),
            "blast_radius": impact.get("blast_radius").cloned().unwrap_or(json!([])),
        }));
        let loop_progress = 45 + (((idx + 1) * 35) / total) as u8;
        if !advance_job_progress(
            state,
            scope,
            job_id,
            loop_progress.min(80),
            &format!("analyzed impact for {path}"),
            delay_ms,
        )? {
            return Ok(json!({}));
        }
    }
    let mut sections = BTreeMap::new();
    sections.insert(
        "impact_rows".to_owned(),
        json!({"files": target_files, "impacts": impact_rows}),
    );
    if !advance_job_progress(
        state,
        scope,
        job_id,
        90,
        "writing impact analysis",
        delay_ms,
    )? {
        return Ok(json!({}));
    }
    super::report_contract::make_handle_response(
        state,
        "impact_report",
        stable_cache_key("impact_report", arguments, &["path", "changed_files"]),
        "Diff-aware impact report with bounded blast radius and importer evidence.".to_owned(),
        top_findings,
        0.88,
        vec!["Expand only the highest-impact file before deeper review".to_owned()],
        sections,
        target_files,
        None,
        Some(arguments),
    )
    .map(|(payload, _meta)| payload)
    .map_err(|error| error.to_string())
}

fn run_refactor_safety_report_job(
    state: &AppState,
    scope: &str,
    job_id: &str,
    arguments: &Value,
    delay_ms: u64,
) -> Result<Value, String> {
    let path = arguments
        .get("path")
        .and_then(|value| value.as_str())
        .unwrap_or(".");
    let task = arguments.get("task").and_then(|value| value.as_str());
    let symbol = arguments.get("symbol").and_then(|value| value.as_str());
    if !advance_job_progress(
        state,
        scope,
        job_id,
        20,
        "analyzing module boundaries",
        delay_ms,
    )? {
        return Ok(json!({}));
    }
    let boundary = super::reports::module_boundary_report(state, &json!({"path": path}))
        .map(|output| output.0)
        .map_err(|error| error.to_string())?;
    if !advance_job_progress(
        state,
        scope,
        job_id,
        40,
        "summarizing symbol impact",
        delay_ms,
    )? {
        return Ok(json!({}));
    }
    let symbol_impact = if let Some(symbol) = symbol {
        super::reports::summarize_symbol_impact(
            state,
            &json!({"symbol": symbol, "file_path": arguments.get("file_path").and_then(|v| v.as_str())}),
        )
        .map(|output| output.0)
        .unwrap_or_else(|error| json!({"symbol": symbol, "error": error.to_string()}))
    } else {
        json!({"skipped": true, "reason": "no symbol provided"})
    };
    if !advance_job_progress(
        state,
        scope,
        job_id,
        60,
        "ranking refactor context",
        delay_ms,
    )? {
        return Ok(json!({}));
    }
    let change_request = task
        .map(|task| {
            super::reports::analyze_change_request(state, &json!({"task": task}))
                .map(|output| output.0)
        })
        .transpose()
        .map_err(|error| error.to_string())?
        .unwrap_or_else(|| json!({"skipped": true, "reason": "no task provided"}));
    if !advance_job_progress(
        state,
        scope,
        job_id,
        80,
        "collecting related tests",
        delay_ms,
    )? {
        return Ok(json!({}));
    }
    let tests = super::filesystem::find_tests(state, &json!({"path": path, "max_results": 10}))
        .map(|output| output.0)
        .unwrap_or_else(|_| json!({"tests": []}));

    let mut top_findings = Vec::new();
    if let Some(symbol) = symbol {
        top_findings.push(format!(
            "Validate symbol-level callers before refactoring `{symbol}`."
        ));
    }
    if let Some(task) = task {
        top_findings.push(format!("Keep the refactor aligned with `{task}`."));
    }
    top_findings.push(format!(
        "Check tests around `{path}` before applying broad edits."
    ));

    let mut sections = BTreeMap::new();
    sections.insert("module_boundary".to_owned(), boundary);
    sections.insert("symbol_impact".to_owned(), symbol_impact);
    sections.insert("change_request".to_owned(), change_request);
    sections.insert("related_tests".to_owned(), tests);
    super::report_contract::make_handle_response(
        state,
        "refactor_safety_report",
        stable_cache_key(
            "refactor_safety_report",
            arguments,
            &["task", "symbol", "path", "file_path"],
        ),
        format!("Preview-first refactor safety report for `{path}`."),
        top_findings,
        0.9,
        vec!["Use safe_rename_report or focused edits only after checking blockers".to_owned()],
        sections,
        vec![
            arguments
                .get("file_path")
                .and_then(|value| value.as_str())
                .unwrap_or(path)
                .to_owned(),
        ],
        symbol.map(ToOwned::to_owned),
        Some(arguments),
    )
    .map(|(payload, _meta)| payload)
    .map_err(|error| error.to_string())
}

pub(crate) fn run_analysis_job_from_queue(
    worker_state: &AppState,
    job_id: String,
    kind: String,
    arguments: Value,
) -> crate::runtime_types::JobLifecycle {
    use crate::runtime_types::JobLifecycle;
    let scope = worker_state.project_scope_for_arguments(&arguments);
    if worker_state
        .get_analysis_job_for_scope(&scope, &job_id)
        .as_ref()
        .map(|job| job.status)
        == Some(crate::runtime_types::JobLifecycle::Cancelled)
    {
        return JobLifecycle::Cancelled;
    }
    if let Err(error) = worker_state.switch_project(&scope) {
        patch_job_file(
            &scope,
            &job_id,
            Some(JobLifecycle::Error),
            Some(100),
            Some(Some("failed".to_owned())),
            Some(None),
            Some(Some(format!(
                "analysis worker failed to bind project scope `{scope}`: {error}"
            ))),
        );
        return JobLifecycle::Error;
    }
    patch_job_file(
        &scope,
        &job_id,
        Some(crate::runtime_types::JobLifecycle::Running),
        Some(5),
        Some(Some("worker started".to_owned())),
        None,
        None,
    );
    let worker = std::panic::catch_unwind(std::panic::AssertUnwindSafe(
        || -> Result<JobLifecycle, String> {
            if worker_state
                .get_analysis_job_for_scope(&scope, &job_id)
                .as_ref()
                .map(|job| job.status)
                == Some(JobLifecycle::Cancelled)
            {
                return Ok(JobLifecycle::Cancelled);
            }
            // Retry transient failures up to 2 times with backoff
            let mut last_err = None;
            let mut result = None;
            for attempt in 0..3 {
                match run_job_kind_with_progress(worker_state, &scope, &job_id, &kind, &arguments) {
                    Ok(payload) => {
                        result = Some(payload);
                        break;
                    }
                    Err(e) if attempt < 2 => {
                        last_err = Some(e);
                        std::thread::sleep(std::time::Duration::from_millis(
                            100 * (attempt as u64 + 1),
                        ));
                        continue;
                    }
                    Err(e) => {
                        last_err = Some(e);
                        break;
                    }
                }
            }
            match result.map(Ok).unwrap_or_else(|| Err(last_err.unwrap())) {
                Ok(payload) if payload.is_object() => {
                    let (analysis_id, estimated_sections) = extract_handle_fields(&payload);
                    let current = worker_state.get_analysis_job_for_scope(&scope, &job_id);
                    if current.as_ref().map(|job| job.status) == Some(JobLifecycle::Cancelled) {
                        return Ok(JobLifecycle::Cancelled);
                    }
                    worker_state
                        .update_analysis_job(
                            &scope,
                            &job_id,
                            Some(JobLifecycle::Completed),
                            Some(100),
                            Some(Some("completed".to_owned())),
                            Some(estimated_sections),
                            Some(analysis_id),
                            Some(None),
                        )
                        .map_err(|error| error.to_string())?;
                    Ok(JobLifecycle::Completed)
                }
                Ok(_) => Ok(JobLifecycle::Error),
                Err(error) => {
                    worker_state
                        .update_analysis_job(
                            &scope,
                            &job_id,
                            Some(JobLifecycle::Error),
                            Some(100),
                            Some(Some("failed".to_owned())),
                            None,
                            Some(None),
                            Some(Some(error.to_string())),
                        )
                        .map_err(|error| error.to_string())?;
                    Ok(JobLifecycle::Error)
                }
            }
        },
    ));
    match worker {
        Err(panic) => {
            let message = if let Some(text) = panic.downcast_ref::<&str>() {
                (*text).to_owned()
            } else if let Some(text) = panic.downcast_ref::<String>() {
                text.clone()
            } else {
                "analysis worker panicked".to_owned()
            };
            patch_job_file(
                &scope,
                &job_id,
                Some(JobLifecycle::Error),
                Some(100),
                Some(Some("failed".to_owned())),
                Some(None),
                Some(Some(message)),
            );
            JobLifecycle::Error
        }
        Ok(Err(message)) => {
            patch_job_file(
                &scope,
                &job_id,
                Some(JobLifecycle::Error),
                Some(100),
                Some(Some("failed".to_owned())),
                Some(None),
                Some(Some(message)),
            );
            JobLifecycle::Error
        }
        Ok(Ok(status)) => status,
    }
}

pub fn start_analysis_job(state: &AppState, arguments: &Value) -> ToolResult {
    let kind = required_string(arguments, "kind")?.to_owned();
    let scope = state.project_scope_for_arguments(arguments);
    let profile_hint = arguments
        .get("profile_hint")
        .and_then(|value| value.as_str())
        .map(ToOwned::to_owned);
    let estimated_sections = estimated_sections_for_kind(&kind);
    let job = state.store_analysis_job(
        &scope,
        &kind,
        profile_hint.clone(),
        estimated_sections.clone(),
        crate::runtime_types::JobLifecycle::Queued,
        0,
        Some("queued".to_owned()),
        None,
        None,
    )?;
    state.enqueue_analysis_job(
        job.id.clone(),
        kind,
        arguments.clone(),
        profile_hint.clone(),
    )?;
    let handle_fields = job_handle_fields(job.analysis_id.as_deref(), &estimated_sections);
    Ok((
        json!({
            "job_id": job.id,
            "status": job.status,
            "progress": job.progress,
            "current_step": job.current_step,
            "analysis_id": job.analysis_id,
            "estimated_sections": estimated_sections,
            "summary_resource": handle_fields["summary_resource"].clone(),
            "section_handles": handle_fields["section_handles"].clone(),
        }),
        success_meta(BackendKind::Hybrid, 0.92),
    ))
}

pub fn get_analysis_job(state: &AppState, arguments: &Value) -> ToolResult {
    let job_id = required_string(arguments, "job_id")?;
    let scope = state.project_scope_for_arguments(arguments);
    let job = state
        .get_analysis_job_for_scope(&scope, job_id)
        .ok_or_else(|| CodeLensError::NotFound(format!("unknown job_id `{job_id}`")))?;
    let handle_fields = job_handle_fields(job.analysis_id.as_deref(), &job.estimated_sections);
    Ok((
        json!({
            "job_id": job.id,
            "kind": job.kind,
            "status": job.status,
            "progress": job.progress,
            "current_step": job.current_step,
            "profile_hint": job.profile_hint,
            "estimated_sections": job.estimated_sections,
            "analysis_id": job.analysis_id,
            "summary_resource": handle_fields["summary_resource"].clone(),
            "section_handles": handle_fields["section_handles"].clone(),
            "error": job.error,
            "updated_at_ms": job.updated_at_ms,
        }),
        success_meta(BackendKind::Memory, 1.0),
    ))
}

pub fn cancel_analysis_job(state: &AppState, arguments: &Value) -> ToolResult {
    let job_id = required_string(arguments, "job_id")?;
    let scope = state.project_scope_for_arguments(arguments);
    let job = state.cancel_analysis_job_for_scope(&scope, job_id)?;
    let handle_fields = job_handle_fields(job.analysis_id.as_deref(), &job.estimated_sections);
    Ok((
        json!({
            "job_id": job.id,
            "status": job.status,
            "progress": job.progress,
            "current_step": job.current_step,
            "analysis_id": job.analysis_id,
            "summary_resource": handle_fields["summary_resource"].clone(),
            "section_handles": handle_fields["section_handles"].clone(),
        }),
        success_meta(BackendKind::Memory, 1.0),
    ))
}

pub fn get_analysis_section(state: &AppState, arguments: &Value) -> ToolResult {
    let session = crate::session_context::SessionRequestContext::from_json(arguments);
    let analysis_id = required_string(arguments, "analysis_id")?;
    let section = required_string(arguments, "section")?;
    let scope = state.project_scope_for_arguments(arguments);
    let artifact = state
        .get_analysis_for_scope(&scope, analysis_id)
        .ok_or_else(|| CodeLensError::NotFound(format!("unknown analysis_id `{analysis_id}`")))?;
    state
        .metrics()
        .record_analysis_read_for_session(true, Some(session.session_id.as_str()));
    let content =
        state
            .get_analysis_section(analysis_id, section)
            .map_err(|error| match error {
                CodeLensError::Io(io) if io.kind() == std::io::ErrorKind::NotFound => {
                    CodeLensError::NotFound(format!(
                        "analysis `{analysis_id}` has no section `{section}`"
                    ))
                }
                other => other,
            })?;
    Ok((
        json!({
            "analysis_id": analysis_id,
            "section": section,
            "content": content,
            "tool_name": artifact.tool_name,
            "surface": artifact.surface,
        }),
        success_meta(BackendKind::Memory, artifact.confidence),
    ))
}

pub fn list_analysis_jobs(state: &AppState, arguments: &Value) -> ToolResult {
    let scope = state.project_scope_for_arguments(arguments);
    let status_filter = arguments
        .get("status")
        .and_then(|v| v.as_str())
        .map(|s| s.to_owned());
    let jobs = state.list_analysis_jobs_for_scope(&scope, status_filter.as_deref());
    let mut status_counts = BTreeMap::new();
    for job in &jobs {
        *status_counts
            .entry(job.status.as_str().to_owned())
            .or_insert(0usize) += 1;
    }
    let items = jobs
        .iter()
        .map(|job| {
            let handle_fields =
                job_handle_fields(job.analysis_id.as_deref(), &job.estimated_sections);
            json!({
                "job_id": job.id,
                "kind": job.kind,
                "status": job.status,
                "progress": job.progress,
                "current_step": job.current_step,
                "analysis_id": job.analysis_id,
                "summary_resource": handle_fields["summary_resource"].clone(),
                "section_handles": handle_fields["section_handles"].clone(),
                "error": job.error,
                "updated_at_ms": job.updated_at_ms,
            })
        })
        .collect::<Vec<_>>();
    Ok((
        json!({
            "jobs": items,
            "count": items.len(),
            "active_count": jobs.iter().filter(|job| matches!(job.status, crate::runtime_types::JobLifecycle::Queued | crate::runtime_types::JobLifecycle::Running)).count(),
            "status_counts": status_counts,
        }),
        success_meta(BackendKind::Memory, 1.0),
    ))
}

pub fn list_analysis_artifacts(state: &AppState, arguments: &Value) -> ToolResult {
    let scope = state.project_scope_for_arguments(arguments);
    let summaries = state.list_analysis_summaries_for_scope(&scope);
    let mut tool_counts = BTreeMap::new();
    for summary in &summaries {
        *tool_counts
            .entry(summary.tool_name.clone())
            .or_insert(0usize) += 1;
    }
    let latest_created_at_ms = summaries
        .iter()
        .map(|summary| summary.created_at_ms)
        .max()
        .unwrap_or_default();
    let items = summaries
        .iter()
        .map(|s| {
            json!({
                "analysis_id": s.id,
                "tool_name": s.tool_name,
                "summary": s.summary,
                "created_at_ms": s.created_at_ms,
                "surface": s.surface,
                "summary_resource": analysis_summary_resource(&s.id),
            })
        })
        .collect::<Vec<_>>();
    Ok((
        json!({
            "artifacts": items,
            "count": items.len(),
            "latest_created_at_ms": latest_created_at_ms,
            "tool_counts": tool_counts,
        }),
        success_meta(BackendKind::Memory, 1.0),
    ))
}

pub fn retry_analysis_job(state: &AppState, arguments: &Value) -> ToolResult {
    let job_id = required_string(arguments, "job_id")?;
    let scope = state.project_scope_for_arguments(arguments);
    let original = state
        .get_analysis_job_for_scope(&scope, job_id)
        .ok_or_else(|| CodeLensError::NotFound(format!("unknown job_id `{job_id}`")))?;
    if !matches!(
        original.status,
        crate::runtime_types::JobLifecycle::Error | crate::runtime_types::JobLifecycle::Cancelled
    ) {
        return Err(CodeLensError::Validation(format!(
            "job `{job_id}` has status `{}` — only error or cancelled jobs can be retried",
            original.status
        )));
    }
    let kind = original.kind.clone();
    let profile_hint = original.profile_hint.clone();
    let estimated_sections = estimated_sections_for_kind(&kind);
    let job = state.store_analysis_job(
        &scope,
        &kind,
        profile_hint.clone(),
        estimated_sections.clone(),
        crate::runtime_types::JobLifecycle::Queued,
        0,
        Some("queued".to_owned()),
        None,
        None,
    )?;
    // Re-enqueue with the original arguments embedded in the new job's arguments.
    // Since we don't persist the original call arguments, we reconstruct from job fields.
    let retry_arguments = json!({
        "kind": kind,
        "profile_hint": profile_hint,
    });
    state.enqueue_analysis_job(job.id.clone(), kind, retry_arguments, profile_hint)?;
    Ok((
        json!({
            "job_id": job.id,
            "retried_from": job_id,
            "status": job.status,
            "progress": job.progress,
            "current_step": job.current_step,
            "estimated_sections": estimated_sections,
        }),
        success_meta(BackendKind::Hybrid, 0.92),
    ))
}

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

    #[test]
    fn debug_step_delay_ms_default_zero() {
        assert_eq!(debug_step_delay_ms(&json!({})), 0);
    }

    #[test]
    fn debug_step_delay_ms_extracts_and_caps() {
        assert_eq!(
            debug_step_delay_ms(&json!({"debug_step_delay_ms": 100})),
            100
        );
        assert_eq!(
            debug_step_delay_ms(&json!({"debug_step_delay_ms": 500})),
            250
        );
    }

    #[test]
    fn estimated_sections_for_known_kinds() {
        assert_eq!(
            estimated_sections_for_kind("impact_report"),
            vec!["impact_rows".to_owned()]
        );
        assert_eq!(
            estimated_sections_for_kind("eval_session_audit"),
            vec!["audit_pass_rate".to_owned(), "session_rows".to_owned()]
        );
    }

    #[test]
    fn estimated_sections_for_unknown_kind_is_empty() {
        assert!(estimated_sections_for_kind("unknown_kind").is_empty());
    }

    #[test]
    fn job_handle_fields_with_analysis_id() {
        let fields = job_handle_fields(Some("aid123"), &["sec1".to_owned(), "sec2".to_owned()]);
        assert_eq!(
            fields["summary_resource"]["uri"],
            "codelens://analysis/aid123/summary"
        );
        let handles = fields["section_handles"].as_array().unwrap();
        assert_eq!(handles.len(), 2);
    }

    #[test]
    fn job_handle_fields_without_analysis_id() {
        let fields = job_handle_fields(None, &["sec1".to_owned()]);
        assert!(fields["summary_resource"].is_null());
        assert_eq!(fields["section_handles"], json!(Vec::<Value>::new()));
    }
}
