// modules.mbt — core algorithms (mirrors Python `modules.py`)
//
// Classification, deduplication, conflict detection, priority recomputation,
// self-evolution and graded retrieval. Time is handled as epoch milliseconds.

///|
/// Keyword hints per memory type, used by `classify`.
let _TYPE_KEYWORDS : Array[(MemoryType, Array[String])] = [
  (
    Instruction,
    [
      "必须", "不要", "禁止", "总是", "务必", "指令", "规则", "不允许",
      "要求", "应当",
    ],
  ),
  (
    Preference,
    ["偏好", "喜欢", "习惯", "倾向", "更愿意", "风格", "口味"],
  ),
  (
    Fact,
    [
      "成立于", "位于", "定义", "数据", "统计", "出生于", "总部",
      "发布于", "创始于",
    ],
  ),
]

///|
/// Polarity pairs used by conflict detection.
let _POLARITY : Array[(String, String)] = [
  ("支持", "反对"),
  ("是", "不是"),
  ("允许", "禁止"),
  ("喜欢", "讨厌"),
  ("正确", "错误"),
  ("启用", "禁用"),
]

///|
fn is_punct(c : Char) -> Bool {
  let s = c.to_string()
  s == "," ||
  s == "。" ||
  s == "、" ||
  s == "!" ||
  s == "?" ||
  s == "." ||
  s == "," ||
  s == "!" ||
  s == "?"
}

///|
/// Validate raw input (mirrors `validate_input`).
pub fn validate_input(content : String) -> (Bool, String) {
  let cleaned = content.trim()
  if cleaned.length() == 0 {
    return (false, "内容为空")
  }
  if cleaned.length() < 4 {
    return (false, "内容过短,缺少明确主体")
  }
  let mut all_punct = true
  for c in cleaned.iter() {
    if !is_punct(c) {
      all_punct = false
      break
    }
  }
  if all_punct {
    return (false, "内容为纯标点噪声")
  }
  (true, "")
}

///|
/// Classify content into a memory type by keyword scoring.
pub fn classify(content : String) -> MemoryType {
  let mut best = CommonSense
  let mut best_score = 0
  for item in _TYPE_KEYWORDS {
    let (t, kws) = item
    let mut sc = 0
    for kw in kws {
      if content.contains(kw) {
        sc = sc + 1
      }
    }
    if sc > best_score {
      best_score = sc
      best = t
    }
  }
  if best_score == 0 {
    CommonSense
  } else {
    best
  }
}

///|
/// Find the most similar existing entry above `threshold` (duplicate check).
pub fn find_duplicate(
  content : String,
  candidates : Array[MemoryEntry],
  threshold? : Double = 0.92,
) -> MemoryEntry? {
  let mut best = None
  let mut best_sim = 0.0
  for c in candidates {
    let s = similarity(content, c.content)
    if s > best_sim {
      best_sim = s
      best = Some(c)
    }
  }
  if best_sim >= threshold {
    best
  } else {
    None
  }
}

///|
fn has_opposite_polarity(a : String, b : String) -> Bool {
  for item in _POLARITY {
    let (pos, neg) = item
    if (a.contains(pos) && b.contains(neg)) ||
      (a.contains(neg) && b.contains(pos)) {
      return true
    }
  }
  false
}

///|
/// Detect conflicting entries (similar + opposite polarity).
pub fn detect_conflict(
  new_entry : MemoryEntry,
  candidates : Array[MemoryEntry],
  threshold? : Double = 0.55,
) -> Array[MemoryEntry] {
  let conflicts : Array[MemoryEntry] = []
  for c in candidates {
    if c.id != new_entry.id {
      let sim = similarity(new_entry.content, c.content)
      if sim >= threshold && has_opposite_polarity(new_entry.content, c.content) {
        conflicts.push(c)
      }
    }
  }
  conflicts
}

///|
fn fmin(a : Double, b : Double) -> Double {
  if a < b {
    a
  } else {
    b
  }
}

///|
fn fmax(a : Double, b : Double) -> Double {
  if a > b {
    a
  } else {
    b
  }
}

///|
fn round4(x : Double) -> Double {
  @math.round(x * 10000.0) / 10000.0
}

///|
/// Age in days from a last-accessed timestamp (epoch ms) to `now` (epoch ms).
fn age_days(last_ms : Int64, now : Int64) -> Double {
  if last_ms == 0L {
    return 9999.0
  }
  let days = (now - last_ms).to_double() / 86400000.0
  if days < 0.0 {
    0.0
  } else {
    days
  }
}

///|
/// Recompute an entry's priority score.
pub fn recompute_priority(
  entry : MemoryEntry,
  now : Int64,
  w? : (Double, Double, Double) = (0.5, 0.3, 0.2),
  half_life? : Double = 30.0,
) -> Double {
  let (w1, w2, w3) = w
  let freq_norm = fmin(entry.access_count.to_double() / 20.0, 1.0)
  let age = age_days(entry.last_accessed, now)
  let recency = @math.pow(0.5, age / half_life)
  let feedback_norm = if entry.feedback_score != 0.0 {
    (fmax(fmin(entry.feedback_score, 1.0), -1.0) + 1.0) / 2.0
  } else {
    0.0
  }
  round4(w1 * freq_norm + w2 * recency + w3 * feedback_norm)
}

///|
/// Self-evolution: recompute priorities and deprecate stale low-value entries.
pub fn evolve(
  storage : Storage,
  now? : Int64 = now_ms(),
  deprecate_below? : Double = 0.15,
  deprecate_age? : Double = 30.0,
) -> Map[String, Json] raise @fs.IOError {
  let n = now
  let mut changed = 0
  let mut deprecated = 0
  for entry in storage.all_entries() {
    if entry.status == MemoryStatus::Archived.value() {
      continue
    }
    let np = recompute_priority(entry, n)
    let age = age_days(entry.last_accessed, n)
    let mut status = entry.status
    if np < deprecate_below && age > deprecate_age {
      status = MemoryStatus::Deprecated.value()
      deprecated = deprecated + 1
    }
    let updated = { ..entry, priority: np, status }
    storage.put(updated)
    changed = changed + 1
  }
  let res : Map[String, Json] = Map([], capacity=2)
  res.set("recomputed", Json::number(changed.to_double()))
  res.set("deprecated", Json::number(deprecated.to_double()))
  res
}

///|
/// Graded retrieval: rank active entries by similarity x priority x recency.
pub fn graded_retrieval(
  storage : Storage,
  query : String,
  top_k? : Int = 5,
  now? : Int64 = now_ms(),
) -> Array[(MemoryEntry, Double)] {
  let n = now
  let results : Array[(MemoryEntry, Double)] = []
  for entry in storage.all_entries() {
    if entry.status != MemoryStatus::Active.value() {
      continue
    }
    let sim = similarity(query, entry.content)
    if sim <= 0.0 {
      continue
    }
    let age = age_days(entry.last_accessed, n)
    let recency = @math.pow(0.5, age / 30.0)
    let score = sim * entry.priority * (0.5 + 0.5 * recency)
    results.push((entry, round4(score)))
  }
  results.sort_by(fn(p, q) {
    if q.1 > p.1 {
      1
    } else if q.1 < p.1 {
      -1
    } else {
      0
    }
  })
  let n = if top_k < results.length() { top_k } else { results.length() }
  results.view(end=n).to_owned()
}