#!/usr/bin/env nu
# reflection/modules/migrate.nu — ontoref protocol migration runner.
#
# Migrations live in $ONTOREF_ROOT/reflection/migrations/NNN-slug.ncl
# Each has: id, slug, description, check, instructions
# "Applied" = check passes. No state file — checks are the source of truth.
#
# Commands:
#   migrate list            — all migrations with applied/pending status
#   migrate pending         — only pending migrations
#   migrate show <id>       — instructions for a specific migration

use env.nu *
use store.nu [daemon-export-safe]
use describe.nu [nickel-import-path]

def project-root []: nothing -> string {
  $env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT
}

def migrations-dir []: nothing -> string {
  [$env.ONTOREF_ROOT, "reflection", "migrations"] | path join
}

# Run a check record against the given project root. Returns { passed, detail }.
def run-migration-check [check: record, root: string]: nothing -> record {
  match $check.tag {
    "FileExists" => {
      let p = ([$root, $check.path] | path join)
      let exists = ($p | path exists)
      let want = ($check.present? | default true)
      {
        passed: ($exists == $want),
        detail: (if ($exists == $want) { "ok" } else if $want { $"missing: ($p)" } else { $"unexpected: ($p)" })
      }
    }
    "Grep" => {
      let paths = ($check.paths | each { |p| [$root, $p] | path join } | where { |p| $p | path exists })
      if ($paths | is-empty) {
        return { passed: false, detail: "no target paths exist" }
      }
      let r = do { ^rg --no-heading -l $check.pattern ...$paths } | complete
      let has_matches = ($r.exit_code == 0)
      let must_empty = ($check.must_be_empty? | default false)
      if $must_empty {
        let files = ($r.stdout | str trim)
        { passed: (not $has_matches), detail: (if $has_matches { $"pattern found in: ($files)" } else { "ok" }) }
      } else {
        { passed: $has_matches, detail: (if $has_matches { "ok" } else { "required pattern absent" }) }
      }
    }
    "NuCmd" => {
      # KNOWN BUG: Including $ONTOREF_ROOT/ontology (the parent ontology dir) in
      # NICKEL_IMPORT_PATH causes the installed ontoref binary to fail with
      # "Not a directory" inside its nu subprocess. Subdirs (ontology/schemas,
      # ontology/defaults) are safe. Root cause: uninvestigated interaction between
      # nickel's directory traversal and nushell's path handling in the subprocess chain.
      # Fix: build the import path explicitly, omitting $ONTOREF_ROOT/ontology.
      # When root is a consumer project (not the ontoref root), include root/ontology
      # so consumer-local ontology defaults (e.g. defaults/manifest.ncl) are resolvable.
      # Excluded when root == ONTOREF_ROOT: that specific path triggers a "Not a directory"
      # bug in the nu subprocess chain (see comment above).
      let project_ontology = if $root != $env.ONTOREF_ROOT { [$"($root)/ontology"] } else { [] }
      let safe_ip = (
        [
          $"($root)/.ontology"
          $"($root)/adrs"
          $"($root)/.ontoref/ontology/schemas"
          $"($root)/.ontoref/adrs"
          $root
          $"($env.ONTOREF_ROOT)/ontology/schemas"
          $"($env.ONTOREF_ROOT)/adrs"
          $env.ONTOREF_ROOT
        ]
        | append $project_ontology
        | where { |p| $p | path exists }
        | uniq
        | str join ":"
      )
      let r = (do {
        with-env {
          NICKEL_IMPORT_PATH:     $safe_ip
          ONTOREF_PROJECT_ROOT:   $root
        } { nu --no-config-file -c $check.cmd }
      } | complete)
      let expected = ($check.expect_exit? | default 0)
      {
        passed: ($r.exit_code == $expected),
        detail: (if ($r.exit_code == $expected) { "ok" } else { $"exit ($r.exit_code) ≠ ($expected): ($r.stderr | str trim | str substring 0..120)" })
      }
    }
    _ => { passed: false, detail: $"unknown check tag: ($check.tag)" }
  }
}

