#!/usr/bin/env nu
# store.nu — Nushell client for ontoref-daemon HTTP API.
#
# Provides cached nickel export via daemon (with subprocess fallback),
# plus query/sync/nodes/dimensions when daemon has DB enabled.
#
# All HTTP calls use ^curl (external command) because Nushell's internal
# http get/post cannot be captured with `| complete` on connection errors.
#
# Usage:
#   use ../modules/store.nu *
#
#   daemon-export ".ontology/core.ncl"                    # cached export
#   daemon-export ".ontology/core.ncl" --import-path $ip  # with NICKEL_IMPORT_PATH
#   store nodes --level Axiom                              # query ontology nodes
#   store dimensions                                       # query dimensions
#   daemon-health                                          # check daemon status

# ── Utilities (self-contained to avoid circular imports with shared.nu) ─────────

def project-root []: nothing -> string {
  let pr = ($env.ONTOREF_PROJECT_ROOT? | default "")
  if ($pr | is-not-empty) and ($pr != $env.ONTOREF_ROOT) { $pr } else { $env.ONTOREF_ROOT }
}

def nickel-import-path [root: string]: nothing -> string {
  let entries = [
    $"($root)/.ontology"
    $"($root)/adrs"
    $"($root)/.ontoref/ontology/schemas"
    $"($root)/.ontoref/adrs"
    $"($root)/.onref"
    $root
    $"($env.ONTOREF_ROOT)/ontology"
    $"($env.ONTOREF_ROOT)/ontology/schemas"
    $"($env.ONTOREF_ROOT)/adrs"
    $env.ONTOREF_ROOT
  ]
  let valid = ($entries | where { |p| $p | path exists } | uniq)
  let existing = ($env.NICKEL_IMPORT_PATH? | default "")
  if ($existing | is-not-empty) {
    ($valid | append $existing) | str join ":"
  } else {
    $valid | str join ":"
  }
}

# ── Configuration ────────────────────────────────────────────────────────────────

def daemon-url []: nothing -> string {
  $env.ONTOREF_DAEMON_URL? | default "http://127.0.0.1:7891"
}

# Load project config to check if DB is enabled
def project-config-db-status []: nothing -> record<enabled: bool, url: string, namespace: string> {
  let root = (project-root)
  let config_path = $"($root)/.ontoref/config.ncl"
  if not ($config_path | path exists) {
    return { enabled: false, url: "", namespace: "" }
  }

  # Export config and extract db section
  let result = (do { ^nickel export $config_path } | complete)
  if $result.exit_code != 0 {
    return { enabled: false, url: "", namespace: "" }
  }

  # Parse JSON safely
  let parse_result = (do { $result.stdout | from json } | complete)
  if $parse_result.exit_code != 0 {
    return { enabled: false, url: "", namespace: "" }
  }

  let config = $parse_result.stdout
  let db = ($config.db? | default {})
  {
    enabled: ($db.enabled? | default false),
    url: ($db.url? | default ""),
    namespace: ($db.namespace? | default "ontoref"),
  }
}

# ── HTTP helpers (external ^curl) ────────────────────────────────────────────────

def http-get [url: string]: nothing -> record {
  do { ^curl -sf $url } | complete
}

def http-post-json [url: string, body: string]: nothing -> record {
  do { ^curl -sf -X POST -H "Content-Type: application/json" -d $body $url } | complete
}

# ── Availability check ───────────────────────────────────────────────────────────

# Check if daemon is reachable. Caches "true" in env var for the session.
# A "false" result is NOT cached — allows recovery when daemon starts mid-session.
export def --env daemon-available []: nothing -> bool {
  let cached = ($env.ONTOREF_DAEMON_AVAILABLE? | default "")
  if $cached == "true" { return true }

  let url = $"(daemon-url)/health"
  let result = (http-get $url)
  if $result.exit_code == 0 {
    $env.ONTOREF_DAEMON_AVAILABLE = "true"
    true
  } else {
    false
  }
}

# ── Health ───────────────────────────────────────────────────────────────────────

# Show daemon health status with optional DB info from config.
# Returns record with { status, uptime_secs, cache_*, db_enabled?, db_config? }
# or null if daemon unreachable or response is not valid JSON.
export def daemon-health []: nothing -> any {
  let url = $"(daemon-url)/health"
  let result = (http-get $url)
  if $result.exit_code != 0 {
    null
  } else {
    let body = ($result.stdout | str trim)
    if not ($body | str starts-with "{") {
      null
    } else {
      let parse_result = (do { $body | from json } | complete)
      if $parse_result.exit_code != 0 {
        null
      } else {
        let health = $parse_result.stdout
        let db_config = (project-config-db-status)
        $health | insert db_config $db_config
      }
    }
  }
}

# ── NCL Export (core function) ───────────────────────────────────────────────────

