use ../lib_provisioning/workspace *
use ../lib_provisioning/workspace/notation.nu *
use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval, ncl-eval-soft]

# Resolve the provisioning root for --import-path resolution.
def provisioning-root [] : nothing -> string {
    $env.PROVISIONING? | default "/usr/local/provisioning"
}

# Export a Nickel file as parsed JSON, or error with stderr context.
def nickel-export [path: string] : nothing -> record {
    ncl-eval $path [(provisioning-root)]
}

# Show the workspace DAG composition for a given infra.
#
# Renders each formula_id with its depends_on edges, conditions, health gates,
# and parallel flag. Marks root and terminal nodes.
export def "main dag show" [
    --workspace (-w): string   # Workspace name (default: active)
    --infra (-i): string = "wuji"  # Infra name
] : nothing -> nothing {
    let ws_name = if ($workspace | is-not-empty) {
        $workspace
    } else {
        let details = (get-active-workspace-details)
        if ($details == null) {
            error make { msg: "No active workspace. Pass --workspace or activate one first." }
        }
        $details.name
    }

    let ws_root = (get-workspace-path $ws_name)
    if ($ws_root | is-empty) {
        error make { msg: $"Workspace '($ws_name)' not found in registry." }
    }

    let dag_path = ($ws_root | path join "infra" $infra "dag.ncl")
    if not ($dag_path | path exists) {
        error make { msg: $"dag.ncl not found at ($dag_path)" }
    }

    let dag = (nickel-export $dag_path)
    let formulas = $dag.composition.formulas

    # Determine roots (no depends_on) and terminals (not depended upon by others).
    let all_dep_targets = ($formulas | each { |e| $e.depends_on | each { |d| $d.formula_id } } | flatten)
    let roots     = ($formulas | where ($it.depends_on | length) == 0 | each { |e| $e.formula_id })
    let terminals = ($formulas | where { |e| not ($all_dep_targets | any { |d| $d == $e.formula_id }) } | each { |e| $e.formula_id })

    print $"DAG: ($dag.workspace) / ($dag.infra)"
    print ""

    for entry in $formulas {
        let is_root     = ($roots | any { |r| $r == $entry.formula_id })
        let is_terminal = ($terminals | any { |t| $t == $entry.formula_id })
        let tags = ([
            (if $is_root     { "[root]"     } else { "" })
            (if $is_terminal { "[terminal]" } else { "" })
            (if $entry.parallel { "[parallel]" } else { "" })
        ] | where ($it | is-not-empty) | str join " ")

        print $"  ($entry.formula_id) ($tags)"

        if ($entry.depends_on | length) > 0 {
            for dep in $entry.depends_on {
                print $"    └─ depends_on: ($dep.formula_id) [($dep.condition)]"
            }
        }

        if "health_gate" in $entry and ($entry.health_gate != null) {
            let g = $entry.health_gate
            print $"    └─ health_gate: ($g.check_cmd) | expect=($g.expect) timeout=($g.timeout_ms)ms retries=($g.retries)"
        }

        print ""
    }
}

