///|
pub(all) struct SearchHit {
  path : String
  line : Int32
  snippet : String
  kind : String
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct SearchResult {
  hits : Array[SearchHit]
  total : Int32
  issues : Array[Issue]
} derive(Eq, Debug, ToJson, FromJson)

///|
fn search_excerpt(line : String, needle : String) -> String {
  let offset = line.to_lower().find(needle).unwrap_or(0)
  let chars = line.to_array()
  let mut position = 0
  let mut index = 0
  while index < chars.length() && position < offset {
    position += chars[index].to_string().to_lower().length()
    index += 1
  }
  let start = Int32::max(0, index - 50)
  let end = Int32::min(chars.length(), start + 240)
  (if start > 0 { "…" } else { "" }) +
  chars[start:end].to_owned().map(c => c.to_string()).join("") +
  (if end < chars.length() { "…" } else { "" })
}

///|
/// Case-insensitive literal search; line zero denotes metadata rather than body evidence.
pub fn search_documents(
  documents : Array[Document],
  preferences : Array[Preference],
  query : String,
) -> SearchResult {
  let atlas = build_atlas(documents, preferences, [])
  if !atlas.issues.is_empty() {
    return { hits: [], total: 0, issues: atlas.issues, }
  }
  let needle = query.trim().to_owned().to_lower()
  if needle.length() > 200 {
    return {
      hits: [],
      total: 0,
      issues: [problem("query", "", "搜索词最多 200 个文本码元")],
    }
  }
  if needle == "" {
    return { hits: [], total: 0, issues: [], }
  }
  let source : Map[String, String] = Map([])
  for d in documents {
    source[d.path] = d.content
  }
  let hits : Array[SearchHit] = []
  let mut total = 0
  for page in atlas.pages {
    if [page.path, page.title, page.topic, page.tags.join(" ")]
      .join("\n")
      .to_lower()
      .contains(needle) {
      total += 1
      if hits.length() < 200 {
        hits.push({
          path: page.path,
          line: 0,
          snippet: search_excerpt(
            page.title +
            " · " +
            page.topic +
            " · " +
            page.path +
            " · " +
            page.tags.join(" "),
            needle,
          ),
          kind: "metadata",
        })
      }
    }
    let lines = source[page.path].split("\n").to_array()
    for i = 0; i < lines.length(); i = i + 1 {
      let text = lines[i].to_owned()
      if text.to_lower().contains(needle) {
        total += 1
        if hits.length() < 200 {
          hits.push({
            path: page.path,
            line: i + 1,
            snippet: search_excerpt(text, needle),
            kind: "body",
          })
        }
      }
    }
  }
  { hits, total, issues: [], }
}

///|
pub(all) struct BatchClassification {
  preferences : Array[Preference]
  issues : Array[Issue]
} derive(Eq, Debug, ToJson, FromJson)

///|
/// One atomic classification result; preserve titles and reject stale selections.
pub fn classify_documents(
  documents : Array[Document],
  preferences : Array[Preference],
  paths : Array[String],
  topic : String,
) -> BatchClassification {
  let issues = validate_documents(documents)
  issues.append(validate_choices(preferences, []))
  if paths.is_empty() ||
    paths.length() > 200 ||
    topic.trim() == "" ||
    topic.length() > 120 {
    issues.push(
      problem(
        "batch", "", "请选择 1–200 篇文档,并填写不超过 120 个文本码元的主题",
      ),
    )
  }
  let selected : Map[String, Bool] = Map([])
  for path in paths {
    if selected.contains(path) || !documents.any(d => d.path == path) {
      issues.push(problem("batch", path, "重复或已不存在的文档"))
    }
    selected[path] = true
  }
  if !issues.is_empty() {
    return { preferences, issues, }
  }
  let result = preferences.filter(p => !selected.contains(p.path))
  for path in paths {
    let title = preferences
      .iter()
      .find_first(p => p.path == path)
      .map(p => p.title)
      .unwrap_or("")
    result.push({ path, title, topic: topic.trim().to_owned(), })
  }
  let errors = validate_choices(result, [])
  {
    preferences: if errors.is_empty() {
      result
    } else {
      preferences
    },
    issues: errors,
  }
}