// =========================================================================
// prompt_builder.mbt — PromptBuilder: context files + SOUL + guidance + assembly
// Prefix: pb_ for all private helpers to avoid cross-file collisions.
// =========================================================================

// ---------------------------------------------------------------------------
// Filesystem / path helpers — all cross-target via moonbitlang/x/fs plus
// a couple of pure-MoonBit path utilities. No `extern "js"` in this file.
// ---------------------------------------------------------------------------

fn pb_exists(path : String) -> Bool {
  @fs.path_exists(path)
}

fn pb_read_utf8(path : String) -> String {
  (try? @fs.read_file_to_string(path)).unwrap_or("")
}

fn pb_path_join2(a : String, b : String) -> String {
  if a.is_empty() {
    b
  } else if a.has_suffix("/") {
    a + b
  } else {
    a + "/" + b
  }
}

///| Returns "" if the path has no parent (i.e. already the filesystem
///| root or a segment-less name). Mirrors Node's path.dirname semantics.
fn pb_path_parent_or_empty(p : String) -> String {
  let view = p.view()
  let mut last = -1
  let mut i = 0
  while i < view.length() {
    if view.get_char(i) is Some('/') {
      last = i
    }
    i = i + 1
  }
  if last < 0 {
    // No slash → no parent.
    ""
  } else if last == 0 {
    // Root "/foo" → parent is "/". We return "" to match the prior
    // behavior (parent === p short-circuit when passed "/").
    if p == "/" {
      ""
    } else {
      "/"
    }
  } else {
    view[0:last].to_string()
  }
}

// ---------------------------------------------------------------------------
// Truncation constants
// ---------------------------------------------------------------------------

let pb_ctx_max_chars : Int = 20000
let pb_head_ratio_num : Int = 70   // 70/100 = 0.70
let pb_tail_ratio_num : Int = 20   // 20/100 = 0.20

pub fn pb_truncate_content(content : String) -> String {
  let len = content.length()
  if len <= pb_ctx_max_chars {
    return content
  }
  let head_chars = pb_ctx_max_chars * pb_head_ratio_num / 100
  let tail_chars = pb_ctx_max_chars * pb_tail_ratio_num / 100
  let skipped = len - head_chars - tail_chars
  let head = content[0:head_chars].to_string()
  let tail = content[len - tail_chars:len].to_string()
  let marker = "\n\n[... " + skipped.to_string() + " chars truncated ...]\n\n"
  head + marker + tail
}

// ---------------------------------------------------------------------------
// Context file discovery
// ---------------------------------------------------------------------------

// Read file, trim, return content or "" if absent/empty
fn pb_read_trimmed(path : String) -> String {
  let raw = pb_read_utf8(path)
  raw.trim().to_string()
}

// Walk up from start_dir to git root looking for .hermes.md / HERMES.md
fn pb_find_hermes_md_walk_up(start_dir : String) -> String {
  let mut current = start_dir
  let mut i = 0
  while i < 50 {
    // Check .hermes.md first, then HERMES.md
    let candidate1 = pb_path_join2(current, ".hermes.md")
    let c1 = pb_read_trimmed(candidate1)
    if !c1.is_empty() {
      return pb_truncate_content("## .hermes.md\n\n" + c1)
    }
    let candidate2 = pb_path_join2(current, "HERMES.md")
    let c2 = pb_read_trimmed(candidate2)
    if !c2.is_empty() {
      return pb_truncate_content("## HERMES.md\n\n" + c2)
    }
    // Stop if this is the git root (contains .git/)
    let git_dir = pb_path_join2(current, ".git")
    if pb_exists(git_dir) {
      break
    }
    let parent = pb_path_parent_or_empty(current)
    if parent.is_empty() || parent == current {
      break
    }
    current = parent
    i = i + 1
  }
  ""
}

// Returns the formatted context block (with ## header) or "" if not found
pub fn pb_find_context_file(cwd : String) -> String {
  // Priority 1: .hermes.md / HERMES.md (walk up)
  let hermes = pb_find_hermes_md_walk_up(cwd)
  if !hermes.is_empty() {
    return hermes
  }
  // Priority 2: AGENTS.md (cwd only)
  let agents = pb_read_trimmed(pb_path_join2(cwd, "AGENTS.md"))
  if !agents.is_empty() {
    return pb_truncate_content("## AGENTS.md\n\n" + agents)
  }
  let agents_lower = pb_read_trimmed(pb_path_join2(cwd, "agents.md"))
  if !agents_lower.is_empty() {
    return pb_truncate_content("## agents.md\n\n" + agents_lower)
  }
  // Priority 3: CLAUDE.md (cwd only)
  let claude = pb_read_trimmed(pb_path_join2(cwd, "CLAUDE.md"))
  if !claude.is_empty() {
    return pb_truncate_content("## CLAUDE.md\n\n" + claude)
  }
  let claude_lower = pb_read_trimmed(pb_path_join2(cwd, "claude.md"))
  if !claude_lower.is_empty() {
    return pb_truncate_content("## claude.md\n\n" + claude_lower)
  }
  // Priority 4: .cursorrules (cwd only)
  let cursorrules = pb_read_trimmed(pb_path_join2(cwd, ".cursorrules"))
  if !cursorrules.is_empty() {
    return pb_truncate_content("## .cursorrules\n\n" + cursorrules)
  }
  ""
}