# Validate dag.ncl against its Nickel schema and cross-check formula_ids against servers.ncl.
export def "main dag validate" [
    --workspace (-w): string   # Workspace name (default: active)
    --infra (-i): string = "wuji"  # Infra name
] : nothing -> nothing {
    let ws_name = if ($workspace | is-not-empty) {
        $workspace
    } else {
        let details = (get-active-workspace-details)
        if ($details == null) {
            error make { msg: "No active workspace. Pass --workspace or activate one first." }
        }
        $details.name
    }

    let ws_root = (get-workspace-path $ws_name)
    if ($ws_root | is-empty) {
        error make { msg: $"Workspace '($ws_name)' not found in registry." }
    }

    let dag_path     = ($ws_root | path join "infra" $infra "dag.ncl")
    let servers_path = ($ws_root | path join "infra" $infra "servers.ncl")
    let prov_root    = (provisioning-root)

    mut passed = true

    # Step 1: schema + contract validation via nickel export
    print "  [1/3] Nickel schema + WorkspaceComposition contract ..."
    let dag_data = (ncl-eval-soft $dag_path [$prov_root] null)
    if ($dag_data | is-not-empty) {
        print "        PASS"
    } else {
        print "        FAIL: nickel export failed or empty"
        $passed = false
    }

    # Step 2: load servers.ncl formula IDs
    print "  [2/3] Cross-check formula_ids against servers.ncl ..."
    let servers_data = (ncl-eval-soft $servers_path [$prov_root] null)
    if ($servers_data | is-empty) {
        print "        SKIP (servers.ncl export failed)"
    } else if ($dag_data | is-not-empty) {
        let dag_ids = ($dag_data | get composition.formulas | each { |e| $e.formula_id })
        let server_ids = ($servers_data | get formulas | each { |f| $f.id })
        let dangling = ($dag_ids | where { |id| not ($server_ids | any { |sid| $sid == $id }) })
        if ($dangling | length) == 0 {
            print "        PASS"
        } else {
            print $"        FAIL: dag.ncl references unknown formula_ids: ($dangling | str join ', ')"
            $passed = false
        }
    }

    # Step 3: check all formulas in servers.ncl are covered by dag.ncl
    print "  [3/3] Coverage — all servers.ncl formulas present in dag.ncl ..."
    if ($servers_data | is-not-empty) and ($dag_data | is-not-empty) {
        let dag_ids = ($dag_data | get composition.formulas | each { |e| $e.formula_id })
        let server_ids = ($servers_data | get formulas | each { |f| $f.id })
        let uncovered = ($server_ids | where { |id| not ($dag_ids | any { |did| $did == $id }) })
        if ($uncovered | length) == 0 {
            print "        PASS"
        } else {
            print $"        WARN: servers.ncl formulas not in dag.ncl (intentional?): ($uncovered | str join ', ')"
        }
    }

    print ""
    if $passed {
        print "dag validate: OK"
    } else {
        print "dag validate: FAILED"
        exit 1
    }
}

# Export dag.ncl in various formats.
export def "main dag export" [
    --workspace (-w): string   # Workspace name (default: active)
    --infra (-i): string = "wuji"  # Infra name
    --format (-f): string = "json"  # Output format: json, dot, cytoscape-json
] : nothing -> nothing {
    let ws_name = if ($workspace | is-not-empty) {
        $workspace
    } else {
        let details = (get-active-workspace-details)
        if ($details == null) {
            error make { msg: "No active workspace. Pass --workspace or activate one first." }
        }
        $details.name
    }

    let ws_root = (get-workspace-path $ws_name)
    if ($ws_root | is-empty) {
        error make { msg: $"Workspace '($ws_name)' not found in registry." }
    }

    let dag_path = ($ws_root | path join "infra" $infra "dag.ncl")
    let dag = (nickel-export $dag_path)

    match $format {
        "json" => {
            print ($dag | to json)
        }
        "dot" => {
            print "digraph dag {"
            print "  rankdir=LR;"
            for entry in $dag.composition.formulas {
                let shape = if ($entry.depends_on | length) == 0 { "shape=invhouse" } else { "shape=box" }
                print $"  \"($entry.formula_id)\" [($shape)];"
                for dep in $entry.depends_on {
                    let label = $dep.condition
                    print $"  \"($dep.formula_id)\" -> \"($entry.formula_id)\" [label=\"($label)\"];"
                }
                if "health_gate" in $entry and ($entry.health_gate != null) and (($entry.depends_on | length) > 0) {
                    let gate_id = $"health_gate__($entry.depends_on.0.formula_id)__($entry.formula_id)"
                    print $"  \"($gate_id)\" [shape=hexagon label=\"health gate\"];"
                    print $"  \"($entry.depends_on.0.formula_id)\" -> \"($gate_id)\" [style=dashed];"
                    print $"  \"($gate_id)\" -> \"($entry.formula_id)\" [style=dashed];"
                }
            }
            print "}"
        }
        "cytoscape-json" => {
            let nodes = ($dag.composition.formulas | each { |e|
                {
                    data: {
                        id: $e.formula_id,
                        label: $e.formula_id,
                        shape: "rectangle",
                        parallel: $e.parallel,
                    }
                }
            })
            let edges = ($dag.composition.formulas | each { |e|
                $e.depends_on | each { |dep|
                    {
                        data: {
                            id: $"($dep.formula_id)__($e.formula_id)",
                            source: $dep.formula_id,
                            target: $e.formula_id,
                            label: $dep.condition,
                        }
                    }
                }
            } | flatten)
            print ({ elements: { nodes: $nodes, edges: $edges } } | to json)
        }
        _ => {
            error make { msg: $"Unknown format '($format)'. Valid: json, dot, cytoscape-json" }
        }
    }
}
