#!/usr/bin/env nu
# reflection/modules/generator.nu — project output generators.
# Composes full documentation from ontology, ADRs, modes, crates, and scenarios.
# Output formats: md (human), json (agent/MCP), mdbook (publishable HTML).
#
# This is the implementation behind the generate-docs and generate-mdbook modes.
# Modes declare steps; this module does the actual work.

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 }
}

use ../modules/store.nu [daemon-export-safe]

def nickel-import-path [root: string]: nothing -> string {
  let entries = [
    $"($root)/.ontology"
    $"($root)/adrs"
    $"($root)/.ontoref/ontology/schemas"
    $"($root)/.ontoref/adrs"
    $"($root)/.onref"
    $root
    $"($env.ONTOREF_ROOT)/ontology"
    $"($env.ONTOREF_ROOT)/ontology/schemas"
    $"($env.ONTOREF_ROOT)/adrs"
    $env.ONTOREF_ROOT
  ]
  let valid = ($entries | where { |p| $p | path exists } | uniq)
  let existing = ($env.NICKEL_IMPORT_PATH? | default "")
  if ($existing | is-not-empty) {
    ($valid | append $existing) | str join ":"
  } else {
    $valid | str join ":"
  }
}

def ncl-export-safe [root: string, file: string]: nothing -> record {
  let ip = (nickel-import-path $root)
  daemon-export-safe $file --import-path $ip | default {}
}

# ── Data extraction ──────────────────────────────────────────────────────────

def extract-identity [root: string]: nothing -> record {
  let cargo = $"($root)/Cargo.toml"
  mut name = ($root | path basename)
  mut version = ""
  mut description = ""
  mut crates = []

  if ($cargo | path exists) {
    let cargo_data = (open $cargo)
    if ($cargo_data | get -o package.name | is-not-empty) {
      $name = ($cargo_data | get package.name)
      $version = ($cargo_data | get -o package.version | default "")
      $description = ($cargo_data | get -o package.description | default "")
    }
    # Workspace members
    let members = ($cargo_data | get -o workspace.members | default [])
    if ($members | is-not-empty) {
      $crates = ($members | each { |m|
        let member_cargo = $"($root)/($m)/Cargo.toml"
        # Expand globs: crates/* → actual directories
        if ($m | str contains "*") {
          glob $"($root)/($m)/Cargo.toml" | each { |f|
            let crate_dir = ($f | path dirname)
            let crate_data = (open $f)
            {
              name: ($crate_data | get -o package.name | default ($crate_dir | path basename)),
              path: ($crate_dir | str replace $"($root)/" ""),
              description: ($crate_data | get -o package.description | default ""),
              version: ($crate_data | get -o package.version | default ""),
            }
          }
        } else if ($member_cargo | path exists) {
          let crate_data = (open $member_cargo)
          [{
            name: ($crate_data | get -o package.name | default ($m | path basename)),
            path: $m,
            description: ($crate_data | get -o package.description | default ""),
            version: ($crate_data | get -o package.version | default ""),
          }]
        } else { [] }
      } | flatten)
    }
  }

  { name: $name, version: $version, description: $description, crates: $crates, root: $root }
}

def extract-ontology [root: string]: nothing -> record {
  let core_file = $"($root)/.ontology/core.ncl"
  if not ($core_file | path exists) { return { nodes: [], edges: [] } }
  let core = (ncl-export-safe $root $core_file)
  if ($core | is-empty) { return { nodes: [], edges: [] } }
  {
    nodes: ($core.nodes? | default []),
    edges: ($core.edges? | default []),
  }
}

def extract-state [root: string]: nothing -> list<record> {
  let state_file = $"($root)/.ontology/state.ncl"
  if not ($state_file | path exists) { return [] }
  let state = (ncl-export-safe $root $state_file)
  if ($state | is-empty) { return [] }
  $state.dimensions? | default []
}

def extract-gates [root: string]: nothing -> list<record> {
  let gate_file = $"($root)/.ontology/gate.ncl"
  if not ($gate_file | path exists) { return [] }
  let gate = (ncl-export-safe $root $gate_file)
  if ($gate | is-empty) { return [] }
  $gate.gates? | default [] | where { |g| ($g.active? | default false) == true }
}

