#!/usr/bin/env nu
# reflection/modules/run.nu — step execution tracking and run lifecycle.
# Provides: run start, step report, run status
#
# Storage: .coder/<actor>/runs/<run_id>/
#   run.json      — run metadata
#   steps.jsonl   — one JSON record per reported step
#   current.json  — pointer to active run (in .coder/<actor>/runs/)

use ../modules/store.nu [daemon-export-safe]
use ../modules/describe.nu [nickel-import-path]

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 runs-dir []: nothing -> string {
  let root = (project-root)
  let actor = ($env.ONTOREF_ACTOR? | default "developer")
  $"($root)/.coder/($actor)/runs"
}

def current-run-file []: nothing -> string {
  $"(runs-dir)/current.json"
}

def load-current-run []: nothing -> record {
  let f = (current-run-file)
  if not ($f | path exists) { return {} }
  open $f
}

def load-run-steps [run_id: string]: nothing -> list<record> {
  let f = $"(runs-dir)/($run_id)/steps.jsonl"
  if not ($f | path exists) { return [] }
  open $f | lines | where { |l| $l | str trim | is-not-empty } | each { |l| $l | from json }
}

def find-mode-file [mode: string]: nothing -> string {
  let root = (project-root)
  let project_file = $"($root)/reflection/modes/($mode).ncl"
  let ontoref_file = $"($env.ONTOREF_ROOT)/reflection/modes/($mode).ncl"
  if ($project_file | path exists) { $project_file } else if ($ontoref_file | path exists) { $ontoref_file } else { "" }
}

def load-mode-dag [mode: string]: nothing -> record {
  let root = (project-root)
  let mode_file = (find-mode-file $mode)
  if ($mode_file | is-empty) {
    error make { msg: $"Mode '($mode)' not found in project or ontoref reflection/modes/" }
  }
  let mode_root = if ($mode_file | str starts-with $root) and ($root != $env.ONTOREF_ROOT) {
    $root
  } else {
    $env.ONTOREF_ROOT
  }
  let ip = (nickel-import-path $mode_root)
  let result = (daemon-export-safe $mode_file --import-path $ip)
  if $result == null {
    error make { msg: $"Failed to export mode '($mode)' — check NCL syntax and import paths" }
  }
  $result
}

def now-iso []: nothing -> string {
  date now | format date "%Y-%m-%dT%H:%M:%SZ"
}

def run-id-for [mode: string, task: string]: nothing -> string {
  if ($task | is-not-empty) {
    $"($mode)-($task)"
  } else {
    let ts = (date now | format date "%Y%m%dT%H%M%S")
    $"($mode)-($ts)"
  }
}

# Start a new run for a mode. Establishes run context for subsequent `step report` calls.
export def "run start" [
  mode: string,              # Mode ID (must exist in reflection/modes/)
  --task (-t): string = "",  # Backlog task ID to associate with this run
  --fmt (-f): string = "",
]: nothing -> nothing {
  let actor = ($env.ONTOREF_ACTOR? | default "developer")
  let fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }

  let mode_data = (load-mode-dag $mode)
  let run_id = (run-id-for $mode $task)
  let dir = $"(runs-dir)/($run_id)"

  mkdir $dir

  let step_count = ($mode_data.steps? | default [] | length)
  let state = {
    run_id: $run_id,
    mode: $mode,
    task: $task,
    actor: $actor,
    started_at: (now-iso),
    steps_total: $step_count,
  }

  $state | to json | save --force $"($dir)/run.json"
  $state | to json | save --force (current-run-file)
  # Seed empty steps.jsonl so append works consistently
  "" | save --force $"($dir)/steps.jsonl"

  if $fmt == "json" {
    print ($state | to json)
  } else {
    print $"Run started: ($run_id)"
    print $"  Mode:  ($mode)  ($step_count) steps"
    if ($task | is-not-empty) { print $"  Task:  ($task)" }
    print $"  Dir:   ($dir)"
    print $"  Next:  ontoref step report ($mode) <step_id> --status pass|fail"
  }
}

