///|
/// Quadratic family: inverse construction, presentation, and difficulty model.
///
/// Construction runs *forward from the answer*: the schema fixes the roots and
/// the leading coefficient, and Vieta's relations produce `b` and `c`. The
/// verifier in `verify.mbt` runs *backward from the coefficients*. Nothing is
/// shared between the two except the definition of the discriminant.

///|
pub fn build_quadratic(
  schema : Schema,
  env : Map[String, Rat],
) -> Result[Item, String] {
  let a = get_rat(env, "a")
  let b = get_rat(env, "b")
  let c = get_rat(env, "c")
  let r1 = get_rat(env, schema.answers[0].name)
  let r2 = get_rat(env, schema.answers[1].name)
  let p = Poly::new([c, b, a])
  let (verdict, cert) = verify_quadratic(p, [r1, r2])
  if !verdict.is_well_posed() {
    return Err(verdict.to_string())
  }
  let roots = sort_roots([r1, r2])
  let poly_text = p.to_string("x")
  let question = replace_all(schema.ask, "{poly}", poly_text)
  let (level, reasons) = classify_quadratic(b, c, roots)
  let tags = quadratic_tags(b, c, roots)
  Ok({
    schema_name: schema.name,
    question,
    answers: roots,
    level,
    reasons,
    fingerprint: p.canonical_key(),
    certificate: cert,
    robust: cert.robust,
    signature: "a=\{a.to_string()}, b=\{b.to_string()}, c=\{c.to_string()}",
    family: FQuadratic,
    tags,
  })
}

///|
/// Structural tags. These exist so the assembler can refuse a paper where four
/// out of ten problems happen to have a zero root — which is what happens if you
/// select purely by difficulty.
fn quadratic_tags(b : Rat, c : Rat, roots : Array[Rat]) -> Array[String] {
  let tags : Array[String] = []
  if c.is_zero() {
    tags.push("zero-root")
  }
  if b.is_zero() {
    tags.push("no-linear-term")
  }
  if c.sign() < 0 {
    tags.push("opposite-signs")
  }
  let mut both_negative = true
  let mut unit = false
  for r in roots {
    if r.sign() >= 0 {
      both_negative = false
    }
    if r.abs().equal(rat_one()) {
      unit = true
    }
  }
  if both_negative && roots.length() == 2 {
    tags.push("both-negative")
  }
  if unit {
    tags.push("unit-root")
  }
  tags
}

///|
fn get_rat(env : Map[String, Rat], name : String) -> Rat {
  match env.get(name) {
    Some(v) => v
    None => abort("missing value for '\{name}'")
  }
}

///|
fn sort_roots(xs : Array[Rat]) -> Array[Rat] {
  let out = xs.copy()
  let mut i = 1
  while i < out.length() {
    let key = out[i]
    let mut j = i - 1
    while j >= 0 && out[j].compare(key) > 0 {
      out[j + 1] = out[j]
      j = j - 1
    }
    out[j + 1] = key
    i = i + 1
  }
  out
}

///|
fn abs_int64(x : Int64) -> Int64 {
  if x < 0L {
    -x
  } else {
    x
  }
}

///|
/// Explainable difficulty: a small additive model over *named* factors.
///
/// The point is not psychometric precision — that needs response data we do not
/// have. The point is that every level comes with the reasons that produced it,
/// so a teacher can disagree with the model and tune the schema instead of
/// treating the number as magic.
pub fn classify_quadratic(
  b : Rat,
  c : Rat,
  roots : Array[Rat],
) -> (Int32, Array[String]) {
  let reasons : Array[String] = []
  let mut score = 1
  let mut biggest : Int64 = 0L
  for r in roots {
    if r.is_integer() {
      let v = abs_int64(r.num())
      if v > biggest {
        biggest = v
      }
    } else {
      biggest = 99L
    }
  }
  if biggest >= 7L {
    score = score + 2
    reasons.push("根绝对值较大")
  } else if biggest >= 4L {
    score = score + 1
    reasons.push("根超出最小整数区间")
  }
  if c.sign() < 0 {
    score = score + 1
    reasons.push("两根异号")
  }
  let mut both_negative = true
  for r in roots {
    if r.sign() >= 0 {
      both_negative = false
    }
  }
  if both_negative && roots.length() == 2 {
    score = score + 1
    reasons.push("两根同为负")
  }
  if b.is_zero() || c.is_zero() {
    score = score - 1
    reasons.push("方程缺项(偏易)")
  }
  let d = b.mul(b).sub(rat_int(4L).mul(c))
  if d.compare(rat_int(100L)) > 0 {
    score = score + 1
    reasons.push("判别式较大")
  }
  let mut level = score
  if level < 1 {
    level = 1
  }
  if level > 5 {
    level = 5
  }
  (level, reasons)
}