//! Index-health and repository-response freshness assembly for workspace status reporting.
//!
//! Builds index-health and freshness fields that gate response-cache reuse and workspace status
//! reporting.

use super::*;

impl FriggMcpServer {
    fn response_freshness_cache_eligible(&self, workspaces: &[AttachedWorkspace]) -> bool {
        !workspaces.is_empty()
            && workspaces
                .iter()
                .all(|workspace| self.repository_has_active_watch_lease(&workspace.repository_id))
    }

    pub(in crate::mcp::server) fn invalidate_repository_response_freshness_cache(
        &self,
        repository_id: &str,
    ) {
        self.cache_state
            .repository_response_freshness_cache_epoch
            .fetch_add(1, Ordering::AcqRel);
        self.cache_state
            .repository_response_freshness_cache
            .write()
            .unwrap_or_else(|poisoned| poisoned.into_inner())
            .retain(|key, _| {
                !key.scoped_repository_ids
                    .iter()
                    .any(|candidate| candidate == repository_id)
            });
    }

    fn repository_response_freshness_cache_epoch(&self) -> u64 {
        self.cache_state
            .repository_response_freshness_cache_epoch
            .load(Ordering::Acquire)
    }

    fn cached_repository_response_freshness(
        &self,
        scoped_repository_ids: &[String],
        mode: RepositoryResponseCacheFreshnessMode,
    ) -> Option<RepositoryResponseCacheFreshness> {
        let cache_key = RepositoryResponseFreshnessCacheKey {
            scoped_repository_ids: scoped_repository_ids.to_vec(),
            mode: mode.as_str(),
        };
        let cache = self
            .cache_state
            .repository_response_freshness_cache
            .read()
            .unwrap_or_else(|poisoned| poisoned.into_inner());
        let current_epoch = self.repository_response_freshness_cache_epoch();
        cache
            .get(&cache_key)
            .filter(|entry| entry.epoch == current_epoch)
            .map(|entry| entry.freshness.clone())
    }

    fn cache_repository_response_freshness(
        &self,
        scoped_repository_ids: &[String],
        mode: RepositoryResponseCacheFreshnessMode,
        freshness: &RepositoryResponseCacheFreshness,
        expected_epoch: u64,
    ) {
        let cache_key = RepositoryResponseFreshnessCacheKey {
            scoped_repository_ids: scoped_repository_ids.to_vec(),
            mode: mode.as_str(),
        };
        let mut cache = self
            .cache_state
            .repository_response_freshness_cache
            .write()
            .unwrap_or_else(|poisoned| poisoned.into_inner());
        if self.repository_response_freshness_cache_epoch() != expected_epoch {
            return;
        }
        cache.insert(
            cache_key,
            CachedRepositoryResponseFreshness {
                freshness: freshness.clone(),
                epoch: expected_epoch,
            },
        );
        while cache.len() > Self::REPOSITORY_RESPONSE_FRESHNESS_CACHE_MAX_ENTRIES {
            let _ = cache.pop_first();
        }
    }

    pub(in crate::mcp::server) fn workspace_index_health_summary(
        &self,
        workspace: &AttachedWorkspace,
        storage: &WorkspaceStorageSummary,
    ) -> WorkspaceIndexHealthSummary {
        let scip_discovery = Self::collect_scip_artifact_digests(&workspace.root);
        WorkspaceIndexHealthSummary {
            lexical: self.workspace_lexical_index_summary(workspace, storage),
            semantic: self.workspace_semantic_index_summary(workspace, storage),
            scip: self.workspace_scip_index_summary_from_discovery(&scip_discovery),
            precise_ingest: Some(
                self.workspace_precise_ingest_summary_from_discovery(workspace, &scip_discovery),
            ),
            precise_generators: self.workspace_precise_generator_summaries(workspace),
        }
    }

    fn public_precise_coverage_mode(
        coverage_mode: PreciseCoverageMode,
    ) -> WorkspacePreciseCoverageMode {
        match coverage_mode {
            PreciseCoverageMode::Full => WorkspacePreciseCoverageMode::Full,
            PreciseCoverageMode::Partial => WorkspacePreciseCoverageMode::Partial,
            PreciseCoverageMode::None => WorkspacePreciseCoverageMode::None,
        }
    }

    fn precise_ingest_failure_summaries(
        stats: &PreciseIngestStats,
    ) -> Vec<WorkspacePreciseArtifactFailureSummary> {
        stats
            .failed_artifacts
            .iter()
            .map(|failure| WorkspacePreciseArtifactFailureSummary {
                artifact_label: failure.artifact_label.clone(),
                stage: failure.stage.clone(),
                detail: failure.detail.clone(),
            })
            .collect()
    }

    fn precise_ingest_reason(
        state: WorkspacePreciseIngestState,
        stats: &PreciseIngestStats,
    ) -> Option<String> {
        match state {
            WorkspacePreciseIngestState::Missing => Some("no_scip_artifacts_discovered".to_owned()),
            WorkspacePreciseIngestState::Ready => None,
            WorkspacePreciseIngestState::Partial => Some(format!(
                "scip ingest partial: {} of {} artifacts failed",
                stats.artifacts_failed, stats.artifacts_discovered
            )),
            WorkspacePreciseIngestState::Failed => Some(format!(
                "scip ingest failed: {} of {} artifacts failed",
                stats.artifacts_failed, stats.artifacts_discovered
            )),
            WorkspacePreciseIngestState::Error => None,
        }
    }