def extract-adrs [root: string]: nothing -> list<record> {
  let files = (glob $"($root)/adrs/adr-*.ncl")
  $files | each { |f|
    let data = (ncl-export-safe $root $f)
    if ($data | is-not-empty) {
      {
        id: ($data.id? | default ""),
        title: ($data.title? | default ""),
        status: ($data.status? | default ""),
        date: ($data.date? | default ""),
        decision: ($data.decision? | default ""),
        constraints: ($data.constraints? | default []),
      }
    } else { null }
  } | compact
}

def extract-modes [root: string]: nothing -> list<record> {
  let ontoref_modes = (glob $"($env.ONTOREF_ROOT)/reflection/modes/*.ncl")
  let project_modes = if $root != $env.ONTOREF_ROOT {
    glob $"($root)/reflection/modes/*.ncl"
  } else { [] }
  let all_files = ($ontoref_modes | append $project_modes | uniq)

  $all_files | each { |f|
    let data = (ncl-export-safe $root $f)
    if ($data | is-not-empty) and ($data.id? | is-not-empty) {
      {
        id: ($data.id? | default ""),
        trigger: ($data.trigger? | default ""),
        steps: ($data.steps? | default [] | length),
        source: (if ($f | str starts-with $root) { $f | str replace $"($root)/" "" } else { $f | str replace $"($env.ONTOREF_ROOT)/" "onref/" }),
      }
    } else { null }
  } | compact
}

def extract-scenarios [root: string]: nothing -> list<record> {
  let scenarios_dir = $"($root)/reflection/scenarios"
  if not ($scenarios_dir | path exists) { return [] }
  let dirs = (ls $scenarios_dir | where type == dir | get name)
  $dirs | each { |d|
    let scenario_ncl = $"($d)/scenario.ncl"
    let meta = if ($scenario_ncl | path exists) {
      ncl-export-safe $root $scenario_ncl
    } else { {} }
    let files = (ls $d | where type == file | get name | each { |f| $f | path basename })
    {
      category: ($d | path basename),
      path: ($d | str replace $"($root)/" ""),
      files: $files,
      actor: ($meta.actor? | default ""),
      purpose: ($meta.purpose? | default ""),
    }
  }
}

# ── Compose full document ────────────────────────────────────────────────────

def compose-doc-data [root: string]: nothing -> record {
  let identity = (extract-identity $root)
  let ontology = (extract-ontology $root)
  let state = (extract-state $root)
  let gates = (extract-gates $root)
  let adrs = (extract-adrs $root)
  let modes = (extract-modes $root)
  let scenarios = (extract-scenarios $root)

  # Classify ontology nodes by level.
  let nodes = ($ontology.nodes? | default [])
  let axioms = ($nodes | where { |n| ($n.invariant? | default false) == true })
  let tensions = ($nodes | where { |n| ($n.level? | default "") == "Tension" })
  let practices = ($nodes | where { |n| let l = ($n.level? | default ""); $l == "Practice" or $l == "Spiral" })

  {
    identity: $identity,
    architecture: {
      axioms: ($axioms | each { |n| { id: $n.id, name: ($n.name? | default ""), description: ($n.description? | default ""), artifact_paths: ($n.artifact_paths? | default []) } }),
      tensions: ($tensions | each { |n| { id: $n.id, name: ($n.name? | default ""), description: ($n.description? | default "") } }),
      practices: ($practices | each { |n| { id: $n.id, name: ($n.name? | default ""), description: ($n.description? | default ""), artifact_paths: ($n.artifact_paths? | default []) } }),
      edges: ($ontology.edges? | default []),
    },
    state: ($state | each { |d| { id: ($d.id? | default ""), name: ($d.name? | default ""), current: ($d.current_state? | default ""), desired: ($d.desired_state? | default "") } }),
    gates: ($gates | each { |g| { id: ($g.id? | default ""), protects: ($g.protects? | default []), condition: ($g.opening_condition? | default "") } }),
    decisions: ($adrs | where { |a| $a.status == "Accepted" }),
    decisions_all: $adrs,
    modes: $modes,
    scenarios: $scenarios,
  }
}

