//! Semantic chunk construction and embedding provider bridge.
//!
//! Turns manifest entries into bounded text chunks, assigns deterministic chunk ids, and batches
//! embedding requests through the configured semantic runtime.

use std::fs::File;
use std::future::Future;
use std::io::Read;
use std::pin::Pin;
use std::sync::Arc;

use rayon::prelude::*;
use tracing::warn;

use super::*;
use crate::embeddings::{
    LocalArtifactPolicy, SemanticEmbeddingProviderFactoryConfig,
    cached_semantic_embedding_provider, provider_factory::canonical_provider_model,
};
use crate::indexer::manifest::normalize_repository_relative_path;
use crate::settings::{SemanticRuntimeConfig, SemanticRuntimeCredentials, SemanticRuntimeProvider};
use crate::storage::{
    DEFAULT_VECTOR_DIMENSIONS, ManifestEntry, SemanticChunkEmbeddingRecord, SemanticHeadRecord,
    Storage, StorageSession,
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SemanticChunkCandidate {
    pub(crate) chunk_id: blake3::Hash,
    pub(crate) repository_id: Arc<str>,
    pub(crate) snapshot_id: Arc<str>,
    pub(crate) path: Arc<str>,
    pub(crate) language: Arc<str>,
    pub(crate) chunk_index: usize,
    pub(crate) start_line: usize,
    pub(crate) end_line: usize,
    pub(crate) content_hash_blake3: blake3::Hash,
    pub(crate) content_text: String,
}

impl SemanticChunkCandidate {
    fn chunk_id_string(&self) -> String {
        semantic_chunk_id_string(&self.chunk_id)
    }

    fn content_hash_blake3_string(&self) -> String {
        self.content_hash_blake3.to_hex().to_string()
    }
}

pub(super) trait SemanticRuntimeEmbeddingExecutor: Sync {
    #[allow(clippy::type_complexity)]
    fn embed_documents<'a>(
        &'a self,
        provider: SemanticRuntimeProvider,
        model: &'a str,
        input: Vec<String>,
        trace_id: Option<String>,
    ) -> Pin<Box<dyn Future<Output = FriggResult<Vec<Vec<f32>>>> + Send + 'a>>;
}

pub(super) trait SemanticIndexStorage {
    fn load_semantic_head_for_repository_model(
        &self,
        repository_id: &str,
        provider: &str,
        model: &str,
    ) -> FriggResult<Option<SemanticHeadRecord>>;

    fn load_manifest_for_snapshot(&self, snapshot_id: &str) -> FriggResult<Vec<ManifestEntry>>;

    fn load_semantic_embeddings_for_repository_model_chunk_ids(
        &self,
        repository_id: &str,
        provider: &str,
        model: &str,
        chunk_ids: &[String],
    ) -> FriggResult<std::collections::BTreeMap<String, SemanticChunkEmbeddingRecord>>;
}

impl SemanticIndexStorage for Storage {
    fn load_semantic_head_for_repository_model(
        &self,
        repository_id: &str,
        provider: &str,
        model: &str,
    ) -> FriggResult<Option<SemanticHeadRecord>> {
        Storage::load_semantic_head_for_repository_model(self, repository_id, provider, model)
    }

    fn load_manifest_for_snapshot(&self, snapshot_id: &str) -> FriggResult<Vec<ManifestEntry>> {
        Storage::load_manifest_for_snapshot(self, snapshot_id)
    }

    fn load_semantic_embeddings_for_repository_model_chunk_ids(
        &self,
        repository_id: &str,
        provider: &str,
        model: &str,
        chunk_ids: &[String],
    ) -> FriggResult<std::collections::BTreeMap<String, SemanticChunkEmbeddingRecord>> {
        Storage::load_semantic_embeddings_for_repository_model_chunk_ids(
            self,
            repository_id,
            provider,
            model,
            chunk_ids,
        )
    }
}

impl SemanticIndexStorage for StorageSession {
    fn load_semantic_head_for_repository_model(
        &self,
        repository_id: &str,
        provider: &str,
        model: &str,
    ) -> FriggResult<Option<SemanticHeadRecord>> {
        StorageSession::load_semantic_head_for_repository_model(
            self,
            repository_id,
            provider,
            model,
        )
    }

