///|
/// Compare two parsed contracts and explain compatibility changes.
///
/// Backward mode asks whether every payload accepted by `old_contract` is also
/// accepted by `new_contract`. Forward mode reverses that data flow. Full mode
/// checks both directions.
pub fn analyze(
  old_contract : Contract,
  new_contract : Contract,
  mode? : CompatibilityMode = Full,
) -> AnalysisReport {
  let changes : Array[Change] = []
  if old_contract.name != new_contract.name {
    let direction = mode.render()
    changes.push(
      breaking_change(
        "CONTRACT_RENAMED",
        direction,
        "$",
        "contract name changed from '" +
        old_contract.name +
        "' to '" +
        new_contract.name +
        "'",
        "Keep the contract name stable or publish the new contract independently.",
        make_witness(
          "{}", old_contract, new_contract, "the payload belongs to a differently named contract",
        ),
      ),
    )
  } else {
    match mode {
      Backward =>
        analyze_data_flow(old_contract, new_contract, "backward", changes)
      Forward =>
        analyze_data_flow(new_contract, old_contract, "forward", changes)
      Full => {
        analyze_data_flow(old_contract, new_contract, "backward", changes)
        analyze_data_flow(new_contract, old_contract, "forward", changes)
      }
    }
  }
  {
    contract_name: old_contract.name,
    old_version: old_contract.version,
    new_version: new_contract.version,
    mode,
    changes,
  }
}

///|
/// Convenience API for callers that keep both contracts as text.
pub fn analyze_text(
  old_source : String,
  new_source : String,
  mode? : CompatibilityMode = Full,
) -> Result[AnalysisReport, ParseError] {
  let old_contract = match parse_contract(old_source) {
    Ok(contract) => contract
    Err(error) => return Err(error)
  }
  let new_contract = match parse_contract(new_source) {
    Ok(contract) => contract
    Err(error) => return Err(error)
  }
  Ok(analyze(old_contract, new_contract, mode~))
}

///|
/// Analyze whether all values produced under `source` remain valid under `target`.
fn analyze_data_flow(
  source : Contract,
  target : Contract,
  direction : String,
  changes : Array[Change],
) -> Unit {
  for source_object in source.objects {
    match target.find_object(source_object.name) {
      None =>
        changes.push(
          breaking_change(
            "TYPE_REMOVED",
            direction,
            "$." + source_object.name,
            "type '" +
            source_object.name +
            "' is not present in the receiving contract",
            "Retain the type until all producers using it have migrated.",
            make_witness(
              minimal_object_json(source, source_object),
              source,
              target,
              "the receiver has no declaration for this object type",
            ),
          ),
        )
      Some(target_object) =>
        analyze_object(
          source, target, source_object, target_object, direction, changes,
        )
    }
  }
  for target_object in target.objects {
    if source.find_object(target_object.name) is None {
      changes.push(
        info_change(
          "TYPE_ADDED",
          direction,
          "$." + target_object.name,
          "type '" +
          target_object.name +
          "' exists only in the receiving contract",
          "Adding an independently addressable type does not reject existing payloads.",
        ),
      )
    }
  }
}