    fn precise_ingest_summary_from_stats(
        stats: &PreciseIngestStats,
        coverage_mode: PreciseCoverageMode,
    ) -> WorkspacePreciseIngestSummary {
        let state = if stats.artifacts_discovered == 0 {
            WorkspacePreciseIngestState::Missing
        } else {
            match coverage_mode {
                PreciseCoverageMode::Full => WorkspacePreciseIngestState::Ready,
                PreciseCoverageMode::Partial => WorkspacePreciseIngestState::Partial,
                PreciseCoverageMode::None => {
                    if stats.artifacts_failed > 0 {
                        WorkspacePreciseIngestState::Failed
                    } else {
                        WorkspacePreciseIngestState::Missing
                    }
                }
            }
        };
        WorkspacePreciseIngestSummary {
            state,
            coverage_mode: Some(Self::public_precise_coverage_mode(coverage_mode)),
            reason: Self::precise_ingest_reason(state, stats),
            artifacts_discovered: stats.artifacts_discovered,
            artifacts_discovered_bytes: stats.artifacts_discovered_bytes,
            artifacts_ingested: stats.artifacts_ingested,
            artifacts_ingested_bytes: stats.artifacts_ingested_bytes,
            artifacts_failed: stats.artifacts_failed,
            artifacts_failed_bytes: stats.artifacts_failed_bytes,
            failed_artifacts: Self::precise_ingest_failure_summaries(stats),
        }
    }

    fn workspace_precise_ingest_summary_from_discovery(
        &self,
        workspace: &AttachedWorkspace,
        discovery: &ScipArtifactDiscovery,
    ) -> WorkspacePreciseIngestSummary {
        if discovery.artifact_digests.is_empty() {
            return WorkspacePreciseIngestSummary {
                state: WorkspacePreciseIngestState::Missing,
                coverage_mode: Some(WorkspacePreciseCoverageMode::None),
                reason: Some("no_scip_artifacts_discovered".to_owned()),
                artifacts_discovered: 0,
                artifacts_discovered_bytes: 0,
                artifacts_ingested: 0,
                artifacts_ingested_bytes: 0,
                artifacts_failed: 0,
                artifacts_failed_bytes: 0,
                failed_artifacts: Vec::new(),
            };
        }

        if let Some(cached) = self.try_reuse_latest_precise_graph_for_repository(
            &workspace.repository_id,
            &workspace.root,
        ) {
            return Self::precise_ingest_summary_from_stats(
                &cached.ingest_stats,
                cached.coverage_mode,
            );
        }

        match self.precise_graph_for_repository_root_with_discovery(
            &workspace.repository_id,
            &workspace.root,
            discovery,
            self.find_references_resource_budgets(),
        ) {
            Ok(cached) => {
                Self::precise_ingest_summary_from_stats(&cached.ingest_stats, cached.coverage_mode)
            }
            Err(err) => WorkspacePreciseIngestSummary {
                state: WorkspacePreciseIngestState::Error,
                coverage_mode: None,
                reason: Some(err.message.to_string()),
                artifacts_discovered: discovery.artifact_digests.len(),
                artifacts_discovered_bytes: discovery
                    .artifact_digests
                    .iter()
                    .map(|artifact| artifact.size_bytes)
                    .sum(),
                artifacts_ingested: 0,
                artifacts_ingested_bytes: 0,
                artifacts_failed: 0,
                artifacts_failed_bytes: 0,
                failed_artifacts: Vec::new(),
            },
        }
    }

    pub(in crate::mcp::server) fn workspace_repository_freshness_status(
        &self,
        workspace: &AttachedWorkspace,
        semantic_runtime: &SemanticRuntimeConfig,
    ) -> Result<crate::manifest_validation::RepositoryFreshnessStatus, String> {
        if !workspace.db_path.is_file() {
            return Ok(crate::manifest_validation::RepositoryFreshnessStatus {
                snapshot_id: None,
                manifest_entry_count: None,
                manifest: RepositoryManifestFreshness::MissingSnapshot,
                semantic: if semantic_runtime.enabled {
                    RepositorySemanticFreshness::MissingManifestSnapshot
                } else {
                    RepositorySemanticFreshness::Disabled
                },
                validated_manifest_digests: None,
                semantic_target: None,
            });
        }

        let storage = Storage::new(&workspace.db_path);
        let schema_version = storage.schema_version().map_err(|err| err.to_string())?;
        if schema_version == 0 {
            return Ok(crate::manifest_validation::RepositoryFreshnessStatus {
                snapshot_id: None,
                manifest_entry_count: None,
                manifest: RepositoryManifestFreshness::MissingSnapshot,
                semantic: if semantic_runtime.enabled {
                    RepositorySemanticFreshness::MissingManifestSnapshot
                } else {
                    RepositorySemanticFreshness::Disabled
                },
                validated_manifest_digests: None,
                semantic_target: None,
            });
        }
        storage
            .require_current_schema()
            .map_err(|err| err.to_string())?;

        repository_freshness_status(
            &storage,
            &workspace.runtime_repository_id,
            &workspace.root,
            semantic_runtime,
            |_| false,
        )
        .map_err(|err| err.to_string())
    }