    fn load_manifest_for_snapshot(&self, snapshot_id: &str) -> FriggResult<Vec<ManifestEntry>> {
        StorageSession::load_manifest_for_snapshot(self, snapshot_id)
    }

    fn load_semantic_embeddings_for_repository_model_chunk_ids(
        &self,
        repository_id: &str,
        provider: &str,
        model: &str,
        chunk_ids: &[String],
    ) -> FriggResult<std::collections::BTreeMap<String, SemanticChunkEmbeddingRecord>> {
        StorageSession::load_semantic_embeddings_for_repository_model_chunk_ids(
            self,
            repository_id,
            provider,
            model,
            chunk_ids,
        )
    }
}

fn build_semantic_embedding_runtime() -> FriggResult<tokio::runtime::Runtime> {
    tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .map_err(|err| {
            FriggError::Internal(format!(
                "failed to build tokio runtime for semantic embedding requests: {err}"
            ))
        })
}

fn execute_semantic_embedding_batch(
    runtime: &tokio::runtime::Runtime,
    executor: &dyn SemanticRuntimeEmbeddingExecutor,
    provider: SemanticRuntimeProvider,
    model: &str,
    input: Vec<String>,
    trace_id: Option<String>,
) -> FriggResult<Vec<Vec<f32>>> {
    runtime.block_on(executor.embed_documents(provider, model, input, trace_id))
}

#[derive(Debug, Default)]
pub(super) struct RuntimeSemanticEmbeddingExecutor {
    credentials: SemanticRuntimeCredentials,
}

impl RuntimeSemanticEmbeddingExecutor {
    pub(super) fn new(credentials: SemanticRuntimeCredentials) -> Self {
        Self { credentials }
    }
}

impl SemanticRuntimeEmbeddingExecutor for RuntimeSemanticEmbeddingExecutor {
    fn embed_documents<'a>(
        &'a self,
        provider: SemanticRuntimeProvider,
        model: &'a str,
        input: Vec<String>,
        trace_id: Option<String>,
    ) -> Pin<Box<dyn Future<Output = FriggResult<Vec<Vec<f32>>>> + Send + 'a>> {
        let model = model.trim().to_owned();
        Box::pin(async move {
            let request = EmbeddingRequest {
                model,
                input,
                purpose: EmbeddingPurpose::Document,
                dimensions: Some(DEFAULT_VECTOR_DIMENSIONS),
                trace_id,
            };
            let client =
                cached_semantic_embedding_provider(SemanticEmbeddingProviderFactoryConfig {
                    provider,
                    model: &request.model,
                    credentials: &self.credentials,
                    local_artifact_policy: LocalArtifactPolicy::AllowPreparation,
                })
                .map_err(|err| {
                    FriggError::Internal(format!(
                        "semantic embedding provider construction failed: {err}"
                    ))
                })?;
            let response = client.embed(request).await.map_err(|err| {
                FriggError::Internal(format!("semantic embedding provider call failed: {err}"))
            })?;

            Ok(response
                .vectors
                .into_iter()
                .map(|vector| vector.values)
                .collect::<Vec<_>>())
        })
    }
}

pub(super) fn resolve_semantic_runtime_config_from_env() -> FriggResult<SemanticRuntimeConfig> {
    let enabled = parse_optional_bool_env(FRIGG_SEMANTIC_RUNTIME_ENABLED_ENV)?.unwrap_or(false);
    if !enabled {
        return Ok(SemanticRuntimeConfig::default());
    }
    let strict_mode =
        parse_optional_bool_env(FRIGG_SEMANTIC_RUNTIME_STRICT_MODE_ENV)?.unwrap_or(false);
    let provider = std::env::var(FRIGG_SEMANTIC_RUNTIME_PROVIDER_ENV)
        .ok()
        .map(|raw| {
            SemanticRuntimeProvider::from_str(raw.trim()).map_err(|message| {
                FriggError::InvalidInput(format!(
                    "invalid {} value: {message}",
                    FRIGG_SEMANTIC_RUNTIME_PROVIDER_ENV
                ))
            })
        })
        .transpose()?
        .or(Some(SemanticRuntimeProvider::Local));
    let model = std::env::var(FRIGG_SEMANTIC_RUNTIME_MODEL_ENV)
        .ok()
        .map(|raw| raw.trim().to_owned());

    Ok(SemanticRuntimeConfig {
        enabled,
        provider,
        model,
        strict_mode,
    })
}