# Report the result of a step. Validates step exists and dependencies are satisfied.
export def "step report" [
  mode: string,                        # Mode ID
  step_id: string,                     # Step ID to report
  --status (-s): string,               # pass | fail | skip
  --exit-code (-e): int = 0,
  --artifacts (-a): list<string> = [],
  --warnings (-w): int = 0,
  --run (-r): string = "",             # Override run ID (default: active run)
  --fmt (-f): string = "",
]: nothing -> nothing {
  let actor = ($env.ONTOREF_ACTOR? | default "developer")
  let fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }

  if not ($status in ["pass", "fail", "skip"]) {
    error make { msg: $"Invalid status '($status)'. Must be: pass | fail | skip" }
  }

  # Resolve run context
  let current = (load-current-run)
  let run_id = if ($run | is-not-empty) {
    $run
  } else if ($current | is-not-empty) and ($current.mode? | default "") == $mode {
    $current.run_id? | default ""
  } else {
    error make { msg: $"No active run for mode '($mode)'. Start one with: ontoref run start ($mode)" }
  }

  let dir = $"(runs-dir)/($run_id)"
  if not ($dir | path exists) {
    error make { msg: $"Run directory not found: ($dir). Start with: ontoref run start ($mode)" }
  }
  let steps_file = $"($dir)/steps.jsonl"

  # Validate step exists in mode DAG
  let mode_data = (load-mode-dag $mode)

  # Validate guards — warn if any Block guard would fail (informational in step report context)
  let guards = ($mode_data.guards? | default [])
  let blocking_guards = ($guards | where { |g| ($g.severity? | default "Block") == "Block" })
  for g in $blocking_guards {
    let result = (do { bash -c $g.cmd } | complete)
    if $result.exit_code != 0 {
      if $fmt != "json" {
        print $"(ansi yellow)WARN(ansi reset): guard '($g.id)' would block mode execution: ($g.reason)"
      }
    }
  }

  let matching_steps = ($mode_data.steps? | default [] | where { |s| $s.id == $step_id })
  let step_def = if ($matching_steps | is-not-empty) { $matching_steps | first } else { null }
  if ($step_def | is-empty) {
    error make { msg: $"Step '($step_id)' not found in mode '($mode)'. Valid steps: ($mode_data.steps? | default [] | each { |s| $s.id } | str join ', ')" }
  }

  # Validate blocking dependencies are satisfied
  let completed = (load-run-steps $run_id)
  let blocking = ($step_def.depends_on? | default [] | where { |d|
    ($d.kind? | default "Always") in ["Always", "OnSuccess"]
  })
  for dep in $blocking {
    let prev_list = ($completed | where { |r| $r.step == $dep.step })
    let prev = if ($prev_list | is-not-empty) { $prev_list | first } else { null }
    if ($prev | is-empty) {
      error make { msg: $"Step '($step_id)' requires '($dep.step)' to be reported first" }
    }
    if ($dep.kind? | default "Always") == "OnSuccess" and ($prev.status? | default "") != "pass" {
      error make { msg: $"Step '($step_id)' depends on '($dep.step)' with OnSuccess, but it ($prev.status? | default 'unknown')" }
    }
  }

  let entry = {
    run_id: $run_id,
    mode: $mode,
    step: $step_id,
    status: $status,
    exit_code: $exit_code,
    warnings: $warnings,
    artifacts: $artifacts,
    actor: $actor,
    ts: (now-iso),
  }

  $"\n($entry | to json -r)" | save --append $steps_file

  # Emit side-effect on fail+Stop
  let on_error_strategy = ($step_def.on_error?.strategy? | default "Stop")
  if $status == "fail" and $on_error_strategy == "Stop" {
    if $fmt != "json" {
      print $"(ansi red)BLOCKED(ansi reset): step '($step_id)' failed with on_error=Stop"
      print $"  Run '($run_id)' is now blocked. Resolve and re-run this step."
    }
  }

  if $fmt == "json" {
    print ($entry | to json)
  } else {
    let mark = if $status == "pass" { $"(ansi green)✓(ansi reset)" } else if $status == "fail" { $"(ansi red)✗(ansi reset)" } else { $"(ansi yellow)-(ansi reset)" }
    print $"  ($mark) ($step_id)  [($mode) / ($run_id)]"
    if $warnings > 0 { print $"    ($warnings) warning(s)" }
    if ($artifacts | is-not-empty) { print $"    artifacts: ($artifacts | str join ', ')" }
  }
}

