//! Overlay projection records, decoding, and recall boost accumulation.
//!
//! Encodes test-subject and entrypoint-surface links with scored flags, then merges overlay
//! bonuses into path-witness candidate frontiers during hybrid search.

use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;

use crate::domain::{FriggError, FriggResult, PathClass, SourceClass};
use crate::languages::SymbolLanguage;
use crate::path_class::classify_repository_path;
use crate::searcher::lexical_channel::HybridPathWitnessQueryContext;
use crate::storage::{
    EntrypointSurfaceProjection, PathRelationProjection, PathSurfaceTermProjection,
    TestSubjectProjection,
};
use serde::{Deserialize, Serialize};

use super::{
    GenericWitnessSurfaceFamily, HybridRankingIntent, StoredPathWitnessProjection,
    generic_surface_families_for_projection, generic_surface_families_from_bits,
    hybrid_identifier_tokens, hybrid_overlap_count, hybrid_path_overlap_tokens,
    hybrid_source_class, is_ci_workflow_path, is_entrypoint_build_workflow_path,
    is_entrypoint_runtime_path, is_runtime_config_artifact_path, is_scripts_ops_path,
    is_test_harness_path, is_test_support_path,
};

const MAX_TEST_SUBJECT_LINKS_PER_TEST: usize = 6;
const MAX_TEST_SUBJECT_LINKS_PER_SUBJECT: usize = 6;

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(in crate::searcher) struct PathOverlayBoost {
    pub(in crate::searcher) bonus_millis: u32,
    pub(in crate::searcher) provenance_ids: Vec<String>,
}

impl PathOverlayBoost {
    pub(in crate::searcher) fn merge(&mut self, other: Self) {
        self.bonus_millis = self.bonus_millis.saturating_add(other.bonus_millis);
        self.provenance_ids.extend(other.provenance_ids);
        self.provenance_ids.sort();
        self.provenance_ids.dedup();
    }