fn parse_optional_bool_env(name: &str) -> FriggResult<Option<bool>> {
    let Some(raw) = std::env::var(name).ok() else {
        return Ok(None);
    };
    let normalized = raw.trim().to_ascii_lowercase();
    let value = match normalized.as_str() {
        "1" | "true" => true,
        "0" | "false" => false,
        _ => {
            return Err(FriggError::InvalidInput(format!(
                "{name} must be one of: true,false,1,0 (received: {normalized})"
            )));
        }
    };
    Ok(Some(value))
}

#[allow(clippy::too_many_arguments)]
pub(super) fn build_semantic_embedding_records(
    repository_id: &str,
    workspace_root: &Path,
    snapshot_id: &str,
    current_manifest: &[FileDigest],
    semantic_runtime: &SemanticRuntimeConfig,
    credentials: &SemanticRuntimeCredentials,
    executor: &dyn SemanticRuntimeEmbeddingExecutor,
    storage: Option<&dyn SemanticIndexStorage>,
) -> FriggResult<SemanticEmbeddingBuild> {
    semantic_runtime
        .validate_startup(credentials)
        .map_err(|err| {
            FriggError::InvalidInput(format!(
                "semantic runtime validation failed code={}: {err}",
                err.code()
            ))
        })?;

    let provider = semantic_runtime.provider.ok_or_else(|| {
        FriggError::Internal("semantic runtime provider missing after validation".to_owned())
    })?;
    let model = semantic_runtime.normalized_model().ok_or_else(|| {
        FriggError::Internal("semantic runtime model missing after validation".to_owned())
    })?;
    let model = canonical_provider_model(provider, model).map_err(|err| {
        FriggError::InvalidInput(format!("semantic runtime model validation failed: {err}"))
    })?;
    let model = model.as_str();
    let SemanticChunkBuild { candidates: chunks } = build_semantic_chunk_candidates(
        repository_id,
        workspace_root,
        snapshot_id,
        current_manifest,
    )?;

    if chunks.is_empty() {
        return Ok(SemanticEmbeddingBuild {
            records: Vec::new(),
        });
    }

    let (mut records, chunks_to_embed) = reuse_existing_semantic_embedding_records(
        repository_id,
        snapshot_id,
        provider,
        model,
        &chunks,
        storage,
    )?;
    let trace_id = deterministic_semantic_trace_id(repository_id, snapshot_id, provider, model);
    if !chunks_to_embed.is_empty() {
        records.extend(execute_semantic_embedding_batches(
            provider,
            model,
            &chunks_to_embed,
            &trace_id,
            executor,
        )?);
    }
    sort_semantic_embedding_records(&mut records);
    Ok(SemanticEmbeddingBuild { records })
}

fn reuse_existing_semantic_embedding_records(
    repository_id: &str,
    snapshot_id: &str,
    provider: SemanticRuntimeProvider,
    model: &str,
    chunks: &[SemanticChunkCandidate],
    storage: Option<&dyn SemanticIndexStorage>,
) -> FriggResult<(
    Vec<SemanticChunkEmbeddingRecord>,
    Vec<SemanticChunkCandidate>,
)> {
    let Some(storage) = storage else {
        return Ok((Vec::new(), chunks.to_vec()));
    };
    let chunk_ids = chunks
        .iter()
        .map(SemanticChunkCandidate::chunk_id_string)
        .collect::<Vec<_>>();
    let existing_records = storage.load_semantic_embeddings_for_repository_model_chunk_ids(
        repository_id,
        provider.as_str(),
        model,
        &chunk_ids,
    )?;
    let mut reused_records = Vec::new();
    let mut chunks_to_embed = Vec::new();
    for chunk in chunks {
        let chunk_id = chunk.chunk_id_string();
        let Some(existing_record) = existing_records.get(&chunk_id) else {
            chunks_to_embed.push(chunk.clone());
            continue;
        };
        if reusable_semantic_embedding_record(
            chunk,
            existing_record,
            repository_id,
            provider,
            model,
        ) {
            reused_records.push(rewrite_reused_semantic_embedding_record(
                chunk,
                existing_record,
                snapshot_id,
            ));
        } else {
            chunks_to_embed.push(chunk.clone());
        }
    }

    Ok((reused_records, chunks_to_embed))
}

