///|
pub struct Client {
  priv sdk : Sdk
  priv metadata : ClientMetadata
  priv mut context : EvaluationContext
  priv hooks : Array[Hook]
}

///|
/// Clients are live domain handles, not snapshots of the provider at construction.
pub fn Sdk::client(
  self : Sdk,
  domain~ : String = "",
  name~ : String = "",
  version~ : String = "",
) -> Client {
  self.known_domains[domain] = true
  {
    sdk: self,
    metadata: { domain, name, version, },
    context: EvaluationContext::empty(),
    hooks: [],
  }
}

///|
pub fn Client::metadata(self : Client) -> ClientMetadata {
  self.metadata
}

///|
pub fn Client::provider_status(self : Client) -> ProviderStatus {
  self.sdk.provider_status(domain=self.metadata.domain)
}

///|
pub fn Client::context(self : Client) -> EvaluationContext {
  self.context.copy()
}

///|
pub fn Client::set_context(self : Client, context : EvaluationContext) -> Unit {
  self.context = context.copy()
}

///|
pub fn Client::add_hook(self : Client, hook : Hook) -> Unit {
  self.hooks.push(hook)
}

///|
pub fn Client::clear_hooks(self : Client) -> Unit {
  self.hooks.clear()
}

///|
pub fn Client::hooks(self : Client) -> Array[Hook] {
  self.hooks.copy()
}

///|
pub fn Client::on(
  self : Client,
  event_type : EventType,
  callback : (ProviderEvent) -> Result[Unit, SdkError],
) -> Int32 {
  let id = self.sdk
    .events()
    .on(
      callback,
      event_type=Some(event_type),
      domain=Some(self.metadata.domain),
    )
  let provider = self.sdk.provider(domain=self.metadata.domain)
  if event_type.matches_status(provider.status()) {
    match
      callback(
        ProviderEvent::new(
          event_type,
          provider.name(),
          domain=Some(self.metadata.domain),
        ),
      ) {
      Ok(_) => ()
      Err(error) => self.sdk.record_diagnostic(error)
    }
  }
  id
}

///|
pub fn Client::off(self : Client, id : Int32) -> Bool {
  self.sdk.events().off(id)
}

///|
pub fn Client::evaluate(
  self : Client,
  key : String,
  default : FlagValue,
  options~ : EvaluationOptions = EvaluationOptions::new(),
) -> EvaluationDetails {
  let expected = default.flag_type()
  match default.validate() {
    Err(error) =>
      return EvaluationDetails::from_resolution(
        key,
        Resolution::failure(default.copy(), error),
      )
    Ok(_) => ()
  }
  let frozen_default = default.copy()
  let frozen_options = options.copy()
  let provider = self.sdk.provider(domain=self.metadata.domain)
  let merged = self.sdk
    .context()
    .merge(frozen_options.transaction_context())
    .merge(self.context.copy())
    .merge(frozen_options.context())
  let plan = HookPlan::new(
    expected,
    self.sdk.hooks(),
    self.hooks.copy(),
    frozen_options.hooks(),
    provider.hooks(),
  )
  let base : HookContext = {
    flag_key: key,
    flag_type: expected,
    default_value: frozen_default.copy(),
    evaluation_context: merged,
    client: self.metadata,
    provider_name: provider.name(),
    data: HookData::new(),
  }
  let hints = frozen_options.hints()
  let before = plan.before(base, hints)
  let final_context = base.with_context(before.context)
  let resolution = match before.error {
    Some(error) => Resolution::failure(frozen_default.copy(), error)
    None =>
      if self.sdk.is_closed() {
        Resolution::failure(
          frozen_default.copy(),
          SdkError::new(ProviderNotReady, "SDK is shut down"),
        )
      } else {
        provider.resolve(
          key,
          expected,
          frozen_default.copy(),
          before.context.copy(),
        )
      }
  }
  let mut details = EvaluationDetails::from_resolution(
    key,
    resolution.checked(expected, frozen_default),
  )
  if details.error == None {
    match plan.after(final_context, details, hints) {
      Ok(_) => ()
      Err(error) =>
        details = EvaluationDetails::from_resolution(
          key,
          Resolution::failure(frozen_default.copy(), error),
        )
    }
  }
  match details.error {
    Some(error) =>
      for observer_error in plan.on_error(final_context, error, hints) {
        self.sdk.record_diagnostic(observer_error)
      }
    None => ()
  }
  for observer_error in plan.run_finally(final_context, details, hints) {
    self.sdk.record_diagnostic(observer_error)
  }
  details.copy()
}

///|
pub fn Client::track_checked(
  self : Client,
  event : TrackingEvent,
  context~ : EvaluationContext = EvaluationContext::empty(),
  transaction_context~ : EvaluationContext = EvaluationContext::empty(),
) -> Result[Unit, SdkError] {
  if self.sdk.is_closed() {
    return Err(SdkError::new(ProviderNotReady, "SDK is shut down"))
  }
  let merged = self.sdk
    .context()
    .merge(transaction_context)
    .merge(self.context)
    .merge(context)
  self.sdk.provider(domain=self.metadata.domain).track(event.copy(), merged)
}

///|
/// Standard no-throw tracking: optional provider failures are diagnostics, not
/// evaluation failures. Use track_checked when an integration needs a Result.
pub fn Client::track(
  self : Client,
  event : TrackingEvent,
  context~ : EvaluationContext = EvaluationContext::empty(),
  transaction_context~ : EvaluationContext = EvaluationContext::empty(),
) -> Unit {
  match self.track_checked(event, context~, transaction_context~) {
    Ok(_) => ()
    Err(error) => self.sdk.record_diagnostic(error)
  }
}