//! Navigation response metadata, structural follow-up derivation, and compact selection notes.
//!
//! Attaches navigation follow-up hints, compact selection notes, and structural metadata to
//! location tool responses.

use super::*;
use crate::languages::rust_implementation_candidates_from_facts;

#[derive(Debug, Clone)]
pub(in crate::mcp::server) struct StructuralFollowUpSourceFile {
    language: SymbolLanguage,
    source: String,
}

impl FriggMcpServer {
    pub(in crate::mcp::server) fn generated_follow_up_structural_for_anchor(
        root: &Path,
        repository_id: &str,
        display_path: &str,
        line: usize,
        column: usize,
        file_cache: &mut BTreeMap<PathBuf, Option<StructuralFollowUpSourceFile>>,
    ) -> Vec<GeneratedStructuralFollowUp> {
        let relative_path = PathBuf::from(display_path);
        let absolute_path = if relative_path.is_absolute() {
            relative_path
        } else {
            root.join(&relative_path)
        };

        if !Self::navigation_path_within_root(root, &absolute_path) {
            return Vec::new();
        }

        if !file_cache.contains_key(&absolute_path) {
            let source_file =
                supported_language_for_path(&absolute_path, LanguageCapability::StructuralSearch)
                    .and_then(|language| {
                        fs::read_to_string(&absolute_path)
                            .ok()
                            .map(|source| StructuralFollowUpSourceFile { language, source })
                    });
            file_cache.insert(absolute_path.clone(), source_file);
        }

        let Some(source_file) = file_cache.get(&absolute_path).and_then(Option::as_ref) else {
            return Vec::new();
        };

        generated_follow_up_structural_at_location_in_source(
            source_file.language,
            &absolute_path,
            display_path,
            &source_file.source,
            line,
            column,
            repository_id,
        )
        .unwrap_or_default()
    }

    pub(in crate::mcp::server) fn populate_navigation_location_follow_up_structural(
        root: &Path,
        matches: &mut [NavigationLocation],
    ) {
        let mut file_cache: BTreeMap<PathBuf, Option<StructuralFollowUpSourceFile>> =
            BTreeMap::new();
        for navigation_match in matches {
            navigation_match.follow_up_structural = Self::generated_follow_up_structural_for_anchor(
                root,
                &navigation_match.repository_id,
                &navigation_match.path,
                navigation_match.line,
                navigation_match.column,
                &mut file_cache,
            );
        }
    }

    pub(in crate::mcp::server) fn populate_reference_match_follow_up_structural(
        root: &Path,
        matches: &mut [ReferenceMatch],
    ) {
        let mut file_cache: BTreeMap<PathBuf, Option<StructuralFollowUpSourceFile>> =
            BTreeMap::new();
        for reference_match in matches {
            reference_match.follow_up_structural = Self::generated_follow_up_structural_for_anchor(
                root,
                &reference_match.repository_id,
                &reference_match.path,
                reference_match.line,
                reference_match.column,
                &mut file_cache,
            );
        }
    }

    pub(in crate::mcp::server) fn populate_implementation_match_follow_up_structural(
        root: &Path,
        matches: &mut [ImplementationMatch],
    ) {
        let mut file_cache: BTreeMap<PathBuf, Option<StructuralFollowUpSourceFile>> =
            BTreeMap::new();
        for implementation_match in matches {
            implementation_match.follow_up_structural =
                Self::generated_follow_up_structural_for_anchor(
                    root,
                    &implementation_match.repository_id,
                    &implementation_match.path,
                    implementation_match.line,
                    implementation_match.column,
                    &mut file_cache,
                );
        }
    }

    pub(in crate::mcp::server) fn populate_call_hierarchy_match_follow_up_structural(
        root: &Path,
        matches: &mut [CallHierarchyMatch],
    ) {
        let mut file_cache: BTreeMap<PathBuf, Option<StructuralFollowUpSourceFile>> =
            BTreeMap::new();
        for call_match in matches {
            call_match.follow_up_structural = Self::generated_follow_up_structural_for_anchor(
                root,
                &call_match.repository_id,
                &call_match.path,
                call_match.line,
                call_match.column,
                &mut file_cache,
            );
        }
    }