# ── Format: Markdown ─────────────────────────────────────────────────────────

def render-md [data: record]: nothing -> string {
  let id = $data.identity
  mut lines = [$"# ($id.name)"]

  if ($id.version | is-not-empty) { $lines = ($lines | append $"**Version**: ($id.version)") }
  if ($id.description | is-not-empty) {
    $lines = ($lines | append "")
    $lines = ($lines | append $id.description)
  }
  $lines = ($lines | append "")

  # Crates
  if ($id.crates | is-not-empty) {
    $lines = ($lines | append "## Crates")
    $lines = ($lines | append "")
    $lines = ($lines | append "| Crate | Description |")
    $lines = ($lines | append "|-------|-------------|")
    for c in $id.crates {
      $lines = ($lines | append $"| `($c.name)` | ($c.description) |")
    }
    $lines = ($lines | append "")
  }

  # Architecture — Axioms
  if ($data.architecture.axioms | is-not-empty) {
    $lines = ($lines | append "## Invariants")
    $lines = ($lines | append "")
    for a in $data.architecture.axioms {
      $lines = ($lines | append $"### ($a.name)")
      $lines = ($lines | append "")
      $lines = ($lines | append $a.description)
      if ($a.artifact_paths | is-not-empty) {
        let paths = ($a.artifact_paths | each { |p| $"`($p)`" } | str join ", ")
        $lines = ($lines | append $"Artifacts: ($paths)")
      }
      $lines = ($lines | append "")
    }
  }

  # Architecture — Tensions
  if ($data.architecture.tensions | is-not-empty) {
    $lines = ($lines | append "## Tensions")
    $lines = ($lines | append "")
    for t in $data.architecture.tensions {
      let poles = if ($t.poles | is-not-empty) {
        let pole_strs = ($t.poles | each { |p| $"($p.pole? | default '')" })
        let joined = ($pole_strs | str join " ↔ ")
        $" (char lparen)($joined)(char rparen)"
      } else { "" }
      $lines = ($lines | append $"- **($t.name)**($poles): ($t.description)")
    }
    $lines = ($lines | append "")
  }

  # Architecture — Systems/Practices
  if ($data.architecture.practices | is-not-empty) {
    $lines = ($lines | append "## Systems")
    $lines = ($lines | append "")
    for p in $data.architecture.practices {
      $lines = ($lines | append $"### ($p.name)")
      $lines = ($lines | append "")
      $lines = ($lines | append $p.description)
      if ($p.artifact_paths | is-not-empty) {
        $lines = ($lines | append "")
        for ap in $p.artifact_paths { $lines = ($lines | append $"- `($ap)`") }
      }
      $lines = ($lines | append "")
    }
  }

  # State dimensions
  if ($data.state | is-not-empty) {
    $lines = ($lines | append "## State")
    $lines = ($lines | append "")
    $lines = ($lines | append "| Dimension | Current | Desired |")
    $lines = ($lines | append "|-----------|---------|---------|")
    for d in $data.state {
      let marker = if $d.current == $d.desired { " ✓" } else { "" }
      $lines = ($lines | append $"| ($d.name) | `($d.current)` | `($d.desired)`($marker) |")
    }
    $lines = ($lines | append "")
  }

  # Gates
  if ($data.gates | is-not-empty) {
    $lines = ($lines | append "## Active Gates")
    $lines = ($lines | append "")
    for g in $data.gates {
      $lines = ($lines | append $"- **($g.id)**: protects `($g.protects)` — ($g.condition)")
    }
    $lines = ($lines | append "")
  }

  # Decisions
  if ($data.decisions | is-not-empty) {
    $lines = ($lines | append "## Decisions (ADRs)")
    $lines = ($lines | append "")
    for a in $data.decisions {
      $lines = ($lines | append $"### ($a.id): ($a.title)")
      $lines = ($lines | append "")
      $lines = ($lines | append $a.decision)
      if ($a.constraints | is-not-empty) {
        let hard = ($a.constraints | where { |c| ($c.severity? | default "") == "Hard" })
        if ($hard | is-not-empty) {
          $lines = ($lines | append "")
          $lines = ($lines | append "**Hard constraints:**")
          $lines = ($lines | append "")
          for c in $hard {
            $lines = ($lines | append $"- ($c.description? | default $c.scope)")
          }
        }
      }
      $lines = ($lines | append "")
    }
  }

  # Modes
  if ($data.modes | is-not-empty) {
    $lines = ($lines | append "## Operational Modes")
    $lines = ($lines | append "")
    for m in $data.modes {
      let step_count = $"(char lparen)($m.steps) steps(char rparen)"
      $lines = ($lines | append $"- **($m.id)** ($step_count): ($m.trigger)")
    }
    $lines = ($lines | append "")
  }

  # Scenarios
  if ($data.scenarios | is-not-empty) {
    $lines = ($lines | append "## Scenarios")
    $lines = ($lines | append "")
    for s in $data.scenarios {
      let purpose_str = if ($s.purpose | is-not-empty) { $" [($s.purpose)]" } else { "" }
      let file_count = $"(char lparen)($s.files | length) files(char rparen)"
      $lines = ($lines | append $"- **($s.category)**($purpose_str): `($s.path)/` ($file_count)")
    }
    $lines = ($lines | append "")
  }

  $lines | str join "\n"
}

