///|
/// A call of a denied Extism host function: the function and, for `http_request`, the method and the
/// URL of the refused request.
#derive(Eq, Debug)
pub(all) struct DeniedCall {
  function : String
  method_ : String?
  url : String?
}

///|
pub extend DeniedCall with Eq::{equal, not_equal}

///|
pub extend DeniedCall with @debug.Debug::{to_repr}

///|
/// The memory whose ceiling refused a grow.
#derive(Eq, Debug)
pub(all) enum MemoryKind {
  PluginMemory
  KernelMemory
}

///|
pub extend MemoryKind with Eq::{equal, not_equal}

///|
pub extend MemoryKind with @debug.Debug::{to_repr}

///|
/// An error of a plugin call.
#derive(Debug)
pub(all) suberror CallError {
  /// The export returned a code that is not 0, or set an error: the code and the error text.
  Plugin(Int32, String)
  /// The plugin called `proc_exit` with a code that is not 0.
  Exit(Int32)
  /// The plugin called an Extism host function that the policy denies.
  Denied(DeniedCall)
  /// A registered host function failed: the function and its message.
  HostFunction(String, String)
  /// A grow of the plugin or the kernel memory was refused, and the call failed.
  MemoryLimit(MemoryKind)
  /// The plugin went past a limit of the library: the name of the limit.
  Limit(String)
  /// The engine stopped the call: a trap, an exhausted budget, an interruption.
  Engine(@engine.CallError)
  /// The start-up of the guest runtime failed.
  Initialization(@engine.CallError)
  /// The plugin exports no function with this name.
  UnknownExport(String)
}

///|
pub extend CallError with @debug.Debug::{to_repr}

///|
/// Why a host function stopped the call.
priv enum Failure {
  Denied(DeniedCall)
  Limit(String)
  HostFunction(String, String)
  Exit(Int32)
}

///|
/// The state of one plugin that its host functions share.
priv struct PluginState {
  kernel : @engine.Instance
  policy : Policy
  variables : Map[String, Bytes]
  mut variable_bytes : Int32
  mut http_status : Int32
  mut http_headers : Map[String, String]?
  mut failure : Failure?
}

///|
fn PluginState::kernel_call(
  self : PluginState,
  function : @kernel.KernelFunction,
  args : Array[@engine.Value],
) -> Int64 raise @engine.CallError {
  match self.kernel.call(function.name(), args) {
    [I64(value)] => value
    [I32(value)] => value.to_int64()
    _ => 0L
  }
}

///|
fn PluginState::memory(self : PluginState) -> @engine.Memory {
  match self.kernel.memory() {
    Some(memory) => memory
    None => abort("the kernel has a memory")
  }
}

///|
/// The bytes of the block at `handle`; no bytes for handle 0 or an unknown handle.
fn PluginState::read(
  self : PluginState,
  handle : Int64,
) -> Bytes raise @engine.MemoryError {
  if handle == 0L {
    return b""
  }
  let length = self.kernel_call(Length, [I64(handle)]) catch {
    _ => raise OutOfBounds(handle, 0)
  }
  self.memory().read(handle, length.to_int())
}

///|
/// A new block with `bytes`; handle 0 for no bytes.
fn PluginState::allocate(
  self : PluginState,
  bytes : BytesView,
) -> Int64 raise @engine.MemoryError {
  if bytes.length() == 0 {
    return 0L
  }
  let handle = self.kernel_call(Alloc, [I64(bytes.length().to_int64())]) catch {
    _ => raise OutOfBounds(0L, bytes.length())
  }
  if handle == 0L {
    raise OutOfBounds(0L, bytes.length())
  }
  self.memory().write(handle, bytes)
  handle
}

///|
/// Records why the call stops and stops it.
fn PluginState::stop(
  self : PluginState,
  failure : Failure,
) -> @engine.HostError {
  self.failure = Some(failure)
  @engine.HostError("moonrockz/extism: the call stops")
}