// ---------------------------------------------------------------------------
// SOUL.md loader
// ---------------------------------------------------------------------------

pub fn pb_load_soul_md(hermes_home : String) -> String {
  let soul_path = pb_path_join2(hermes_home, "SOUL.md")
  let content = pb_read_trimmed(soul_path)
  if content.is_empty() {
    return ""
  }
  pb_truncate_content(content)
}

// ---------------------------------------------------------------------------
// Guidance constants (MVP subset — see design decisions for omissions)
// ---------------------------------------------------------------------------

let pb_agent_identity : String =
  "You are Ermes Agent, an intelligent AI assistant. " +
  "You are helpful, knowledgeable, and direct. You assist users with a wide " +
  "range of tasks including answering questions, writing and editing code, " +
  "analyzing information, creative work, and executing actions via your tools. " +
  "You communicate clearly, admit uncertainty when appropriate, and prioritize " +
  "being genuinely useful over being verbose unless otherwise directed below. " +
  "Be targeted and efficient in your exploration and investigations."

let pb_memory_guidance : String =
  "You have persistent memory across sessions. Save durable facts using the memory " +
  "tool: user preferences, environment details, tool quirks, and stable conventions. " +
  "Memory is injected into every turn, so keep it compact and focused on facts that " +
  "will still matter later.\n" +
  "Prioritize what reduces future user steering — the most valuable memory is one " +
  "that prevents the user from having to correct or remind you again. " +
  "User preferences and recurring corrections matter more than procedural task details.\n" +
  "Do NOT save task progress, session outcomes, completed-work logs, or temporary TODO " +
  "state to memory; use session_search to recall those from past transcripts. " +
  "If you've discovered a new way to do something, solved a problem that could be " +
  "necessary later, save it as a skill with the skill tool.\n" +
  "Write memories as declarative facts, not instructions to yourself. " +
  "'User prefers concise responses' ✓ — 'Always respond concisely' ✗."

let pb_session_search_guidance : String =
  "When the user references something from a past conversation or you suspect " +
  "relevant cross-session context exists, use session_search to recall it before " +
  "asking them to repeat themselves."

let pb_skills_guidance : String =
  "After completing a complex task (5+ tool calls), fixing a tricky error, " +
  "or discovering a non-trivial workflow, save the approach as a " +
  "skill with skill_manage so you can reuse it next time.\n" +
  "When using a skill and finding it outdated, incomplete, or wrong, " +
  "patch it immediately with skill_manage(action='patch') — don't wait to be asked. " +
  "Skills that aren't maintained become liabilities."

let pb_tool_use_enforcement : String =
  "# Tool-use enforcement\n" +
  "You MUST use your tools to take action — do not describe what you would do " +
  "or plan to do without actually doing it. When you say you will perform an " +
  "action (e.g. 'I will run the tests', 'Let me check the file', 'I will create " +
  "the project'), you MUST immediately make the corresponding tool call in the same " +
  "response. Never end your turn with a promise of future action — execute it now.\n" +
  "Keep working until the task is actually complete. Do not stop with a summary of " +
  "what you plan to do next time.\n" +
  "Every response should either (a) contain tool calls that make progress, or " +
  "(b) deliver a final result to the user. Responses that only describe intentions " +
  "without acting are not acceptable."

// MVP platform hints: cli + cron only. Others are commented for Phase 6.
fn pb_get_platform_hint(platform : String) -> String {
  match platform {
    "cli" =>
      "You are a CLI AI Agent. Try not to use markdown but simple text " +
      "renderable inside a terminal."
    "cron" =>
      "You are running as a scheduled cron job. There is no user present — you " +
      "cannot ask questions, request clarification, or wait for follow-up. Execute " +
      "the task fully and autonomously, making reasonable decisions where needed. " +
      "Your final response is automatically delivered to the job's configured " +
      "destination — put the primary content directly in your response."
    _ => ""
  }
}

