#!/bin/bash
# Tree parser utilities for YAML cookbook definitions
#
# Usage:
#   source "$(dirname "${BASH_SOURCE[0]}")/tree-parser.sh"
#
# Provides:
#   - tree_parse() - Parse a tree YAML file
#   - tree_get_step() - Get a specific step definition
#   - tree_get_deps() - Get dependencies for a step
#   - tree_execution_order() - Get topological sort of steps
#   - tree_step_ready() - Check if step's dependencies are satisfied
#   - tree_expand_template() - Expand Go template variables
#   - tree_list() - List all available trees
#   - tree_get_teardown() - Get teardown steps

# Trees directory
TREES_DIR="${TREES_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../trees}"

# Check for yq
_tree_check_yq() {
    if ! command -v yq &> /dev/null; then
        echo "Error: yq is required but not installed" >&2
        echo "Install with: brew install yq" >&2
        return 1
    fi
}

# Get tree file path
_tree_path() {
    local tree_name="$1"
    echo "$TREES_DIR/${tree_name}.yaml"
}

# Parse a tree YAML file
# Arguments: tree_name
# Returns: tree JSON on stdout
# Example: tree_json=$(tree_parse "landing-page")
tree_parse() {
    local tree_name="$1"
    local path
    path="$(_tree_path "$tree_name")"

    _tree_check_yq || return 1

    if [[ ! -f "$path" ]]; then
        echo "Error: Tree '$tree_name' not found at $path" >&2
        return 1
    fi

    yq -o=json "$path"
}

# Get tree metadata
# Arguments: tree_name
# Returns: JSON with name, description, version
tree_get_meta() {
    local tree_name="$1"

    # Pipe directly to avoid newline corruption in bash variables
    tree_parse "$tree_name" | jq '{name: .name, description: .description, version: .version}'
}

# Get default vars from tree
# Arguments: tree_name
# Returns: JSON object of default vars
tree_get_default_vars() {
    local tree_name="$1"

    # Pipe directly to avoid newline corruption in bash variables
    tree_parse "$tree_name" | jq '.vars // {}'
}

# Get a specific step definition
# Arguments: tree_name step_name
# Returns: step JSON on stdout
# Example: step=$(tree_get_step "landing-page" "create-project")
tree_get_step() {
    local tree_name="$1"
    local step_name="$2"

    # Use a temp file to handle multi-line JSON safely
    local tmpfile
    tmpfile=$(mktemp)

    # Parse tree directly to temp file to avoid bash variable corruption
    if ! tree_parse "$tree_name" > "$tmpfile" 2>/dev/null; then
        rm -f "$tmpfile"
        return 1
    fi

    # Check if step exists
    local step_exists
    step_exists=$(jq --arg step "$step_name" '.steps | has($step)' "$tmpfile")

    if [[ "$step_exists" != "true" ]]; then
        echo "Error: Step '$step_name' not found in tree '$tree_name'" >&2
        rm -f "$tmpfile"
        return 1
    fi

    # Extract step and add name field
    local result
    result=$(jq --arg step "$step_name" --arg name "$step_name" '.steps[$step] + {name: $name}' "$tmpfile")
    rm -f "$tmpfile"
    echo "$result"
}

# Get all step names
# Arguments: tree_name
# Returns: newline-separated list of step names
tree_get_steps() {
    local tree_name="$1"

    # Pipe directly to avoid newline corruption in bash variables
    tree_parse "$tree_name" | jq -r '.steps | keys[]'
}

# Get dependencies for a step
# Arguments: tree_name step_name
# Returns: newline-separated list of dependency step names
# Example: deps=$(tree_get_deps "landing-page" "add-component")
tree_get_deps() {
    local tree_name="$1"
    local step_name="$2"

    local step
    step=$(tree_get_step "$tree_name" "$step_name") || return 1

    echo "$step" | jq -r '.depends_on // [] | .[]'
}

