// ---------------------------------------------------------------------------
// ScriptedDecisionPort — fixed semantic-judgement script + request recording.
// ---------------------------------------------------------------------------

///|
pub(all) enum ScriptedDecisionStep {
  Return(@port.DecisionResult)
  Fail(@error.DecisionError)
}

///|
fn snapshot_optional_json(value : Json?) -> Json? {
  match value {
    Some(value) => Some(snapshot_json(value))
    None => None
  }
}

///|
fn snapshot_decision_question(
  question : @port.DecisionQuestion,
) -> @port.DecisionQuestion {
  match question {
    Boolean(id~, instructions~, true_criteria~, false_criteria~) =>
      Boolean(
        id~,
        instructions=snapshot_json(instructions),
        true_criteria=snapshot_optional_json(true_criteria),
        false_criteria=snapshot_optional_json(false_criteria),
      )
    Choice(id~, instructions~, options~) =>
      Choice(
        id~,
        instructions=snapshot_json(instructions),
        options=options.map(fn(option) {
          {
            id: option.id,
            description: snapshot_optional_json(option.description),
          }
        }),
      )
    Score(id~, instructions~, levels~) =>
      Score(
        id~,
        instructions=snapshot_json(instructions),
        levels=levels.map(snapshot_json),
      )
  }
}

///|
fn snapshot_decision_request(
  request : @port.DecisionRequest,
) -> @port.DecisionRequest {
  {
    state: snapshot_json(request.state),
    questions: request.questions.map(snapshot_decision_question),
  }
}

///|
fn snapshot_decision_answer(
  answer : @port.DecisionAnswer,
) -> @port.DecisionAnswer {
  match answer {
    BooleanAnswer(id~, probability_true~) =>
      BooleanAnswer(id~, probability_true~)
    ChoiceAnswer(id~, selected~, probabilities~, confidence~) =>
      ChoiceAnswer(
        id~,
        selected~,
        probabilities=probabilities.map(fn(entry) {
          { id: entry.id, probability: entry.probability, }
        }),
        confidence~,
      )
    ScoreAnswer(id~, score~, probabilities~, confidence~) =>
      ScoreAnswer(id~, score~, probabilities=probabilities.copy(), confidence~)
  }
}

///|
fn snapshot_decision_result(
  result : @port.DecisionResult,
) -> @port.DecisionResult {
  {
    answers: result.answers.map(snapshot_decision_answer),
    model: result.model,
    usage: result.usage,
  }
}

///|
fn snapshot_decision_step(step : ScriptedDecisionStep) -> ScriptedDecisionStep {
  match step {
    Return(result) => Return(snapshot_decision_result(result))
    Fail(error) => Fail(error)
  }
}

///|
/// DecisionPort fake that consumes one scripted step per evaluation and
/// records a deep snapshot of every request. Exhaustion is loud.
pub(all) struct ScriptedDecisionPort {
  steps : Array[ScriptedDecisionStep]
  mut index : Int
  received : Array[@port.DecisionRequest]
}

///|
pub fn ScriptedDecisionPort::ScriptedDecisionPort(
  steps : Array[ScriptedDecisionStep],
) -> ScriptedDecisionPort {
  { steps: steps.map(snapshot_decision_step), index: 0, received: [], }
}

///|
pub fn ScriptedDecisionPort::call_count(self : ScriptedDecisionPort) -> Int {
  self.received.length()
}

///|
pub fn ScriptedDecisionPort::received_requests(
  self : ScriptedDecisionPort,
) -> Array[@port.DecisionRequest] {
  self.received.map(snapshot_decision_request)
}

///|
pub async fn ScriptedDecisionPort::evaluate_direct(
  self : ScriptedDecisionPort,
  request : @port.DecisionRequest,
) -> @port.DecisionResult raise @error.DecisionError {
  (self as &@port.DecisionPort).evaluate(request)
}

///|
pub extend ScriptedDecisionPort with @port.DecisionPort::{evaluate}

///|
pub impl @port.DecisionPort for ScriptedDecisionPort with fn evaluate(
  self,
  request,
) -> @port.DecisionResult raise @error.DecisionError {
  self.received.push(snapshot_decision_request(request))
  if self.index >= self.steps.length() {
    raise @error.DecisionError::Transport(
      "scripted_decision_exhausted at call \{self.received.length()} (script had \{self.steps.length()} steps)",
    )
  }
  let step = self.steps[self.index]
  self.index = self.index + 1
  match step {
    Return(result) => snapshot_decision_result(result)
    Fail(error) => raise error
  }
}