// OpenAI-specific execution guidance (gpt / codex models).
// Ported verbatim from hermes-agent/agent/prompt_builder.py OPENAI_MODEL_EXECUTION_GUIDANCE.
let pb_openai_guidance : String =
  "# Execution discipline\n" +
  "\n" +
  "- Use tools whenever they improve correctness, completeness, or grounding.\n" +
  "- Do not stop early when another tool call would materially improve the result.\n" +
  "- If a tool returns empty or partial results, retry with a different query or " +
  "strategy before giving up.\n" +
  "- Keep calling tools until: (1) the task is complete, AND (2) you have verified " +
  "the result.\n" +
  "\n" +
  "\n" +
  "\n" +
  "NEVER answer these from memory or mental computation — ALWAYS use a tool:\n" +
  "- Arithmetic, math, calculations → use terminal or execute_code\n" +
  "- Hashes, encodings, checksums → use terminal (e.g. sha256sum, base64)\n" +
  "- Current time, date, timezone → use terminal (e.g. date)\n" +
  "- System state: OS, CPU, memory, disk, ports, processes → use terminal\n" +
  "- File contents, sizes, line counts → use read_file, search_files, or terminal\n" +
  "- Git history, branches, diffs → use terminal\n" +
  "- Current facts (weather, news, versions) → use web_search\n" +
  "Your memory and user profile describe the USER, not the system you are " +
  "running on. The execution environment may differ from what the user profile " +
  "says about their personal setup.\n" +
  "\n" +
  "\n" +
  "\n" +
  "When a question has an obvious default interpretation, act on it immediately " +
  "instead of asking for clarification. Examples:\n" +
  "- 'Is port 443 open?' → check THIS machine (don't ask 'open where?')\n" +
  "- 'What OS am I running?' → check the live system (don't use user profile)\n" +
  "- 'What time is it?' → run `date` (don't guess)\n" +
  "Only ask for clarification when the ambiguity genuinely changes what tool " +
  "you would call.\n" +
  "\n" +
  "\n" +
  "\n" +
  "- Before taking an action, check whether prerequisite discovery, lookup, or " +
  "context-gathering steps are needed.\n" +
  "- Do not skip prerequisite steps just because the final action seems obvious.\n" +
  "- If a task depends on output from a prior step, resolve that dependency first.\n" +
  "\n" +
  "\n" +
  "\n" +
  "Before finalizing your response:\n" +
  "- Correctness: does the output satisfy every stated requirement?\n" +
  "- Grounding: are factual claims backed by tool outputs or provided context?\n" +
  "- Formatting: does the output match the requested format or schema?\n" +
  "- Safety: if the next step has side effects (file writes, commands, API calls), " +
  "confirm scope before executing.\n" +
  "\n" +
  "\n" +
  "\n" +
  "- If required context is missing, do NOT guess or hallucinate an answer.\n" +
  "- Use the appropriate lookup tool when missing information is retrievable " +
  "(search_files, web_search, read_file, etc.).\n" +
  "- Ask a clarifying question only when the information cannot be retrieved by tools.\n" +
  "- If you must proceed with incomplete information, label assumptions explicitly.\n" +
  ""

// Google-specific operational guidance (gemini / gemma models).
// Ported verbatim from hermes-agent/agent/prompt_builder.py GOOGLE_MODEL_OPERATIONAL_GUIDANCE.
let pb_google_guidance : String =
  "# Google model operational directives\n" +
  "Follow these operational rules strictly:\n" +
  "- **Absolute paths:** Always construct and use absolute file paths for all " +
  "file system operations. Combine the project root with relative paths.\n" +
  "- **Verify first:** Use read_file/search_files to check file contents and " +
  "project structure before making changes. Never guess at file contents.\n" +
  "- **Dependency checks:** Never assume a library is available. Check " +
  "package.json, requirements.txt, Cargo.toml, etc. before importing.\n" +
  "- **Conciseness:** Keep explanatory text brief — a few sentences, not " +
  "paragraphs. Focus on actions and results over narration.\n" +
  "- **Parallel tool calls:** When you need to perform multiple independent " +
  "operations (e.g. reading several files), make all the tool calls in a " +
  "single response rather than sequentially.\n" +
  "- **Non-interactive commands:** Use flags like -y, --yes, --non-interactive " +
  "to prevent CLI tools from hanging on prompts.\n" +
  "- **Keep going:** Work autonomously until the task is fully resolved. " +
  "Don't stop with a plan — execute it.\n"

// Returns OpenAI guidance if model contains "gpt" or "codex", else ""
fn pb_get_openai_guidance(model : String) -> String {
  let lower = model.to_lower()
  if lower.contains("gpt") || lower.contains("codex") {
    pb_openai_guidance
  } else {
    ""
  }
}

// Returns Google guidance if model contains "gemini" or "gemma", else ""
fn pb_get_google_guidance(model : String) -> String {
  let lower = model.to_lower()
  if lower.contains("gemini") || lower.contains("gemma") {
    pb_google_guidance
  } else {
    ""
  }
}

