# reflection/nulib/modes.nu — Mode listing, detail, rendering, and execution.

use ./shared.nu [all-mode-files, project-root]
use ./help.nu [help-group]
use ../modules/store.nu [daemon-export, daemon-export-safe]

# ── List / Show ──────────────────────────────────────────────────────────────

export def list-modes []: nothing -> list<record> {
  let mode_files = (all-mode-files)
  $mode_files | each { |mf|
    let m = (daemon-export-safe $mf)
    if $m != null {
      {
        id: ($m.id? | default ""),
        trigger: ($m.trigger? | default ""),
        steps: ($m.steps? | default [] | length),
        preconditions: ($m.preconditions? | default [] | length),
      }
    } else { null }
  } | compact
}

export def show-mode [id: string, fmt_resolved: string] {
  let mode_files = (all-mode-files)
  let candidates = ($mode_files | where { |p| ($p | path basename | str replace ".ncl" "") == $id })
  if ($candidates | is-empty) {
    let available = ($mode_files | each { |p| $p | path basename | str replace ".ncl" "" })
    error make { msg: $"Mode '($id)' not found. Available: ($available | str join ', ')" }
  }
  let m = (daemon-export ($candidates | first))
  match $fmt_resolved {
    "json"  => { $m | to json },
    "yaml"  => { $m | to yaml },
    "toml"  => { $m | to toml },
    "table" => { $m | table --expand },
    _       => { mode-to-md $m },
  }
}

def mode-to-md [m: record]: nothing -> string {
  mut lines = []
  $lines = ($lines | append $"# ($m.id)")
  $lines = ($lines | append "")
  $lines = ($lines | append $"**Trigger**: ($m.trigger)")
  $lines = ($lines | append "")
  if ($m.preconditions? | is-not-empty) and (($m.preconditions | length) > 0) {
    $lines = ($lines | append "## Preconditions")
    for p in $m.preconditions { $lines = ($lines | append $"- ($p)") }
    $lines = ($lines | append "")
  }
  $lines = ($lines | append "## Steps")
  for s in $m.steps {
    $lines = ($lines | append $"### ($s.id) [($s.actor)]")
    $lines = ($lines | append $s.action)
    if ($s.cmd? | is-not-empty) { $lines = ($lines | append $"```\n($s.cmd)\n```") }
    if ($s.depends_on? | is-not-empty) and (($s.depends_on | length) > 0) {
      $lines = ($lines | append $"Depends on: ($s.depends_on | each { |d| $d.step } | str join ', ')")
    }
    $lines = ($lines | append $"On error: ($s.on_error.strategy)")
    $lines = ($lines | append "")
  }
  if ($m.postconditions? | is-not-empty) and (($m.postconditions | length) > 0) {
    $lines = ($lines | append "## Postconditions")
    for p in $m.postconditions { $lines = ($lines | append $"- ($p)") }
  }
  $lines | str join "\n"
}

export def run-modes-interactive [modes: list<record>] {
  let ids = ($modes | get id)
  let picked = ($ids | input list $"(ansi cyan_bold)Mode:(ansi reset) ")
  if ($picked | is-not-empty) {
    let actor = ($env.ONTOREF_ACTOR? | default "developer")
    let f = if $actor == "agent" { "json" } else { "md" }
    show-mode $picked $f
  }
}

# ── Authorization ────────────────────────────────────────────────────────────

# Load mode_run config from .ontoref/config.ncl.
def load-mode-run-config [root: string]: nothing -> record {
  let defaults = {
    rules: [],
    default_allow: false,
    confirm_each_step: true,
  }

  let config_file = $"($root)/.ontoref/config.ncl"
  if not ($config_file | path exists) { return $defaults }

  let cfg = (daemon-export-safe $config_file)
  if $cfg == null { return $defaults }

  let mr = ($cfg.mode_run? | default {})
  {
    rules:             ($mr.rules?             | default $defaults.rules),
    default_allow:     ($mr.default_allow?     | default $defaults.default_allow),
    confirm_each_step: ($mr.confirm_each_step? | default $defaults.confirm_each_step),
  }
}

# Load profile name from .ontoref/config.ncl.
def load-profile [root: string]: nothing -> string {
  let config_file = $"($root)/.ontoref/config.ncl"
  if not ($config_file | path exists) { return "unknown" }

  let cfg = (daemon-export-safe $config_file)
  if $cfg == null { return "unknown" }

  $cfg.profile? | default "unknown"
}

# Check if a single rule's `when` clause matches the context.
# All present fields must match (AND). Missing fields = don't care.
def rule-matches [when_clause: record, context: record]: nothing -> bool {
  let profile_ok = if ($when_clause.profile? | is-not-empty) {
    ($when_clause.profile == $context.profile)
  } else { true }

  let actor_ok = if ($when_clause.actor? | is-not-empty) {
    ($when_clause.actor == $context.actor)
  } else { true }

  let mode_ok = if ($when_clause.mode_id? | is-not-empty) {
    ($when_clause.mode_id == $context.mode_id)
  } else { true }

  $profile_ok and $actor_ok and $mode_ok
}