///|
/// The text of the block at `handle`.
fn PluginState::text(
  self : PluginState,
  handle : Int64,
) -> String raise @engine.HostError {
  let bytes = self.read(handle) catch {
    OutOfBounds(_, _) =>
      raise @engine.HostError("the plugin gave a handle outside kernel memory")
  }
  @utf8.decode_lossy(bytes)
}

///|
fn handle_arg(args : Array[@engine.Value], index : Int32) -> Int64 {
  match args.get(index) {
    Some(I64(value)) => value
    _ => 0L
  }
}

///|
fn result_handle(handle : Int64) -> Array[@engine.Value] {
  [I64(handle)]
}

///|
fn PluginState::give(
  self : PluginState,
  bytes : BytesView,
) -> Array[@engine.Value] raise @engine.HostError {
  let handle = self.allocate(bytes) catch {
    OutOfBounds(_, _) =>
      raise @engine.HostError("kernel memory has no room for the result")
  }
  result_handle(handle)
}

///|
fn PluginState::config_get(
  self : PluginState,
  args : Array[@engine.Value],
) -> Array[@engine.Value] raise @engine.HostError {
  match self.policy.config {
    Deny =>
      raise self.stop(
        Denied({ function: "config_get", method_: None, url: None, }),
      )
    Empty => result_handle(0L)
    Values(values) =>
      match values.get(self.text(handle_arg(args, 0))) {
        Some(value) => self.give(@utf8.encode(value))
        None => result_handle(0L)
      }
  }
}

///|
fn PluginState::var_get(
  self : PluginState,
  args : Array[@engine.Value],
) -> Array[@engine.Value] raise @engine.HostError {
  match self.policy.variables {
    Deny =>
      raise self.stop(
        Denied({ function: "var_get", method_: None, url: None, }),
      )
    Empty => result_handle(0L)
    Session(_) =>
      match self.variables.get(self.text(handle_arg(args, 0))) {
        Some(value) => self.give(value)
        None => result_handle(0L)
      }
  }
}

///|
fn PluginState::var_set(
  self : PluginState,
  args : Array[@engine.Value],
) -> Array[@engine.Value] raise @engine.HostError {
  guard self.policy.variables is Session(limit) else {
    raise self.stop(Denied({ function: "var_set", method_: None, url: None, }))
  }
  let name = self.text(handle_arg(args, 0))
  let name_bytes = @utf8.encode(name).length()
  let old = match self.variables.get(name) {
    Some(value) => name_bytes + value.length()
    None => 0
  }
  let value_handle = handle_arg(args, 1)
  if value_handle == 0L {
    self.variables.remove(name)
    self.variable_bytes -= old
    return []
  }
  let value = self.read(value_handle) catch {
    OutOfBounds(_, _) =>
      raise @engine.HostError("the plugin gave a handle outside kernel memory")
  }
  let total = self.variable_bytes - old + name_bytes + value.length()
  if total > limit.bytes() {
    raise self.stop(Limit("variable store"))
  }
  self.variables[name] = value
  self.variable_bytes = total
  []
}

///|
fn PluginState::log(
  self : PluginState,
  level : LogLevel,
  args : Array[@engine.Value],
) -> Array[@engine.Value] raise @engine.HostError {
  if self.policy.logging is Sink(threshold, sink) &&
    level.code() >= threshold.code() {
    sink(level, self.text(handle_arg(args, 0)))
  }
  []
}

///|
fn PluginState::get_log_level(self : PluginState) -> Array[@engine.Value] {
  match self.policy.logging {
    Discard => [I32(logging_off)]
    Sink(level, _) => [I32(level.code())]
  }
}