    pub(in crate::mcp::server) fn repository_response_cache_freshness(
        &self,
        workspaces: &[AttachedWorkspace],
        mode: RepositoryResponseCacheFreshnessMode,
    ) -> Result<RepositoryResponseCacheFreshness, ErrorData> {
        let mut scoped_repository_ids = workspaces
            .iter()
            .map(|workspace| workspace.repository_id.clone())
            .collect::<Vec<_>>();
        scoped_repository_ids.sort();
        let has_active_runtime_work = workspaces
            .iter()
            .any(|workspace| self.repository_has_active_runtime_work(&workspace.repository_id));
        let has_dirty_root = workspaces
            .iter()
            .any(|workspace| self.workspace_has_dirty_root(workspace));
        let cache_eligible = self.response_freshness_cache_eligible(workspaces)
            && !has_active_runtime_work
            && !has_dirty_root;
        let cache_epoch = self.repository_response_freshness_cache_epoch();
        if cache_eligible
            && let Some(cached) =
                self.cached_repository_response_freshness(&scoped_repository_ids, mode)
        {
            return Ok(cached);
        }

        let semantic_runtime = self.cache_freshness_runtime(mode);
        let mut cacheable = true;
        let mut scopes = Vec::with_capacity(workspaces.len());
        let mut repositories = Vec::with_capacity(workspaces.len());

        for workspace in workspaces {
            let status = self
                .workspace_repository_freshness_status(workspace, &semantic_runtime)
                .map_err(|err| {
                    Self::internal(
                        format!(
                            "failed to compute response cache freshness for repository '{}': {err}",
                            workspace.repository_id
                        ),
                        None,
                    )
                })?;
            if let (Some(snapshot_id), Some(validated_digests)) = (
                status.snapshot_id.as_deref(),
                status.validated_manifest_digests.as_ref(),
            ) {
                self.runtime_state
                    .validated_manifest_candidate_cache
                    .write()
                    .unwrap_or_else(|poisoned| poisoned.into_inner())
                    .store_validated_shared(
                        &workspace.root,
                        snapshot_id,
                        std::sync::Arc::new(validated_digests.clone()),
                    );
            }
            let dirty_root = self
                .runtime_state
                .validated_manifest_candidate_cache
                .read()
                .unwrap_or_else(|poisoned| poisoned.into_inner())
                .is_dirty_root(&workspace.root);

            let manifest = Self::repository_manifest_freshness_label(&status.manifest);
            let semantic = Self::repository_semantic_freshness_label(&status.semantic);
            let snapshot_id = status.snapshot_id.clone();
            let semantic_target = status.semantic_target.clone();
            let active_index_tasks = self
                .runtime_state
                .runtime_task_registry
                .read()
                .unwrap_or_else(|poisoned| poisoned.into_inner())
                .active_tasks()
                .into_iter()
                .filter(|task| {
                    matches!(
                        task.kind,
                        RuntimeTaskKind::ChangedIndex
                            | RuntimeTaskKind::SemanticRefresh
                            | RuntimeTaskKind::WorkspaceIndex
                            | RuntimeTaskKind::WorkspacePrepare
                    ) && (task.repository_id == workspace.repository_id
                        || (workspace.runtime_repository_id != workspace.repository_id
                            && task.repository_id == workspace.runtime_repository_id))
                })
                .collect::<Vec<_>>();
            let refresh_in_progress = !active_index_tasks.is_empty();
            let live_walk_reason = if dirty_root {
                Some("dirty_root")
            } else if !matches!(status.manifest, RepositoryManifestFreshness::Ready) {
                Some(manifest)
            } else if status.snapshot_id.is_none() {
                Some("missing_snapshot_id")
            } else {
                None
            };
            let cacheable_reason =
                live_walk_reason.or_else(|| refresh_in_progress.then_some("refresh_in_progress"));
            let using_live_walk = live_walk_reason.is_some();

            repositories.push(json!({
                "repository_id": workspace.repository_id,
                "snapshot_id": snapshot_id,
                "manifest": manifest,
                "semantic": semantic,
                "dirty_root": dirty_root,
                "cacheable_reason": cacheable_reason,
                "candidate_source": if using_live_walk { "live_walk" } else { "manifest_snapshot" },
                "using_live_walk": using_live_walk,
                "refresh_in_progress": refresh_in_progress,
                "active_index_tasks": active_index_tasks,
                "recommended_client_behavior": if using_live_walk {
                    "continue_using_frigg_live_fallback"
                } else if refresh_in_progress {
                    "requery_after_refresh"
                } else {
                    "use_cached_frigg_results"
                },
                "provider": semantic_target.as_ref().map(|target| target.provider.clone()),
                "model": semantic_target.as_ref().map(|target| target.model.clone()),
            }));

            if refresh_in_progress {
                cacheable = false;
                continue;
            }
            if dirty_root || !matches!(status.manifest, RepositoryManifestFreshness::Ready) {
                cacheable = false;
                continue;
            }
            let Some(snapshot_id) = status.snapshot_id else {
                cacheable = false;
                continue;
            };

            scopes.push(RepositoryFreshnessCacheScope {
                repository_id: workspace.repository_id.clone(),
                snapshot_id,
                semantic_state: matches!(mode, RepositoryResponseCacheFreshnessMode::SemanticAware)
                    .then(|| semantic.to_owned()),
                semantic_provider: matches!(
                    mode,
                    RepositoryResponseCacheFreshnessMode::SemanticAware
                )
                .then(|| {
                    semantic_target
                        .as_ref()
                        .map(|target| target.provider.clone())
                })
                .flatten(),
                semantic_model: matches!(mode, RepositoryResponseCacheFreshnessMode::SemanticAware)
                    .then(|| semantic_target.as_ref().map(|target| target.model.clone()))
                    .flatten(),
            });
        }

        scopes.sort();

        let freshness = RepositoryResponseCacheFreshness {
            scopes: cacheable.then_some(scopes),
            basis: json!({
                "mode": mode.as_str(),
                "cacheable": cacheable,
                "repositories": repositories,
                "runtime_cache_contract": self.runtime_cache_contract_summary(&[
                    crate::mcp::server_cache::RuntimeCacheFamily::ValidatedManifestCandidate,
                    ]),
            }),
        };
        if cache_eligible && freshness.scopes.is_some() {
            self.cache_repository_response_freshness(
                &scoped_repository_ids,
                mode,
                &freshness,
                cache_epoch,
            );
        }
        Ok(freshness)
    }

