///|
/// OpenAI/LLM provider adapter for RLM runtime.

///|
pub struct RLMOpenAIOptions {
  model : String
  max_tokens : Int
  system_prompt : String
  timeout_sec : Int
  max_retries : Int
}

///|
pub fn default_rlm_openai_options(
  model? : String = "gpt-4.1-mini",
  max_tokens? : Int = 4096,
  system_prompt? : String = "",
  timeout_sec? : Int = 300,
  max_retries? : Int = 3,
) -> RLMOpenAIOptions {
  { model, max_tokens, system_prompt, timeout_sec, max_retries }
}

///|
pub fn run_rlm_with_openai(
  prompt : String,
  api_key : String,
  rlm_options? : RLMRunOptions = default_rlm_options(),
  openai_options? : RLMOpenAIOptions = default_rlm_openai_options(),
) -> Result[RLMResultPack, String] {
  let provider = @llm_openai.OpenAIProvider::new(
    api_key,
    model=openai_options.model,
    max_tokens=openai_options.max_tokens,
    system_prompt=openai_options.system_prompt,
    timeout_sec=openai_options.timeout_sec,
    max_retries=openai_options.max_retries,
  )
  run_rlm_with_provider(prompt, provider, options=rlm_options)
}

///|
pub fn run_rlm_with_provider(
  prompt : String,
  provider : &@llm.Provider,
  options? : RLMRunOptions = default_rlm_options(),
) -> Result[RLMResultPack, String] {
  run_rlm_from_json(
    prompt,
    fn(messages, _depth) {
      let llm_messages = to_llm_messages(messages)
      @llm.collect_text(provider, llm_messages)
    },
    options~,
  )
}

///|
fn to_llm_messages(messages : Array[RLMChatMessage]) -> Array[@llm.Message] {
  let out : Array[@llm.Message] = []
  for message in messages {
    let mapped = match message.role {
      System => @llm.Message::system(message.content)
      User => @llm.Message::user(message.content)
      Assistant => @llm.Message::assistant(message.content)
    }
    out.push(mapped)
  }
  out
}

///|
pub fn default_planner_system_prompt() -> String {
  (
    #|You are an RLM planner.
    #|Convert user input to one JSON plan object.
    #|Output exactly one JSON object and nothing else.
    #|Schema:
    #|{
    #|  "mode": "single" | "long_run",
    #|  "task": string,
    #|  "profile"?: "pure" | "hybrid",
    #|  "symbols"?: string[],
    #|  "budget"?: { "maxSteps"?: number, "maxSubCalls"?: number, "maxDepth"?: number, "maxPromptReadChars"?: number },
    #|  "requirePromptReadBeforeFinalize"?: boolean,
    #|  "longRun"?: {
    #|    "objectives": [{ "key": string, "direction": "minimize"|"maximize", "weight"?: number }],
    #|    "constraints"?: [{ "key": string, "comparator": "lt"|"lte"|"gt"|"gte"|"eq", "value": number, "source"?: "absolute"|"delta"|"ratio"|"delta_ratio" }],
    #|    "maxIterations"?: number,
    #|    "stopWhenNoAccept"?: boolean,
    #|    "minScoreDelta"?: number
    #|  }
    #|}
    #|Use mode=single unless the task explicitly needs iterative metric optimization.
  )
}

///|
pub fn create_plan_with_provider(
  input : String,
  prompt : String,
  provider : &@llm.Provider,
  available_symbols? : Array[String] = [],
  planner_system_prompt? : String = default_planner_system_prompt(),
) -> RLMPlannerPlan {
  let messages : Array[@llm.Message] = [
    @llm.Message::system(planner_system_prompt),
    @llm.Message::user(
      build_planner_request_payload(input, prompt, available_symbols).stringify(),
    ),
  ]
  let text = @llm.collect_text(provider, messages)
  parse_planner_plan_text(text, input, available_symbols)
}

///|
pub fn[T, S] run_planned_rlm_with_provider(
  input : String,
  prompt : String,
  provider : &@llm.Provider,
  available_symbols? : Array[String] = [],
  runtime_options? : RLMRunOptions? = None,
  long_run? : PlannedLongRunHooks[T, S]? = None,
  planner_system_prompt? : String = default_planner_system_prompt(),
) -> Result[PlannedRLMResult[T, S], String] {
  run_planned_rlm_with_providers(
    input,
    prompt,
    provider,
    provider,
    available_symbols~,
    runtime_options~,
    long_run~,
    planner_system_prompt~,
  )
}