fn reusable_semantic_embedding_record(
    chunk: &SemanticChunkCandidate,
    record: &SemanticChunkEmbeddingRecord,
    repository_id: &str,
    provider: SemanticRuntimeProvider,
    model: &str,
) -> bool {
    record.repository_id == repository_id
        && record.provider == provider.as_str()
        && record.model == model
        && record.chunk_id == chunk.chunk_id_string()
        && record.content_hash_blake3 == chunk.content_hash_blake3_string()
        && !record.embedding.is_empty()
        && record.embedding.iter().all(|value| value.is_finite())
}

fn rewrite_reused_semantic_embedding_record(
    chunk: &SemanticChunkCandidate,
    record: &SemanticChunkEmbeddingRecord,
    snapshot_id: &str,
) -> SemanticChunkEmbeddingRecord {
    SemanticChunkEmbeddingRecord {
        chunk_id: chunk.chunk_id_string(),
        repository_id: chunk.repository_id.to_string(),
        snapshot_id: snapshot_id.to_owned(),
        path: chunk.path.to_string(),
        language: chunk.language.to_string(),
        chunk_index: chunk.chunk_index,
        start_line: chunk.start_line,
        end_line: chunk.end_line,
        provider: record.provider.clone(),
        model: record.model.clone(),
        trace_id: record.trace_id.clone(),
        content_hash_blake3: chunk.content_hash_blake3_string(),
        content_text: chunk.content_text.clone(),
        embedding: record.embedding.clone(),
    }
}

fn sort_semantic_embedding_records(records: &mut [SemanticChunkEmbeddingRecord]) {
    records.sort_by(|left, right| {
        left.path
            .cmp(&right.path)
            .then(left.chunk_index.cmp(&right.chunk_index))
            .then(left.chunk_id.as_bytes().cmp(right.chunk_id.as_bytes()))
    });
}

fn execute_semantic_embedding_batches(
    provider: SemanticRuntimeProvider,
    model: &str,
    chunks: &[SemanticChunkCandidate],
    trace_id: &str,
    executor: &dyn SemanticRuntimeEmbeddingExecutor,
) -> FriggResult<Vec<SemanticChunkEmbeddingRecord>> {
    if tokio::runtime::Handle::try_current().is_ok() {
        return std::thread::scope(|scope| {
            let handle = scope.spawn(|| {
                let runtime = build_semantic_embedding_runtime()?;
                build_semantic_embedding_records_with_runtime(
                    provider, model, chunks, trace_id, executor, &runtime,
                )
            });
            match handle.join() {
                Ok(result) => result,
                Err(_) => Err(FriggError::Internal(
                    "semantic embedding provider thread panicked under an active tokio runtime"
                        .to_owned(),
                )),
            }
        });
    }

    let runtime = build_semantic_embedding_runtime()?;
    build_semantic_embedding_records_with_runtime(
        provider, model, chunks, trace_id, executor, &runtime,
    )
}

