#!/usr/bin/env nu
# reflection/bin/jjw.nu — jj + ontoref + rad orchestration for agent workspaces.
#
# Wraps three CLIs (jj, ontoref, rad) into a coherent agent lifecycle:
#   jjw agent create  → workspace + ontoref run
#   jjw agent status  → list active workspaces and their run state
#   jjw agent step    → report a step in the ontoref run
#   jjw agent publish → validate + push + open Radicle patch
#   jjw agent merge   → merge commit + advance main + cleanup
#   jjw agent discard → abandon workspace and cancel run
#
# Requires: jj >= 0.40, ontoref in PATH, rad >= 1.0 (optional for publish)
#
# NCL auto-merge for .ontology/ conflicts:
#   Register jjw-ncl-merge.nu as a jj merge tool in ~/.config/jj/config.toml:
#     [merge-tools.ncl]
#     program = "nu"
#     args = ["{ONTOREF_ROOT}/reflection/bin/jjw-ncl-merge.nu", "$left", "$right", "$base", "$output"]
#   Then resolve: jj resolve --tool ncl -- .ontology/core.ncl
#
# .ontoref-run metadata file lives in each workspace dir:
#   .agents/<run_id>/.ontoref-run
#
# ONTOREF_ROOT must be set (done by `use env.nu *` in ontoref sessions).
# When called standalone, set ONTOREF_ROOT to the ontoref installation path.

const ONTOREF_RUN_FILE = ".ontoref-run"
const AGENTS_DIR       = ".agents"

# ── Helpers ──────────────────────────────────────────────────────────────────

def project-root []: nothing -> string {
    let pr = ($env.ONTOREF_PROJECT_ROOT? | default "")
    if ($pr | is-not-empty) { $pr } else { pwd | path expand }
}

def agents-dir []: nothing -> string {
    $"(project-root)/($AGENTS_DIR)"
}

def workspace-path [run_id: string]: nothing -> string {
    $"(agents-dir)/($run_id)"
}

def run-file [run_id: string]: nothing -> string {
    $"(workspace-path $run_id)/($ONTOREF_RUN_FILE)"
}

def ontoref-bin []: nothing -> string {
    let root = ($env.ONTOREF_ROOT? | default "")
    if ($root | is-not-empty) {
        let p = $"($root)/ontoref"
        if ($p | path exists) { return $p }
    }
    # Fall back to PATH
    let which_r = do { ^which ontoref } | complete
    if $which_r.exit_code == 0 { $which_r.stdout | str trim } else { "ontoref" }
}

def jj [args: list<string>]: nothing -> string {
    let root = (project-root)
    let r = do { ^jj --no-pager --repository $root ...$args } | complete
    if $r.exit_code != 0 {
        error make { msg: $"jj ($args | str join ' '): ($r.stderr | str trim)" }
    }
    $r.stdout
}

def onto [args: list<string>]: nothing -> string {
    let bin = (ontoref-bin)
    let r = do { ^nu $bin ...$args } | complete
    if $r.exit_code != 0 {
        error make { msg: $"ontoref ($args | str join ' '): ($r.stderr | str trim)" }
    }
    $r.stdout
}

def read-run-file [run_id: string]: nothing -> record {
    let f = (run-file $run_id)
    if not ($f | path exists) {
        error make { msg: $"No .ontoref-run for workspace '($run_id)'" }
    }
    open $f | from json
}

def get-change-id [run_id: string]: nothing -> string {
    let ws_name = $run_id
    jj ["log", "--no-graph", "-r", $"($ws_name)@", "-T", "change_id ++ \"\\n\""] | str trim
}

def has-rad-remote []: nothing -> bool {
    let r = do { ^jj --no-pager git remote list } | complete
    if $r.exit_code != 0 { return false }
    $r.stdout | lines | any { |l| ($l | str trim | str starts-with "rad") }
}

# ── Commands ─────────────────────────────────────────────────────────────────

