///|
fn require(condition : Bool, message : String) -> Unit {
  if !condition {
    abort(message)
  }
}

///|
fn show(label : String, details : @feature.EvaluationDetails) -> Unit {
  println(label + ": " + details.to_json().stringify())
}

///|
fn main {
  let sdk = @feature.Sdk::new()
  let client = sdk.client(name="checkout-service", version="1.0")
  sdk
  .set_context(
    @feature.EvaluationContext::from_attributes({
      "region": Json::string("sdk-region"),
      "application": Json::string("store"),
    })
    .unwrap()
    .with_targeting_key("sdk-user"),
  )
  .unwrap()
  client.set_context(
    @feature.EvaluationContext::from_attributes({
      "region": Json::string("client-region"),
      "plan": Json::string("gold"),
    })
    .unwrap()
    .with_targeting_key("client-user"),
  )
  let options = @feature.EvaluationOptions::new(
    transaction_context=@feature.EvaluationContext::from_attributes({
        "region": Json::string("transaction-region"),
        "requestId": Json::string("req-42"),
      })
      .unwrap()
      .with_targeting_key("transaction-user"),
    context=@feature.EvaluationContext::from_attributes({
        "region": Json::string("eu-west"),
        "basketSize": Json::number(3.0),
      })
      .unwrap()
      .with_targeting_key("customer-17"),
  )
  // This is application-owned configuration, injected into a callback adapter.
  // The SDK itself does not load files, contact a vendor, or invent decisions.
  let flags : Map[String, @feature.FlagValue] = {
    "order-sequence": @feature.FlagValue::Integer(9007199254740993L),
    "discount": @feature.FlagValue::Float(0.15),
    "banner": @feature.FlagValue::String("Gold delivery"),
    "shipping": @feature.FlagValue::Structure(
      Json::object({
        "methods": Json::array([
          Json::string("express"),
          Json::string("standard"),
        ]),
        "freeAbove": Json::number(50.0),
      }),
    ),
    "wrong-type": @feature.FlagValue::String("not a boolean"),
  }
  let seen : Array[@feature.EvaluationContext] = []
  let metadata = @feature.FlagMetadata::from_values({
    "source": Json::string("injected-catalog"),
    "revision": Json::number(7.0),
  }).unwrap()
  let provider = @feature.Provider::new(
    "catalog-adapter",
    (key, _, default, context) => {
      seen.push(context.copy())
      if key == "express-checkout" {
        let eligible = context.targeting_key() == Some("customer-17") &&
          context.get("plan") == Some(Json::string("gold")) &&
          context.get("region") == Some(Json::string("eu-west"))
        @feature.Resolution::success(
          @feature.FlagValue::Boolean(eligible),
          reason="TARGETING_MATCH",
          variant=Some(if eligible { "gold-eu" } else { "standard" }),
          metadata~,
        )
      } else if key == "backend-error" {
        @feature.Resolution::failure(
          default,
          @feature.SdkError::new(ParseError, "catalog revision is malformed"),
        )
      } else {
        match flags.get(key) {
          Some(value) =>
            @feature.Resolution::success(
              value.copy(),
              reason="STATIC",
              metadata~,
            )
          None =>
            @feature.Resolution::failure(
              default,
              @feature.SdkError::new(
                FlagNotFound,
                "catalog has no flag: " + key,
              ),
            )
        }
      }
    },
    // An initializer returning Ok does not imply readiness. Publish it explicitly.
    initialize=Some((_, context) => {
      println("initialize context: " + context.to_json().stringify())
      Ok(())
    }),
  )
  sdk.set_provider(provider).unwrap()
  let pending = client.boolean_details("express-checkout", false, options~)
  require(
    !pending.value &&
    pending.details.error is Some({ code: ProviderNotReady, .. }),
    "not-ready default",
  )
  require(seen.length() == 0, "not-ready calls must not reach resolver")
  show("not ready", pending.details)
  provider.emit_ready()

  let boolean = client.boolean_details("express-checkout", false, options~)
  require(boolean.value && !boolean.is_error(), "typed boolean targeting")
  show("boolean", boolean.details)
  let effective = seen[0]
  require(
    effective.targeting_key() == Some("customer-17"),
    "invocation key precedence",
  )
  require(
    effective.get("region") == Some(Json::string("eu-west")),
    "invocation attributes win",
  )
  require(
    effective.get("application") == Some(Json::string("store")),
    "SDK context retained",
  )
  require(
    effective.get("requestId") == Some(Json::string("req-42")),
    "transaction context retained",
  )
  require(
    effective.get("plan") == Some(Json::string("gold")),
    "client context retained",
  )
  println("effective context: " + effective.to_json().stringify())
  require(
    sdk.context().targeting_key() == Some("sdk-user"),
    "evaluation must not mutate SDK context",
  )
  require(
    client.context().get("region") == Some(Json::string("client-region")),
    "evaluation must not mutate client context",
  )

  let integer = client.integer_details("order-sequence", -1L, options~)
  let discount = client.float_details("discount", 0.0, options~)
  let banner = client.string_details("banner", "Safe banner", options~)
  let shipping = client.structure_details(
    "shipping",
    Json::object({}),
    options~,
  )
  require(
    integer.value == 9007199254740993L && !integer.is_error(),
    "Int64 precision",
  )
  require(discount.value == 0.15 && !discount.is_error(), "typed double")
  require(banner.value == "Gold delivery" && !banner.is_error(), "typed string")
  require(
    shipping.value == flags["shipping"].as_structure().unwrap() &&
    !shipping.is_error(),
    "typed structure",
  )
  show("integer", integer.details)
  show("float", discount.details)
  show("string", banner.details)
  show("structure", shipping.details)

  let missing = client.integer_details("missing", 42L, options~)
  let mismatch = client.boolean_details("wrong-type", false, options~)
  let failed = client.string_details("backend-error", "Safe banner", options~)
  require(
    missing.value == 42L &&
    missing.details.error is Some({ code: FlagNotFound, .. }),
    "missing flag default",
  )
  require(
    !mismatch.value &&
    mismatch.details.error is Some({ code: TypeMismatch, .. }),
    "type mismatch default",
  )
  require(
    failed.value == "Safe banner" &&
    failed.details.error is Some({ code: ParseError, .. }),
    "provider error default",
  )
  for details in [missing.details, mismatch.details, failed.details] {
    require(
      details.reason == "ERROR" && details.variant == None,
      "fallback diagnostics",
    )
  }
  show("missing default", missing.details)
  show("type mismatch default", mismatch.details)
  show("provider error default", failed.details)
  require(
    seen.length() == 8,
    "each ready evaluation must reach the injected resolver exactly once",
  )
  require(
    sdk.take_diagnostics().length() == 0,
    "evaluation errors belong in details, not observer diagnostics",
  )
  sdk.shutdown().unwrap()
}