///|
/// The method, the URL and the headers of the JSON request record of `http_request`.
fn http_request_of(text : String) -> (String, String, Map[String, String]) {
  let json = @json.parse(text) catch { _ => Json::null() }
  let method_ = match json {
    { "method": String(m), .. } => m
    _ => "GET"
  }
  let url = match json {
    { "url": String(u), .. } => u
    _ => ""
  }
  let headers : Map[String, String] = Map([])
  if json is { "headers": Object(fields), .. } {
    for name, value in fields {
      if value is String(text) {
        headers[name] = text
      }
    }
  }
  (method_, url, headers)
}

///|
fn PluginState::http_request(
  self : PluginState,
  args : Array[@engine.Value],
) -> Array[@engine.Value] raise @engine.HostError {
  // The policy comes first, so a denied call is Denied whatever the plugin gives. The method and the
  // URL are in the denial when the request can be read.
  guard self.policy.http is Handler(handler) else {
    let request = Some(http_request_of(self.text(handle_arg(args, 0)))) catch {
      _ => None
    }
    raise self.stop(
      Denied({
        function: "http_request",
        method_: request.map(r => r.0),
        url: request.map(r => r.1),
      }),
    )
  }
  let (method_, url, headers) = http_request_of(self.text(handle_arg(args, 0)))
  let body = self.read(handle_arg(args, 1)) catch {
    OutOfBounds(_, _) =>
      raise @engine.HostError("the plugin gave a handle outside kernel memory")
  }
  let response = handler({ method_, url, headers, body, })
  self.http_status = response.status
  self.http_headers = Some(response.headers)
  self.give(response.body)
}

///|
fn PluginState::http_headers(
  self : PluginState,
) -> Array[@engine.Value] raise @engine.HostError {
  match self.http_headers {
    Some(headers) => {
      let object : Map[String, Json] = Map([])
      for name, value in headers {
        object[name] = Json::string(value)
      }
      self.give(@utf8.encode(Json::object(object).stringify()))
    }
    None => result_handle(0L)
  }
}

///|
fn PluginState::extism(
  self : PluginState,
  function : ExtismFunction,
  args : Array[@engine.Value],
) -> Array[@engine.Value] raise @engine.HostError {
  match function {
    ConfigGet => self.config_get(args)
    VarGet => self.var_get(args)
    VarSet => self.var_set(args)
    LogTrace => self.log(Trace, args)
    LogDebug => self.log(Debug, args)
    LogInfo => self.log(Info, args)
    LogWarn => self.log(Warn, args)
    LogError => self.log(Error, args)
    GetLogLevel => self.get_log_level()
    HttpRequest => self.http_request(args)
    HttpStatusCode => [I32(self.http_status)]
    HttpHeaders => self.http_headers()
  }
}

///|
/// The bytes of the block at `handle` in kernel memory.
pub fn CallContext::read(
  self : CallContext,
  handle : Handle,
) -> Bytes raise @engine.MemoryError {
  self.plugin.read(handle.offset())
}

///|
/// A new block in kernel memory with `bytes`.
pub fn CallContext::allocate(
  self : CallContext,
  bytes : BytesView,
) -> Handle raise @engine.MemoryError {
  let offset = self.plugin.allocate(bytes)
  Handle::parse(offset) catch {
    _ => raise OutOfBounds(0L, bytes.length())
  }
}

///|
/// A loaded plugin: its own kernel instance and its instance, with the imports linked.
pub struct Plugin {
  priv state : PluginState
  priv instance : @engine.Instance
  priv limits : Limits
  priv bounds : @engine.Bounds
  priv mut initialized : Bool
}