    pub(in crate::mcp::server) fn metadata_note_pair(
        metadata: Value,
    ) -> (Option<crate::mcp::types::MetadataObject>, Option<String>) {
        let note =
            Some(serde_json::to_string(&metadata).expect("metadata payload should serialize"));
        let metadata = crate::mcp::types::MetadataObject::try_from(metadata)
            .expect("metadata payload should be a JSON object");
        (Some(metadata), note)
    }

    pub(in crate::mcp::server) fn metadata_with_freshness_basis(
        mut metadata: Value,
        freshness_basis: &Value,
    ) -> Value {
        metadata
            .as_object_mut()
            .expect("metadata payload should be an object")
            .insert("freshness_basis".to_owned(), freshness_basis.clone());
        metadata
    }

    #[allow(clippy::type_complexity)]
    pub(in crate::mcp::server) fn precise_call_site_fields(
        root: &Path,
        occurrence: &crate::graph::PreciseOccurrenceRecord,
    ) -> (
        Option<String>,
        Option<usize>,
        Option<usize>,
        Option<usize>,
        Option<usize>,
    ) {
        (
            Some(Self::canonicalize_navigation_path(root, &occurrence.path)),
            Some(occurrence.range.start_line),
            Some(occurrence.range.start_column),
            Some(occurrence.range.end_line),
            Some(occurrence.range.end_column),
        )
    }

    pub(in crate::mcp::server) fn is_heuristic_callable_kind(kind: &str) -> bool {
        matches!(
            kind.trim().to_ascii_lowercase().as_str(),
            "function" | "method"
        )
    }

    pub(in crate::mcp::server) fn navigation_target_selection_note(
        symbol_query: &str,
        target: &SymbolCandidate,
        candidate_count: usize,
        selected_rank_candidate_count: usize,
    ) -> serde_json::Value {
        json!({
            "query": symbol_query,
            "selected_symbol_id": target.symbol.stable_id,
            "selected_symbol": target.symbol.name,
            "selected_kind": target.symbol.kind,
            "selected_repository_id": target.repository_id,
            "selected_path": Self::relative_display_path(&target.root, &target.symbol.path),
            "selected_path_class": target.path_class,
            "selected_line": target.symbol.line,
            "selected_rank": target.rank,
            "candidate_count": candidate_count,
            "same_rank_candidate_count": selected_rank_candidate_count,
            "ambiguous_query": selected_rank_candidate_count > 1,
        })
    }

    fn symbol_match_from_symbol_candidate(
        corpora: &[Arc<RepositorySymbolCorpus>],
        candidate: &SymbolCandidate,
    ) -> SymbolMatch {
        let (container, signature) = corpora
            .iter()
            .find(|corpus| corpus.repository_id == candidate.repository_id)
            .map(|corpus| Self::symbol_context_for_stable_id(corpus, &candidate.symbol.stable_id))
            .unwrap_or((None, None));
        SymbolMatch {
            match_id: None,
            stable_symbol_id: Some(candidate.symbol.stable_id.clone()),
            repository_id: candidate.repository_id.clone(),
            symbol: candidate.symbol.name.clone(),
            kind: candidate.symbol.kind.as_str().to_owned(),
            path: Self::relative_display_path(&candidate.root, &candidate.symbol.path),
            line: candidate.symbol.line,
            container,
            signature,
        }
    }

    pub(in crate::mcp::server) fn navigation_target_selection_summary_for_resolved(
        symbol_query: &str,
        target: &ResolvedSymbolTarget,
    ) -> NavigationTargetSelectionSummary {
        NavigationTargetSelectionSummary {
            status: NavigationTargetSelectionStatus::Resolved,
            symbol_query: symbol_query.to_owned(),
            selected_stable_symbol_id: Some(target.candidate.symbol.stable_id.clone()),
            candidate_count: target.candidate_count,
            same_rank_candidate_count: target.selected_rank_candidate_count,
            ambiguous_query: target.selected_rank_candidate_count > 1,
            candidates: Vec::new(),
        }
    }

    pub(in crate::mcp::server) fn navigation_target_selection_summary_for_disambiguation(
        corpora: &[Arc<RepositorySymbolCorpus>],
        symbol_query: &str,
        target: &DisambiguationRequiredSymbolTarget,
    ) -> NavigationTargetSelectionSummary {
        NavigationTargetSelectionSummary {
            status: NavigationTargetSelectionStatus::DisambiguationRequired,
            symbol_query: symbol_query.to_owned(),
            selected_stable_symbol_id: None,
            candidate_count: target.candidate_count,
            same_rank_candidate_count: target.selected_rank_candidate_count,
            ambiguous_query: true,
            candidates: target
                .candidates
                .iter()
                .map(|candidate| Self::symbol_match_from_symbol_candidate(corpora, candidate))
                .collect(),
        }
    }