///|
pub fn[T, S] run_planned_rlm_with_providers(
  input : String,
  prompt : String,
  planner_provider : &@llm.Provider,
  executor_provider : &@llm.Provider,
  available_symbols? : Array[String] = [],
  runtime_options? : RLMRunOptions? = None,
  long_run? : PlannedLongRunHooks[T, S]? = None,
  planner_system_prompt? : String = default_planner_system_prompt(),
) -> Result[PlannedRLMResult[T, S], String] {
  let plan = create_plan_with_provider(
    input,
    prompt,
    planner_provider,
    available_symbols~,
    planner_system_prompt~,
  )

  match plan.mode {
    Single => {
      let options = compile_plan_to_rlm_options(plan, base=runtime_options)
      match
        run_rlm_from_json(
          prompt,
          fn(messages, _depth) {
            @llm.collect_text(executor_provider, to_llm_messages(messages))
          },
          options~,
        ) {
        Ok(result) => Ok(SingleResult(plan, result))
        Err(message) => Err(message)
      }
    }
    LongRun =>
      run_planned_rlm(
        prompt,
        fn(_messages, _depth) { PromptMeta },
        plan,
        runtime_options~,
        long_run~,
      )
  }
}

///|
pub fn[T, S] run_planned_rlm_with_openai(
  input : String,
  prompt : String,
  api_key : String,
  available_symbols? : Array[String] = [],
  runtime_options? : RLMRunOptions? = None,
  long_run? : PlannedLongRunHooks[T, S]? = None,
  planner_system_prompt? : String = default_planner_system_prompt(),
  planner_openai_options? : RLMOpenAIOptions = default_rlm_openai_options(),
  executor_openai_options? : RLMOpenAIOptions? = None,
) -> Result[PlannedRLMResult[T, S], String] {
  let planner_provider = @llm_openai.OpenAIProvider::new(
    api_key,
    model=planner_openai_options.model,
    max_tokens=planner_openai_options.max_tokens,
    system_prompt=planner_openai_options.system_prompt,
    timeout_sec=planner_openai_options.timeout_sec,
    max_retries=planner_openai_options.max_retries,
  )
  let exec_opts = match executor_openai_options {
    Some(v) => v
    None => planner_openai_options
  }
  let executor_provider = @llm_openai.OpenAIProvider::new(
    api_key,
    model=exec_opts.model,
    max_tokens=exec_opts.max_tokens,
    system_prompt=exec_opts.system_prompt,
    timeout_sec=exec_opts.timeout_sec,
    max_retries=exec_opts.max_retries,
  )
  run_planned_rlm_with_providers(
    input,
    prompt,
    planner_provider,
    executor_provider,
    available_symbols~,
    runtime_options~,
    long_run~,
    planner_system_prompt~,
  )
}

///|
fn parse_planner_plan_text(
  text : String,
  fallback_task : String,
  available_symbols : Array[String],
) -> RLMPlannerPlan {
  let parsed = try? @json.parse(text)
  match parsed {
    Ok(value) =>
      coerce_planner_plan_from_json(value, fallback_task, available_symbols)
    Err(_) =>
      match openai_extract_first_json_object_text(text) {
        Some(object_text) => {
          let nested = try? @json.parse(object_text)
          match nested {
            Ok(value) =>
              coerce_planner_plan_from_json(
                value, fallback_task, available_symbols,
              )
            Err(_) =>
              coerce_planner_plan_from_json(
                Json::null(),
                fallback_task,
                available_symbols,
              )
          }
        }
        None =>
          coerce_planner_plan_from_json(
            Json::null(),
            fallback_task,
            available_symbols,
          )
      }
  }
}

///|
fn build_planner_request_payload(
  input : String,
  prompt : String,
  available_symbols : Array[String],
) -> Json {
  let symbols : Array[Json] = []
  for symbol in available_symbols {
    symbols.push(Json::string(symbol))
  }

  Json::object({
    "kind": Json::string("rlm_plan_request"),
    "input": Json::string(input),
    "prompt": Json::object({
      "length": Json::number(prompt.length().to_double()),
      "preview": Json::string(openai_cut_text(prompt, 200)),
    }),
    "availableSymbols": Json::array(symbols),
  })
}

///|
fn openai_cut_text(value : String, max_chars : Int) -> String {
  if max_chars <= 0 {
    return ""
  }
  let end = openai_min_int(max_chars, value.length())
  if end <= 0 {
    ""
  } else {
    value.unsafe_substring(start=0, end~)
  }
}

///|
fn openai_min_int(a : Int, b : Int) -> Int {
  if a < b {
    a
  } else {
    b
  }
}

///|
fn openai_extract_first_json_object_text(text : String) -> String? {
  let chars = text.to_array()
  let mut start = -1
  let mut depth = 0
  let mut in_string = false
  let mut escaped = false

  for i in 0..