///|
/// Loads the plugin of `manifest` on `engine`: the import check, the kernel instance, then the
/// plugin instance with each import linked.
pub fn Plugin::load(
  manifest : Manifest,
  engine~ : @engine.Engine = @engine.Engine::default(),
) -> Plugin raise LoadError {
  let module_ = engine.compile(manifest.module_) catch {
    error => raise Engine(error)
  }
  let links = manifest.links(module_.imports())
  let kernel = engine
    .compile(@kernel.bytes())
    .instantiate(
      @engine.Imports::new(),
      memory_ceiling=manifest.limits.kernel_memory,
    ) catch {
      error => raise Engine(error)
    }
  let state = {
    kernel,
    policy: manifest.policy,
    variables: Map([]),
    variable_bytes: 0,
    http_status: 0,
    http_headers: None,
    failure: None,
  }
  let imports = @engine.Imports::new()
  for index, import_ in module_.imports() {
    match links[index] {
      Kernel(function) =>
        match kernel.export_function(function.name()) {
          Some(exported) =>
            imports.function(import_.module_, import_.name, exported) |> ignore
          None => raise UnlinkedImport(import_.module_, import_.name)
        }
      Extism(function) =>
        imports.host(import_.module_, import_.name, function.signature(), (
          _,
          args,
        ) => state.extism(function, args))
        |> ignore
      Registered(function) => {
        let context = { plugin: state, }
        imports.host(import_.module_, import_.name, function.signature, (
          _,
          args,
        ) => {
          (function.function)(context, args) catch {
            HostError(message) =>
              raise state.stop(HostFunction(function.label(), message))
            Exit(code) =>
              raise state.stop(
                HostFunction(
                  function.label(),
                  "the function exits with \{code}",
                ),
              )
          }
        })
        |> ignore
      }
      Wasi(function) => {
        let sandbox = match manifest.sandbox {
          Some(sandbox) => sandbox
          None => @wasi.Sandbox::new()
        }
        let signature = wasi_signature(function)
        imports.host(import_.module_, import_.name, signature, (caller, args) => {
          guard caller.memory() is Some(memory) else {
            raise @engine.HostError("the plugin has no memory for WASI")
          }
          let values = args.map(v => {
            match v {
              I32(x) => x.to_int64()
              I64(x) => x
              F32(_) | F64(_) => 0L
            }
          })
          match sandbox.call(function, memory, values) {
            Return(errno) => [I32(errno.code())]
            Exit(code) => {
              state.failure = Some(Exit(code))
              raise @engine.Exit(code)
            }
          }
        })
        |> ignore
      }
    }
  }
  let instance = module_.instantiate(
    imports,
    memory_ceiling=manifest.limits.plugin_memory,
    budget?=manifest.limits.budget,
    deadline?=manifest.limits.deadline,
  ) catch {
    error => raise Engine(error)
  }
  {
    state,
    instance,
    limits: manifest.limits,
    bounds: engine.bounds(),
    initialized: false,
  }
}

///|
/// Whether the plugin exports a function with the name `name`. No guest code runs.
pub fn Plugin::has_function(self : Plugin, name : ExportName) -> Bool {
  self.instance.has_function(name.text())
}

///|
/// The bounds that the engine of the plugin enforces.
pub fn Plugin::bounds(self : Plugin) -> @engine.Bounds {
  self.bounds
}

///|
fn Plugin::export_is_unit(self : Plugin, name : String) -> Bool {
  match self.instance.export_function(name) {
    Some(function) => function.signature() == @engine.Signature::new([], [])
    None => false
  }
}

///|
/// Starts the guest runtime once, as upstream Extism does: `_initialize` and `hs_init(0, 0)` for a
/// Haskell module, else `__wasm_call_ctors`, else `_initialize`. A `__wasm_call_ctors` or an
/// `_initialize` of another type than `() -> ()` is not called, and a `__wasm_call_ctors` of another
/// type does not make `_initialize` run. Each call has the budget and the deadline of the limits. The
/// engine runs no start-up export itself.
fn Plugin::initialize(self : Plugin) -> Unit raise CallError {
  if self.initialized {
    return
  }
  self.initialized = true
  let run = (name : String, args : Array[@engine.Value]) => {
    (self.instance.call(
      name,
      args,
      budget?=self.limits.budget,
      deadline?=self.limits.deadline,
    )
    |> ignore) catch {
      error => raise Initialization(error)
    }
  }
  if self.instance.has_function("hs_init") {
    if self.export_is_unit("_initialize") {
      run("_initialize", [])
    }
    run("hs_init", [I32(0), I32(0)])
  } else if self.instance.has_function("__wasm_call_ctors") {
    if self.export_is_unit("__wasm_call_ctors") {
      run("__wasm_call_ctors", [])
    }
  } else if self.export_is_unit("_initialize") {
    run("_initialize", [])
  }
}