fn build_semantic_embedding_records_with_runtime(
    provider: SemanticRuntimeProvider,
    model: &str,
    chunks: &[SemanticChunkCandidate],
    trace_id: &str,
    executor: &dyn SemanticRuntimeEmbeddingExecutor,
    runtime: &tokio::runtime::Runtime,
) -> FriggResult<Vec<SemanticChunkEmbeddingRecord>> {
    let mut output = Vec::with_capacity(chunks.len());
    let total_batches = chunks.len().div_ceil(SEMANTIC_EMBEDDING_BATCH_SIZE);
    for (batch_index, batch) in chunks.chunks(SEMANTIC_EMBEDDING_BATCH_SIZE).enumerate() {
        let batch_input = batch
            .iter()
            .map(|chunk| chunk.content_text.clone())
            .collect::<Vec<_>>();
        let vectors = execute_semantic_embedding_batch(
            runtime,
            executor,
            provider,
            model,
            batch_input,
            Some(trace_id.to_owned()),
        )
        .map_err(|error| {
            let first_anchor = batch
                .first()
                .map(|chunk| format!("{}:{}-{}", chunk.path, chunk.start_line, chunk.end_line))
                .unwrap_or_else(|| "-".to_owned());
            let last_anchor = batch
                .last()
                .map(|chunk| format!("{}:{}-{}", chunk.path, chunk.start_line, chunk.end_line))
                .unwrap_or_else(|| "-".to_owned());
            FriggError::Internal(format!(
                "semantic embedding batch failed batch_index={} total_batches={} batch_size={} first_chunk={} last_chunk={}: {}",
                batch_index,
                total_batches,
                batch.len(),
                first_anchor,
                last_anchor,
                error
            ))
        })?;
        if vectors.len() != batch.len() {
            return Err(FriggError::Internal(format!(
                "semantic embedding provider response length mismatch: expected {} vectors, received {}",
                batch.len(),
                vectors.len()
            )));
        }

        for (chunk, embedding) in batch.iter().zip(vectors) {
            if embedding.is_empty() {
                return Err(FriggError::Internal(format!(
                    "semantic embedding provider returned an empty vector for chunk_id={}",
                    chunk.chunk_id_string()
                )));
            }
            if embedding.iter().any(|value| !value.is_finite()) {
                return Err(FriggError::Internal(format!(
                    "semantic embedding provider returned non-finite vector values for chunk_id={}",
                    chunk.chunk_id_string()
                )));
            }

            output.push(SemanticChunkEmbeddingRecord {
                chunk_id: chunk.chunk_id_string(),
                repository_id: chunk.repository_id.to_string(),
                snapshot_id: chunk.snapshot_id.to_string(),
                path: chunk.path.to_string(),
                language: chunk.language.to_string(),
                chunk_index: chunk.chunk_index,
                start_line: chunk.start_line,
                end_line: chunk.end_line,
                provider: provider.as_str().to_owned(),
                model: model.to_owned(),
                trace_id: Some(trace_id.to_owned()),
                content_hash_blake3: chunk.content_hash_blake3_string(),
                content_text: chunk.content_text.clone(),
                embedding,
            });
        }
    }

    sort_semantic_embedding_records(&mut output);
    Ok(output)
}

pub(crate) struct SemanticEmbeddingBuild {
    pub(crate) records: Vec<SemanticChunkEmbeddingRecord>,
}

pub(crate) struct SemanticChunkBuild {
    pub(crate) candidates: Vec<SemanticChunkCandidate>,
}