    pub(in crate::mcp::server) fn navigation_target_selection_summary_for_selection(
        corpora: &[Arc<RepositorySymbolCorpus>],
        symbol_query: &str,
        selection: &NavigationTargetSelection,
    ) -> NavigationTargetSelectionSummary {
        match selection {
            NavigationTargetSelection::Resolved(target) => {
                Self::navigation_target_selection_summary_for_resolved(symbol_query, target)
            }
            NavigationTargetSelection::DisambiguationRequired(target) => {
                Self::navigation_target_selection_summary_for_disambiguation(
                    corpora,
                    symbol_query,
                    target,
                )
            }
        }
    }

    pub(in crate::mcp::server) fn navigation_target_selection_summary_value(
        summary: &NavigationTargetSelectionSummary,
    ) -> Value {
        serde_json::to_value(summary).expect("target selection summary should serialize")
    }

    pub(in crate::mcp::server) fn precise_absence_reason(
        coverage_mode: PreciseCoverageMode,
        stats: &PreciseIngestStats,
        precise_match_count: usize,
    ) -> &'static str {
        if stats.artifacts_discovered == 0 {
            return "no_scip_artifacts_discovered";
        }

        match coverage_mode {
            PreciseCoverageMode::Partial if precise_match_count == 0 => {
                return "precise_partial_non_authoritative_absence";
            }
            PreciseCoverageMode::None if stats.artifacts_failed > 0 => {
                return "scip_artifact_ingest_failed";
            }
            PreciseCoverageMode::Full | PreciseCoverageMode::Partial
                if stats.artifacts_ingested > 0 && precise_match_count == 0 =>
            {
                return "required_precise_matches_not_present_in_precise_graph";
            }
            PreciseCoverageMode::None => {
                return "no_usable_precise_data";
            }
            _ => {}
        }

