//// Cache accounting when a session prompt changes.

import albedo/daemon/conversation
import albedo/daemon/events as view
import albedo/daemon/note
import albedo/daemon/session_history
import albedo/daemon/session_state
import albedo/harness/loop
import albedo/harness/runtime
import albedo/openai_api/types
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/result
import gleam/string

/// A changed prompt invalidates the provider's cached prefix, so the next
/// request uses the current prompt: any pin is released and cached-usage
/// metadata, which described the old prefix, is cleared.
pub fn reset_prompt_cache(
  state: session_state.State(message),
  namespace: String,
) -> session_state.State(message) {
  let ledger = runtime.ledger(state.host)
  let released = case state.pin {
    loop.Pinned(..) -> conversation.clear_prompt_pin(ledger, state.info.id)
    loop.Unpinned -> Ok(Nil)
  }
  // A failed release keeps the pin; the metadata it described is stale either way.
  let pin = case released {
    Ok(_) -> loop.Unpinned
    Error(_) -> state.pin
  }
  let state =
    session_state.State(
      ..state,
      pin: pin,
      prepared_head: None,
      latest_usage: None,
    )
  let state =
    session_state.emit(
      state,
      view.text(
        "note",
        "extensions reloaded; prompt cache usage reset; " <> namespace,
      ),
    )
  case released, conversation.clear_usage(ledger, state.info.id) {
    Ok(_), Ok(_) -> state
    Error(error), _ | _, Error(error) ->
      session_state.emit(
        state,
        view.text(
          "error",
          "extensions reloaded but the previous prompt cache state could not be cleared: "
            <> error,
        ),
      )
  }
}

fn context_blocks(context: List(types.Input)) -> List(#(String, String)) {
  list.filter_map(context, fn(input) {
    case input {
      types.User(text) -> {
        use rest <- result.try(string.split_once(
          text,
          "<extension-context name=\"",
        ))
        use named <- result.try(string.split_once(rest.1, "\""))
        Ok(#(named.0, text))
      }
      _ -> Error(Nil)
    }
  })
}

fn context_changes(
  previous: List(types.Input),
  current: List(types.Input),
) -> String {
  let before = context_blocks(previous)
  let after = context_blocks(current)
  let replaced =
    after
    |> list.filter(fn(block) { !list.contains(before, block) })
    |> list.map(fn(block) {
      "Current context for "
      <> block.0
      <> " (supersedes any earlier version):\n"
      <> block.1
    })
  let names = list.map(after, fn(block) { block.0 })
  let removed =
    before
    |> list.filter(fn(block) { !list.contains(names, block.0) })
    |> list.map(fn(block) {
      "Context removed: "
      <> block.0
      <> ". Its earlier instructions and catalog no longer apply."
    })
  let appendix = fn(context) {
    list.filter_map(context, fn(input) {
      case input {
        types.User(text) ->
          case string.starts_with(text, "<extension-context name=\"") {
            True -> Error(Nil)
            False -> Ok(text)
          }
        _ -> Error(Nil)
      }
    })
  }
  let before_append = appendix(previous)
  let after_append = appendix(current)
  let append_change = case before_append == after_append {
    True -> []
    False ->
      case after_append {
        [] -> ["APPEND_SYSTEM.md no longer applies."]
        _ -> [
          "Current APPEND_SYSTEM.md (supersedes earlier contents):\n"
          <> string.join(after_append, "\n\n"),
        ]
      }
  }
  list.append(list.append(replaced, removed), append_change)
  |> string.join("\n\n")
}

fn changes(previous: String, current: String) -> String {
  case previous == current {
    True -> ""
    False ->
      "Current system instructions (supersede earlier system instructions):\n"
      <> current
      <> "\n\n"
  }
}

/// Pin the old prompt until compaction, while reporting live capability changes.
pub fn pin_changed_prompt(
  state: session_state.State(message),
  previous: Option(#(String, List(types.Input))),
) -> Result(#(session_state.State(message), String), String) {
  let current = runtime.peek_prompt(state.host, state.info.id)
  case previous, current {
    Some(#(old_instructions, old_context)), Some(#(instructions, context))
      if old_instructions != instructions || old_context != context
    -> {
      use state <- result.try(session_history.ensure_history(state))
      case state.history {
        Some([_, ..]) -> {
          let #(pinned, head) = case state.pin {
            loop.Pinned(prompt, head) -> #(prompt, head)
            loop.Unpinned -> #(
              conversation.PinnedPrompt(old_instructions, old_context),
              state.prepared_head,
            )
          }
          let baseline = option.unwrap(head, 0)
          let update =
            note.wrap(
              "capabilities changed",
              "\nSession capabilities changed. The cached system prompt remains in use until compaction.\n"
                <> changes(old_instructions, instructions)
                <> context_changes(old_context, context)
                <> "\nCall commands.catalog() for the current command and skill catalog.\n",
            )
          use timestamp <- result.try(conversation.append_capability_update(
            runtime.ledger(state.host),
            state.info.id,
            pinned,
            baseline,
            update,
          ))
          let state =
            session_history.remember(state, [types.User(update)], timestamp)
          Ok(#(
            session_state.State(
              ..state,
              pin: loop.Pinned(pinned, Some(baseline)),
            ),
            "; the cached system prompt remains until compaction",
          ))
        }
        _ -> Ok(#(state, ""))
      }
    }
    _, _ -> Ok(#(state, ""))
  }
}
