///|
/// Severity override for one classification `detail` key.
///
/// - `category` is `"breaking"`, `"compatible"`, or `"ignore"`.
/// - `"ignore"` drops matching findings from the report entirely.
pub(all) struct RuleOverride {
  detail : String
  category : String
} derive(Eq, Debug)

///|
/// Compatibility policy applied after structural classification.
///
/// Use `default_compat_policy()` for MoonBit-oriented defaults, or
/// `strict_compat_policy()` to treat nearly all signature diffs as
/// breaking unless overridden.
pub(all) struct CompatPolicy {
  overrides : Array[RuleOverride]
} derive(Debug)

///|
/// MoonBit-oriented defaults: adding labeled optional parameters is
/// compatible; enum variant additions remain breaking.
pub fn default_compat_policy() -> CompatPolicy {
  {
    overrides: [
      { detail: "optional-parameter-added", category: "compatible" },
      { detail: "raise-removed", category: "compatible" },
      { detail: "visibility-widened", category: "compatible" },
      { detail: "derive-added", category: "compatible" },
    ],
  }
}

///|
/// Strict policy with no built-in remaps. Attribute-only deprecations
/// remain compatible because they are classified before policy remaps.
pub fn strict_compat_policy() -> CompatPolicy {
  { overrides: [] }
}

///|
/// Remap one detail key to `"compatible"`.
pub fn policy_allow(policy : CompatPolicy, detail : String) -> CompatPolicy {
  policy_with_override(policy, detail, "compatible")
}

///|
/// Drop findings with the given detail key from reports.
pub fn policy_ignore(policy : CompatPolicy, detail : String) -> CompatPolicy {
  policy_with_override(policy, detail, "ignore")
}

///|
/// Insert or replace an override for `detail`.
pub fn policy_with_override(
  policy : CompatPolicy,
  detail : String,
  category : String,
) -> CompatPolicy {
  let overrides : Array[RuleOverride] = []
  let mut replaced = false
  for item in policy.overrides {
    if item.detail == detail {
      overrides.push({ detail, category })
      replaced = true
    } else {
      overrides.push(item)
    }
  }
  if !replaced {
    overrides.push({ detail, category })
  }
  { overrides, }
}

///|
fn CompatPolicy::category_for(
  self : CompatPolicy,
  detail : String,
  fallback : String,
) -> String {
  for item in self.overrides {
    if item.detail == detail {
      return item.category
    }
  }
  fallback
}

///|
/// Build a `CompatPolicy` from a JSON policy document.
///
/// Expected shape:
/// ```text
/// {
///   "strict": false,
///   "allow": ["variant-added"],
///   "ignore": ["deprecated"]
/// }
/// ```
///
/// Missing fields default to `strict=false` and empty allow/ignore lists.
/// Returns `None` when the text is not valid policy JSON.
pub fn policy_from_json_text(text : String) -> CompatPolicy? {
  let json = @json.parse(text) catch { _ => return None }
  guard json is Object(obj) else { return None }
  let strict = match obj.get("strict") {
    None => false
    Some(True) => true
    Some(False) => false
    _ => return None
  }
  let allow = match json_string_array(obj.get("allow")) {
    Some(items) => items
    None => return None
  }
  let ignore = match json_string_array(obj.get("ignore")) {
    Some(items) => items
    None => return None
  }
  let mut policy = if strict {
    strict_compat_policy()
  } else {
    default_compat_policy()
  }
  for detail in allow {
    policy = policy_allow(policy, detail)
  }
  for detail in ignore {
    policy = policy_ignore(policy, detail)
  }
  Some(policy)
}

///|
/// Serialize a policy into the JSON shape accepted by `policy_from_json_text`.
pub fn policy_to_json_text(
  policy : CompatPolicy,
  strict? : Bool = false,
) -> String {
  let allow : Array[String] = []
  let ignore : Array[String] = []
  for item in policy.overrides {
    if item.category == "compatible" {
      allow.push(json_string(item.detail))
    } else if item.category == "ignore" {
      ignore.push(json_string(item.detail))
    }
  }
  let strict_text = if strict { "true" } else { "false" }
  "{\"strict\":" +
  strict_text +
  ",\"allow\":[" +
  join_csv_json(allow) +
  "],\"ignore\":[" +
  join_csv_json(ignore) +
  "]}"
}

///|
fn join_csv_json(items : Array[String]) -> String {
  let mut out = ""
  let mut first = true
  for item in items {
    if !first {
      out = out + ","
    }
    out = out + item
    first = false
  }
  out
}

///|
fn json_string_array(value : Json?) -> Array[String]? {
  match value {
    None => Some([])
    Some(Array(items)) => {
      let out : Array[String] = []
      for item in items {
        guard item is String(s) else { return None }
        out.push(s)
      }
      Some(out)
    }
    _ => None
  }
}