///|
fn analyze_object(
  source_contract : Contract,
  target_contract : Contract,
  source : ObjectType,
  target : ObjectType,
  direction : String,
  changes : Array[Change],
) -> Unit {
  let object_path = "$." + source.name
  if source.open && !target.open {
    changes.push(
      breaking_change(
        "OBJECT_CLOSED",
        direction,
        object_path,
        "object changed from open to closed and now rejects unknown fields",
        "Keep the object open during migration or explicitly declare every producer field.",
        make_witness(
          object_with_extra_field(source_contract, source),
          source_contract,
          target_contract,
          "the additional field '__extra' is accepted only by the open object",
        ),
      ),
    )
  } else if !source.open && target.open {
    changes.push(
      info_change(
        "OBJECT_OPENED", direction, object_path, "object changed from closed to open",
        "The receiver now accepts additional fields, which widens its input set.",
      ),
    )
  }
  for source_field in source.fields {
    match target.find_field(source_field.name) {
      None =>
        if target.open {
          changes.push(
            info_change(
              "FIELD_UNDECLARED_BUT_ACCEPTED",
              direction,
              field_path(source.name, source_field.name),
              "field is no longer declared but remains accepted by the open object",
              "Keep it documented if downstream users still depend on the field.",
            ),
          )
        } else {
          let value = representative_value(source_contract, source_field)
          changes.push(
            breaking_change(
              "FIELD_REMOVED",
              direction,
              field_path(source.name, source_field.name),
              "field '" +
              source_field.name +
              "' was removed from a closed receiving object",
              "Keep the field optional during a deprecation window before removing it.",
              make_witness(
                object_with_field(source_contract, source, source_field, value),
                source_contract,
                target_contract,
                "the closed receiver rejects the now-unknown field",
              ),
            ),
          )
        }
      Some(target_field) =>
        analyze_field(
          source_contract, target_contract, source, source_field, target_field, direction,
          changes,
        )
    }
  }
  for target_field in target.fields {
    if source.find_field(target_field.name) is None {
      let path = field_path(source.name, target_field.name)
      if target_field.required {
        changes.push(
          breaking_change(
            "REQUIRED_FIELD_ADDED",
            direction,
            path,
            "required field '" +
            target_field.name +
            "' is absent from source payloads",
            "Add the field as optional first, migrate producers, then make it required.",
            make_witness(
              minimal_object_json(source_contract, source),
              source_contract,
              target_contract,
              "a source-valid object may omit the newly required field",
            ),
          ),
        )
      } else {
        changes.push(
          info_change(
            "OPTIONAL_FIELD_ADDED",
            direction,
            path,
            "optional field '" + target_field.name + "' was added",
            "Optional additions preserve the existing input set.",
          ),
        )
      }
    }
  }
}

///|
fn analyze_field(
  source_contract : Contract,
  target_contract : Contract,
  source_object : ObjectType,
  source : Field,
  target : Field,
  direction : String,
  changes : Array[Change],
) -> Unit {
  let path = field_path(source_object.name, source.name)
  if !source.required && target.required {
    changes.push(
      breaking_change(
        "FIELD_BECAME_REQUIRED",
        direction,
        path,
        "optional field '" + source.name + "' became required",
        "Keep the field optional until every producer always emits it.",
        make_witness(
          object_without_field(source_contract, source_object, source.name),
          source_contract,
          target_contract,
          "the source accepts an object with this optional field omitted",
        ),
      ),
    )
  } else if source.required && !target.required {
    changes.push(
      info_change(
        "FIELD_BECAME_OPTIONAL",
        direction,
        path,
        "required field '" + source.name + "' became optional",
        "The receiving contract accepts a wider set of objects.",
      ),
    )
  }
  if !type_accepts(target.type_expr, source.type_expr) {
    let code = if source.type_expr is EnumType(_) &&
      target.type_expr is EnumType(_) {
      "ENUM_VALUE_REMOVED"
    } else {
      "FIELD_TYPE_NARROWED"
    }
    let value = value_for_type_break(source_contract, source, target)
    changes.push(
      breaking_change(
        code,
        direction,
        path,
        "receiver type '" +
        target.type_expr.render() +
        "' does not accept every '" +
        source.type_expr.render() +
        "' value",
        "Use a widening type change or coordinate producer migration before narrowing.",
        make_witness(
          object_with_field(source_contract, source_object, source, value),
          source_contract,
          target_contract,
          "the selected field value is valid for the source type but invalid for the receiver type",
        ),
      ),
    )
    return
  }
  if source.type_expr != target.type_expr {
    changes.push(
      info_change(
        "FIELD_TYPE_WIDENED",
        direction,
        path,
        "field type widened from '" +
        source.type_expr.render() +
        "' to '" +
        target.type_expr.render() +
        "'",
        "The receiving field accepts all source values.",
      ),
    )
  } else if source.type_expr is EnumType(source_values) &&
    target.type_expr is EnumType(target_values) {
    if source_values != target_values &&
      source_values.all(value => target_values.contains(value)) {
      changes.push(
        info_change(
          "ENUM_VALUE_ADDED", direction, path, "receiver enum contains additional values",
          "Existing source values remain valid in this direction.",
        ),
      )
    }
  }
  analyze_constraints(
    source_contract, target_contract, source_object, source, target, direction, changes,
  )
}