pub(crate) fn build_semantic_chunk_candidates(
    repository_id: &str,
    workspace_root: &Path,
    snapshot_id: &str,
    current_manifest: &[FileDigest],
) -> FriggResult<SemanticChunkBuild> {
    let repository_id = Arc::<str>::from(repository_id);
    let snapshot_id = Arc::<str>::from(snapshot_id);
    let estimated_capacity =
        estimate_semantic_chunk_capacity(current_manifest).max(current_manifest.len());
    let mut output = current_manifest
        .par_iter()
        .map(|entry| {
            let Some(language) = semantic_chunk_language_for_path(&entry.path) else {
                return Ok::<Vec<SemanticChunkCandidate>, FriggError>(Vec::new());
            };
            let repository_relative_path =
                normalize_repository_relative_path(workspace_root, &entry.path)?;
            let mut source = String::new();
            let mut file = match File::open(&entry.path) {
                Ok(file) => file,
                Err(err) => {
                    warn!(
                        repository_id = %repository_id,
                        path = %entry.path.display(),
                        error = %err,
                        "skipping semantic chunking for file that failed to open"
                    );
                    return Ok::<Vec<SemanticChunkCandidate>, FriggError>(Vec::new());
                }
            };
            if let Err(err) = file.read_to_string(&mut source) {
                warn!(
                    repository_id = %repository_id,
                    path = %entry.path.display(),
                    error = %err,
                    "skipping semantic chunking for file that failed to read"
                );
                return Ok::<Vec<SemanticChunkCandidate>, FriggError>(Vec::new());
            }

            let mut chunks = Vec::new();
            append_file_semantic_chunks(
                &mut chunks,
                Arc::clone(&repository_id),
                Arc::clone(&snapshot_id),
                Arc::<str>::from(repository_relative_path.as_str()),
                language,
                source.as_str(),
            );
            Ok(chunks)
        })
        .try_reduce(
            || Vec::with_capacity(estimated_capacity),
            |mut left, mut right| {
                left.append(&mut right);
                Ok::<Vec<SemanticChunkCandidate>, FriggError>(left)
            },
        )?;

    output.sort_by(|left, right| {
        left.path
            .cmp(&right.path)
            .then(left.chunk_index.cmp(&right.chunk_index))
            .then(left.chunk_id.as_bytes().cmp(right.chunk_id.as_bytes()))
    });
    Ok(SemanticChunkBuild { candidates: output })
}

pub(crate) fn build_file_semantic_chunks(
    repository_id: impl Into<Arc<str>>,
    snapshot_id: impl Into<Arc<str>>,
    path: impl Into<Arc<str>>,
    language: impl Into<Arc<str>>,
    source: &str,
) -> Vec<SemanticChunkCandidate> {
    let file_context = SemanticChunkFileContext::new(
        repository_id.into(),
        snapshot_id.into(),
        path.into(),
        language.into(),
    );
    let estimated_chunks = source
        .len()
        .max(1)
        .div_ceil(SEMANTIC_CHUNK_MAX_CHARS.max(1));
    let mut chunks = Vec::with_capacity(estimated_chunks);
    append_file_semantic_chunks_with_context(&mut chunks, &file_context, source);
    chunks
}

fn append_file_semantic_chunks(
    output: &mut Vec<SemanticChunkCandidate>,
    repository_id: impl Into<Arc<str>>,
    snapshot_id: impl Into<Arc<str>>,
    path: impl Into<Arc<str>>,
    language: impl Into<Arc<str>>,
    source: &str,
) {
    let file_context = SemanticChunkFileContext::new(
        repository_id.into(),
        snapshot_id.into(),
        path.into(),
        language.into(),
    );
    append_file_semantic_chunks_with_context(output, &file_context, source);
}

fn append_file_semantic_chunks_with_context(
    output: &mut Vec<SemanticChunkCandidate>,
    file_context: &SemanticChunkFileContext,
    source: &str,
) {
    let markdown_chunking = file_context.language.as_ref() == "markdown";
    if let Some(single_chunk) =
        build_single_semantic_chunk_candidate_if_small(file_context, markdown_chunking, source)
    {
        output.extend(single_chunk);
        return;
    }

    let mut current_chunk_start = 0usize;
    let mut current_chars = 0usize;
    let mut start_line = 1usize;
    let mut chunk_index = 0usize;
    let mut current_line = 0usize;

    for (line_idx, raw_line) in source.split_inclusive('\n').enumerate() {
        let line = raw_line.trim_end_matches(['\n', '\r']);
        let line_number = line_idx + 1;
        current_line = line_number;
        let markdown_heading_boundary =
            markdown_chunking && current_chars > 0 && is_markdown_heading(line);
        let projected_chars = current_chars + line.len() + usize::from(current_chars > 0);
        let should_flush = markdown_heading_boundary
            || (current_chars > 0
                && (line_number.saturating_sub(start_line) >= SEMANTIC_CHUNK_MAX_LINES
                    || projected_chars > SEMANTIC_CHUNK_MAX_CHARS));

        if should_flush {
            let created = append_semantic_chunk_candidates(
                output,
                file_context,
                chunk_index,
                start_line,
                line_number.saturating_sub(1),
                semantic_chunk_text_from_source(
                    source,
                    current_chunk_start,
                    raw_line.as_ptr() as usize - source.as_ptr() as usize,
                ),
            );
            chunk_index += created.len();
            current_chars = 0;
            start_line = line_number;
            current_chunk_start = raw_line.as_ptr() as usize - source.as_ptr() as usize;
        }

        current_chars += line.len() + usize::from(current_chars > 0);
    }

    append_semantic_chunk_candidates(
        output,
        file_context,
        chunk_index,
        start_line,
        current_line.max(start_line),
        semantic_chunk_text_from_source(source, current_chunk_start, source.len()),
    );
}

