///|
pub(all) struct ClientMetadata {
  name : String
  version : String
  domain : String
}

///|
pub(all) struct HookContext {
  flag_key : String
  flag_type : FlagType
  default_value : FlagValue
  evaluation_context : EvaluationContext
  client : ClientMetadata
  provider_name : String
  data : HookData
}

///|
pub fn HookContext::copy(self : HookContext) -> HookContext {
  {
    ..self,
    default_value: self.default_value.copy(),
    evaluation_context: self.evaluation_context.copy(),
  }
}

///|
pub fn HookContext::with_context(
  self : HookContext,
  context : EvaluationContext,
) -> HookContext {
  {
    ..self,
    default_value: self.default_value.copy(),
    evaluation_context: context.copy(),
  }
}

///|
/// Hook callbacks return errors explicitly. Exceptions/panics from user callbacks
/// are not caught; integrations must use this Result contract.
pub struct Hook {
  priv name : String
  priv types : Array[FlagType]?
  priv before : (HookContext, EvaluationContext) -> Result[
    EvaluationContext?,
    SdkError,
  ]
  priv after : (HookContext, EvaluationDetails, EvaluationContext) -> Result[
    Unit,
    SdkError,
  ]
  priv error : (HookContext, SdkError, EvaluationContext) -> Result[
    Unit,
    SdkError,
  ]
  priv on_finally : (HookContext, EvaluationDetails, EvaluationContext) -> Result[
    Unit,
    SdkError,
  ]
}

///|
pub fn Hook::new(
  name : String,
  types~ : Array[FlagType]? = None,
  before~ : (HookContext, EvaluationContext) -> Result[
    EvaluationContext?,
    SdkError,
  ] = (_, _) => Ok(None),
  after~ : (HookContext, EvaluationDetails, EvaluationContext) -> Result[
    Unit,
    SdkError,
  ] = (_, _, _) => Ok(()),
  error~ : (HookContext, SdkError, EvaluationContext) -> Result[Unit, SdkError] = (
    _,
    _,
    _,
  ) => Ok(()),
  on_finally~ : (HookContext, EvaluationDetails, EvaluationContext) -> Result[
    Unit,
    SdkError,
  ] = (_, _, _) => Ok(()),
) -> Hook {
  { name, types: types.map(ts => ts.copy()), before, after, error, on_finally, }
}

///|
pub fn Hook::name(self : Hook) -> String {
  self.name
}

///|
pub fn Hook::supports(self : Hook, kind : FlagType) -> Bool {
  match self.types {
    None => true
    Some(types) => types.contains(kind)
  }
}

///|
/// An invocation freezes its hooks before any user callbacks execute.
pub struct HookPlan {
  priv hooks : Array[Hook]
  priv data : Array[HookData]
}

///|
pub fn HookPlan::new(
  kind : FlagType,
  api : Array[Hook],
  client : Array[Hook],
  invocation : Array[Hook],
  provider : Array[Hook],
) -> HookPlan {
  let hooks = []
  for scope in [api, client, invocation, provider] {
    for hook in scope {
      if hook.supports(kind) {
        hooks.push(hook)
      }
    }
  }
  { hooks, data: hooks.map(_ => HookData::new()), }
}

///|
pub fn HookPlan::names(self : HookPlan) -> Array[String] {
  self.hooks.map(h => h.name())
}

///|
pub fn HookPlan::length(self : HookPlan) -> Int32 {
  self.hooks.length()
}

///|
pub(all) struct BeforeResult {
  context : EvaluationContext
  error : SdkError?
}

///|
pub fn HookPlan::before(
  self : HookPlan,
  context : HookContext,
  hints : EvaluationContext,
) -> BeforeResult {
  let mut merged = context.evaluation_context.copy()
  for index, hook in self.hooks {
    let current = { ..context.with_context(merged), data: self.data[index], }
    match (hook.before)(current, hints.copy()) {
      Ok(None) => ()
      Ok(Some(additional)) => merged = merged.merge(additional)
      Err(error) => return { context: merged, error: Some(error), }
    }
  }
  { context: merged, error: None, }
}

///|
pub fn HookPlan::after(
  self : HookPlan,
  context : HookContext,
  details : EvaluationDetails,
  hints : EvaluationContext,
) -> Result[Unit, SdkError] {
  let mut index = self.hooks.length() - 1
  while index >= 0 {
    match
      (self.hooks[index].after)(
        { ..context.copy(), data: self.data[index], },
        details.copy(),
        hints.copy(),
      ) {
      Ok(_) => ()
      Err(error) => return Err(error)
    }
    index -= 1
  }
  Ok(())
}

///|
/// Errors raised by error/finally observers are collected, not recursively
/// propagated into evaluation. One broken observer cannot suppress the others.
pub fn HookPlan::on_error(
  self : HookPlan,
  context : HookContext,
  error : SdkError,
  hints : EvaluationContext,
) -> Array[SdkError] {
  let diagnostics = []
  let mut index = self.hooks.length() - 1
  while index >= 0 {
    match
      (self.hooks[index].error)(
        { ..context.copy(), data: self.data[index], },
        error,
        hints.copy(),
      ) {
      Ok(_) => ()
      Err(observer_error) => diagnostics.push(observer_error)
    }
    index -= 1
  }
  diagnostics
}

///|
pub fn HookPlan::run_finally(
  self : HookPlan,
  context : HookContext,
  details : EvaluationDetails,
  hints : EvaluationContext,
) -> Array[SdkError] {
  let diagnostics = []
  let mut index = self.hooks.length() - 1
  while index >= 0 {
    match
      (self.hooks[index].on_finally)(
        { ..context.copy(), data: self.data[index], },
        details.copy(),
        hints.copy(),
      ) {
      Ok(_) => ()
      Err(observer_error) => diagnostics.push(observer_error)
    }
    index -= 1
  }
  diagnostics
}