# Evaluate authorization rules. Returns { allowed: bool, reason: string }.
def authorize-mode [mode_id: string, root: string]: nothing -> record {
  let mr_cfg = (load-mode-run-config $root)
  let profile = (load-profile $root)
  let actor = ($env.ONTOREF_ACTOR? | default "developer")

  let context = { profile: $profile, actor: $actor, mode_id: $mode_id }

  # First matching rule wins.
  mut result = { allowed: $mr_cfg.default_allow, reason: "no matching rule — default policy" }
  for rule in $mr_cfg.rules {
    if (rule-matches $rule.when $context) {
      let reason = if ($rule.reason? | is-not-empty) { $rule.reason } else { "matched rule" }
      $result = { allowed: $rule.allow, reason: $reason }
      break
    }
  }
  $result
}

# ── Runner ───────────────────────────────────────────────────────────────────

# Load a mode by id, returning the full NCL-exported record.
def load-mode [id: string]: nothing -> record {
  let mode_files = (all-mode-files)
  let candidates = ($mode_files | where { |p| ($p | path basename | str replace ".ncl" "") == $id })
  if ($candidates | is-empty) {
    let available = ($mode_files | each { |p| $p | path basename | str replace ".ncl" "" })
    error make { msg: $"Mode '($id)' not found. Available: ($available | str join ', ')" }
  }
  daemon-export ($candidates | first)
}

# Check if the current actor can execute a step based on the step's actor field.
def actor-can-run-step [step_actor: string]: nothing -> bool {
  let current = ($env.ONTOREF_ACTOR? | default "developer")
  match $step_actor {
    "Both" => true,
    "Human" => ($current == "developer" or $current == "admin"),
    "Agent" => ($current == "agent" or $current == "ci"),
    _ => true,
  }
}

# Execute a single step's command. Returns { success: bool, output: string }.
def exec-step-cmd [cmd: string]: nothing -> record {
  let nu_patterns = ["| from json", "| get ", "| where ", "| each ", "| select ", "| sort-by"]
  let is_nu = ($nu_patterns | any { |p| $cmd | str contains $p })
  let result = if $is_nu {
    do { ^nu -c $cmd } | complete
  } else {
    do { ^bash -c $cmd } | complete
  }
  {
    success: ($result.exit_code == 0),
    output: (if $result.exit_code == 0 { $result.stdout } else { $result.stderr }),
  }
}

# Print step summary before execution.
def print-step-header [step: record, index: int, total: int] {
  let actor_color = match ($step.actor? | default "Both") {
    "Human" => (ansi yellow),
    "Agent" => (ansi magenta),
    _       => (ansi cyan),
  }
  print $"  (ansi white_bold)\(($index + 1)/($total)\)(ansi reset) (ansi cyan_bold)($step.id)(ansi reset) (ansi dark_gray)[($actor_color)($step.actor? | default 'Both')(ansi reset)(ansi dark_gray)](ansi reset)"
  print $"  ($step.action? | default '')"
  if ($step.cmd? | is-not-empty) {
    print $"  (ansi dark_gray)$ ($step.cmd)(ansi reset)"
  }
}