fn build_single_semantic_chunk_candidate_if_small(
    file_context: &SemanticChunkFileContext,
    markdown_chunking: bool,
    source: &str,
) -> Option<Vec<SemanticChunkCandidate>> {
    if markdown_chunking || !source.is_ascii() || source.len() > SEMANTIC_CHUNK_MAX_CHARS {
        return None;
    }

    let content_text = semantic_chunk_text_from_source(source, 0, source.len());
    if content_text.trim().is_empty() {
        return Some(Vec::new());
    }

    let line_count = content_text
        .as_bytes()
        .iter()
        .filter(|byte| **byte == b'\n')
        .count()
        + 1;
    if line_count > SEMANTIC_CHUNK_MAX_LINES {
        return None;
    }

    Some(vec![build_semantic_chunk_candidate(
        file_context,
        0,
        1,
        line_count,
        content_text.to_owned(),
    )])
}

fn append_semantic_chunk_candidates(
    output: &mut Vec<SemanticChunkCandidate>,
    file_context: &SemanticChunkFileContext,
    chunk_index: usize,
    start_line: usize,
    end_line: usize,
    content_text: &str,
) -> std::ops::Range<usize> {
    if content_text.trim().is_empty() {
        return 0..0;
    }

    let output_start = output.len();
    if content_text.is_ascii() {
        let mut segment_start = 0usize;
        let mut segment_offset = 0usize;
        while segment_start < content_text.len() {
            let segment_end = (segment_start + SEMANTIC_CHUNK_MAX_CHARS).min(content_text.len());
            push_nonblank_semantic_chunk_candidate(
                output,
                file_context,
                chunk_index + segment_offset,
                start_line,
                end_line,
                &content_text[segment_start..segment_end],
            );
            segment_start = segment_end;
            segment_offset = output.len().saturating_sub(output_start);
        }
        return output_start..output.len();
    }

    let unicode_char_count = content_text.chars().count();
    if unicode_char_count <= SEMANTIC_CHUNK_MAX_CHARS {
        output.push(build_semantic_chunk_candidate(
            file_context,
            chunk_index,
            start_line,
            end_line,
            content_text.to_owned(),
        ));
        return output_start..output.len();
    }

    let mut segment_start = 0usize;
    let mut chars_in_segment = 0usize;
    let mut segment_offset = 0usize;
    for (byte_index, _) in content_text.char_indices() {
        if chars_in_segment == SEMANTIC_CHUNK_MAX_CHARS {
            push_nonblank_semantic_chunk_candidate(
                output,
                file_context,
                chunk_index + segment_offset,
                start_line,
                end_line,
                &content_text[segment_start..byte_index],
            );
            segment_start = byte_index;
            chars_in_segment = 0;
            segment_offset = output.len().saturating_sub(output_start);
        }
        chars_in_segment += 1;
    }
    if segment_start < content_text.len() {
        push_nonblank_semantic_chunk_candidate(
            output,
            file_context,
            chunk_index + segment_offset,
            start_line,
            end_line,
            &content_text[segment_start..],
        );
    }

    output_start..output.len()
}

fn push_nonblank_semantic_chunk_candidate(
    output: &mut Vec<SemanticChunkCandidate>,
    file_context: &SemanticChunkFileContext,
    chunk_index: usize,
    start_line: usize,
    end_line: usize,
    content_text: &str,
) {
    if content_text.trim().is_empty() {
        return;
    }
    output.push(build_semantic_chunk_candidate(
        file_context,
        chunk_index,
        start_line,
        end_line,
        content_text.to_owned(),
    ));
}