def load-all-migrations []: nothing -> list<record> {
  let dir = (migrations-dir)
  if not ($dir | path exists) { return [] }
  let ip = (nickel-import-path $env.ONTOREF_ROOT)
  glob $"($dir)/*.ncl"
  | sort
  | each { |f|
    try {
      let m = (daemon-export-safe $f --import-path $ip)
      if $m != null { [$m] } else { [] }
    } catch { [] }
  }
  | flatten
}

def migration-status [migrations: list<record>, root: string]: nothing -> list<record> {
  $migrations | each { |m|
    let r = (run-migration-check $m.check $root)
    {
      id:          $m.id,
      slug:        $m.slug,
      description: $m.description,
      applied:     $r.passed,
      detail:      $r.detail,
    }
  }
}

# List all protocol migrations with applied/pending status.
export def "migrate list" [
  --fmt (-f): string = "",
  --actor (-a): string = "",
]: nothing -> nothing {
  let root  = (project-root)
  let actor = if ($actor | is-not-empty) { $actor } else { $env.ONTOREF_ACTOR? | default "developer" }
  let fmt   = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }

  let statuses = (migration-status (load-all-migrations) $root)

  if $fmt == "json" {
    print ($statuses | to json)
    return
  }

  let n_applied = ($statuses | where applied == true | length)
  let n_total   = ($statuses | length)
  print $"  migrations: ($n_applied)/($n_total) applied"
  print ""
  for s in $statuses {
    let mark = if $s.applied { "✓" } else { "○" }
    print $"  ($mark)  ($s.id) ($s.slug)"
    print $"       ($s.description)"
    if not $s.applied and $s.detail != "ok" {
      print $"       ($s.detail)"
    }
  }
}

# List only pending (not yet applied) migrations.
export def "migrate pending" [
  --fmt (-f): string = "",
  --actor (-a): string = "",
]: nothing -> nothing {
  let root  = (project-root)
  let actor = if ($actor | is-not-empty) { $actor } else { $env.ONTOREF_ACTOR? | default "developer" }
  let fmt   = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }

  let pending = (
    migration-status (load-all-migrations) $root
    | where applied == false
  )

  if $fmt == "json" {
    print ($pending | to json)
    return
  }

  if ($pending | is-empty) {
    print "  all migrations applied"
    return
  }

  print $"  ($pending | length) pending:"
  for p in $pending {
    print $"  ○  ($p.id) ($p.slug) — ($p.description)"
  }
}

# Show the instructions for a specific migration. Accepts id ("0001") or slug.
export def "migrate show" [
  id: string,
  --fmt (-f): string = "",
  --actor (-a): string = "",
]: nothing -> nothing {
  let root  = (project-root)
  let actor = if ($actor | is-not-empty) { $actor } else { $env.ONTOREF_ACTOR? | default "developer" }
  let fmt   = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }

  let migrations = (load-all-migrations)
  let id_norm = ($id | str replace --regex '^0+' '')
  let id_norm = if $id_norm == "" { "0" } else { $id_norm }
  let matching = ($migrations | where { |m| $m.id == $id or $m.slug == $id or (($m.id | str replace --regex '^0+' '') == $id_norm) })
  if ($matching | is-empty) {
    error make { msg: $"migration '($id)' not found" }
  }
  let m = ($matching | first)
  let r = (run-migration-check $m.check $root)

  # Interpolate runtime values into instruction text
  let project_name = ($root | path basename)
  let instructions = ($m.instructions
    | str replace --all "{project_root}"  $root
    | str replace --all "{project_name}"  $project_name
    | str replace --all "PROJECT_NAME"    $project_name
    | str replace --all "$ONTOREF_PROJECT_ROOT" $root
  )

  if $fmt == "json" {
    print ({
      id:           $m.id,
      slug:         $m.slug,
      description:  $m.description,
      applied:      $r.passed,
      project_root: $root,
      project_name: $project_name,
      instructions: $instructions,
    } | to json)
    return
  }

  let status = if $r.passed { "✓ applied" } else { "○ pending" }
  print $"  ($m.id) ($m.slug) — ($status)"
  print $"  ($m.description)"
  if not $r.passed and $r.detail != "ok" {
    print $"  check: ($r.detail)"
  }
  print ""
  print $instructions
}