# Create a new agent workspace tied to an ontoref run.
export def "agent create" [
    agent_id: string,           # Logical agent identifier (e.g. "architect-01")
    --task (-t): string = "",   # Backlog task ID to associate
    --mode (-m): string = "sync-ontology",
]: nothing -> nothing {
    let root = (project-root)
    let agents = (agents-dir)
    mkdir $agents

    # Start ontoref run — get run_id from JSON output
    let run_json = (onto ["run", "start", $mode, "--task", $task, "--fmt", "json"])
    let run_data = ($run_json | from json)
    let run_id   = $run_data.run_id

    let ws_path = (workspace-path $run_id)
    let ws_path_str = $ws_path

    # jj workspace add — workspace name = run_id (unique UUID-like string)
    jj ["workspace", "add", $ws_path_str, "--name", $run_id] | ignore

    let change_id = (get-change-id $run_id)

    # Write .ontoref-run metadata
    let meta = {
        run_id:          $run_id,
        agent_id:        $agent_id,
        mode:            $mode,
        task:            $task,
        workspace_name:  $run_id,
        workspace_path:  $ws_path_str,
        change_id:       $change_id,
        created_at:      (date now | format date "%Y-%m-%dT%H:%M:%SZ"),
        remote:          (if (has-rad-remote) { "rad" } else { "origin" }),
    }

    $meta | to json | save --force (run-file $run_id)

    print $"  workspace: ($ws_path_str)"
    print $"  run_id:    ($run_id)"
    print $"  change_id: ($change_id)"
}

# List active agent workspaces and their ontoref run state.
export def "agent status" [
    agent_id?: string,          # Filter by agent_id (optional)
]: nothing -> nothing {
    let agents = (agents-dir)
    if not ($agents | path exists) {
        print "  no active agent workspaces"
        return
    }

    let entries = (ls $agents | where type == "dir")
    if ($entries | is-empty) {
        print "  no active agent workspaces"
        return
    }

    let rows = $entries | each { |e|
        let run_id = ($e.name | path basename)
        let rf = $"($e.name)/($ONTOREF_RUN_FILE)"
        if not ($rf | path exists) { return null }
        let meta = (open $rf | from json)
        if ($agent_id | is-not-empty) and ($meta.agent_id != $agent_id) { return null }

        let change_id = (
            do { jj ["log", "--no-graph", "-r", $"($run_id)@", "-T", "change_id ++ \"\\n\""] }
            | complete | if $in.exit_code == 0 { $in.stdout | str trim } else { "unknown" }
        )

        {
            run_id:    $run_id,
            agent_id:  $meta.agent_id,
            mode:      $meta.mode,
            task:      $meta.task,
            change_id: $change_id,
            created:   $meta.created_at,
        }
    } | where { |r| $r != null }

    if ($rows | is-empty) {
        print $"  no workspaces for agent ($agent_id? | default "any")"
    } else {
        print ($rows | table)
    }
}

# Report a step in the ontoref run for an agent workspace.
export def "agent step" [
    run_id: string,             # Workspace / run ID
    step_id: string,            # Step to report
    --status (-s): string = "", # pass | fail | skip
]: nothing -> nothing {
    if ($status | is-empty) or not ($status in ["pass", "fail", "skip"]) {
        error make { msg: $"--status required: pass | fail | skip (got: '($status)')" }
    }
    let meta = (read-run-file $run_id)
    onto ["step", "report", $meta.mode, $step_id, "--status", $status] | ignore
    print $"  ($run_id) step ($step_id) → ($status)"
}