# Show current run progress.
export def "run status" [
  --run (-r): string = "",  # Specific run ID (default: active run)
  --fmt (-f): string = "",
]: nothing -> nothing {
  let actor = ($env.ONTOREF_ACTOR? | default "developer")
  let fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }

  let current = (load-current-run)
  let run_id = if ($run | is-not-empty) { $run } else if ($current | is-not-empty) { $current.run_id? | default "" } else { "" }

  if ($run_id | is-empty) {
    if $fmt == "json" { print "null" } else { print "No active run. Start with: ontoref run start <mode>" }
    return
  }

  let mode_id = ($current.mode? | default "")
  let mode_data = if ($mode_id | is-not-empty) {
    try { load-mode-dag $mode_id } catch { {} }
  } else { {} }

  let all_step_ids = ($mode_data.steps? | default [] | each { |s| $s.id })
  let reported = (load-run-steps $run_id)

  let steps_view = ($all_step_ids | each { |id|
    let r_list = ($reported | where { |s| $s.step == $id })
    let r = if ($r_list | is-not-empty) { $r_list | first } else { null }
    if ($r | is-not-empty) {
      { id: $id, status: $r.status, exit_code: ($r.exit_code? | default 0), ts: ($r.ts? | default ""), warnings: ($r.warnings? | default 0) }
    } else {
      { id: $id, status: "pending", exit_code: null, ts: "", warnings: 0 }
    }
  })

  let data = {
    run_id: $run_id,
    mode: $mode_id,
    task: ($current.task? | default ""),
    started_at: ($current.started_at? | default ""),
    steps_total: ($all_step_ids | length),
    steps_reported: ($reported | length),
    steps: $steps_view,
  }

  if $fmt == "json" {
    print ($data | to json)
  } else {
    print ""
    print $"RUN: ($data.run_id)"
    print "══════════════════════════════════════════════════════════════════"
    if ($data.task | is-not-empty) { print $"  Task:    ($data.task)" }
    print $"  Mode:    ($data.mode)"
    print $"  Started: ($data.started_at)"
    print $"  Progress: ($data.steps_reported)/($data.steps_total) steps reported"
    print ""
    for s in $data.steps {
      let mark = match $s.status {
        "pass"    => $"(ansi green)✓(ansi reset)",
        "fail"    => $"(ansi red)✗(ansi reset)",
        "skip"    => $"(ansi yellow)-(ansi reset)",
        "pending" => $"(ansi dark_gray)·(ansi reset)",
        _         => " ",
      }
      let warn_tag = if ($s.warnings? | default 0) > 0 { $"  ⚠ ($s.warnings) warnings" } else { "" }
      print $"  ($mark) ($s.id)($warn_tag)"
    }
    print ""
  }
}