    pub(in crate::searcher) fn bonus_score(&self) -> f32 {
        self.bonus_millis as f32 / 1000.0
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub(in crate::searcher) struct TestSubjectProjectionFlags {
    pub(in crate::searcher) exact_stem_match: bool,
    pub(in crate::searcher) same_language: bool,
    pub(in crate::searcher) runtime_subject: bool,
    pub(in crate::searcher) support_subject: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct StoredTestSubjectProjection {
    pub(in crate::searcher) test_path: String,
    pub(in crate::searcher) subject_path: String,
    pub(in crate::searcher) shared_terms: Vec<String>,
    pub(in crate::searcher) score_hint: usize,
    pub(in crate::searcher) flags: TestSubjectProjectionFlags,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub(in crate::searcher) struct EntrypointSurfaceProjectionFlags {
    pub(in crate::searcher) is_runtime_entrypoint: bool,
    pub(in crate::searcher) is_build_workflow: bool,
    pub(in crate::searcher) is_runtime_config_artifact: bool,
    pub(in crate::searcher) is_ci_workflow: bool,
    pub(in crate::searcher) is_scripts_ops: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct StoredEntrypointSurfaceProjection {
    pub(in crate::searcher) path: String,
    pub(in crate::searcher) path_class: PathClass,
    pub(in crate::searcher) source_class: SourceClass,
    pub(in crate::searcher) path_terms: Vec<String>,
    pub(in crate::searcher) surface_terms: Vec<String>,
    pub(in crate::searcher) flags: EntrypointSurfaceProjectionFlags,
}

pub(crate) fn build_test_subject_projection_records(
    paths: &[String],
) -> FriggResult<Vec<TestSubjectProjection>> {
    let candidates = paths
        .iter()
        .map(|path| TestSubjectCandidate::from_path(path))
        .collect::<Vec<_>>();
    let test_candidates = candidates
        .iter()
        .filter(|candidate| candidate.is_testish)
        .collect::<Vec<_>>();
    let subject_candidates = candidates
        .iter()
        .filter(|candidate| candidate.is_subjectish)
        .collect::<Vec<_>>();

    let mut by_subject = BTreeMap::<String, Vec<StoredTestSubjectProjection>>::new();
    for test_candidate in test_candidates {
        let mut subject_links = subject_candidates
            .iter()
            .filter_map(|subject_candidate| {
                build_test_subject_projection_from_candidates(test_candidate, subject_candidate)
            })
            .collect::<Vec<_>>();
        subject_links.sort_by(test_subject_projection_order);
        subject_links.truncate(MAX_TEST_SUBJECT_LINKS_PER_TEST);

        for projection in subject_links {
            by_subject
                .entry(projection.subject_path.clone())
                .or_default()
                .push(projection);
        }
    }

    let mut retained = by_subject
        .into_values()
        .flat_map(|mut projections| {
            projections.sort_by(test_subject_projection_order);
            projections.truncate(MAX_TEST_SUBJECT_LINKS_PER_SUBJECT);
            projections
        })
        .collect::<Vec<_>>();
    retained.sort_by(test_subject_projection_order);
    retained.dedup_by(|left, right| {
        left.test_path == right.test_path && left.subject_path == right.subject_path
    });

    retained
        .into_iter()
        .map(|projection| encode_test_subject_projection_record(&projection))
        .collect()
}

pub(crate) fn decode_test_subject_projection_records(
    rows: &[TestSubjectProjection],
) -> FriggResult<Vec<StoredTestSubjectProjection>> {
    rows.iter()
        .map(decode_test_subject_projection_record)
        .collect()
}

pub(in crate::searcher) fn decode_test_subject_projection_record(
    record: &TestSubjectProjection,
) -> FriggResult<StoredTestSubjectProjection> {
    let _stored_terms = &record.shared_terms;
    let _stored_flags: TestSubjectProjectionFlags = serde_json::from_str(&record.flags_json)
        .map_err(|err| {
            FriggError::Internal(format!(
                "failed to decode stored test subject projection flags for '{} -> {}': {err}",
                record.test_path, record.subject_path
            ))
        })?;

    let test_candidate = TestSubjectCandidate::from_path(&record.test_path);
    let subject_candidate = TestSubjectCandidate::from_path(&record.subject_path);
    build_test_subject_projection_from_candidates(&test_candidate, &subject_candidate).ok_or_else(
        || {
            FriggError::Internal(format!(
                "stored test subject projection '{} -> {}' is no longer valid under live generic heuristics",
                record.test_path, record.subject_path
            ))
        },
    )
}

pub(in crate::searcher) fn build_entrypoint_surface_projection_record(
    path: &str,
) -> FriggResult<Option<EntrypointSurfaceProjection>> {
    let Some(projection) = StoredEntrypointSurfaceProjection::from_path(path) else {
        return Ok(None);
    };
    let flags_json = serde_json::to_string(&projection.flags).map_err(|err| {
        FriggError::Internal(format!(
            "failed to encode entrypoint surface flags for '{path}': {err}"
        ))
    })?;

    Ok(Some(EntrypointSurfaceProjection {
        path: path.to_owned(),
        path_class: projection.path_class,
        source_class: projection.source_class,
        path_terms: projection.path_terms,
        surface_terms: projection.surface_terms,
        flags_json,
    }))
}

pub(crate) fn build_entrypoint_surface_projection_records_from_paths(
    paths: &[String],
) -> FriggResult<Vec<EntrypointSurfaceProjection>> {
    let mut rows = paths
        .iter()
        .filter_map(|path| build_entrypoint_surface_projection_record(path).transpose())
        .collect::<FriggResult<Vec<_>>>()?;
    rows.sort_by(|left, right| left.path.cmp(&right.path));
    rows.dedup_by(|left, right| left.path == right.path);
    Ok(rows)
}

pub(in crate::searcher) fn decode_entrypoint_surface_projection_record(
    record: &EntrypointSurfaceProjection,
) -> FriggResult<StoredEntrypointSurfaceProjection> {
    let path_class = record.path_class;
    let source_class = record.source_class;
    let _stored_terms = &record.path_terms;
    let _stored_surface_terms = &record.surface_terms;
    let _stored_flags: EntrypointSurfaceProjectionFlags = serde_json::from_str(&record.flags_json)
        .map_err(|err| {
            FriggError::Internal(format!(
                "failed to decode stored entrypoint surface flags for '{}': {err}",
                record.path
            ))
        })?;

    let Some(projection) = StoredEntrypointSurfaceProjection::from_path(&record.path) else {
        return Err(FriggError::Internal(format!(
            "stored entrypoint surface projection '{}' is no longer valid under live generic heuristics",
            record.path
        )));
    };
    if projection.path_class != path_class || projection.source_class != source_class {
        return Err(FriggError::Internal(format!(
            "stored entrypoint surface projection '{}' no longer matches live path/source classification",
            record.path
        )));
    }

    Ok(projection)
}

pub(crate) fn decode_entrypoint_surface_projection_records(
    rows: &[EntrypointSurfaceProjection],
) -> FriggResult<Vec<StoredEntrypointSurfaceProjection>> {
    rows.iter()
        .map(decode_entrypoint_surface_projection_record)
        .collect()
}

pub(in crate::searcher) fn entrypoint_surface_overlay_boost(
    projection: &StoredEntrypointSurfaceProjection,
    intent: &HybridRankingIntent,
    query_context: &HybridPathWitnessQueryContext,
) -> Option<PathOverlayBoost> {
    if !intent.wants_entrypoint_build_flow
        && !intent.wants_runtime_config_artifacts
        && !intent.wants_ci_workflow_witnesses
        && !intent.wants_scripts_ops_witnesses
    {
        return None;
    }

    let term_overlap = hybrid_overlap_count(
        &projection.surface_terms,
        &query_context.query_overlap_terms,
    ) as u32;
    let path_overlap =
        hybrid_overlap_count(&projection.path_terms, &query_context.query_overlap_terms) as u32;
    let exact_term_match = query_context.exact_terms.iter().any(|term| {
        projection
            .surface_terms
            .iter()
            .any(|candidate| candidate == term)
            || projection
                .path_terms
                .iter()
                .any(|candidate| candidate == term)
    });

    let mut boost = PathOverlayBoost::default();
    if term_overlap > 0 {
        boost.bonus_millis = boost
            .bonus_millis
            .saturating_add(term_overlap.saturating_mul(140));
        boost.provenance_ids.push(format!(
            "overlay:entrypoint_surface:terms:{}",
            projection.path
        ));
    }
    if path_overlap > 0 {
        boost.bonus_millis = boost
            .bonus_millis
            .saturating_add(path_overlap.saturating_mul(80));
        boost.provenance_ids.push(format!(
            "overlay:entrypoint_surface:path:{}",
            projection.path
        ));
    }
    if exact_term_match {
        boost.bonus_millis = boost.bonus_millis.saturating_add(160);
        boost.provenance_ids.push(format!(
            "overlay:entrypoint_surface:exact:{}",
            projection.path
        ));
    }

    if intent.wants_entrypoint_build_flow && projection.flags.is_runtime_entrypoint {
        boost.bonus_millis = boost.bonus_millis.saturating_add(420);
        boost.provenance_ids.push(format!(
            "overlay:entrypoint_surface:runtime:{}",
            projection.path
        ));
    }
    if intent.wants_entrypoint_build_flow && projection.flags.is_build_workflow {
        boost.bonus_millis = boost.bonus_millis.saturating_add(360);
        boost.provenance_ids.push(format!(
            "overlay:entrypoint_surface:build:{}",
            projection.path
        ));
    }
    if intent.wants_runtime_config_artifacts && projection.flags.is_runtime_config_artifact {
        boost.bonus_millis = boost.bonus_millis.saturating_add(380);
        boost.provenance_ids.push(format!(
            "overlay:entrypoint_surface:config:{}",
            projection.path
        ));
    }
    if intent.wants_ci_workflow_witnesses && projection.flags.is_ci_workflow {
        boost.bonus_millis = boost.bonus_millis.saturating_add(320);
        boost
            .provenance_ids
            .push(format!("overlay:entrypoint_surface:ci:{}", projection.path));
    }
    if intent.wants_scripts_ops_witnesses && projection.flags.is_scripts_ops {
        boost.bonus_millis = boost.bonus_millis.saturating_add(320);
        boost.provenance_ids.push(format!(
            "overlay:entrypoint_surface:scripts:{}",
            projection.path
        ));
    }

    (boost.bonus_millis > 0).then_some(boost)
}

pub(in crate::searcher) fn accumulate_test_subject_overlay_boosts(
    projections: &[StoredTestSubjectProjection],
    intent: &HybridRankingIntent,
    query_context: &HybridPathWitnessQueryContext,
) -> BTreeMap<String, PathOverlayBoost> {
    if !intent.wants_tests && !intent.wants_test_witness_recall {
        return BTreeMap::new();
    }

    let mut boosts = BTreeMap::<String, PathOverlayBoost>::new();
    for projection in projections {
        let shared_overlap =
            hybrid_overlap_count(&projection.shared_terms, &query_context.query_overlap_terms)
                as u32;
        let exact_term_match = query_context.exact_terms.iter().any(|term| {
            projection
                .shared_terms
                .iter()
                .any(|candidate| candidate == term)
        });
        if shared_overlap == 0 && !exact_term_match {
            continue;
        }

        let capped_score_hint = projection.score_hint.min(24) as u32;
        let mut test_boost = PathOverlayBoost {
            bonus_millis: shared_overlap
                .saturating_mul(180)
                .saturating_add(capped_score_hint.saturating_mul(10)),
            provenance_ids: vec![format!(
                "overlay:test_subject:test:{}:{}",
                projection.test_path, projection.subject_path
            )],
        };
        if exact_term_match {
            test_boost.bonus_millis = test_boost.bonus_millis.saturating_add(160);
        }
        if projection.flags.exact_stem_match {
            test_boost.bonus_millis = test_boost.bonus_millis.saturating_add(120);
        }
        if projection.flags.same_language {
            test_boost.bonus_millis = test_boost.bonus_millis.saturating_add(60);
        }
        if projection.flags.runtime_subject {
            test_boost.bonus_millis = test_boost.bonus_millis.saturating_add(120);
        }
        if projection.flags.support_subject {
            test_boost.bonus_millis = test_boost.bonus_millis.saturating_add(40);
        }
        boosts
            .entry(projection.test_path.clone())
            .or_default()
            .merge(test_boost);

        let mut subject_boost = PathOverlayBoost {
            bonus_millis: shared_overlap
                .saturating_mul(90)
                .saturating_add(capped_score_hint.saturating_mul(6)),
            provenance_ids: vec![format!(
                "overlay:test_subject:subject:{}:{}",
                projection.subject_path, projection.test_path
            )],
        };
        if exact_term_match {
            subject_boost.bonus_millis = subject_boost.bonus_millis.saturating_add(80);
        }
        if projection.flags.exact_stem_match {
            subject_boost.bonus_millis = subject_boost.bonus_millis.saturating_add(60);
        }
        if projection.flags.runtime_subject {
            subject_boost.bonus_millis = subject_boost.bonus_millis.saturating_add(90);
        }
        if projection.flags.runtime_subject
            && intent.wants_test_witness_recall
            && !intent.wants_entrypoint_build_flow
        {
            subject_boost.bonus_millis = subject_boost.bonus_millis.saturating_add(360);
            subject_boost.provenance_ids.push(format!(
                "overlay:test_subject:reverse_focus:{}:{}",
                projection.subject_path, projection.test_path
            ));
        }
        boosts
            .entry(projection.subject_path.clone())
            .or_default()
            .merge(subject_boost);
    }

    boosts
}

pub(in crate::searcher) fn accumulate_companion_surface_overlay_boosts(
    projections: &[StoredPathWitnessProjection],
    intent: &HybridRankingIntent,
    query_context: &HybridPathWitnessQueryContext,
) -> BTreeMap<String, PathOverlayBoost> {
    if !wants_companion_surface_overlay(intent) {
        return BTreeMap::new();
    }

    let anchors = projections
        .iter()
        .filter_map(|projection| companion_surface_anchor(projection, query_context))
        .collect::<Vec<_>>();
    if anchors.is_empty() {
        return BTreeMap::new();
    }

    let mut boosts = BTreeMap::<String, PathOverlayBoost>::new();
    for projection in projections {
        let Some(subtree_root) = projection.subtree_root.as_deref() else {
            continue;
        };
        let families = generic_surface_families_for_projection(projection);
        if families.is_empty() {
            continue;
        }

        let query_match =
            query_context.match_projection_path(&projection.path, &projection.path_terms);
        let mut best_boost = PathOverlayBoost::default();
        for anchor in &anchors {
            if anchor.path == projection.path {
                continue;
            }
            if !subtree_roots_related(subtree_root, &anchor.subtree_root) {
                continue;
            }

            let family_bonus = companion_surface_family_bonus(&families, &anchor.families, intent);
            if family_bonus == 0 {
                continue;
            }

            let same_family = families
                .iter()
                .any(|family| anchor.families.contains(family));
            let mut bonus_millis = 120_u32.saturating_add(family_bonus);
            if same_family {
                bonus_millis = bonus_millis.saturating_add(70);
            }
            let term_overlap = (query_match.path_overlap.min(3) as u32)
                .saturating_add(anchor.path_overlap.min(3) as u32)
                .min(4);
            bonus_millis = bonus_millis.saturating_add(term_overlap.saturating_mul(45));
            if query_match.has_exact_query_term_match || anchor.exact_term_match {
                bonus_millis = bonus_millis.saturating_add(80);
            }
            if query_match.specific_witness_path_overlap > 0 || anchor.specific_witness_overlap > 0
            {
                bonus_millis = bonus_millis.saturating_add(120);
            }

            let mut provenance_ids = vec![format!(
                "overlay:companion_surface:subtree:{}:{}",
                anchor.path, projection.path
            )];
            if same_family {
                provenance_ids.push(format!(
                    "overlay:companion_surface:family:{}:{}",
                    anchor.path, projection.path
                ));
            } else {
                provenance_ids.push(format!(
                    "overlay:companion_surface:companion:{}:{}",
                    anchor.path, projection.path
                ));
            }
            if query_match.specific_witness_path_overlap > 0 || anchor.specific_witness_overlap > 0
            {
                provenance_ids.push(format!(
                    "overlay:companion_surface:specific:{}:{}",
                    anchor.path, projection.path
                ));
            }
            if query_match.has_exact_query_term_match || anchor.exact_term_match {
                provenance_ids.push(format!(
                    "overlay:companion_surface:exact:{}:{}",
                    anchor.path, projection.path
                ));
            }

            if bonus_millis > best_boost.bonus_millis {
                best_boost = PathOverlayBoost {
                    bonus_millis,
                    provenance_ids,
                };
            }
        }

        if best_boost.bonus_millis > 0 {
            boosts.insert(projection.path.clone(), best_boost);
        }
    }

    boosts
}

pub(in crate::searcher) fn accumulate_relation_overlay_boosts(
    relations: &[PathRelationProjection],
    surface_terms_by_path: &BTreeMap<String, PathSurfaceTermProjection>,
    intent: &HybridRankingIntent,
    query_context: &HybridPathWitnessQueryContext,
) -> BTreeMap<String, PathOverlayBoost> {
    if !wants_companion_surface_overlay(intent) {
        return BTreeMap::new();
    }

    let mut boosts = BTreeMap::<String, PathOverlayBoost>::new();
    for relation in relations {
        let relation_bonus = relation_kind_bonus(relation, intent);
        if relation_bonus == 0 {
            continue;
        }

        let src_match = surface_term_match_score(
            surface_terms_by_path.get(&relation.src_path),
            &relation.shared_terms,
            query_context,
        );
        let dst_match = surface_term_match_score(
            surface_terms_by_path.get(&relation.dst_path),
            &relation.shared_terms,
            query_context,
        );
        if src_match.total == 0 && dst_match.total == 0 {
            continue;
        }

        if src_match.total >= dst_match.total && src_match.total > 0 {
            let boost = relation_overlay_boost(
                relation,
                &relation.dst_path,
                "dst",
                src_match,
                relation_bonus,
            );
            boosts
                .entry(relation.dst_path.clone())
                .or_default()
                .merge(boost);
        }
        if dst_match.total >= src_match.total && dst_match.total > 0 {
            let boost = relation_overlay_boost(
                relation,
                &relation.src_path,
                "src",
                dst_match,
                relation_bonus,
            );
            boosts
                .entry(relation.src_path.clone())
                .or_default()
                .merge(boost);
        }
    }

    boosts
}

#[derive(Debug, Clone)]
struct CompanionSurfaceAnchor {
    path: String,
    subtree_root: String,
    families: Vec<GenericWitnessSurfaceFamily>,
    path_overlap: usize,
    specific_witness_overlap: usize,
    exact_term_match: bool,
}

fn wants_companion_surface_overlay(intent: &HybridRankingIntent) -> bool {
    intent.wants_runtime_witnesses
        || intent.wants_tests
        || intent.wants_test_witness_recall
        || intent.wants_entrypoint_build_flow
        || intent.wants_runtime_config_artifacts
}

fn companion_surface_anchor(
    projection: &StoredPathWitnessProjection,
    query_context: &HybridPathWitnessQueryContext,
) -> Option<CompanionSurfaceAnchor> {
    let subtree_root = projection.subtree_root.clone()?;
    let families = generic_surface_families_for_projection(projection);
    if families.is_empty() {
        return None;
    }

    let query_match = query_context.match_projection_path(&projection.path, &projection.path_terms);
    if query_match.path_overlap == 0
        && query_match.specific_witness_path_overlap == 0
        && !query_match.has_exact_query_term_match
    {
        return None;
    }

    Some(CompanionSurfaceAnchor {
        path: projection.path.clone(),
        subtree_root,
        families,
        path_overlap: query_match.path_overlap,
        specific_witness_overlap: query_match.specific_witness_path_overlap,
        exact_term_match: query_match.has_exact_query_term_match,
    })
}

fn companion_surface_family_bonus(
    left: &[GenericWitnessSurfaceFamily],
    right: &[GenericWitnessSurfaceFamily],
    intent: &HybridRankingIntent,
) -> u32 {
    let mut best = 0_u32;
    for &left_family in left {
        for &right_family in right {
            let bonus = family_pair_bonus(left_family, right_family, intent);
            if bonus > best {
                best = bonus;
            }
        }
    }
    best
}

fn family_pair_bonus(
    left: GenericWitnessSurfaceFamily,
    right: GenericWitnessSurfaceFamily,
    intent: &HybridRankingIntent,
) -> u32 {
    use GenericWitnessSurfaceFamily::{
        BuildConfig, Entrypoint, PackageSurface, Runtime, Tests, WorkspaceConfig,
    };

    if left == right && left == Tests {
        return 0;
    }
    if left == right {
        return 140;
    }

    match (left, right) {
        (Runtime, Tests) | (Tests, Runtime)
            if intent.wants_runtime_witnesses
                || intent.wants_tests
                || intent.wants_test_witness_recall =>
        {
            120
        }
        (Runtime, Entrypoint) | (Entrypoint, Runtime)
            if intent.wants_runtime_witnesses || intent.wants_entrypoint_build_flow =>
        {
            110
        }
        (Runtime, PackageSurface)
        | (PackageSurface, Runtime)
        | (Runtime, BuildConfig)
        | (BuildConfig, Runtime)
        | (Runtime, WorkspaceConfig)
        | (WorkspaceConfig, Runtime)
            if intent.wants_runtime_witnesses || intent.wants_runtime_config_artifacts =>
        {
            100
        }
        (Entrypoint, BuildConfig)
        | (BuildConfig, Entrypoint)
        | (Entrypoint, WorkspaceConfig)
        | (WorkspaceConfig, Entrypoint)
        | (Entrypoint, PackageSurface)
        | (PackageSurface, Entrypoint)
            if intent.wants_entrypoint_build_flow || intent.wants_runtime_config_artifacts =>
        {
            95
        }
        (PackageSurface, BuildConfig)
        | (BuildConfig, PackageSurface)
        | (PackageSurface, WorkspaceConfig)
        | (WorkspaceConfig, PackageSurface)
        | (BuildConfig, WorkspaceConfig)
        | (WorkspaceConfig, BuildConfig)
            if intent.wants_runtime_config_artifacts =>
        {
            90
        }
        (Tests, Entrypoint) | (Entrypoint, Tests)
            if intent.wants_test_witness_recall || intent.wants_entrypoint_build_flow =>
        {
            85
        }
        _ => 0,
    }
}

fn subtree_roots_related(left: &str, right: &str) -> bool {
    left == right
        || left
            .strip_prefix(right)
            .is_some_and(|suffix| suffix.starts_with('/'))
        || right
            .strip_prefix(left)
            .is_some_and(|suffix| suffix.starts_with('/'))
}

#[derive(Debug, Clone, Copy, Default)]
struct SurfaceTermMatch {
    total: u32,
    weighted_overlap: u32,
    shared_overlap: u32,
    exact_term_match: bool,
}

fn surface_term_match_score(
    projection: Option<&PathSurfaceTermProjection>,
    shared_terms: &[String],
    query_context: &HybridPathWitnessQueryContext,
) -> SurfaceTermMatch {
    let Some(projection) = projection else {
        return SurfaceTermMatch::default();
    };
    let weighted_overlap = query_context
        .query_overlap_terms
        .iter()
        .filter_map(|term| projection.term_weights.get(term))
        .map(|weight| *weight as u32)
        .sum::<u32>()
        .min(20);
    let shared_overlap =
        hybrid_overlap_count(shared_terms, &query_context.query_overlap_terms).min(4) as u32;
    let exact_term_match = query_context.exact_terms.iter().any(|term| {
        projection
            .exact_terms
            .iter()
            .any(|candidate| candidate == term)
    });
    let total = weighted_overlap
        .saturating_add(shared_overlap.saturating_mul(3))
        .saturating_add(u32::from(exact_term_match).saturating_mul(6));
    SurfaceTermMatch {
        total,
        weighted_overlap,
        shared_overlap,
        exact_term_match,
    }
}

fn relation_kind_bonus(relation: &PathRelationProjection, intent: &HybridRankingIntent) -> u32 {
    match relation.relation_kind.as_str() {
        "companion_surface" if wants_companion_surface_overlay(intent) => 220,
        "entrypoint_workflow" if intent.wants_entrypoint_build_flow => 240,
        "entrypoint_config"
            if intent.wants_runtime_config_artifacts || intent.wants_entrypoint_build_flow =>
        {
            230
        }
        "entrypoint_package"
            if intent.wants_runtime_config_artifacts || intent.wants_runtime_witnesses =>
        {
            215
        }
        "entrypoint_workspace"
            if intent.wants_runtime_config_artifacts || intent.wants_entrypoint_build_flow =>
        {
            205
        }
        "test_subject" if intent.wants_tests || intent.wants_test_witness_recall => 200,
        _ => {
            let left = generic_surface_families_from_bits(relation.src_family_bits);
            let right = generic_surface_families_from_bits(relation.dst_family_bits);
            companion_surface_family_bonus(&left, &right, intent)
        }
    }
}

fn relation_overlay_boost(
    relation: &PathRelationProjection,
    target_path: &str,
    direction: &str,
    surface_match: SurfaceTermMatch,
    relation_bonus: u32,
) -> PathOverlayBoost {
    let bonus_millis = relation_bonus
        .saturating_add(surface_match.weighted_overlap.saturating_mul(20))
        .saturating_add(surface_match.shared_overlap.saturating_mul(35))
        .saturating_add(u32::from(surface_match.exact_term_match).saturating_mul(90))
        .saturating_add((relation.score_hint.min(32) as u32).saturating_mul(4));
    let mut provenance_ids = vec![format!(
        "overlay:path_relation:{}:{}:{}",
        relation.relation_kind, direction, target_path
    )];
    if surface_match.exact_term_match {
        provenance_ids.push(format!(
            "overlay:path_relation:exact:{}:{}",
            relation.src_path, relation.dst_path
        ));
    }
    if surface_match.shared_overlap > 0 {
        provenance_ids.push(format!(
            "overlay:path_relation:shared:{}:{}",
            relation.src_path, relation.dst_path
        ));
    }
    PathOverlayBoost {
        bonus_millis,
        provenance_ids,
    }
}

fn encode_test_subject_projection_record(
    projection: &StoredTestSubjectProjection,
) -> FriggResult<TestSubjectProjection> {
    let flags_json = serde_json::to_string(&projection.flags).map_err(|err| {
        FriggError::Internal(format!(
            "failed to encode test subject projection flags for '{} -> {}': {err}",
            projection.test_path, projection.subject_path
        ))
    })?;

    Ok(TestSubjectProjection {
        test_path: projection.test_path.clone(),
        subject_path: projection.subject_path.clone(),
        shared_terms: projection.shared_terms.clone(),
        score_hint: projection.score_hint,
        flags_json,
    })
}

fn build_test_subject_projection_from_candidates(
    test_candidate: &TestSubjectCandidate,
    subject_candidate: &TestSubjectCandidate,
) -> Option<StoredTestSubjectProjection> {
    if test_candidate.path == subject_candidate.path {
        return None;
    }
    if !test_candidate.is_testish || !subject_candidate.is_subjectish {
        return None;
    }
    if !test_subject_languages_compatible(test_candidate.language, subject_candidate.language) {
        return None;
    }

    let shared_terms = test_candidate
        .subject_terms
        .iter()
        .filter(|term| subject_candidate.subject_terms.contains(*term))
        .cloned()
        .collect::<Vec<_>>();
    if shared_terms.is_empty() {
        return None;
    }

    let exact_stem_match = test_candidate.file_stem_subject == subject_candidate.file_stem_subject
        && !test_candidate.file_stem_subject.is_empty();
    let same_language =
        test_candidate.language == subject_candidate.language && test_candidate.language.is_some();
    let runtime_subject = subject_candidate.path_class == PathClass::Runtime;
    let support_subject = subject_candidate.path_class == PathClass::Support;
    let score_hint = shared_terms.len().saturating_mul(10)
        + usize::from(exact_stem_match).saturating_mul(6)
        + usize::from(same_language).saturating_mul(3)
        + usize::from(runtime_subject).saturating_mul(2)
        + usize::from(support_subject);

    Some(StoredTestSubjectProjection {
        test_path: test_candidate.path.clone(),
        subject_path: subject_candidate.path.clone(),
        shared_terms,
        score_hint,
        flags: TestSubjectProjectionFlags {
            exact_stem_match,
            same_language,
            runtime_subject,
            support_subject,
        },
    })
}

fn test_subject_projection_order(
    left: &StoredTestSubjectProjection,
    right: &StoredTestSubjectProjection,
) -> std::cmp::Ordering {
    right
        .score_hint
        .cmp(&left.score_hint)
        .then_with(|| left.test_path.cmp(&right.test_path))
        .then_with(|| left.subject_path.cmp(&right.subject_path))
}

#[derive(Debug, Clone)]
struct TestSubjectCandidate {
    path: String,
    path_class: PathClass,
    language: Option<SymbolLanguage>,
    subject_terms: BTreeSet<String>,
    file_stem_subject: String,
    is_testish: bool,
    is_subjectish: bool,
}

impl TestSubjectCandidate {
    fn from_path(path: &str) -> Self {
        let path_class = classify_repository_path(path);
        let source_class = hybrid_source_class(path);
        let language = SymbolLanguage::from_path(Path::new(path));
        let subject_terms = normalized_test_subject_terms(path);
        let file_stem_subject = normalized_subject_stem(path);
        let is_testish = matches!(source_class, SourceClass::Tests)
            || is_test_support_path(path)
            || is_test_harness_path(path)
            || path_contains_test_signal(path);
        let is_subjectish = !is_testish
            && !matches!(
                source_class,
                SourceClass::Documentation
                    | SourceClass::Readme
                    | SourceClass::Fixtures
                    | SourceClass::BenchmarkDocs
                    | SourceClass::Playbooks
                    | SourceClass::Specs
                    | SourceClass::Other
            );

        Self {
            path: path.to_owned(),
            path_class,
            language,
            subject_terms,
            file_stem_subject,
            is_testish,
            is_subjectish,
        }
    }
}

impl StoredEntrypointSurfaceProjection {
    pub(in crate::searcher) fn from_path(path: &str) -> Option<Self> {
        let flags = build_entrypoint_surface_projection_flags(path);
        if !flags.is_runtime_entrypoint
            && !flags.is_build_workflow
            && !flags.is_runtime_config_artifact
            && !flags.is_ci_workflow
            && !flags.is_scripts_ops
        {
            return None;
        }

        Some(Self {
            path: path.to_owned(),
            path_class: classify_repository_path(path),
            source_class: hybrid_source_class(path),
            path_terms: hybrid_path_overlap_tokens(path),
            surface_terms: build_entrypoint_surface_terms(path, &flags),
            flags,
        })
    }
}

fn build_entrypoint_surface_projection_flags(path: &str) -> EntrypointSurfaceProjectionFlags {
    EntrypointSurfaceProjectionFlags {
        is_runtime_entrypoint: is_entrypoint_runtime_path(path),
        is_build_workflow: is_entrypoint_build_workflow_path(path),
        is_runtime_config_artifact: is_runtime_config_artifact_path(path),
        is_ci_workflow: is_ci_workflow_path(path),
        is_scripts_ops: is_scripts_ops_path(path),
    }
}

fn build_entrypoint_surface_terms(
    path: &str,
    flags: &EntrypointSurfaceProjectionFlags,
) -> Vec<String> {
    let mut terms = hybrid_path_overlap_tokens(path)
        .into_iter()
        .collect::<BTreeSet<_>>();
    if flags.is_runtime_entrypoint {
        terms.extend(
            [
                "entrypoint",
                "startup",
                "bootstrap",
                "runtime",
                "main",
                "app",
            ]
            .into_iter()
            .map(str::to_owned),
        );
    }
    if flags.is_build_workflow {
        terms.extend(
            ["build", "workflow", "compile", "pipeline"]
                .into_iter()
                .map(str::to_owned),
        );
    }
    if flags.is_runtime_config_artifact {
        terms.extend(
            ["config", "manifest", "runtime", "settings"]
                .into_iter()
                .map(str::to_owned),
        );
    }
    if flags.is_ci_workflow {
        terms.extend(
            ["ci", "workflow", "automation"]
                .into_iter()
                .map(str::to_owned),
        );
    }
    if flags.is_scripts_ops {
        terms.extend(
            ["script", "ops", "automation"]
                .into_iter()
                .map(str::to_owned),
        );
    }

    terms.into_iter().collect()
}

fn normalized_test_subject_terms(path: &str) -> BTreeSet<String> {
    let mut terms = BTreeSet::new();
    for token in hybrid_path_overlap_tokens(path) {
        if !is_low_signal_test_subject_term(&token) {
            terms.insert(token);
        }
    }
    for token in hybrid_identifier_tokens(&normalized_subject_stem(path)) {
        if !is_low_signal_test_subject_term(&token) {
            terms.insert(token);
        }
    }
    terms
}

fn normalized_subject_stem(path: &str) -> String {
    let stem = Path::new(path)
        .file_stem()
        .and_then(|stem| stem.to_str())
        .unwrap_or_default()
        .to_ascii_lowercase();
    let mut normalized = stem.clone();
    for prefix in [
        "test_",
        "tests_",
        "spec_",
        "specs_",
        "integration_",
        "unit_",
        "e2e_",
    ] {
        if let Some(stripped) = normalized.strip_prefix(prefix) {
            normalized = stripped.to_owned();
            break;
        }
    }
    for suffix in [
        "_test",
        "_tests",
        "_spec",
        "_specs",
        "_integration",
        "_unit",
        "_e2e",
        "_suite",
        "_case",
    ] {
        if let Some(stripped) = normalized.strip_suffix(suffix) {
            normalized = stripped.to_owned();
            break;
        }
    }
    normalized
}

fn is_low_signal_test_subject_term(term: &str) -> bool {
    matches!(
        term,
        "test"
            | "tests"
            | "spec"
            | "specs"
            | "integration"
            | "unit"
            | "e2e"
            | "fixture"
            | "fixtures"
            | "helper"
            | "helpers"
            | "harness"
            | "support"
            | "case"
            | "cases"
            | "suite"
            | "suites"
            | "snapshot"
            | "smoke"
            | "bench"
            | "benches"
            | "benchmark"
    )
}

fn path_contains_test_signal(path: &str) -> bool {
    path.split('/').any(|segment| {
        matches!(
            segment.to_ascii_lowercase().as_str(),
            "test" | "tests" | "spec" | "specs"
        )
    })
}

fn test_subject_languages_compatible(
    test_language: Option<SymbolLanguage>,
    subject_language: Option<SymbolLanguage>,
) -> bool {
    match (test_language, subject_language) {
        (Some(SymbolLanguage::Php), Some(SymbolLanguage::Blade))
        | (Some(SymbolLanguage::Blade), Some(SymbolLanguage::Php)) => true,
        (Some(left), Some(right)) => left == right,
        _ => true,
    }
}

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

    #[test]
    fn test_subject_projection_pairs_generic_test_and_runtime_subject_paths() {
        let rows = build_test_subject_projection_records(&[
            "tests/unit/user_service_test.rs".to_owned(),
            "src/user_service.rs".to_owned(),
            "src/other.rs".to_owned(),
        ])
        .expect("projection rows should build");

        assert_eq!(rows.len(), 1);
        assert_eq!(rows[0].test_path, "tests/unit/user_service_test.rs");
        assert_eq!(rows[0].subject_path, "src/user_service.rs");
        assert!(rows[0].score_hint > 0);
    }

    #[test]
    fn entrypoint_surface_projection_marks_runtime_and_build_artifacts() {
        let runtime = StoredEntrypointSurfaceProjection::from_path("src/main.rs")
            .expect("runtime entrypoint should project");
        assert!(runtime.flags.is_runtime_entrypoint);
        assert!(
            runtime
                .surface_terms
                .iter()
                .any(|term| term == "entrypoint")
        );

        let workflow = StoredEntrypointSurfaceProjection::from_path(".github/workflows/ci.yml")
            .expect("workflow surface should project");
        assert!(workflow.flags.is_build_workflow || workflow.flags.is_ci_workflow);
        assert!(workflow.surface_terms.iter().any(|term| term == "workflow"));
    }

    #[test]
    fn decode_test_subject_projection_record_recomputes_live_terms_and_flags() {
        let record = TestSubjectProjection {
            test_path: "tests/unit/user_service_test.rs".to_owned(),
            subject_path: "src/user_service.rs".to_owned(),
            shared_terms: vec!["stale".to_owned()],
            score_hint: 1,
            flags_json: serde_json::to_string(&TestSubjectProjectionFlags::default())
                .expect("flags json"),
        };

        let decoded =
            decode_test_subject_projection_record(&record).expect("decode should succeed");

        assert!(
            decoded.shared_terms.iter().any(|term| term == "user"),
            "live decode should recover current shared subject terms: {:?}",
            decoded.shared_terms
        );
        assert!(decoded.flags.exact_stem_match);
    }

    #[test]
    fn decode_entrypoint_surface_projection_record_recomputes_live_surface_terms() {
        let record = EntrypointSurfaceProjection {
            path: "Cargo.toml".to_owned(),
            path_class: PathClass::Project,
            source_class: SourceClass::Project,
            path_terms: Vec::new(),
            surface_terms: Vec::new(),
            flags_json: serde_json::to_string(&EntrypointSurfaceProjectionFlags::default())
                .expect("flags json"),
        };

        let decoded = decode_entrypoint_surface_projection_record(&record)
            .expect("decode should succeed for runtime config artifacts");

        assert!(decoded.flags.is_runtime_config_artifact);
        assert!(
            decoded.surface_terms.iter().any(|term| term == "config"),
            "live decode should recover generic config surface terms"
        );
    }

    #[test]
    fn entrypoint_surface_overlay_boost_rewards_matching_runtime_config_surfaces() {
        let projection = StoredEntrypointSurfaceProjection::from_path("Cargo.toml")
            .expect("runtime config artifact should project");
        let intent = HybridRankingIntent::from_query("runtime config manifest settings");
        let query_context =
            HybridPathWitnessQueryContext::from_query_text("runtime config manifest settings");

        let boost = entrypoint_surface_overlay_boost(&projection, &intent, &query_context)
            .expect("matching config query should produce an overlay boost");

        assert!(boost.bonus_millis >= 500);
        assert!(
            boost
                .provenance_ids
                .iter()
                .any(|id| id.starts_with("overlay:entrypoint_surface:config:"))
        );
    }

    #[test]
    fn test_subject_overlay_boosts_promote_related_tests_and_subjects() {
        let projections = vec![StoredTestSubjectProjection {
            test_path: "tests/unit/user_service_test.rs".to_owned(),
            subject_path: "src/user_service.rs".to_owned(),
            shared_terms: vec!["user".to_owned(), "service".to_owned()],
            score_hint: 23,
            flags: TestSubjectProjectionFlags {
                exact_stem_match: true,
                same_language: true,
                runtime_subject: true,
                support_subject: false,
            },
        }];
        let intent = HybridRankingIntent::from_query("user service tests");
        let query_context = HybridPathWitnessQueryContext::from_query_text("user service tests");

        let boosts = accumulate_test_subject_overlay_boosts(&projections, &intent, &query_context);

        assert!(
            boosts
                .get("tests/unit/user_service_test.rs")
                .is_some_and(|boost| boost.bonus_millis > 0)
        );
        assert!(
            boosts
                .get("src/user_service.rs")
                .is_some_and(|boost| boost.bonus_millis > 0)
        );
    }

    #[test]
    fn companion_surface_overlay_boosts_promote_same_subtree_runtime_and_config_surfaces() {
        let projections = vec![
            StoredPathWitnessProjection::from_path("packages/editor-ui/src/main.ts"),
            StoredPathWitnessProjection::from_path("packages/editor-ui/package.json"),
            StoredPathWitnessProjection::from_path("packages/editor-ui/tsconfig.base.json"),
            StoredPathWitnessProjection::from_path("packages/worker/src/main.ts"),
        ];
        let intent = HybridRankingIntent::from_query("editor ui runtime config main tsconfig");
        let query_context = HybridPathWitnessQueryContext::from_query_text(
            "editor ui runtime config main tsconfig",
        );

        let boosts =
            accumulate_companion_surface_overlay_boosts(&projections, &intent, &query_context);

        assert!(
            boosts
                .get("packages/editor-ui/package.json")
                .is_some_and(|boost| boost.bonus_millis > 0),
            "same-subtree package surface should receive a companion boost"
        );
        assert!(
            boosts
                .get("packages/editor-ui/tsconfig.base.json")
                .is_some_and(|boost| boost.bonus_millis > 0),
            "same-subtree workspace config should receive a companion boost"
        );
        assert!(
            boosts
                .get("packages/worker/src/main.ts")
                .is_none_or(|boost| boost.bonus_millis == 0),
            "sibling workspace runtime should not inherit an editor-ui companion boost"
        );
    }
}