// Returns true if model name (lowercase substring) triggers enforcement injection
fn pb_needs_tool_enforcement(model : String) -> Bool {
  let lower = model.to_lower()
  lower.contains("gpt") || lower.contains("codex") ||
  lower.contains("gemini") || lower.contains("gemma") || lower.contains("grok")
}

// ---------------------------------------------------------------------------
// Index-of helper (Iter has no find/position — manual loop)
// ---------------------------------------------------------------------------

fn pb_index_of(s : String, needle : String) -> Int {
  let slen = s.length()
  let nlen = needle.length()
  if nlen == 0 || nlen > slen {
    return -1
  }
  let mut i = 0
  while i <= slen - nlen {
    let slice = s[i:i + nlen].to_string()
    if slice == needle {
      return i
    }
    i = i + 1
  }
  -1
}

// ---------------------------------------------------------------------------
// Section joiner — non-empty parts joined with "\n\n"
// ---------------------------------------------------------------------------

fn pb_join_sections(parts : Array[String]) -> String {
  let mut result = ""
  let mut first = true
  for part in parts {
    if !part.trim().to_string().is_empty() {
      if first {
        result = part
        first = false
      } else {
        result = result + "\n\n" + part
      }
    }
  }
  result
}

// ---------------------------------------------------------------------------
// MemoryStore snapshot — delegate to MemoryStore in memory_store.mbt
// ---------------------------------------------------------------------------

fn pb_memory_snapshot(hermes_home : String) -> String {
  // NOTE: The memory store is loaded in read-only mode here (no writes).
  // We call load() and snapshot() from memory_store.mbt.
  // If the memories dir doesn't exist yet, load() creates it and returns empty.
  let store = MemoryStore::load(hermes_home)
  let mem = store.snapshot(Memory)
  let usr = store.snapshot(User)
  if mem.is_empty() && usr.is_empty() {
    return ""
  }
  if mem.is_empty() {
    return usr
  }
  if usr.is_empty() {
    return mem
  }
  mem + "\n\n" + usr
}

// ---------------------------------------------------------------------------
// build_system_prompt — main entry point
// ---------------------------------------------------------------------------

pub struct BuildPromptOptions {
  hermes_home : String
  cwd : String
  platform : String
  model : String
  available_tools : Array[String]
  disabled_skills : Array[String]
}

pub fn build_system_prompt(opts : BuildPromptOptions) -> String {
  // [1] Identity
  let identity = pb_agent_identity

  // [2] Guidance block
  let guidance = pb_join_sections([
    pb_memory_guidance,
    pb_session_search_guidance,
    pb_skills_guidance,
  ])

  // [3] Tool-use enforcement (model-gated)
  let enforcement = if pb_needs_tool_enforcement(opts.model) {
    pb_tool_use_enforcement
  } else {
    ""
  }

  // [3a] OpenAI-specific guidance (gpt / codex models)
  let openai_guidance = pb_get_openai_guidance(opts.model)

  // [3b] Google-specific guidance (gemini / gemma models)
  let google_guidance = pb_get_google_guidance(opts.model)

  // [4] Platform hint
  let platform_hint = pb_get_platform_hint(opts.platform)

  // [5+6] Memory + User blocks
  let mem_blocks = pb_memory_snapshot(opts.hermes_home)

  // [7] Skills block (delegated to skills_store.mbt if available)
  let skills_block = pb_build_skills_block(
    opts.hermes_home, opts.available_tools, opts.disabled_skills,
  )

  // [8] Context file
  let ctx_raw = pb_find_context_file(opts.cwd)
  let context_block = if ctx_raw.is_empty() {
    ""
  } else {
    "# Project Context\n\n" +
    "The following project context files have been loaded and should be followed:\n\n" +
    ctx_raw
  }

  // [9] SOUL.md
  let soul_raw = pb_load_soul_md(opts.hermes_home)
  let soul_block = if soul_raw.is_empty() {
    ""
  } else {
    "## SOUL.md\n\n" + soul_raw
  }

  pb_join_sections([
    identity,
    guidance,
    enforcement,
    openai_guidance,
    google_guidance,
    platform_hint,
    mem_blocks,
    skills_block,
    context_block,
    soul_block,
  ])
}

// ---------------------------------------------------------------------------
// Skills block builder — delegates to skills_store_build_prompt
// ---------------------------------------------------------------------------

fn pb_build_skills_block(
  hermes_home : String,
  available_tools : Array[String],
  disabled : Array[String]
) -> String {
  let skills_dir = pb_path_join2(hermes_home, "skills")
  if !pb_exists(skills_dir) {
    return ""
  }
  let store = skills_store_load(skills_dir, [])
  let tools_opt : Option[Array[String]] = if available_tools.length() == 0 {
    None
  } else {
    Some(available_tools)
  }
  skills_store_build_prompt(store, tools_opt, None, disabled=disabled)
}