#!/usr/bin/env nu
# reflection/modules/opmode.nu — operational mode detection, transition, and preflight.
#
# Mode is auto-detected on every invocation by comparing:
#   desired  = config.ncl mode field (what the project is configured for)
#   actual   = runtime connectivity probe (what is reachable right now)
#
# .ontoref/mode.lock  stores { mode, since } across invocations.
# state.ncl operational-mode dimension tracks the architectural intent.
#
# Command levels:
#   local      → no service checks. Never blocks on connectivity.
#   service    → daemon optional. Degrades gracefully if unreachable.
#   committed  → daemon + DB required. Fail-fast if unavailable.

use env.nu *
use store.nu [daemon-available]
use ../nulib/shared.nu [project-root]
use ./vcs.nu

# ── Internal helpers ──────────────────────────────────────────────────────────

def mode-lock-path []: nothing -> string {
  let root = (project-root)
  $"($root)/.ontoref/mode.lock"
}

def read-lock []: nothing -> record<mode: string, since: string> {
  let p = (mode-lock-path)
  if not ($p | path exists) { return { mode: "unknown", since: "" } }
  try { open $p | from json } catch { { mode: "unknown", since: "" } }
}

def write-lock [mode: string]: nothing -> nothing {
  let p = (mode-lock-path)
  { mode: $mode, since: (date now | format date "%Y-%m-%dT%H:%M:%SZ") } | to json | save -f $p
}

def desired-mode []: nothing -> string {
  let root = (project-root)
  let cfg  = $"($root)/.ontoref/config.ncl"
  if not ($cfg | path exists) { return "local" }
  let r = (do { ^nickel export --format json $cfg } | complete)
  if $r.exit_code != 0 { return "local" }
  try { $r.stdout | from json | get -o mode | default "local" | into string } catch { "local" }
}

def probe-actual [desired: string]: nothing -> string {
  if $desired == "local" { return "local" }
  if (daemon-available) { return "daemon" } else { return "local" }
}

# ── On-enter / on-exit actions ────────────────────────────────────────────────

def on-enter-daemon []: nothing -> nothing {
  # Push ontology projection to daemon DB
  let r = (do { ^nu -c $"use ($env.ONTOREF_ROOT)/reflection/modules/store.nu *; store sync-push" } | complete)
  if $r.exit_code == 0 {
    print $"  (ansi green)sync(ansi reset) ontology pushed to daemon"
  } else {
    print $"  (ansi yellow)sync(ansi reset) push failed — daemon up but export error"
  }
  # Update hooks to active mode
  (install-hooks "daemon")
}

def on-exit-daemon []: nothing -> nothing {
  (install-hooks "local")
}

def on-enter-local []: nothing -> nothing {
  (install-hooks "local")
}

def install-hooks [mode: string]: nothing -> nothing {
  match (vcs detect) {
    "jj" => {
      # jj has no hook system — hooks are dispatched by the jjw wrapper instead.
      # The jjw wrapper calls `on-vcs-event` from git-event.nu after relevant operations.
      print "  advisory: jj repo detected — skipping hook installation (no jj hook system)"
    },
    "git" => {
      let git_dir = (
        do { ^git rev-parse --git-dir } | complete
        | if $in.exit_code == 0 { $in.stdout | str trim } else { "" }
      )
      if ($git_dir | is-empty) { return }

      let hook_body = if $mode == "daemon" {
        $"#!/usr/bin/env bash\nnu \"($env.ONTOREF_ROOT)/reflection/hooks/git-event.nu\" \"$1\" 2>/dev/null || true\n"
      } else {
        "#!/usr/bin/env bash\n# ontoref local mode — no-op\nexit 0\n"
      }

      for hook in ["post-merge" "post-checkout"] {
        let path = $"($git_dir)/hooks/($hook)"
        $hook_body | save -f $path
        do { ^chmod +x $path } | complete | null
      }
    },
    _ => {} # no-op — no VCS detected
  }
}

# ── Notification helpers ──────────────────────────────────────────────────────