///|
fn type_accepts(receiver : TypeExpr, produced : TypeExpr) -> Bool {
  match (receiver, produced) {
    (StringType, StringType) => true
    (IntType, IntType) => true
    (NumberType, NumberType | IntType) => true
    (BoolType, BoolType) => true
    (EnumType(receiver_values), EnumType(produced_values)) =>
      produced_values.all(value => receiver_values.contains(value))
    (RefType(receiver_name), RefType(produced_name)) =>
      receiver_name == produced_name
    (ListType(receiver_item), ListType(produced_item)) =>
      receiver_item == produced_item
    _ => false
  }
}

///|
fn analyze_constraints(
  source_contract : Contract,
  target_contract : Contract,
  source_object : ObjectType,
  source : Field,
  target : Field,
  direction : String,
  changes : Array[Change],
) -> Unit {
  let path = field_path(source_object.name, source.name)
  let mut found_break = false
  match target.constraints.min_int {
    Some(target_min) => {
      let breaks = match source.constraints.min_int {
        Some(source_min) => target_min > source_min
        None => true
      }
      if breaks {
        let value = match source.constraints.min_int {
          Some(source_min) => source_min
          None => target_min - 1
        }
        found_break = true
        changes.push(
          constraint_break(
            "MIN_INCREASED",
            direction,
            path,
            "minimum increased to " + target_min.to_string(),
            value.to_string(),
            source_contract,
            target_contract,
            source_object,
            source,
          ),
        )
      }
    }
    None => ()
  }
  match target.constraints.max_int {
    Some(target_max) => {
      let breaks = match source.constraints.max_int {
        Some(source_max) => target_max < source_max
        None => true
      }
      if breaks {
        found_break = true
        changes.push(
          constraint_break(
            "MAX_DECREASED",
            direction,
            path,
            "maximum decreased to " + target_max.to_string(),
            (target_max + 1).to_string(),
            source_contract,
            target_contract,
            source_object,
            source,
          ),
        )
      }
    }
    None => ()
  }
  match target.constraints.min_len {
    Some(target_min) => {
      let source_min = source.constraints.min_len.unwrap_or(0)
      if target_min > source_min {
        found_break = true
        changes.push(
          constraint_break(
            "MIN_LENGTH_INCREASED",
            direction,
            path,
            "minimum length increased to " + target_min.to_string(),
            length_value(source.type_expr, source_min),
            source_contract,
            target_contract,
            source_object,
            source,
          ),
        )
      }
    }
    None => ()
  }
  match target.constraints.max_len {
    Some(target_max) => {
      let breaks = match source.constraints.max_len {
        Some(source_max) => target_max < source_max
        None => true
      }
      if breaks {
        found_break = true
        changes.push(
          constraint_break(
            "MAX_LENGTH_DECREASED",
            direction,
            path,
            "maximum length decreased to " + target_max.to_string(),
            length_value(source.type_expr, target_max + 1),
            source_contract,
            target_contract,
            source_object,
            source,
          ),
        )
      }
    }
    None => ()
  }
  if !found_break && source.constraints != target.constraints {
    changes.push(
      info_change(
        "CONSTRAINTS_CHANGED_COMPATIBLY", direction, path, "field constraints changed without narrowing accepted source values",
        "Review the opposite compatibility direction when using full compatibility.",
      ),
    )
  }
}

///|
fn constraint_break(
  code : String,
  direction : String,
  path : String,
  message : String,
  value : String,
  source_contract : Contract,
  target_contract : Contract,
  source_object : ObjectType,
  source_field : Field,
) -> Change {
  breaking_change(
    code,
    direction,
    path,
    message,
    "Relax the receiving bound or migrate producers before tightening it.",
    make_witness(
      object_with_field(source_contract, source_object, source_field, value),
      source_contract,
      target_contract,
      "the boundary value is accepted by the source constraint but rejected by the receiver",
    ),
  )
}

///|
fn length_value(type_expr : TypeExpr, length : Int) -> String {
  match type_expr {
    StringType => string_value(length)
    ListType(item) => {
      let value = match item {
        "string" => "\"\""
        "bool" => "false"
        "int" | "number" => "0"
        _ => "{}"
      }
      "[" + Array::make(length, value).join(",") + "]"
    }
    _ => "null"
  }
}