#!/usr/bin/env nu
# reflection/modules/config.nu — configuration profile management.
#
# Profiles live in reflection/configs/<profile>.ncl (mutable, git-versioned).
# Sealed history in reflection/configs/history/<profile>/cfg-NNN.ncl (append-only).
# Manifest at reflection/configs/manifest.ncl tracks active seal per profile.
#
# Seal = sha256(nickel export <profile.ncl>) written at apply time.
# Rollback = restore values from a history entry + write new seal.
# Verify = sha256 of current file == seal.hash of active history entry.

use env.nu *
use store.nu [daemon-export, daemon-export-safe]

export def "config show" [
  profile: string,
  --fmt: string = "",
] {
  let f    = if ($fmt | is-not-empty) { $fmt } else { "table" }
  let file = (config-file $profile)
  let data = (daemon-export $file)

  let active = (config-active-seal $profile)
  print ""
  print $"Profile: ($profile | str upcase)"
  print $"Active seal: ($active | default '(none)')"
  print ""
  match $f {
    "json" => { print ($data | to json) },
    _      => { print ($data | table) },
  }
  print ""
}

export def "config history" [
  profile: string,
  --fmt: string = "",
] {
  let entries = (config-history-entries $profile)
  if ($entries | is-empty) {
    print $"No history for ($profile)"
    return
  }
  let rows = $entries | each { |e|
    {
      id:         $e.id,
      applied_at: $e.seal.applied_at,
      applied_by: $e.seal.applied_by,
      hash:       ($e.seal.hash | str substring 0..8),
      adr:        ($e.seal.related_adr? | default ""),
      pr:         ($e.seal.related_pr?  | default ""),
      bug:        ($e.seal.related_bug? | default ""),
      note:       ($e.seal.note? | default ""),
    }
  }
  match ($fmt | default "table") {
    "json" => { print ($rows | to json) },
    "md"   => { print ($rows | to md) },
    _      => { print ($rows | table) },
  }
}

export def "config diff" [
  profile: string,
  from_id: string,
  to_id: string,
] {
  let root    = (config-root)
  let hist    = $"($root)/reflection/configs/history/($profile | str downcase)"
  let from_f  = $"($hist)/($from_id).ncl"
  let to_f    = $"($hist)/($to_id).ncl"

  for f in [$from_f, $to_f] {
    if not ($f | path exists) { error make { msg: $"Config state ($f) not found" } }
  }

  let from_data = (daemon-export $from_f)
  let to_data   = (daemon-export $to_f)

  # values_snapshot is stored as a JSON string inside the Nickel file
  let from_vals = $from_data | get values_snapshot | from json
  let to_vals   = $to_data   | get values_snapshot | from json

  let from_json = $from_vals | to json --indent 2
  let to_json   = $to_vals   | to json --indent 2

  let tmp_from = (mktemp --suffix ".json")
  let tmp_to   = (mktemp --suffix ".json")
  $from_json | save --force $tmp_from
  $to_json   | save --force $tmp_to

  print $"diff ($from_id) → ($to_id)"
  do { ^diff $tmp_from $tmp_to } | complete | get stdout | print
  rm $tmp_from $tmp_to
}

export def "config verify" [profile: string] {
  let file   = (config-file $profile)
  let active = (config-active-seal $profile)
  if ($active | is-empty) {
    print $"  ($profile): no active seal — run 'ontoref config apply ($profile)' first"
    return
  }

  let root   = (config-root)
  let hist_f = $"($root)/reflection/configs/history/($profile | str downcase)/($active).ncl"
  if not ($hist_f | path exists) {
    print $"  ($profile): seal ($active) not found in history"
    return
  }

  let hist_data = (daemon-export-safe $hist_f)
  let stored_hash = if $hist_data != null { $hist_data | get seal.hash } else { "" }

  let current_hash = (config-hash $file)

  if $stored_hash == $current_hash {
    print $"  ($profile): ✓ verified  ($current_hash | str substring 0..16)..."
  } else {
    print $"  ($profile): ✗ DRIFT DETECTED"
    print $"    stored:  ($stored_hash | str substring 0..16)..."
    print $"    current: ($current_hash | str substring 0..16)..."
    print $"    File was modified after last seal. Run 'ontoref config apply ($profile)' to reseal."
  }
}

export def "config audit" [] {
  let root     = (config-root)
  let manifest = (config-manifest)
  print ""
  print "CONFIG AUDIT"
  print "────────────────────────────────────────"
  for profile in ($manifest.profiles? | default []) {
    config verify ($profile | into string | str downcase)
  }
  print ""
}