# Get topological sort of steps (execution order)
# Arguments: tree_name
# Returns: newline-separated list of step names in execution order
# Example: order=$(tree_execution_order "landing-page")
tree_execution_order() {
    local tree_name="$1"

    # Pipe directly through jq to avoid bash variable corruption
    # Use a temp file to safely handle multi-line shell commands in YAML
    local tmpfile
    tmpfile=$(mktemp)

    if ! tree_parse "$tree_name" > "$tmpfile" 2>/dev/null; then
        rm -f "$tmpfile"
        return 1
    fi

    # Kahn's algorithm for topological sort - use jq on file directly
    local result
    result=$(jq -r '.steps |
        # Build in-degree map and adjacency list
        . as $steps |
        (keys | map({key: ., value: 0}) | from_entries) as $initial_degrees |
        reduce keys[] as $step (
            {degrees: $initial_degrees, adj: {}};
            . as $state |
            ($steps[$step].depends_on // []) as $deps |
            reduce $deps[] as $dep (
                $state;
                .degrees[$step] += 1 |
                .adj[$dep] = ((.adj[$dep] // []) + [$step])
            )
        ) |

        # Kahns algorithm
        . as $graph |
        {
            result: [],
            queue: [$graph.degrees | to_entries | .[] | select(.value == 0) | .key],
            degrees: $graph.degrees,
            adj: $graph.adj
        } |
        until(.queue | length == 0;
            .queue[0] as $node |
            .result += [$node] |
            .queue = .queue[1:] |
            reduce ((.adj[$node] // [])[] ) as $neighbor (
                .;
                .degrees[$neighbor] -= 1 |
                if .degrees[$neighbor] == 0 then
                    .queue += [$neighbor]
                else
                    .
                end
            )
        ) |
        .result[]
    ' "$tmpfile")
    rm -f "$tmpfile"
    echo "$result"
}

# Check if a step's dependencies are satisfied
# Arguments: tree_name step_name completed_steps_json
# Returns: 0 if ready, 1 if not
# Example: if tree_step_ready "landing-page" "add-component" '["create-project"]'; then ...
tree_step_ready() {
    local tree_name="$1"
    local step_name="$2"
    local completed_steps="$3"  # JSON array of completed step names

    local deps
    deps=$(tree_get_deps "$tree_name" "$step_name") || return 1

    if [[ -z "$deps" ]]; then
        return 0  # No dependencies, always ready
    fi

    # Check each dependency
    while IFS= read -r dep; do
        if ! echo "$completed_steps" | jq -e --arg d "$dep" 'index($d) != null' > /dev/null; then
            return 1  # Dependency not satisfied
        fi
    done <<< "$deps"

    return 0
}

# Expand Go template variables in a string
# Arguments: template_string vars_json outputs_json
# Returns: expanded string
# Example: result=$(tree_expand_template "{{ .vars.project_name }}" '{"project_name":"test"}' '{}')
tree_expand_template() {
    local template="$1"
    local vars_json="${2:-"{}"}"
    local outputs_json="${3:-"{}"}"

    # Build context for template expansion
    local context
    context=$(jq -n \
        --argjson vars "$vars_json" \
        --argjson outputs "$outputs_json" \
        '{vars: $vars, outputs: $outputs}')

    # Simple template expansion using sed
    # Handle {{ .vars.NAME }} patterns
    local result="$template"

    # Extract and replace all {{ .vars.xxx }} patterns
    while [[ "$result" =~ \{\{[[:space:]]*\.vars\.([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*\}\} ]]; do
        local var_name="${BASH_REMATCH[1]}"
        local var_value
        var_value=$(echo "$vars_json" | jq -r ".[\"$var_name\"] // \"\"")
        result="${result//\{\{ .vars.$var_name \}\}/$var_value}"
        result="${result//\{\{.vars.$var_name\}\}/$var_value}"
    done

    # Extract and replace all {{ .outputs.step.key }} patterns
    while [[ "$result" =~ \{\{[[:space:]]*\.outputs\.([a-zA-Z_][a-zA-Z0-9_-]*)\.([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*\}\} ]]; do
        local step_name="${BASH_REMATCH[1]}"
        local key_name="${BASH_REMATCH[2]}"
        local out_value
        out_value=$(echo "$outputs_json" | jq -r ".[\"$step_name\"][\"$key_name\"] // \"\"")
        result="${result//\{\{ .outputs.$step_name.$key_name \}\}/$out_value}"
        result="${result//\{\{.outputs.$step_name.$key_name\}\}/$out_value}"
    done

    echo "$result"
}

# Expand templates in a step definition
# Arguments: step_json vars_json outputs_json
# Returns: step JSON with templates expanded
tree_expand_step() {
    local step_json="$1"
    local vars_json="${2:-"{}"}"
    local outputs_json="${3:-"{}"}"

    # Convert step to string and expand templates
    local step_str
    step_str=$(echo "$step_json" | jq -c '.')

    local expanded
    expanded=$(tree_expand_template "$step_str" "$vars_json" "$outputs_json")

    echo "$expanded"
}

# List all available trees
# Returns: newline-separated list of tree names
# Example: trees=$(tree_list)
tree_list() {
    _tree_check_yq || return 1

    for f in "$TREES_DIR"/*.yaml; do
        [[ -e "$f" ]] || continue
        basename "$f" .yaml
    done
}

# List trees with descriptions
# Returns: formatted list of trees with descriptions
tree_list_detail() {
    _tree_check_yq || return 1

    for f in "$TREES_DIR"/*.yaml; do
        [[ -e "$f" ]] || continue
        local name
        name=$(basename "$f" .yaml)
        local desc
        desc=$(yq -r '.description // "No description"' "$f" 2>/dev/null)
        printf "  %-20s %s\n" "$name" "$desc"
    done
}

# Get teardown steps
# Arguments: tree_name
# Returns: JSON array of teardown steps
# Example: teardown=$(tree_get_teardown "landing-page")
tree_get_teardown() {
    local tree_name="$1"

    # Pipe directly to avoid newline corruption in bash variables
    tree_parse "$tree_name" | jq '.teardown // []'
}

# Get step action type
# Arguments: step_json
# Returns: action type string
tree_step_action() {
    local step_json="$1"
    echo "$step_json" | jq -r '.action // "unknown"'
}

# Get step on_error behavior
# Arguments: step_json
# Returns: "fail" or "continue"
tree_step_on_error() {
    local step_json="$1"
    echo "$step_json" | jq -r '.on_error // "fail"'
}

# Get output extraction rules from step
# Arguments: step_json
# Returns: JSON array of output rules [{name, jq_path}]
tree_step_outputs() {
    local step_json="$1"

    # outputs can be:
    # - array of {key: jq_path}
    # Example: [{project_id: ".data.name"}]
    echo "$step_json" | jq '
        .outputs // [] |
        map(to_entries | .[0] | {name: .key, jq_path: .value})
    '
}

# Validate tree YAML
# Arguments: tree_name
# Returns: 0 if valid, 1 with error messages if not
tree_validate() {
    local tree_name="$1"

    # Use a temp file to handle multi-line JSON safely
    local tmpfile
    tmpfile=$(mktemp)

    if ! tree_parse "$tree_name" > "$tmpfile" 2>/dev/null; then
        echo "Error: Failed to parse tree '$tree_name'" >&2
        rm -f "$tmpfile"
        return 1
    fi

    local errors=()

    # Check required fields
    local name
    name=$(jq -r '.name // ""' "$tmpfile")
    if [[ -z "$name" ]]; then
        errors+=("Missing required field: name")
    fi

    # Check that all steps have action field
    local steps_without_action
    steps_without_action=$(jq -r '.steps | to_entries | .[] | select(.value.action == null) | .key' "$tmpfile")
    if [[ -n "$steps_without_action" ]]; then
        while IFS= read -r step; do
            errors+=("Step '$step' missing required field: action")
        done <<< "$steps_without_action"
    fi

    # Check that dependencies reference existing steps
    local invalid_deps
    invalid_deps=$(jq -r '
        .steps | keys as $all_steps |
        to_entries | .[] |
        .key as $step |
        (.value.depends_on // [])[] |
        select(. as $dep | $all_steps | index($dep) == null) |
        "\($step) depends on non-existent step: \(.)"
    ' "$tmpfile")
    if [[ -n "$invalid_deps" ]]; then
        while IFS= read -r err; do
            errors+=("$err")
        done <<< "$invalid_deps"
    fi

    # Check for cycles (execution_order will fail if there are cycles)
    if ! tree_execution_order "$tree_name" > /dev/null 2>&1; then
        errors+=("Dependency cycle detected")
    fi

    # Check output references have proper depends_on (transitive)
    local output_ref_errors
    output_ref_errors=$(jq -r '
        .steps as $steps |

        # Build transitive dependency closure for each step
        def transitive_deps($step_name):
            def visit($s; $visited):
                if $visited | index($s) then $visited
                else
                    ($visited + [$s]) as $v |
                    reduce (($steps[$s].depends_on // [])[] | select(. as $d | $steps | has($d))) as $dep
                        ($v; visit($dep; .))
                end;
            visit($step_name; []) | .[1:];  # Remove self from result

        $steps | keys[] as $step_name |
        $steps[$step_name] as $step |
        (transitive_deps($step_name)) as $all_deps |
        # Convert step to string and find all {{ .outputs.X.Y }} patterns
        ($step | tostring | [match("\\{\\{\\s*\\.outputs\\.([a-zA-Z_][a-zA-Z0-9_-]*)\\.[a-zA-Z_][a-zA-Z0-9_]*\\s*\\}\\}"; "g")] | map(.captures[0].string) | unique) as $refs |
        $refs[] |
        . as $ref |
        # Check if ref exists as a step
        if ($steps | has($ref) | not) then
          "\($step_name): references outputs from non-existent step \"\($ref)\""
        # Check if ref is in transitive dependencies
        elif ($all_deps | index($ref) == null) then
          "\($step_name): references outputs from \"\($ref)\" but does not depend on it (directly or transitively)"
        else
          empty
        end
    ' "$tmpfile")
    if [[ -n "$output_ref_errors" ]]; then
        while IFS= read -r err; do
            errors+=("$err")
        done <<< "$output_ref_errors"
    fi

    # Clean up temp file
    rm -f "$tmpfile"

    # Report errors
    if [[ ${#errors[@]} -gt 0 ]]; then
        echo "Validation errors in tree '$tree_name':" >&2
        for err in "${errors[@]}"; do
            echo "  - $err" >&2
        done
        return 1
    fi

    return 0
}