        "precise_unavailable"
    }

    pub(in crate::mcp::server) fn call_hierarchy_availability(
        coverage_mode: PreciseCoverageMode,
        stats: &PreciseIngestStats,
        precise_match_count: usize,
        heuristic_match_count: usize,
    ) -> NavigationAvailability {
        if precise_match_count > 0 {
            return NavigationAvailability {
                status: "available".to_owned(),
                reason: None,
                precise_required_for_complete_results: false,
            };
        }
        if heuristic_match_count > 0 {
            return NavigationAvailability {
                status: "heuristic".to_owned(),
                reason: Some(
                    Self::precise_absence_reason(coverage_mode, stats, precise_match_count)
                        .to_owned(),
                ),
                precise_required_for_complete_results: true,
            };
        }
        if coverage_mode == PreciseCoverageMode::Full {
            return NavigationAvailability {
                status: "available".to_owned(),
                reason: None,
                precise_required_for_complete_results: false,
            };
        }

        NavigationAvailability {
            status: "unavailable".to_owned(),
            reason: Some(
                Self::precise_absence_reason(coverage_mode, stats, precise_match_count).to_owned(),
            ),
            precise_required_for_complete_results: true,
        }
    }

    pub(in crate::mcp::server) fn navigation_mode_from_precision_label(
        label: Option<&str>,
    ) -> NavigationMode {
        match label {
            Some("precise") => NavigationMode::Precise,
            Some("precise_partial") => NavigationMode::PrecisePartial,
            Some("heuristic") => NavigationMode::HeuristicNoPrecise,
            _ => NavigationMode::UnavailableNoPrecise,
        }
    }

    pub(in crate::mcp::server) fn precise_coverage_mode(
        stats: &PreciseIngestStats,
    ) -> PreciseCoverageMode {
        if stats.artifacts_ingested == 0 {
            return PreciseCoverageMode::None;
        }
        if stats.artifacts_failed > 0 {
            return PreciseCoverageMode::Partial;
        }
        PreciseCoverageMode::Full
    }

    pub(in crate::mcp::server) fn precise_resolution_precision(
        coverage_mode: PreciseCoverageMode,
    ) -> &'static str {
        match coverage_mode {
            PreciseCoverageMode::Full => "precise",
            PreciseCoverageMode::Partial => "precise_partial",
            PreciseCoverageMode::None => "heuristic",
        }
    }

    pub(in crate::mcp::server) fn precise_match_precision(
        coverage_mode: PreciseCoverageMode,
    ) -> &'static str {
        match coverage_mode {
            PreciseCoverageMode::Full => "precise",
            PreciseCoverageMode::Partial => "precise_partial",
            PreciseCoverageMode::None => "heuristic",
        }
    }

    fn precise_note_metadata(
        coverage_mode: PreciseCoverageMode,
        stats: &PreciseIngestStats,
    ) -> serde_json::Value {
        json!({
            "coverage": coverage_mode.as_str(),
            "candidate_directories": Self::bounded_text_values(&stats.candidate_directories),
            "discovered_artifacts": Self::bounded_text_values(&stats.discovered_artifacts),
            "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_failure_note_entries(stats),
        })
    }

    pub(in crate::mcp::server) fn precise_note_with_count(
        coverage_mode: PreciseCoverageMode,
        stats: &PreciseIngestStats,
        count_key: &str,
        count: usize,
    ) -> serde_json::Value {
        let mut precise = Self::precise_note_metadata(coverage_mode, stats);
        precise[count_key] = json!(count);
        precise
    }

    pub(in crate::mcp::server) fn push_precise_failure_sample(
        stats: &mut PreciseIngestStats,
        artifact_label: impl Into<String>,
        stage: &str,
        detail: impl AsRef<str>,
    ) {
        if stats.failed_artifacts.len() >= Self::PRECISE_FAILURE_SAMPLE_LIMIT {
            return;
        }

        let artifact_label = artifact_label.into();
        stats.failed_artifacts.push(PreciseArtifactFailureSample {
            artifact_label: Self::bounded_text(&artifact_label),
            stage: stage.to_owned(),
            detail: Self::bounded_text(detail.as_ref()),
        });
    }

    fn precise_failure_note_entries(stats: &PreciseIngestStats) -> Vec<Value> {
        stats
            .failed_artifacts
            .iter()
            .map(|sample| {
                json!({
                    "artifact_label": sample.artifact_label,
                    "stage": sample.stage,
                    "detail": sample.detail,
                })
            })
            .collect()
    }

    fn bounded_text_values(values: &[String]) -> Vec<String> {
        values
            .iter()
            .map(|value| Self::bounded_text(value))
            .collect::<Vec<_>>()
    }

    pub(in crate::mcp::server) fn heuristic_implementation_matches_from_symbols(
        target_symbol: &SymbolDefinition,
        target_corpus: &RepositorySymbolCorpus,
        target_root: &Path,
    ) -> Vec<ImplementationMatch> {
        match heuristic_implementation_strategy(target_symbol.language) {
            Some(HeuristicImplementationStrategy::RustImplBlocks) => {
                Self::heuristic_rust_implementation_matches_from_symbols(
                    target_symbol,
                    target_corpus,
                    target_root,
                )
            }
            Some(HeuristicImplementationStrategy::PhpDeclarationRelations) => {
                Self::heuristic_php_implementation_matches_from_symbols(
                    target_symbol,
                    target_corpus,
                    target_root,
                )
            }
            None => Vec::new(),
        }
    }

    fn heuristic_rust_implementation_matches_from_symbols(
        target_symbol: &SymbolDefinition,
        target_corpus: &RepositorySymbolCorpus,
        target_root: &Path,
    ) -> Vec<ImplementationMatch> {
        let candidates = if target_corpus.rust_implementation_facts.is_empty() {
            heuristic_rust_implementation_candidates(target_symbol, &target_corpus.symbols)
        } else {
            let indexed = rust_implementation_candidates_from_facts(
                target_symbol,
                &target_corpus.symbols,
                &target_corpus.rust_implementation_facts,
            );
            if indexed.is_empty() {
                heuristic_rust_implementation_candidates(target_symbol, &target_corpus.symbols)
            } else {
                indexed
            }
        };
        let matches = candidates
            .into_iter()
            .map(|candidate| {
                let (container, signature) = Self::symbol_context_for_stable_id(
                    target_corpus,
                    &candidate.source_symbol.stable_id,
                );
                ImplementationMatch {
                    match_id: None,
                    stable_symbol_id: Some(candidate.source_symbol.stable_id.clone()),
                    symbol: candidate.symbol,
                    kind: Self::display_symbol_kind(candidate.source_symbol.kind.as_str()),
                    repository_id: target_corpus.repository_id.clone(),
                    path: Self::relative_display_path(target_root, &candidate.source_symbol.path),
                    line: candidate.source_symbol.line,
                    column: 1,
                    relation: Some(candidate.relation.to_owned()),
                    container,
                    signature,
                    precision: Some("heuristic".to_owned()),
                    fallback_reason: Some(candidate.fallback_reason.to_owned()),
                    follow_up_structural: Vec::new(),
                }
            })
            .collect::<Vec<_>>();

        Self::dedup_sorted_implementation_matches(matches)
    }

    fn heuristic_php_implementation_matches_from_symbols(
        target_symbol: &SymbolDefinition,
        target_corpus: &RepositorySymbolCorpus,
        target_root: &Path,
    ) -> Vec<ImplementationMatch> {
        let candidate_files = target_corpus
            .source_paths
            .iter()
            .map(|path| (Self::relative_display_path(target_root, path), path.clone()))
            .collect::<Vec<_>>();
        let mut matches = Vec::new();
        for (source_symbol_index, relation) in php_heuristic_implementation_candidates_for_target(
            target_symbol,
            &candidate_files,
            &target_corpus.symbols,
            &target_corpus.symbols_by_relative_path,
            Some(&target_corpus.symbol_indices_by_name),
            Some(&target_corpus.symbol_indices_by_lower_name),
        ) {
            let source_symbol = &target_corpus.symbols[source_symbol_index];
            if source_symbol.stable_id == target_symbol.stable_id {
                continue;
            }

            let (container, signature) =
                Self::symbol_context_for_stable_id(target_corpus, &source_symbol.stable_id);
            matches.push(ImplementationMatch {
                match_id: None,
                stable_symbol_id: Some(source_symbol.stable_id.clone()),
                symbol: source_symbol.name.clone(),
                kind: Self::display_symbol_kind(source_symbol.kind.as_str()),
                repository_id: target_corpus.repository_id.clone(),
                path: Self::relative_display_path(target_root, &source_symbol.path),
                line: source_symbol.line,
                column: 1,
                relation: Some(RelationKind::as_str(relation).to_owned()),
                container,
                signature,
                precision: Some("heuristic".to_owned()),
                fallback_reason: Some("precise_absent".to_owned()),
                follow_up_structural: Vec::new(),
            });
        }

        Self::dedup_sorted_implementation_matches(matches)
    }

    fn dedup_sorted_implementation_matches(
        mut matches: Vec<ImplementationMatch>,
    ) -> Vec<ImplementationMatch> {
        Self::sort_implementation_matches(&mut matches);
        matches.dedup_by(|left, right| {
            left.repository_id == right.repository_id
                && left.path == right.path
                && left.line == right.line
                && left.column == right.column
                && left.symbol == right.symbol
                && left.kind == right.kind
                && left.relation == right.relation
                && left.precision == right.precision
                && left.fallback_reason == right.fallback_reason
        });
        matches
    }

    pub(in crate::mcp::server) fn sort_call_hierarchy_matches(matches: &mut [CallHierarchyMatch]) {
        matches.sort_by(|left, right| {
            left.repository_id
                .cmp(&right.repository_id)
                .then(left.path.cmp(&right.path))
                .then(left.line.cmp(&right.line))
                .then(left.column.cmp(&right.column))
                .then(left.source_symbol.cmp(&right.source_symbol))
                .then(left.target_symbol.cmp(&right.target_symbol))
                .then(left.relation.cmp(&right.relation))
                .then(left.precision.cmp(&right.precision))
                .then(left.call_path.cmp(&right.call_path))
                .then(left.call_line.cmp(&right.call_line))
                .then(left.call_column.cmp(&right.call_column))
                .then(left.call_end_line.cmp(&right.call_end_line))
                .then(left.call_end_column.cmp(&right.call_end_column))
        });
    }

    pub(in crate::mcp::server) fn is_heuristic_call_relation(relation: RelationKind) -> bool {
        matches!(relation, RelationKind::Calls)
    }
}