# ── Format: mdBook ───────────────────────────────────────────────────────────

def render-mdbook [data: record, root: string] {
  let docs_src = $"($root)/docs/src"
  mkdir $docs_src
  mkdir $"($docs_src)/architecture"
  mkdir $"($docs_src)/decisions"
  mkdir $"($docs_src)/modes"

  # README / intro
  let intro = ([
    $"# ($data.identity.name)"
    ""
    ($data.identity.description)
    ""
    $"Generated from project ontology and reflection data."
  ] | str join "\n")
  $intro | save -f $"($docs_src)/README.md"

  # Architecture page
  let arch_data = {
    identity: $data.identity,
    architecture: $data.architecture,
    state: $data.state,
    gates: $data.gates,
    decisions: [],
    decisions_all: [],
    modes: [],
    scenarios: [],
  }
  let arch_md = (render-md $arch_data)
  $arch_md | save -f $"($docs_src)/architecture/overview.md"

  # Individual ADR pages
  for adr in $data.decisions_all {
    let adr_data = {
      identity: { name: "", version: "", description: "", crates: [], root: "" },
      architecture: { axioms: [], tensions: [], practices: [], edges: [] },
      state: [],
      gates: [],
      decisions: (if $adr.status == "Accepted" { [$adr] } else { [] }),
      decisions_all: [$adr],
      modes: [],
      scenarios: [],
    }
    let status_badge = match $adr.status {
      "Accepted" => "✅ Accepted",
      "Proposed" => "📝 Proposed",
      "Superseded" => "🔄 Superseded",
      _ => $adr.status,
    }
    let content = ([
      $"# ($adr.id): ($adr.title)"
      ""
      $"**Status**: ($status_badge)  **Date**: ($adr.date)"
      ""
      $adr.decision
    ] | str join "\n")
    $content | save -f $"($docs_src)/decisions/($adr.id).md"
  }

  # ADR index
  let adr_index_lines = (["# Decisions (ADRs)" ""] | append (
    $data.decisions_all | each { |a|
      let link = $"(char lparen)./($a.id).md(char rparen)"
      $"- [($a.id): ($a.title)]($link) — ($a.status)"
    }
  ))
  ($adr_index_lines | str join "\n") | save -f $"($docs_src)/decisions/README.md"

  # Modes page
  let modes_lines = (["# Operational Modes" ""] | append (
    $data.modes | each { |m|
      let step_count = $"(char lparen)($m.steps) steps(char rparen)"
      $"- **($m.id)** ($step_count): ($m.trigger)"
    }
  ))
  ($modes_lines | str join "\n") | save -f $"($docs_src)/modes/README.md"

  # SUMMARY.md
  mut summary = [
    "# Summary"
    ""
    "[Introduction](README.md)"
    ""
    "# Architecture"
    ""
    "- [Overview](architecture/overview.md)"
    ""
    "# Decisions"
    ""
  ]
  for adr in $data.decisions_all {
    $summary = ($summary | append $"- [($adr.id)](decisions/($adr.id).md)")
  }
  $summary = ($summary | append ["" "# Operations" "" "- [Modes](modes/README.md)"])
  ($summary | str join "\n") | save -f $"($docs_src)/SUMMARY.md"

  # book.toml
  let book_toml = $"[book]
authors = [\"Generated by ontoref\"]
language = \"en\"
multilingual = false
src = \"src\"
title = \"($data.identity.name) Documentation\"

[output.html]
default-theme = \"navy\"
preferred-dark-theme = \"navy\"
"
  let book_file = $"($root)/docs/book.toml"
  if not ($book_file | path exists) {
    $book_toml | save -f $book_file
  }

  print $"  (ansi green)Generated:(ansi reset) ($docs_src)/SUMMARY.md"
  print $"  (ansi green)Generated:(ansi reset) ($docs_src)/README.md"
  print $"  (ansi green)Generated:(ansi reset) ($docs_src)/architecture/overview.md"
  let adr_count = ($data.decisions_all | length)
  print $"  (ansi green)Generated:(ansi reset) ($adr_count) ADR pages in ($docs_src)/decisions/"
  print $"  (ansi green)Generated:(ansi reset) ($docs_src)/modes/README.md"

  # Build if mdbook is available
  let has_mdbook = (do { ^which mdbook } | complete | get exit_code) == 0
  if $has_mdbook {
    print ""
    print $"  (ansi cyan)Building mdBook...(ansi reset)"
    let result = (do { ^mdbook build $"($root)/docs/" } | complete)
    if $result.exit_code == 0 {
      print $"  (ansi green)Book built:(ansi reset) ($root)/docs/book/"
    } else {
      print $"  (ansi yellow)Build failed:(ansi reset) ($result.stderr | str trim)"
      print $"  (ansi dark_gray)Run manually: mdbook build docs/(ansi reset)"
    }
  } else {
    print ""
    print $"  (ansi dark_gray)mdbook not found. Install: cargo install mdbook(ansi reset)"
    print $"  (ansi dark_gray)Then: mdbook build docs/(ansi reset)"
  }
}

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

export def "docs generate" [
  --fmt (-f): string = "",  # Output format: md | json | yaml | mdbook (short: m j y)
]: nothing -> nothing {
  let root = (project-root)
  let actor = ($env.ONTOREF_ACTOR? | default "developer")
  let raw_fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "md" }
  let f = match $raw_fmt {
    "j" => "json",
    "y" => "yaml",
    "m" => "md",
    _ => $raw_fmt,
  }

  let data = (compose-doc-data $root)

  match $f {
    "json" => { print ($data | to json) },
    "yaml" => { print ($data | to yaml) },
    "md" => { print (render-md $data) },
    "mdbook" => { render-mdbook $data $root },
    _ => {
      print $"  Unknown format: ($f). Available: md | json | yaml | mdbook"
    },
  }
}

export def "docs formats" []: nothing -> nothing {
  print ""
  print "  Available documentation formats:"
  print ""
  print $"  (ansi cyan)md(ansi reset)      Markdown document to stdout (default for humans)"
  print $"  (ansi cyan)json(ansi reset)    Structured JSON to stdout (default for agents)"
  print $"  (ansi cyan)yaml(ansi reset)    YAML to stdout"
  print $"  (ansi cyan)mdbook(ansi reset)  Generates docs/src/ + SUMMARY.md, builds if mdbook installed"
  print ""
  print $"  (ansi dark_gray)Usage: strat docs generate --fmt <format>(ansi reset)"
  print $"  (ansi dark_gray)Short: strat docs generate -f j(ansi reset)"
  print ""
}