def notify-mode-change [from: string, to: string]: nothing -> nothing {
  let implications = match $to {
    "daemon" => "Push-based sync active. Hooks will push ontology on git merge/checkout.",
    "local"  => "File-only mode. Hooks are no-ops. DB projection may be stale.",
    _        => "",
  }
  print ""
  print $"  (ansi cyan_bold)Mode changed(ansi reset): (ansi yellow)($from)(ansi reset) → (ansi green)($to)(ansi reset)"
  if ($implications | is-not-empty) {
    print $"  (ansi dark_gray)($implications)(ansi reset)"
  }
  print ""

  # Emit NATS event if available (best-effort)
  let nats_script = $"($env.ONTOREF_ROOT)/reflection/modules/nats.nu"
  if ($nats_script | path exists) {
    do {
      ^nu -c $"use ($nats_script) *; nats-emit 'mode.changed' { from: '($from)', to: '($to)' }"
    } | complete | null
  }
}

# ── Public API ────────────────────────────────────────────────────────────────

# Detect actual operational mode and trigger transition if changed.
# Returns { desired, actual, previous, changed }.
export def "opmode detect" []: nothing -> record {
  let desired  = (desired-mode)
  let actual   = (probe-actual $desired)
  let previous = (read-lock).mode

  if $actual != $previous {
    match [$previous $actual] {
      ["daemon" "local"] => { on-exit-daemon },
      [_        "daemon"] => { on-enter-daemon },
      [_        "local"]  => { on-enter-local },
    }
    write-lock $actual
    if $previous != "unknown" {
      notify-mode-change $previous $actual
    }
  }

  { desired: $desired, actual: $actual, previous: $previous, changed: ($actual != $previous) }
}

# Show current mode status without triggering transitions.
export def "opmode status" []: nothing -> nothing {
  let desired  = (desired-mode)
  let lock     = (read-lock)
  let actual   = (probe-actual $desired)

  let drift_tag  = if ($desired != $actual) { $" (ansi yellow)⚠ drift(ansi reset)" } else { "" }
  let since_tag  = if ($lock.since | is-not-empty) { $" since ($lock.since)" } else { "" }
  let actual_line = $"(ansi cyan)($actual)(ansi reset)($drift_tag)"
  let locked_line = $"($lock.mode)($since_tag)"
  print ""
  print $"  (ansi white_bold)Operational Mode(ansi reset)"
  print $"  desired:  (ansi cyan)($desired)(ansi reset)"
  print $"  actual:   ($actual_line)"
  print $"  locked:   ($locked_line)"
  print ""
}

# Pre-flight check for dispatcher. Levels: local | service | committed.
# local     → no-op (always passes)
# service   → warn if daemon unreachable, continues
# committed → fail-fast if daemon or required services unreachable
export def "opmode preflight" [
  level: string,           # local | service | committed
  --require-db = false,    # also check DB reachability
  --require-nats = false,  # also check NATS reachability
]: nothing -> nothing {
  match $level {
    "local" => { return },
    "service" => {
      if not (daemon-available) {
        print $"  (ansi yellow)WARN(ansi reset) daemon unreachable — running in degraded mode"
      }
    },
    "committed" => {
      if not (daemon-available) {
        error make { msg: "daemon unreachable — this command requires an active daemon. Check ONTOREF_DAEMON_URL." }
      }
      if $require_db {
        let root = (project-root)
        let cfg_path = $"($root)/.ontoref/config.ncl"
        if ($cfg_path | path exists) {
          let r = (do { ^nickel export --format json $cfg_path } | complete)
          let db_enabled = if $r.exit_code == 0 {
            try { $r.stdout | from json | get -o db.enabled | default false } catch { false }
          } else { false }
          if not $db_enabled {
            error make { msg: "DB not enabled in .ontoref/config.ncl — this command requires db. Set db.enabled = true." }
          }
        }
      }
    },
    _ => {
      error make { msg: $"unknown preflight level: ($level). Use: local | service | committed" }
    },
  }
}
