use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use serde::Serialize;
use super::io::{read_codewiki_meta, safe_doc_path, write_doc};
use super::{CitationResolver, CodewikiIndexSnapshot, reanchor_citations};
use crate::models::Symbol;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct CitationRepairSummary {
pub pages_scanned: usize,
pub pages_repaired: usize,
pub citations_repaired: usize,
pub citations_unresolved: usize,
}
struct IndexCitationResolver {
current_spans: BTreeMap<String, Vec<(usize, usize)>>,
current_by_identity: BTreeMap<(String, String, String), (usize, usize)>,
snapshot_anchor: BTreeMap<(String, usize), (String, String)>,
}
impl IndexCitationResolver {
fn build(symbols: &[Symbol], snapshot: &CodewikiIndexSnapshot) -> Self {
let mut current_spans: BTreeMap<String, Vec<(usize, usize)>> = BTreeMap::new();
let mut current_by_identity = BTreeMap::new();
for symbol in symbols {
current_spans
.entry(symbol.file_path.clone())
.or_default()
.push((symbol.line_start, symbol.line_end));
current_by_identity.insert(
(
symbol.file_path.clone(),
symbol.qualified_name.clone(),
symbol.kind.clone(),
),
(symbol.line_start, symbol.line_end),
);
}
let mut snapshot_anchor: BTreeMap<(String, usize), (String, String)> = BTreeMap::new();
let mut ambiguous: BTreeSet<(String, usize)> = BTreeSet::new();
for snap in snapshot.symbols.values() {
let key = (snap.file_path.clone(), snap.line_start);
if snapshot_anchor
.insert(
key.clone(),
(snap.qualified_name.clone(), snap.kind.clone()),
)
.is_some()
{
ambiguous.insert(key);
}
}
for key in ambiguous {
snapshot_anchor.remove(&key);
}
Self {
current_spans,
current_by_identity,
snapshot_anchor,
}
}
}
impl CitationResolver for IndexCitationResolver {
fn is_current(&self, file: &str, line_start: usize, line_end: usize) -> bool {
self.current_spans.get(file).is_some_and(|spans| {
spans
.iter()
.any(|(start, end)| *start <= line_start && line_end <= *end)
})
}
fn resolve(&self, file: &str, line_start: usize) -> Option<(usize, usize)> {
let (qualified_name, kind) = self.snapshot_anchor.get(&(file.to_string(), line_start))?;
self.current_by_identity
.get(&(file.to_string(), qualified_name.clone(), kind.clone()))
.copied()
}
}
fn repair_with_resolver(
out_dir: &Path,
resolver: &dyn CitationResolver,
) -> anyhow::Result<CitationRepairSummary> {
let meta = read_codewiki_meta(out_dir)?;
let mut summary = CitationRepairSummary::default();
for doc_path in meta.docs.keys() {
let target = safe_doc_path(out_dir, doc_path)?;
let content = match std::fs::read_to_string(&target) {
Ok(content) => content,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
Err(err) => return Err(err.into()),
};
summary.pages_scanned += 1;
let result = reanchor_citations(&content, resolver);
summary.citations_repaired += result.repaired;
summary.citations_unresolved += result.unresolved;
if result.repaired > 0 && result.text != content {
write_doc(out_dir, doc_path, &result.text)?;
summary.pages_repaired += 1;
}
}
Ok(summary)
}
pub fn repair_citations(
out_dir: &Path,
symbols: &[Symbol],
) -> anyhow::Result<CitationRepairSummary> {
let snapshot = read_codewiki_meta(out_dir)?
.index_snapshot
.unwrap_or_default();
let resolver = IndexCitationResolver::build(symbols, &snapshot);
repair_with_resolver(out_dir, &resolver)
}