# Export a Nickel file to JSON via daemon (cached) with subprocess fallback.
#
# When daemon is available: POST /nickel/export → cached result.
# When daemon is unreachable: falls back to ^nickel export subprocess.
# System works identically either way — just slower without daemon.
export def --env daemon-export [
  file: string,
  --import-path: string = "",
]: nothing -> any {
  let ip = if ($import_path | is-not-empty) {
    $import_path
  } else {
    nickel-import-path (project-root)
  }
  if (daemon-available) {
    let result = (daemon-export-http $file $ip)
    if $result != null { return $result }
    # HTTP call failed despite health check — clear cache to re-probe next call
    $env.ONTOREF_DAEMON_AVAILABLE = ""
  }
  daemon-export-subprocess $file $ip
}

# Safe version: returns null on failure instead of throwing.
# Use for call sites that handle missing data gracefully (return [] or {}).
export def --env daemon-export-safe [
  file: string,
  --import-path: string = "",
]: nothing -> any {
  if not ($file | path exists) { return null }
  let ip = if ($import_path | is-not-empty) {
    $import_path
  } else {
    nickel-import-path (project-root)
  }
  if (daemon-available) {
    let result = (daemon-export-http $file $ip)
    if $result != null { return $result }
    $env.ONTOREF_DAEMON_AVAILABLE = ""
  }
  let result = do { with-env { NICKEL_IMPORT_PATH: $ip } { ^nickel export $file } } | complete
  if $result.exit_code != 0 { return null }
  $result.stdout | from json
}

# ── Daemon HTTP calls ────────────────────────────────────────────────────────────

def daemon-export-http [file: string, import_path: string]: nothing -> any {
  let url = $"(daemon-url)/nickel/export"
  let body = if ($import_path | is-not-empty) {
    { path: $file, import_path: $import_path } | to json
  } else {
    { path: $file } | to json
  }
  let result = (http-post-json $url $body)
  if $result.exit_code != 0 { return null }
  let response = ($result.stdout | from json)
  $response.data? | default null
}

# ── Subprocess fallback ──────────────────────────────────────────────────────────

def daemon-export-subprocess [file: string, import_path: string]: nothing -> any {
  let ip = if ($import_path | is-not-empty) {
    $import_path
  } else {
    let root = (project-root)
    nickel-import-path $root
  }
  let result = do { with-env { NICKEL_IMPORT_PATH: $ip } { ^nickel export $file } } | complete
  if $result.exit_code != 0 {
    error make { msg: $"nickel export failed for ($file): ($result.stderr)" }
  }
  $result.stdout | from json
}

# ── Store query commands ─────────────────────────────────────────────────────────

# Query ontology nodes. Returns empty list on failure.
export def --env "store nodes" [
  --level: string = "",
]: nothing -> list {
  let root = (project-root)
  let core_file = $"($root)/.ontology/core.ncl"
  if not ($core_file | path exists) { return [] }
  let data = (daemon-export-safe $core_file)
  if $data == null { return [] }
  let nodes = ($data.nodes? | default [])
  if ($level | is-not-empty) {
    $nodes | where { |n| ($n.level? | default "") == $level }
  } else {
    $nodes
  }
}

# Query ontology dimensions. Returns empty list on failure.
export def --env "store dimensions" []: nothing -> list {
  let root = (project-root)
  let state_file = $"($root)/.ontology/state.ncl"
  if not ($state_file | path exists) { return [] }
  let data = (daemon-export-safe $state_file)
  if $data == null { return [] }
  $data.dimensions? | default []
}

# Query membranes. Returns empty list on failure.
export def --env "store membranes" [
  --all = false,
]: nothing -> list {
  let root = (project-root)
  let gate_file = $"($root)/.ontology/gate.ncl"
  if not ($gate_file | path exists) { return [] }
  let data = (daemon-export-safe $gate_file)
  if $data == null { return [] }
  let membranes = ($data.membranes? | default [])
  if $all {
    $membranes
  } else {
    $membranes | where { |m| ($m.active? | default false) == true }
  }
}

# ── Cache management ─────────────────────────────────────────────────────────────

# Show daemon cache statistics.
export def --env "store cache-stats" []: nothing -> any {
  if not (daemon-available) {
    print "  daemon not available"
    return null
  }
  let url = $"(daemon-url)/cache/stats"
  let result = (http-get $url)
  if $result.exit_code == 0 {
    $result.stdout | from json
  } else {
    null
  }
}

# Invalidate daemon cache (all entries or by prefix/file).
export def --env "store cache-invalidate" [
  --prefix: string = "",
  --file: string = "",
  --all = false,
]: nothing -> any {
  if not (daemon-available) {
    print "  daemon not available"
    return null
  }
  let url = $"(daemon-url)/cache/invalidate"
  if (not $all) and ($prefix | is-empty) and ($file | is-empty) {
    error make { msg: "cache-invalidate requires --all, --prefix, or --file" }
  }
  let body = if $all {
    { all: true } | to json
  } else if ($prefix | is-not-empty) {
    { prefix: $prefix } | to json
  } else {
    { file: $file } | to json
  }
  let result = (http-post-json $url $body)
  if $result.exit_code == 0 {
    $result.stdout | from json
  } else {
    null
  }
}