export def "config apply" [
  profile:    string,
  --adr:      string = "",
  --pr:       string = "",
  --bug:      string = "",
  --note:     string = "",
] {
  let root        = (config-root)
  let file        = (config-file $profile)
  let actor       = ($env.ONTOREF_ACTOR? | default "developer") | str replace --all " " "_"
  let today       = (date now | format date "%Y-%m-%dT%H:%M:%S")
  let ts          = (date now | format date "%Y%m%dT%H%M%S")
  # Canonical enum label: "ci" → "CI", "development" → "Development"
  let profile_key = (manifest-canonical-key $root $profile)

  # Export + hash
  let export_data = (daemon-export $file)
  let hash = (config-hash $file)

  # Collision-free ID: timestamp + actor — no sequential counter, no TOCTOU race
  let hist_dir = $"($root)/reflection/configs/history/($profile | str downcase)"
  ^mkdir -p $hist_dir
  let cfg_id  = $"cfg-($ts)-($actor)"
  let hist_f  = $"($hist_dir)/($cfg_id).ncl"

  let schema_rel = "../../../../reflection/schemas/config.ncl"

  let prev_seal  = (config-active-seal $profile | default "")

  # values_snapshot stored as Nickel String (inline JSON is not valid Nickel —
  # `:` is a contract separator, not a key-value delimiter).
  let vals_json_raw = ($export_data | get values | to json)
  let snap_hash     = (config-string-hash $vals_json_raw)

  # Escape for embedding inside a Nickel string literal.
  # Single-quoted Nu strings are raw: '\"' = two chars (\, "), not one.
  # Order matters: escape backslashes first so the new `\` from `\"` isn't re-escaped.
  let vals_json_esc = (
    $vals_json_raw
    | str replace --all '\' '\\'    # literal \ → \\  (single backslash → escaped backslash)
    | str replace --all '"' '\"'    # literal " → \"  (quote → escaped quote)
  )

  let content = $"let s = import \"($schema_rel)\" in

{
  id      = \"($cfg_id)\",
  profile = '($profile_key),

  seal = {
    hash          = \"($hash)\",
    snapshot_hash = \"($snap_hash)\",
    applied_at    = \"($today)\",
    applied_by    = \"($actor)\",
    note          = \"($note)\",
    related_adr   = \"($adr)\",
    related_pr    = \"($pr)\",
    related_bug   = \"($bug)\",
  },

  values_snapshot = \"($vals_json_esc)\",

  supersedes = \"($prev_seal)\",
} | s.ConfigState
"
  $content | save --force $hist_f
  config-set-active $profile $cfg_id

  print $"  applied: ($profile) → ($cfg_id)  hash=($hash | str substring 0..16)..."
  if ($adr | str length) > 0 { print $"  adr: ($adr)" }
  if ($pr  | str length) > 0 { print $"  pr:  ($pr)" }
  if ($bug | str length) > 0 { print $"  bug: ($bug)" }
}

export def "config rollback" [
  profile: string,
  to_id:   string,
  --adr:   string = "",
  --note:  string = "",
] {
  let root    = (config-root)
  let hist_dir = $"($root)/reflection/configs/history/($profile | str downcase)"
  let src_f   = $"($hist_dir)/($to_id).ncl"

  if not ($src_f | path exists) { error make { msg: $"Config state ($to_id) not found" } }

  let entry = (daemon-export $src_f)
  let snapshot_json  = ($entry | get values_snapshot)           # raw JSON string
  let stored_snap_h  = ($entry | get seal.snapshot_hash)       # sha256 of that string at apply time
  let actual_snap_h  = (config-string-hash $snapshot_json)

  if $stored_snap_h != $actual_snap_h {
    error make { msg: $"Integrity check failed for ($to_id): snapshot_hash mismatch. Rollback aborted." }
  }

  let snapshot    = ($snapshot_json | from json)
  let file        = (config-file $profile)
  let profile_key = (manifest-canonical-key $root $profile)

  print $"  rollback: ($profile) → ($to_id)  [snapshot integrity verified]"
  profile-file-write $file $profile_key $snapshot
  print $"  restored: ($file)"

  config apply $profile --adr $adr --note $"rollback to ($to_id): ($note)"
}

# ── Internal ────────────────────────────────────────────────────────────────────

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

def config-file [profile: string]: nothing -> string {
  $"(config-root)/reflection/configs/($profile | str downcase).ncl"
}

def config-manifest []: nothing -> record {
  let f = $"(config-root)/reflection/configs/manifest.ncl"
  if not ($f | path exists) { return {} }
  daemon-export-safe $f | default {}
}

def config-active-seal [profile: string]: nothing -> string {
  let manifest = (config-manifest)
  let key = (manifest-canonical-key (config-root) $profile)
  $manifest.active? | default {} | get --optional $key | default ""
}