fn build_semantic_chunk_candidate(
    file_context: &SemanticChunkFileContext,
    chunk_index: usize,
    start_line: usize,
    end_line: usize,
    content_text: String,
) -> SemanticChunkCandidate {
    let content_hash = blake3::hash(content_text.as_bytes());

    let mut chunk_id_hasher = file_context.chunk_id_prefix.clone();
    chunk_id_hasher.update(&chunk_index.to_le_bytes());
    chunk_id_hasher.update(&[0]);
    chunk_id_hasher.update(&start_line.to_le_bytes());
    chunk_id_hasher.update(&[0]);
    chunk_id_hasher.update(&end_line.to_le_bytes());
    chunk_id_hasher.update(&[0]);
    chunk_id_hasher.update(content_hash.as_bytes());
    let chunk_id = chunk_id_hasher.finalize();

    SemanticChunkCandidate {
        chunk_id,
        repository_id: Arc::clone(&file_context.repository_id),
        snapshot_id: Arc::clone(&file_context.snapshot_id),
        path: Arc::clone(&file_context.path),
        language: Arc::clone(&file_context.language),
        chunk_index,
        start_line,
        end_line,
        content_hash_blake3: content_hash,
        content_text,
    }
}

fn semantic_chunk_text_from_source(source: &str, start: usize, end: usize) -> &str {
    source[start..end].trim_end_matches(['\n', '\r'])
}

fn semantic_chunk_id_string(chunk_id: &blake3::Hash) -> String {
    let chunk_id_hex = chunk_id.to_hex();
    let mut value = String::with_capacity("chunk-".len() + chunk_id_hex.as_str().len());
    value.push_str("chunk-");
    value.push_str(chunk_id_hex.as_str());
    value
}

fn estimate_semantic_chunk_capacity(current_manifest: &[FileDigest]) -> usize {
    current_manifest
        .iter()
        .filter(|entry| semantic_chunk_language_for_path(&entry.path).is_some())
        .map(|entry| {
            usize::try_from(entry.size_bytes)
                .unwrap_or(usize::MAX)
                .max(1)
                .div_ceil(SEMANTIC_CHUNK_MAX_CHARS.max(1))
        })
        .sum()
}

struct SemanticChunkFileContext {
    repository_id: Arc<str>,
    snapshot_id: Arc<str>,
    path: Arc<str>,
    language: Arc<str>,
    chunk_id_prefix: Hasher,
}

impl SemanticChunkFileContext {
    fn new(
        repository_id: Arc<str>,
        snapshot_id: Arc<str>,
        path: Arc<str>,
        language: Arc<str>,
    ) -> Self {
        let mut chunk_id_prefix = Hasher::new();
        chunk_id_prefix.update(repository_id.as_bytes());
        chunk_id_prefix.update(&[0]);
        chunk_id_prefix.update(path.as_bytes());
        chunk_id_prefix.update(&[0]);
        Self {
            repository_id,
            snapshot_id,
            path,
            language,
            chunk_id_prefix,
        }
    }
}

fn is_markdown_heading(line: &str) -> bool {
    let trimmed = line.trim_start();
    let mut heading_hashes = 0usize;
    for ch in trimmed.chars() {
        if ch == '#' {
            heading_hashes += 1;
            continue;
        }
        return heading_hashes > 0 && heading_hashes <= 6 && ch.is_ascii_whitespace();
    }
    false
}

fn deterministic_semantic_trace_id(
    repository_id: &str,
    snapshot_id: &str,
    provider: SemanticRuntimeProvider,
    model: &str,
) -> String {
    let mut hasher = Hasher::new();
    hasher.update(repository_id.as_bytes());
    hasher.update(&[0]);
    hasher.update(snapshot_id.as_bytes());
    hasher.update(&[0]);
    hasher.update(provider.as_str().as_bytes());
    hasher.update(&[0]);
    hasher.update(model.as_bytes());
    format!("trace-semantic-{}", hasher.finalize().to_hex())
}