///|
/// The error text that the plugin set, as upstream decides: a handle that is not 0 with a length
/// that is not 0.
fn Plugin::error_text(self : Plugin) -> String? raise CallError {
  let handle = self.state.kernel_call(ErrorGet, []) catch {
    error => raise Engine(error)
  }
  if handle == 0L {
    return None
  }
  let length = self.state.kernel_call(Length, [I64(handle)]) catch {
    error => raise Engine(error)
  }
  if length == 0L {
    return None
  }
  let bytes = self.state.memory().read(handle, length.to_int()) catch {
      _ => return None
    }
  Some(@utf8.decode_lossy(bytes))
}

///|
fn Plugin::output(self : Plugin) -> Bytes raise CallError {
  let read = () => {
    let offset = self.state.kernel_call(OutputOffset, [])
    let length = self.state.kernel_call(OutputLength, [])
    if length == 0L {
      b""
    } else {
      self.state.memory().read(offset, length.to_int()) catch {
        _ => raise @engine.Trap(MemoryOutOfBounds)
      }
    }
  }
  read() catch {
    error => raise Engine(error)
  }
}

///|
/// Calls the export `name` with `input`: the kernel is reset, the input is put in kernel memory, the
/// export runs with the budget and the deadline of the limits, and the output bytes are returned when
/// it returns 0 and sets no error.
pub fn Plugin::call(
  self : Plugin,
  name : ExportName,
  input : BytesView,
) -> Bytes raise CallError {
  if !self.has_function(name) {
    raise UnknownExport(name.text())
  }
  self.initialize()
  let state = self.state
  state.failure = None
  state.kernel.take_refused_grow() |> ignore
  try {
    state.kernel_call(Reset, []) |> ignore
    if input.length() > 0 {
      let handle = state.kernel_call(Alloc, [I64(input.length().to_int64())])
      if handle == 0L {
        raise @engine.MemoryLimit
      }
      state.memory().write(handle, input) catch {
        _ => raise @engine.MemoryLimit
      }
      state.kernel_call(InputSet, [I64(handle), I64(input.length().to_int64())])
      |> ignore
    }
  } catch {
    MemoryLimit => raise MemoryLimit(KernelMemory)
    error => raise Engine(error)
  }
  let results = self.instance.call(
    name.text(),
    [],
    budget?=self.limits.budget,
    deadline?=self.limits.deadline,
  ) catch {
    error =>
      match (state.failure, error) {
        (Some(Exit(0)), Host(_)) =>
          if self.error_text() is Some(text) {
            raise Plugin(0, text)
          } else {
            return self.output()
          }
        (Some(Exit(code)), Host(_)) => raise Exit(code)
        (Some(Denied(call)), Host(_)) => raise Denied(call)
        (Some(Limit(limit)), Host(_)) => raise Limit(limit)
        (Some(HostFunction(function, message)), Host(_)) =>
          raise HostFunction(function, message)
        (_, MemoryLimit) => raise MemoryLimit(PluginMemory)
        _ =>
          if state.kernel.take_refused_grow() {
            raise MemoryLimit(KernelMemory)
          } else {
            raise Engine(error)
          }
      }
  }
  let code = match results {
    [I32(code)] => code
    _ => 0
  }
  match self.error_text() {
    Some(text) => raise Plugin(code, text)
    None => if code != 0 { raise Plugin(code, "") }
  }
  self.output()
}