def config-set-active [profile: string, cfg_id: string] {
  let root = (config-root)
  let manifest_f = $"($root)/reflection/configs/manifest.ncl"
  # Look up the canonical key from the manifest export — avoids str-capitalize mismatches
  # (e.g. "ci" → "CI" not "Ci", "development" → "Development").
  let key = (manifest-canonical-key $root $profile)
  if ($key | is-empty) {
    print $"  warning: profile '($profile)' not found in manifest.ncl active map — add it manually"
    return
  }
  let script = $"s/($key)[[:space:]]*=[[:space:]]*\"[^\"]*\"/($key) = \"($cfg_id)\"/"
  let r1 = do { ^sed -i '' $script $manifest_f } | complete
  if $r1.exit_code != 0 {
    let r2 = do { ^sed -i $script $manifest_f } | complete
    if $r2.exit_code != 0 {
      print $"  warning: could not patch manifest.ncl active.($key) — update manually to ($cfg_id)"
    }
  }
}

# Resolve the canonical key name as it appears in manifest.ncl active map.
# Matches case-insensitively so "ci" → "CI", "development" → "Development".
def manifest-canonical-key [root: string, profile: string]: nothing -> string {
  let manifest_f = $"($root)/reflection/configs/manifest.ncl"
  let data = (daemon-export-safe $manifest_f)
  if $data == null { return ($profile | str capitalize) }
  let keys = ($data | get active | columns)
  $keys | where { |k| ($k | str downcase) == ($profile | str downcase) } | first | default ($profile | str capitalize)
}

def config-hash [file: string]: nothing -> string {
  let r = do { ^nickel export $file } | complete
  if $r.exit_code != 0 { return "" }
  let tmp = (mktemp --suffix ".json")
  $r.stdout | save --force $tmp
  let hash = do {
    let h = (do { ^sha256sum $tmp } | complete)
    if $h.exit_code == 0 { $h.stdout | split words | first } else {
      # macOS shasum fallback
      let h2 = do { ^shasum -a 256 $tmp } | complete
      if $h2.exit_code == 0 { $h2.stdout | split words | first } else { "" }
    }
  }
  rm $tmp
  $hash
}

# Hash an arbitrary string (not a file). Used for snapshot_hash in seals.
def config-string-hash [s: string]: nothing -> string {
  let tmp = (mktemp --suffix ".json")
  $s | save --force $tmp
  let hash = do {
    let h = (do { ^sha256sum $tmp } | complete)
    if $h.exit_code == 0 { $h.stdout | split words | first } else {
      let h2 = do { ^shasum -a 256 $tmp } | complete
      if $h2.exit_code == 0 { $h2.stdout | split words | first } else { "" }
    }
  }
  rm $tmp
  $hash
}

# Recursively convert a Nu value to a Nickel literal string.
# depth controls indentation (2-space units).
def nickel-indent [n: int]: nothing -> string {
  0..<$n | reduce --fold "" { |_, acc| $acc + "  " }
}

def value-to-nickel [v: any, depth: int = 0]: nothing -> string {
  let pad  = (nickel-indent $depth)
  let pad2 = (nickel-indent ($depth + 1))
  let type = ($v | describe | str replace --regex '<.*>' '')
  if $type == "string" {
    let s   = ($v | into string)
    let esc = ($s | str replace --all '\\' '\\\\' | str replace --all '"' '\\"')
    '"' + $esc + '"'
  } else if $type == "int" {
    $v | into string
  } else if $type == "float" {
    $v | into string
  } else if $type == "bool" {
    $v | into string
  } else if $type == "list" {
    if ($v | is-empty) { "[]" } else {
      let items = ($v | each { |item| $"($pad2)(value-to-nickel $item ($depth + 1))" } | str join ",\n")
      $"[\n($items),\n($pad)]"
    }
  } else if $type == "record" {
    let fields = ($v | transpose k val | each { |f|
      $"($pad2)($f.k) = (value-to-nickel $f.val ($depth + 1)),"
    } | str join "\n")
    $"{\n($fields)\n($pad)}"
  } else {
    $v | to json
  }
}

# Reconstruct a profile .ncl file from the canonical profile key and a values record.
# Preserves the file's import line; replaces the values block wholesale.
def profile-file-write [file: string, profile_key: string, values: record] {
  let vals_nickel = (value-to-nickel $values 2)
  let content = $"let s = import \"../schemas/config.ncl\" in\n\n\{\n  profile = '($profile_key),\n\n  values = ($vals_nickel),\n}\n"
  $content | save --force $file
}

def config-history-entries [profile: string] {
  let root     = (config-root)
  let hist_dir = $"($root)/reflection/configs/history/($profile | str downcase)"
  if not ($hist_dir | path exists) { return [] }
  ls $hist_dir
  | where name =~ 'cfg-.*\.ncl$'
  | sort-by name
  | each { |f|
      let d = (daemon-export-safe $f.name)
      if $d != null {
        { id: $d.id, seal: $d.seal, supersedes: ($d.supersedes? | default "") }
      } else { null }
    }
  | compact
}