# Verify a run is complete and emit side effects (backlog close, artifacts summary).
# Fails with a specific message if any required step is missing or blocked.
export def "mode complete" [
  mode: string,              # Mode ID
  --task (-t): string = "",  # Backlog task ID to close on success
  --run (-r): string = "",   # Override run ID (default: active run)
  --fmt (-f): string = "",
]: nothing -> nothing {
  let actor = ($env.ONTOREF_ACTOR? | default "developer")
  let fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }

  # Resolve run context
  let current = (load-current-run)
  let run_id = if ($run | is-not-empty) { $run } else if ($current | is-not-empty) and ($current.mode? | default "") == $mode { $current.run_id? | default "" } else { "" }

  if ($run_id | is-empty) {
    error make { msg: $"No active run for mode '($mode)'. Start one with: ontoref run start ($mode)" }
  }

  let dir = $"(runs-dir)/($run_id)"
  if not ($dir | path exists) {
    error make { msg: $"Run directory not found: ($dir)" }
  }

  # Load mode DAG and reported steps
  let mode_data = (load-mode-dag $mode)
  let all_steps = ($mode_data.steps? | default [])
  let reported = (load-run-steps $run_id)

  # Build verification results — one record per step
  let results = ($all_steps | each { |step_def|
    let rep_list = ($reported | where { |r| $r.step == $step_def.id })
    let rep = if ($rep_list | is-not-empty) { $rep_list | first } else { null }
    let strategy = ($step_def.on_error?.strategy? | default "Stop")

    {
      id: $step_def.id,
      reported: ($rep | is-not-empty),
      status: (if ($rep | is-not-empty) { $rep.status } else { "pending" }),
      strategy: $strategy,
      # A step blocks completion if: not reported AND strategy=Stop,
      # OR reported as fail AND strategy=Stop
      blocks: (
        (($rep | is-empty) and $strategy == "Stop") or
        (($rep | is-not-empty) and $rep.status == "fail" and $strategy == "Stop")
      ),
    }
  })

  let blockers = ($results | where blocks == true)
  let warnings = ($results | where { |r| not $r.blocks and $r.status in ["fail", "pending"] })
  let all_artifacts = ($reported | each { |r| $r.artifacts? | default [] } | flatten)
  let total_warnings = ($reported | each { |r| $r.warnings? | default 0 } | math sum)

  let resolved_task = if ($task | is-not-empty) { $task } else { $current.task? | default "" }
  let ok = ($blockers | is-empty)

  let result = {
    run_id: $run_id,
    mode: $mode,
    task: $resolved_task,
    ok: $ok,
    steps_total: ($all_steps | length),
    steps_reported: ($reported | length),
    blockers: ($blockers | each { |b| { id: $b.id, status: $b.status, strategy: $b.strategy } }),
    warnings: ($warnings | each { |w| { id: $w.id, status: $w.status } }),
    artifacts: $all_artifacts,
    total_warnings: $total_warnings,
  }

  if $ok {
    # Mark run as completed in run.json
    let run_meta = (open $"($dir)/run.json")
    $run_meta | merge { completed_at: (now-iso), ok: true } | to json | save --force $"($dir)/run.json"
    # Clear current.json — no active run
    "{}" | save --force (current-run-file)
  }

  if $fmt == "json" {
    print ($result | to json)
  } else if $ok {
    print ""
    print $"(ansi green)✓ RUN COMPLETE(ansi reset): ($run_id)"
    print "══════════════════════════════════════════════════════════════════"
    print $"  Mode:    ($mode)"
    if ($resolved_task | is-not-empty) { print $"  Task:    ($resolved_task)" }
    print $"  Steps:   ($result.steps_reported)/($result.steps_total) reported"
    if $total_warnings > 0 { print $"  Warnings: ($total_warnings) advisory (non-blocking)" }
    if ($all_artifacts | is-not-empty) {
      print ""
      print "  Artifacts:"
      for a in $all_artifacts { print $"    ($a)" }
    }
    if ($resolved_task | is-not-empty) {
      print ""
      print $"  Close backlog item with: ontoref backlog done ($resolved_task)"
    }
    print ""
  } else {
    print ""
    print $"(ansi red)✗ RUN INCOMPLETE(ansi reset): ($run_id)"
    print "══════════════════════════════════════════════════════════════════"
    print $"  ($blockers | length) blocker(s):"
    for b in $blockers {
      let reason = if $b.status == "pending" { "not reported" } else { $"($b.status) with on_error=Stop" }
      print $"    ($b.id) — ($reason)"
    }
    print ""
  }
}