# Validate, push, and open a Radicle patch (or git push) for an agent workspace.
export def "agent publish" [
    run_id: string,
]: nothing -> nothing {
    let meta = (read-run-file $run_id)

    # 1. Ontology drift check
    onto ["sync", "diff", "--fail-on-drift"] | ignore

    # 2. Constraint validation
    onto ["validate", "check-all"] | ignore

    # 3. Verify all mode steps are reported
    onto ["mode", "complete", $meta.mode] | ignore

    # 4. Set commit description
    let desc = $"agent(($meta.agent_id)): ($meta.task)"
    jj ["describe", "-m", $desc, "-r", $meta.change_id] | ignore

    # 5. Create/update bookmark — `set` is idempotent (safe on re-push after amend)
    let bookmark = $"agent/($meta.agent_id)/($run_id)"
    jj ["bookmark", "set", $bookmark, "-r", $meta.change_id] | ignore

    # 6. Push
    let remote = $meta.remote
    jj ["git", "push", "--remote", $remote, "--bookmark", $bookmark] | ignore

    # 7. Open Radicle patch only if rad remote is configured
    if $remote == "rad" {
        let r = do { ^rad patch open --title $desc } | complete
        if $r.exit_code != 0 {
            print $"  warning: rad patch open failed: ($r.stderr | str trim)"
        } else {
            print $"  patch opened on Radicle"
        }
    } else {
        print $"  pushed to ($remote) — no Radicle remote, skipped rad patch open"
        let modified = (jj ["diff", "--name-only", "-r", $meta.change_id])
        if ($modified | lines | any { |l| $l | str starts-with ".ontology/" }) {
            print $"  WARNING: workspace modified .ontology/ files but remote is git-only."
            print $"           merge may be rejected by deny_ontology_writes on the server."
        }
    }

    print $"  published: ($bookmark) → ($remote)"
}

# Merge workspace into main, advance bookmark, and clean up.
export def "agent merge" [
    run_id: string,
]: nothing -> nothing {
    let meta = (read-run-file $run_id)
    let change_id = $meta.change_id

    # 1. Create merge commit (no-edit — doesn't touch working copies)
    jj ["new", "--no-edit", "main", $change_id] | ignore

    # 2. Find the merge commit: heads of change_id's descendants, excluding
    #    change_id itself. `heads(change_id::)` alone can match change_id if it
    #    is a head in the workspace — `~ $change_id` excludes the original commit.
    let merge_revset = $"heads(($change_id)::) ~ ($change_id)"

    # 3. Check for conflicts — only block on .ontology/ conflicts
    let conflict_out = (jj ["log", "--no-graph", "-r", $merge_revset,
        "-T", "if(conflict, \"CONFLICT\", \"CLEAN\") ++ \"\\n\""])

    if ($conflict_out | str trim) == "CONFLICT" {
        let files_out = (
            do { jj ["diff", "--name-only", "-r", $merge_revset] }
            | complete | get -o result | default ""
        )
        let onto_conflicts = ($files_out | lines | where { |l| $l | str starts-with ".ontology/" })
        if ($onto_conflicts | is-not-empty) {
            # Abandon the conflicted merge commit before aborting
            do { jj ["abandon", $merge_revset] } | complete | ignore
            error make {
                msg: $".ontology/ conflicts require NCL merge resolution: ($onto_conflicts | str join ', ')"
            }
        }
        print $"  WARNING: merge commit has non-ontology conflicts (stored as jj data)"
        print $"           use `jj resolve -r ($merge_revset)` in workspace to clean up"
    }

    # 4. Advance main bookmark to the merge commit
    jj ["bookmark", "set", "main", "-r", $merge_revset] | ignore

    # 5. Dispatch VCS event so ontoref syncs
    let hooks = $"($env.ONTOREF_ROOT? | default '')/reflection/hooks/git-event.nu"
    if ($hooks | path exists) {
        do { ^nu -c $"use ($hooks) *; on-vcs-event 'post-merge'" } | complete | ignore
    }

    # 6. Forget workspace and remove directory
    do { ^jj --no-pager workspace forget $run_id } | complete | ignore
    let ws_path = (workspace-path $run_id)
    if ($ws_path | path exists) {
        rm -rf $ws_path
    }

    print $"  merged ($run_id) into main, workspace removed"
}

# Abandon an agent workspace without merging. Cancels the ontoref run.
export def "agent discard" [
    run_id: string,
]: nothing -> nothing {
    # Forget from jj
    do { ^jj --no-pager workspace forget $run_id } | complete | ignore

    # Remove workspace directory
    let ws_path = (workspace-path $run_id)
    if ($ws_path | path exists) {
        rm -rf $ws_path
    }

    # Cancel the ontoref run (best-effort — run cancel may not exist in all versions)
    let r = do { onto ["run", "cancel", $run_id] } | complete
    if $r.exit_code != 0 {
        print $"  advisory: ontoref run cancel returned non-zero (run may already be complete)"
    }

    print $"  discarded workspace ($run_id)"
}