# Run a mode: authorize → preconditions → steps → postconditions.
export def run-mode [id: string, --dry-run, --yes] {
  let root = (project-root)

  # ── Load mode ──
  let m = (load-mode $id)

  # ── Authorization ──
  let auth = (authorize-mode $id $root)
  if not $auth.allowed {
    let profile = (load-profile $root)
    let actor = ($env.ONTOREF_ACTOR? | default "developer")
    let steps = ($m.steps? | default [])

    print ""
    print $"  (ansi white_bold)($m.id)(ansi reset)  (ansi dark_gray)($steps | length) steps(ansi reset)"
    print $"  ($m.trigger? | default '')"
    print ""

    # Show steps with their commands so the user knows what to run manually.
    if ($steps | is-not-empty) {
      print $"  (ansi white_bold)Steps \(manual execution\):(ansi reset)"
      for step in ($steps | enumerate) {
        let s = $step.item
        let cmd_display = if ($s.cmd? | is-not-empty) { $"(ansi dark_gray)$ ($s.cmd)(ansi reset)" } else { $"(ansi dark_gray)— informational(ansi reset)" }
        print $"  (ansi dark_gray)($step.index + 1).(ansi reset) (ansi cyan)($s.id)(ansi reset) [($s.actor? | default 'Both')]  ($cmd_display)"
        print $"     ($s.action? | default '')"
      }
      print ""
    }

    print $"  (ansi red_bold)DENIED(ansi reset)  Automated execution blocked."
    print $"  (ansi dark_gray)Reason: ($auth.reason)(ansi reset)"
    print $"  (ansi dark_gray)Context: profile=($profile), actor=($actor)(ansi reset)"
    print $"  (ansi dark_gray)Configure in .ontoref/config.ncl → mode_run.rules(ansi reset)"
    print ""
    return
  }
  let steps = ($m.steps? | default [])
  let preconditions = ($m.preconditions? | default [])
  let postconditions = ($m.postconditions? | default [])
  let mr_cfg = (load-mode-run-config $root)

  print ""
  print $"  (ansi green_bold)AUTHORIZED(ansi reset)  ($auth.reason)"
  print $"  (ansi white_bold)Mode:(ansi reset) ($m.id)  (ansi dark_gray)($steps | length) steps(ansi reset)"
  print $"  ($m.trigger? | default '')"
  print ""

  # ── Preconditions ──
  if ($preconditions | is-not-empty) {
    print $"  (ansi white_bold)Preconditions(ansi reset)"
    for p in $preconditions {
      print $"  (ansi dark_gray)●(ansi reset) ($p)"
    }
    print ""
  }

  if $dry_run {
    print $"  (ansi yellow_bold)DRY RUN(ansi reset) — showing steps without executing"
    print ""
    for step in ($steps | enumerate) {
      print-step-header $step.item $step.index ($steps | length)
      print ""
    }
    if ($postconditions | is-not-empty) {
      print $"  (ansi white_bold)Postconditions(ansi reset)"
      for p in $postconditions { print $"  (ansi dark_gray)●(ansi reset) ($p)" }
    }
    return
  }

  # ── Execute steps ──
  mut failed_steps = []
  for step in ($steps | enumerate) {
    let s = $step.item
    let idx = $step.index

    # Skip steps whose actor doesn't match current actor.
    if not (actor-can-run-step ($s.actor? | default "Both")) {
      print $"  (ansi dark_gray)SKIP(ansi reset)  ($s.id) (ansi dark_gray)[requires ($s.actor)](ansi reset)"
      continue
    }

    # Check depends_on: if a dependency failed and its kind is OnSuccess, skip.
    let deps = ($s.depends_on? | default [])
    mut dep_blocked = false
    for dep in $deps {
      let dep_kind = ($dep.kind? | default "OnSuccess")
      if $dep_kind == "OnSuccess" and ($failed_steps | any { |f| $f == $dep.step }) {
        $dep_blocked = true
        break
      }
    }
    if $dep_blocked {
      print $"  (ansi yellow)BLOCKED(ansi reset)  ($s.id) (ansi dark_gray)— dependency failed(ansi reset)"
      continue
    }

    print-step-header $s $idx ($steps | length)

    # No command = informational step, just display.
    if ($s.cmd? | is-empty) or ($s.cmd == "") {
      print $"  (ansi dark_gray)no command — informational step(ansi reset)"
      print ""
      continue
    }

    # Confirm if required and not --yes.
    if $mr_cfg.confirm_each_step and (not $yes) {
      let answer = (input $"  (ansi cyan)Execute? [y/N/q](ansi reset) " | str trim | str downcase)
      if $answer == "q" {
        print $"  (ansi yellow)Aborted by user.(ansi reset)"
        return
      }
      if $answer != "y" {
        print $"  (ansi dark_gray)skipped(ansi reset)"
        print ""
        continue
      }
    }

    # Execute.
    let result = (exec-step-cmd $s.cmd)
    if $result.success {
      print $"  (ansi green)OK(ansi reset)"
      if ($result.output | is-not-empty) {
        let trimmed = ($result.output | str trim)
        if ($trimmed | is-not-empty) {
          $trimmed | lines | each { |l| print $"  (ansi dark_gray)│(ansi reset) ($l)" } | ignore
        }
      }
    } else {
      let strategy = ($s.on_error? | default {} | get -o strategy | default "Stop")
      $failed_steps = ($failed_steps | append $s.id)
      if $strategy == "Stop" {
        print $"  (ansi red_bold)FAILED(ansi reset) (ansi dark_gray)— strategy: Stop(ansi reset)"
        if ($result.output | is-not-empty) {
          $result.output | str trim | lines | each { |l| print $"  (ansi red)│(ansi reset) ($l)" } | ignore
        }
        print ""
        print $"  (ansi red)Execution halted at step ($s.id).(ansi reset)"
        return
      } else {
        print $"  (ansi yellow)FAILED(ansi reset) (ansi dark_gray)— strategy: Continue(ansi reset)"
        if ($result.output | is-not-empty) {
          $result.output | str trim | lines | each { |l| print $"  (ansi yellow)│(ansi reset) ($l)" } | ignore
        }
      }
    }
    print ""
  }

  # ── Postconditions ──
  if ($postconditions | is-not-empty) {
    print $"  (ansi white_bold)Postconditions(ansi reset)"
    for p in $postconditions { print $"  (ansi dark_gray)●(ansi reset) ($p)" }
    print ""
  }

  let fail_count = ($failed_steps | length)
  if $fail_count == 0 {
    print $"  (ansi green_bold)COMPLETE(ansi reset)  All steps executed successfully."
  } else {
    let step_word = if $fail_count == 1 { "step" } else { "steps" }
    print $"  (ansi yellow_bold)PARTIAL(ansi reset)  ($fail_count) ($step_word) failed: ($failed_steps | str join ', ')"
  }
  print ""
}
