#!/usr/bin/env nu
# reflection/bin/jjw-ncl-merge.nu — 3-way NCL merge driver for jj resolve.
#
# jj calls this as:
#   nu jjw-ncl-merge.nu <left> <right> <base> <output>
#
# Register as a jj merge tool in ~/.config/jj/config.toml:
#   [merge-tools.ncl]
#   program = "nu"
#   args = ["/path/to/reflection/bin/jjw-ncl-merge.nu", "$left", "$right", "$base", "$output"]
#
# Usage from jjw:
#   jj resolve --tool ncl -- .ontology/core.ncl
#
# Strategy (two-layer):
#   1. git merge-file — 3-way text merge. Handles disjoint record additions
#      without parsing NCL: side A adds node_x, side B adds node_y, both
#      added to the same record → clean text merge.
#   2. nickel export — validates the text-merged result is legal NCL.
#      Catches pathological cases where text merge succeeded but produced
#      structurally invalid NCL (e.g., duplicate field names in different
#      positions, mismatched let-bindings).
#
# Exit 0: $output written with merged content.
# Exit 1: auto-merge not possible — jj will fall back to manual resolution.
#
# Requires: git, nickel in PATH.
# ONTOREF_ROOT or ONTOREF_PROJECT_ROOT must be set (provides --import-path
# so nickel can locate schema.ncl and other imports).

const ONLY_ONTOLOGY_FILES = [
    "core.ncl",
    "state.ncl",
    "gate.ncl",
    "manifest.ncl",
]

def main [
    left:   path,   # jj "ours" side (current workspace change)
    right:  path,   # jj "theirs" side (the other side being merged)
    base:   path,   # jj common ancestor
    output: path,   # jj expects the resolved content here
]: nothing -> nothing {
    let left_name  = ($left  | path basename)
    let right_name = ($right | path basename)

    # ── Verify both sides are valid NCL before attempting merge ──────────────

    let import_path = resolve-import-path $left

    let check_left  = (validate-ncl $left  $import_path)
    let check_right = (validate-ncl $right $import_path)

    if not $check_left.ok {
        error make {
            msg: $"left side is not valid NCL:\n($check_left.err)"
        }
    }
    if not $check_right.ok {
        error make {
            msg: $"right side is not valid NCL:\n($check_right.err)"
        }
    }

    # ── Layer 1: git merge-file (3-way text merge) ───────────────────────────
    # --stdout writes result to stdout; none of the input files are modified.
    # Exits 0 = clean, 1 = conflict markers present, >1 = error.

    let merge_r = (do {
        ^git merge-file --stdout ($left) ($base) ($right)
    } | complete)

    if $merge_r.exit_code > 1 {
        error make {
            msg: $"git merge-file error (exit ($merge_r.exit_code)): ($merge_r.stderr | str trim)"
        }
    }

    if $merge_r.exit_code == 1 {
        let conflicts = (count-conflict-markers $merge_r.stdout)
        error make {
            msg: $"text-level merge has ($conflicts) conflict(s) — manual resolution required.\nUse: jj resolve -r @ -- <file>"
        }
    }

    let merged_text = $merge_r.stdout

    # ── Layer 2: validate merged NCL ─────────────────────────────────────────
    # Write to a temp file so nickel can read it; defer ensures cleanup even
    # if nickel export throws or the process is interrupted.

    let tmp_merged = (mktemp --suffix ".ncl")
    defer { rm -f $tmp_merged }

    $merged_text | save --force $tmp_merged

    let validate = (validate-ncl $tmp_merged $import_path)

    if not $validate.ok {
        error make {
            msg: $"text merge succeeded but result is invalid NCL:\n($validate.err)\nManual resolution required."
        }
    }

    # ── Write output ─────────────────────────────────────────────────────────

    $merged_text | save --force $output
    print $"  NCL auto-merge: ($left_name) ← ($right_name) [text+NCL clean]"
}

# Resolve the nickel --import-path from the file location or env.
#
# jj places its temp merge files somewhere under .jj/rewrite/ — they retain
# the original filename but the directory is different. We resolve the import
# path from env vars (set by ontoref sessions) or fall back to cwd.
def resolve-import-path [file: path]: nothing -> string {
    let from_env = (
        $env.ONTOREF_PROJECT_ROOT? | default (
            $env.ONTOREF_ROOT? | default ""
        )
    )
    if ($from_env | is-not-empty) { return $from_env }

    # Best-effort: walk up from the file's real path looking for .ontology/
    let mut dir = ($file | path expand | path dirname)
    for _ in 0..8 {
        if ($dir | path join ".ontology" | path exists) { return $dir }
        let parent = ($dir | path dirname)
        if $parent == $dir { break }  # reached filesystem root
        $dir = $parent
    }

    pwd | path expand
}

# Run nickel export on a file, return {ok: bool, err: string}.
def validate-ncl [file: path, import_path: string]: nothing -> record {
    let r = (do {
        ^nickel export --import-path $import_path $file
    } | complete)

    if $r.exit_code == 0 {
        { ok: true, err: "" }
    } else {
        { ok: false, err: ($r.stderr | str trim) }
    }
}

# Count jj/git conflict markers in merged text.
def count-conflict-markers [text: string]: nothing -> int {
    $text | lines | where { |l| ($l | str starts-with "<<<<<<<") } | length
}

#[test]
def test-conflict-marker-count [] {
    let text = "<<<<<<< left\nfoo\n=======\nbar\n>>>>>>> right\n"
    assert equal (count-conflict-markers $text) 1
}

#[test]
def test-conflict-marker-count-zero [] {
    let text = "let s = import \"schema.ncl\" in\n{ node_a = {} }\n"
    assert equal (count-conflict-markers $text) 0
}