    fn cache_freshness_runtime(
        &self,
        mode: RepositoryResponseCacheFreshnessMode,
    ) -> SemanticRuntimeConfig {
        let mut runtime = self.config.semantic_runtime.clone();
        if matches!(mode, RepositoryResponseCacheFreshnessMode::ManifestOnly) {
            runtime.enabled = false;
        }
        runtime
    }

    fn repository_manifest_freshness_label(
        freshness: &RepositoryManifestFreshness,
    ) -> &'static str {
        match freshness {
            RepositoryManifestFreshness::MissingSnapshot => "missing_snapshot",
            RepositoryManifestFreshness::StaleSnapshot => "stale_snapshot",
            RepositoryManifestFreshness::Ready => "ready",
        }
    }

    fn repository_semantic_freshness_label(
        freshness: &RepositorySemanticFreshness,
    ) -> &'static str {
        match freshness {
            RepositorySemanticFreshness::Disabled => "disabled",
            RepositorySemanticFreshness::MissingManifestSnapshot => "missing_manifest_snapshot",
            RepositorySemanticFreshness::StaleManifestSnapshot => "stale_manifest_snapshot",
            RepositorySemanticFreshness::NoEligibleEntries => "no_eligible_entries",
            RepositorySemanticFreshness::MissingForActiveModel => "missing_for_active_model",
            RepositorySemanticFreshness::Ready => "ready",
        }
    }

    pub(in crate::mcp::server) fn workspace_manifest_entry_count(
        &self,
        workspace: &AttachedWorkspace,
    ) -> Option<usize> {
        let db_path = resolve_provenance_db_path(&workspace.root).ok()?;
        if db_path.exists() {
            let storage = Storage::new(db_path.clone());
            if let Ok(Some(snapshot)) =
                crate::manifest_validation::latest_validated_manifest_snapshot_shared(
                    &storage,
                    &workspace.runtime_repository_id,
                    &workspace.root,
                    Some(&self.runtime_state.validated_manifest_candidate_cache),
                )
            {
                return Some(snapshot.digests.len());
            }
        }

        Self::load_latest_manifest_snapshot(&workspace.root, &workspace.runtime_repository_id)
            .map(|snapshot| snapshot.entries.len())
    }

    pub(in crate::mcp::server) fn workspace_lexical_index_summary(
        &self,
        workspace: &AttachedWorkspace,
        storage: &WorkspaceStorageSummary,
    ) -> WorkspaceIndexComponentSummary {
        if let Some(summary) = Self::storage_error_health_summary(storage) {
            return summary;
        }

        let mut manifest_only_runtime = self.config.semantic_runtime.clone();
        manifest_only_runtime.enabled = false;
        let freshness =
            match self.workspace_repository_freshness_status(workspace, &manifest_only_runtime) {
                Ok(freshness) => freshness,
                Err(err) => {
                    return WorkspaceIndexComponentSummary {
                        state: WorkspaceIndexComponentState::Error,
                        reason: Some(err),
                        snapshot_id: None,
                        compatible_snapshot_id: None,
                        provider: None,
                        model: None,
                        artifact_count: None,
                    };
                }
            };
        let manifest_entry_count = self.workspace_manifest_entry_count(workspace);
        let dirty_root = self.workspace_has_dirty_root(workspace);
        if dirty_root && freshness.snapshot_id.is_some() {
            return WorkspaceIndexComponentSummary {
                state: WorkspaceIndexComponentState::Stale,
                reason: Some("dirty_root".to_owned()),
                snapshot_id: freshness.snapshot_id,
                compatible_snapshot_id: None,
                provider: None,
                model: None,
                artifact_count: manifest_entry_count
                    .or_else(|| freshness.validated_manifest_digests.as_ref().map(Vec::len)),
            };
        }
        let manifest_state = freshness.manifest.clone();
        let (state, reason) = match manifest_state {
            RepositoryManifestFreshness::MissingSnapshot => (
                WorkspaceIndexComponentState::Missing,
                Some("missing_manifest_snapshot".to_owned()),
            ),
            RepositoryManifestFreshness::StaleSnapshot => (
                WorkspaceIndexComponentState::Stale,
                Some("stale_manifest_snapshot".to_owned()),
            ),
            RepositoryManifestFreshness::Ready => (WorkspaceIndexComponentState::Ready, None),
        };
        WorkspaceIndexComponentSummary {
            state,
            reason,
            snapshot_id: freshness.snapshot_id,
            compatible_snapshot_id: None,
            provider: None,
            model: None,
            artifact_count: match freshness.manifest {
                RepositoryManifestFreshness::MissingSnapshot => None,
                RepositoryManifestFreshness::StaleSnapshot => manifest_entry_count,
                RepositoryManifestFreshness::Ready => manifest_entry_count
                    .or_else(|| freshness.validated_manifest_digests.as_ref().map(Vec::len)),
            },
        }
    }

    pub(in crate::mcp::server) fn workspace_semantic_index_summary(
        &self,
        workspace: &AttachedWorkspace,
        storage: &WorkspaceStorageSummary,
    ) -> WorkspaceIndexComponentSummary {
        if !self.config.semantic_runtime.enabled {
            return WorkspaceIndexComponentSummary {
                state: WorkspaceIndexComponentState::Disabled,
                reason: Some("semantic_runtime_disabled".to_owned()),
                snapshot_id: None,
                compatible_snapshot_id: None,
                provider: None,
                model: None,
                artifact_count: None,
            };
        }

        let provider = self
            .config
            .semantic_runtime
            .provider
            .map(|value| value.as_str().to_owned());
        let model = self
            .config
            .semantic_runtime
            .normalized_model()
            .map(ToOwned::to_owned);
        if self.config.semantic_runtime.validate().is_err() || provider.is_none() || model.is_none()
        {
            return WorkspaceIndexComponentSummary {
                state: WorkspaceIndexComponentState::Error,
                reason: Some("semantic_runtime_invalid_config".to_owned()),
                snapshot_id: None,
                compatible_snapshot_id: None,
                provider,
                model,
                artifact_count: None,
            };
        }
        if let Some(summary) = Self::storage_error_health_summary(storage) {
            return WorkspaceIndexComponentSummary {
                provider,
                model,
                ..summary
            };
        }

        let freshness = match self
            .workspace_repository_freshness_status(workspace, &self.config.semantic_runtime)
        {
            Ok(freshness) => freshness,
            Err(err) => {
                return WorkspaceIndexComponentSummary {
                    state: WorkspaceIndexComponentState::Error,
                    reason: Some(err),
                    snapshot_id: None,
                    compatible_snapshot_id: None,
                    provider,
                    model,
                    artifact_count: None,
                };
            }
        };
        let storage_reader = Storage::new(&workspace.db_path);
        let provider_ref = provider
            .as_deref()
            .expect("semantic provider should exist after config validation");
        let model_ref = model
            .as_deref()
            .expect("semantic model should exist after config validation");
        let semantic_health = storage_reader
            .collect_semantic_storage_health_for_repository_model(
                &workspace.runtime_repository_id,
                provider_ref,
                model_ref,
            )
            .ok();
        let semantic_state = freshness.semantic.clone();
        match semantic_state {
            RepositorySemanticFreshness::MissingManifestSnapshot => {
                WorkspaceIndexComponentSummary {
                    state: WorkspaceIndexComponentState::Missing,
                    reason: Some("missing_manifest_snapshot".to_owned()),
                    snapshot_id: None,
                    compatible_snapshot_id: None,
                    provider,
                    model,
                    artifact_count: None,
                }
            }
            RepositorySemanticFreshness::StaleManifestSnapshot => WorkspaceIndexComponentSummary {
                state: WorkspaceIndexComponentState::Stale,
                reason: Some("stale_manifest_snapshot".to_owned()),
                snapshot_id: freshness.snapshot_id,
                compatible_snapshot_id: None,
                provider,
                model,
                artifact_count: None,
            },
            RepositorySemanticFreshness::NoEligibleEntries => WorkspaceIndexComponentSummary {
                state: WorkspaceIndexComponentState::Ready,
                reason: Some("manifest_valid_no_semantic_eligible_entries".to_owned()),
                snapshot_id: freshness.snapshot_id,
                compatible_snapshot_id: None,
                provider,
                model,
                artifact_count: Some(0),
            },
            RepositorySemanticFreshness::Ready => {
                let snapshot_id = freshness
                    .snapshot_id
                    .expect("ready semantic freshness should carry a snapshot id");
                if semantic_health
                    .as_ref()
                    .is_some_and(|health| !health.vector_consistent)
                {
                    return WorkspaceIndexComponentSummary {
                        state: WorkspaceIndexComponentState::Error,
                        reason: Some("semantic_vector_partition_out_of_sync".to_owned()),
                        snapshot_id: Some(snapshot_id),
                        compatible_snapshot_id: None,
                        provider: provider.clone(),
                        model: model.clone(),
                        artifact_count: semantic_health
                            .as_ref()
                            .map(|health| health.live_embedding_rows),
                    };
                }
                WorkspaceIndexComponentSummary {
                    state: WorkspaceIndexComponentState::Ready,
                    reason: None,
                    snapshot_id: Some(snapshot_id.clone()),
                    compatible_snapshot_id: None,
                    provider: provider.clone(),
                    model: model.clone(),
                    artifact_count: semantic_health
                        .as_ref()
                        .map(|health| health.live_embedding_rows)
                        .or_else(|| {
                            storage_reader
                                .count_semantic_embeddings_for_repository_snapshot_model(
                                    &workspace.runtime_repository_id,
                                    &snapshot_id,
                                    provider_ref,
                                    model_ref,
                                )
                                .ok()
                        }),
                }
            }
            RepositorySemanticFreshness::MissingForActiveModel => {
                let snapshot_id = freshness.snapshot_id.clone();
                WorkspaceIndexComponentSummary {
                    state: WorkspaceIndexComponentState::Missing,
                    reason: Some("semantic_snapshot_missing_for_active_model".to_owned()),
                    snapshot_id,
                    compatible_snapshot_id: None,
                    provider: provider.clone(),
                    model: model.clone(),
                    artifact_count: None,
                }
            }
            RepositorySemanticFreshness::Disabled => WorkspaceIndexComponentSummary {
                state: WorkspaceIndexComponentState::Disabled,
                reason: Some("semantic_runtime_disabled".to_owned()),
                snapshot_id: None,
                compatible_snapshot_id: None,
                provider,
                model,
                artifact_count: None,
            },
        }
    }

    fn workspace_scip_index_summary_from_discovery(
        &self,
        discovery: &ScipArtifactDiscovery,
    ) -> WorkspaceIndexComponentSummary {
        let artifact_count = discovery.artifact_digests.len();
        WorkspaceIndexComponentSummary {
            state: if artifact_count == 0 {
                WorkspaceIndexComponentState::Missing
            } else {
                WorkspaceIndexComponentState::Ready
            },
            reason: if artifact_count == 0 {
                Some("no_scip_artifacts_discovered".to_owned())
            } else {
                None
            },
            snapshot_id: None,
            compatible_snapshot_id: None,
            provider: None,
            model: None,
            artifact_count: Some(artifact_count),
        }
    }

    pub(in crate::mcp::server) fn workspace_semantic_refresh_plan(
        &self,
        workspace: &AttachedWorkspace,
    ) -> Option<WorkspaceSemanticRefreshPlan> {
        if !self.config.semantic_runtime.enabled {
            return None;
        }

        self.config.semantic_runtime.validate().ok()?;
        let freshness = self
            .workspace_repository_freshness_status(workspace, &self.config.semantic_runtime)
            .ok()?;
        let latest_snapshot_id = freshness.snapshot_id?;
        match (freshness.manifest.clone(), freshness.semantic.clone()) {
            (RepositoryManifestFreshness::StaleSnapshot, _) => Some(WorkspaceSemanticRefreshPlan {
                latest_snapshot_id,
                reason: "stale_manifest_snapshot",
            }),
            (
                RepositoryManifestFreshness::Ready,
                RepositorySemanticFreshness::MissingForActiveModel,
            ) => Some(WorkspaceSemanticRefreshPlan {
                latest_snapshot_id,
                reason: "semantic_snapshot_missing_for_active_model",
            }),
            _ => None,
        }
    }

    pub(in crate::mcp::server) fn refresh_workspace_semantic_snapshot_with_plan(
        &self,
        workspace: &AttachedWorkspace,
        plan: &WorkspaceSemanticRefreshPlan,
    ) -> Result<(), String> {
        self.invalidate_repository_response_freshness_cache(&workspace.repository_id);
        let started_at = Instant::now();
        tracing::info!(
            repository_id = %workspace.repository_id,
            root = %workspace.root.display(),
            snapshot_id = %plan.latest_snapshot_id,
            reason = plan.reason,
            provider = self
                .config
                .semantic_runtime
                .provider
                .map(|value| value.as_str())
                .unwrap_or(""),
            model = self
                .config
                .semantic_runtime
                .normalized_model()
                .unwrap_or(""),
            "workspace semantic refresh started"
        );
        let credentials = SemanticRuntimeCredentials::from_process_env();
        self.config
            .semantic_runtime
            .validate_startup(&credentials)
            .map_err(|err| err.to_string())?;

        index_repository_with_runtime_config(
            &workspace.runtime_repository_id,
            &workspace.root,
            &workspace.db_path,
            IndexMode::Full,
            &self.config.semantic_runtime,
            &credentials,
        )
        .map(|_| {
            self.invalidate_repository_response_freshness_cache(&workspace.repository_id);
            tracing::info!(
                repository_id = %workspace.repository_id,
                root = %workspace.root.display(),
                snapshot_id = %plan.latest_snapshot_id,
                reason = plan.reason,
                duration_ms = started_at.elapsed().as_millis() as u64,
                "workspace semantic refresh completed"
            );
        })
        .map_err(|err| {
            self.invalidate_repository_response_freshness_cache(&workspace.repository_id);
            let error = err.to_string();
            warn!(
                repository_id = %workspace.repository_id,
                root = %workspace.root.display(),
                snapshot_id = %plan.latest_snapshot_id,
                reason = plan.reason,
                duration_ms = started_at.elapsed().as_millis() as u64,
                error = %error,
                "workspace semantic refresh failed"
            );
            error
        })
    }

    pub(in crate::mcp::server) fn maybe_spawn_workspace_runtime_prewarm(
        &self,
        workspace: &AttachedWorkspace,
    ) {
        let semantic_plan = self.workspace_semantic_refresh_plan(workspace);
        let should_refresh_semantic = semantic_plan.is_some();
        let should_prewarm_precise = !Self::collect_scip_artifact_digests(&workspace.root)
            .artifact_digests
            .is_empty();
        if !should_refresh_semantic && !should_prewarm_precise {
            return;
        }

        if should_refresh_semantic {
            let server = self.clone();
            let workspace = workspace.clone();
            let semantic_plan = semantic_plan.clone();
            let task_guard = self.try_start_repository_runtime_task(
                &workspace,
                crate::mcp::types::RuntimeTaskKind::SemanticRefresh,
                "semantic_attach_refresh",
                semantic_plan.as_ref().map(|plan| {
                    format!(
                        "attach root {} snapshot {} reason {}",
                        workspace.root.display(),
                        plan.latest_snapshot_id,
                        plan.reason
                    )
                }),
            );
            if let Ok(task_guard) = task_guard {
                let spawn_task_id = task_guard.task_id().to_owned();
                let spawn_repository_id = workspace.repository_id.clone();
                let spawn_result = std::thread::Builder::new()
                    .name(format!(
                        "frigg-semantic-refresh-{}",
                        workspace.repository_id
                    ))
                    .spawn(move || {
                        let result = semantic_plan
                            .as_ref()
                            .ok_or_else(|| "missing semantic refresh plan".to_owned())
                            .and_then(|plan| {
                                server
                                    .refresh_workspace_semantic_snapshot_with_plan(&workspace, plan)
                            });
                        let (status, detail) = match result {
                            Ok(()) => (crate::mcp::types::RuntimeTaskStatus::Succeeded, None),
                            Err(err) => {
                                warn!(
                                    repository_id = workspace.repository_id,
                                    error = %err,
                                    "workspace semantic refresh failed during runtime prewarm"
                                );
                                (crate::mcp::types::RuntimeTaskStatus::Failed, Some(err))
                            }
                        };
                        let mut task_guard = task_guard;
                        task_guard.finish(status, detail);
                    });
                if let Err(err) = spawn_result {
                    self.runtime_state
                        .runtime_task_registry
                        .write()
                        .expect("runtime task registry poisoned")
                        .update_task_detail(
                            &spawn_task_id,
                            Some(format!("failed to spawn semantic prewarm thread: {err}")),
                        );
                    warn!(
                        repository_id = spawn_repository_id,
                        error = %err,
                        "failed to spawn semantic prewarm thread"
                    );
                }
            } else {
                tracing::trace!(
                    repository_id = %workspace.repository_id,
                    root = %workspace.root.display(),
                    "workspace semantic refresh skipped because runtime index work is active"
                );
            }
        }

        if should_prewarm_precise {
            let server = self.clone();
            let workspace = workspace.clone();
            let task_guard = RuntimeTaskGuard::start(
                Arc::clone(&self.runtime_state.runtime_task_registry),
                crate::mcp::types::RuntimeTaskKind::PrecisePrewarm,
                workspace.repository_id.clone(),
                "precise_attach_prewarm",
                Some(format!("attach root {}", workspace.root.display())),
            );
            let spawn_task_id = task_guard.task_id().to_owned();
            let spawn_repository_id = workspace.repository_id.clone();
            let spawn_result = std::thread::Builder::new()
                .name(format!("frigg-precise-prewarm-{}", workspace.repository_id))
                .spawn(move || {
                    let result = server.prewarm_precise_graph_for_workspace(&workspace);
                    let (status, detail) = match result {
                        Ok(()) => (crate::mcp::types::RuntimeTaskStatus::Succeeded, None),
                        Err(err) => {
                            warn!(
                                repository_id = workspace.repository_id,
                                error = %err,
                                "failed to prewarm precise graph during workspace attach"
                            );
                            (crate::mcp::types::RuntimeTaskStatus::Failed, Some(err))
                        }
                    };
                    let mut task_guard = task_guard;
                    task_guard.finish(status, detail);
                });
            if let Err(err) = spawn_result {
                self.runtime_state
                    .runtime_task_registry
                    .write()
                    .expect("runtime task registry poisoned")
                    .update_task_detail(
                        &spawn_task_id,
                        Some(format!("failed to spawn precise prewarm thread: {err}")),
                    );
                warn!(
                    repository_id = spawn_repository_id,
                    error = %err,
                    "failed to spawn precise prewarm thread"
                );
            }
        }
    }

    pub(in crate::mcp::server) fn maybe_spawn_workspace_precise_generation_for_paths(
        &self,
        workspace: &AttachedWorkspace,
        changed_paths: &[String],
        deleted_paths: &[String],
    ) -> WorkspacePreciseGenerationAction {
        self.maybe_spawn_workspace_precise_generation(workspace, changed_paths, deleted_paths)
    }

    pub(in crate::mcp::server) fn workspace_precise_summary_for_workspace(
        &self,
        workspace: &AttachedWorkspace,
        generation_action: Option<WorkspacePreciseGenerationAction>,
    ) -> WorkspacePreciseSummary {
        let storage = Self::workspace_storage_summary(workspace);
        let health = self.workspace_index_health_summary(workspace, &storage);
        let default_action =
            generation_action.or(Some(WorkspacePreciseGenerationAction::NotApplicable));

        if let Some(precise_ingest) = health.precise_ingest.as_ref() {
            let failure_summary = precise_ingest.reason.clone().or_else(|| {
                precise_ingest.failed_artifacts.first().map(|failure| {
                    format!(
                        "{} [{}]: {}",
                        failure.artifact_label, failure.stage, failure.detail
                    )
                })
            });
            match precise_ingest.state {
                WorkspacePreciseIngestState::Ready => {
                    return WorkspacePreciseSummary {
                        state: WorkspacePreciseState::Ok,
                        failure_tool: None,
                        failure_class: None,
                        failure_summary: None,
                        recommended_action: None,
                        generation_action: default_action,
                    };
                }
                WorkspacePreciseIngestState::Partial => {
                    return WorkspacePreciseSummary {
                        state: WorkspacePreciseState::Partial,
                        failure_tool: None,
                        failure_class: None,
                        failure_summary,
                        recommended_action: Some(WorkspaceRecommendedAction::UseHeuristicMode),
                        generation_action: default_action,
                    };
                }
                WorkspacePreciseIngestState::Failed | WorkspacePreciseIngestState::Error => {
                    return WorkspacePreciseSummary {
                        state: WorkspacePreciseState::Failed,
                        failure_tool: None,
                        failure_class: None,
                        failure_summary,
                        recommended_action: Some(WorkspaceRecommendedAction::UseHeuristicMode),
                        generation_action: default_action,
                    };
                }
                WorkspacePreciseIngestState::Missing => {}
            }
        }

        let failed_generator =
            health.precise_generators.iter().find(|generator| {
                generator.last_generation.as_ref().is_some_and(|summary| {
                    summary.status == WorkspacePreciseGenerationStatus::Failed
                }) || generator.state == WorkspacePreciseGeneratorState::Error
            });
        if let Some(generator) = failed_generator {
            let last_generation = generator.last_generation.as_ref();
            let failure_class = last_generation.and_then(|summary| summary.failure_class);
            let failure_detail = last_generation
                .and_then(|summary| summary.detail.as_deref())
                .or(generator.reason.as_deref());
            return WorkspacePreciseSummary {
                state: if health.scip.state == WorkspaceIndexComponentState::Stale {
                    WorkspacePreciseState::Partial
                } else {
                    WorkspacePreciseState::Failed
                },
                failure_tool: generator.tool.clone(),
                failure_class,
                failure_summary: Self::concise_precise_failure_summary(
                    generator.tool.as_deref(),
                    failure_class,
                    failure_detail,
                ),
                recommended_action: last_generation
                    .and_then(|summary| summary.recommended_action)
                    .or(Some(WorkspaceRecommendedAction::UseHeuristicMode)),
                generation_action: default_action,
            };
        }

        let missing_tool_generator = health.precise_generators.iter().find(|generator| {
            generator.state == WorkspacePreciseGeneratorState::MissingTool
                || generator.last_generation.as_ref().is_some_and(|summary| {
                    summary.status == WorkspacePreciseGenerationStatus::MissingTool
                })
        });
        if let Some(generator) = missing_tool_generator {
            let failure_detail = generator.reason.as_deref().or_else(|| {
                generator
                    .last_generation
                    .as_ref()
                    .and_then(|summary| summary.detail.as_deref())
            });
            return WorkspacePreciseSummary {
                state: WorkspacePreciseState::Unavailable,
                failure_tool: generator.tool.clone(),
                failure_class: Some(WorkspacePreciseFailureClass::MissingTool),
                failure_summary: Self::concise_precise_failure_summary(
                    generator.tool.as_deref(),
                    Some(WorkspacePreciseFailureClass::MissingTool),
                    failure_detail,
                ),
                recommended_action: Some(WorkspaceRecommendedAction::InstallTool),
                generation_action: default_action,
            };
        }

        WorkspacePreciseSummary {
            state: WorkspacePreciseState::Unavailable,
            failure_tool: None,
            failure_class: None,
            failure_summary: health
                .precise_ingest
                .as_ref()
                .and_then(|summary| summary.reason.clone())
                .or_else(|| health.scip.reason.clone()),
            recommended_action: Some(WorkspaceRecommendedAction::UseHeuristicMode),
            generation_action: default_action,
        }
    }

    pub(in crate::mcp::server) fn storage_error_health_summary(
        storage: &WorkspaceStorageSummary,
    ) -> Option<WorkspaceIndexComponentSummary> {
        let (state, reason) = match storage.index_state {
            WorkspaceStorageIndexState::MissingDb => (
                WorkspaceIndexComponentState::Missing,
                Some("missing_db".to_owned()),
            ),
            WorkspaceStorageIndexState::Uninitialized => (
                WorkspaceIndexComponentState::Missing,
                Some(if storage.initialized {
                    "missing_manifest_snapshot".to_owned()
                } else {
                    "uninitialized_db".to_owned()
                }),
            ),
            WorkspaceStorageIndexState::Ready => return None,
            WorkspaceStorageIndexState::Error => (
                WorkspaceIndexComponentState::Error,
                storage
                    .error
                    .clone()
                    .or_else(|| Some("storage_error".to_owned())),
            ),
        };
        Some(WorkspaceIndexComponentSummary {
            state,
            reason,
            snapshot_id: None,
            compatible_snapshot_id: None,
            provider: None,
            model: None,
            artifact_count: None,
        })
    }
}

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

    fn response_freshness_test_server() -> FriggMcpServer {
        let root = std::env::current_dir().expect("test current directory should exist");
        FriggMcpServer::new(
            FriggConfig::from_workspace_roots(vec![root])
                .expect("test workspace root should build a config"),
        )
    }

    fn cacheable_response_freshness(repository_id: &str) -> RepositoryResponseCacheFreshness {
        RepositoryResponseCacheFreshness {
            scopes: Some(vec![RepositoryFreshnessCacheScope {
                repository_id: repository_id.to_owned(),
                snapshot_id: "snapshot-001".to_owned(),
                semantic_state: None,
                semantic_provider: None,
                semantic_model: None,
            }]),
            basis: json!({
                "mode": RepositoryResponseCacheFreshnessMode::ManifestOnly.as_str(),
                "cacheable": true,
            }),
        }
    }

    #[test]
    fn response_freshness_cache_skips_insert_after_invalidation_epoch_changes() {
        let server = response_freshness_test_server();
        let repository_id = "repo-001";
        let scoped_repository_ids = vec![repository_id.to_owned()];
        let freshness = cacheable_response_freshness(repository_id);

        let current_epoch = server.repository_response_freshness_cache_epoch();
        server.cache_repository_response_freshness(
            &scoped_repository_ids,
            RepositoryResponseCacheFreshnessMode::ManifestOnly,
            &freshness,
            current_epoch,
        );
        assert_eq!(
            server
                .cache_state
                .repository_response_freshness_cache
                .read()
                .expect("response freshness cache should not be poisoned")
                .len(),
            1
        );

        let stale_epoch = server.repository_response_freshness_cache_epoch();
        server.invalidate_repository_response_freshness_cache(repository_id);
        server.cache_repository_response_freshness(
            &scoped_repository_ids,
            RepositoryResponseCacheFreshnessMode::ManifestOnly,
            &freshness,
            stale_epoch,
        );
        assert!(
            server
                .cache_state
                .repository_response_freshness_cache
                .read()
                .expect("response freshness cache should not be poisoned")
                .is_empty(),
            "stale in-flight freshness computations must not repopulate after invalidation"
        );

        server
            .cache_state
            .repository_response_freshness_cache
            .write()
            .expect("response freshness cache should not be poisoned")
            .insert(
                RepositoryResponseFreshnessCacheKey {
                    scoped_repository_ids: scoped_repository_ids.clone(),
                    mode: RepositoryResponseCacheFreshnessMode::ManifestOnly.as_str(),
                },
                CachedRepositoryResponseFreshness {
                    freshness,
                    epoch: stale_epoch,
                },
            );
        assert!(
            server
                .cached_repository_response_freshness(
                    &scoped_repository_ids,
                    RepositoryResponseCacheFreshnessMode::ManifestOnly,
                )
                .is_none(),
            "stale entries still present during invalidation must not be returned"
        );
    }
}
