#!/usr/bin/env bash

:<<'DOC'
Extracts a value from a JSON string by path, with optional fallback paths.

Usage:
  jq_get "$json" '.name'
  jq_get "$json" '.name' 'unnamed'
  jq_get "$json" '.nodes' '.edges' '[]'

With two args, returns the value at the path (null if missing).
With three or more args, tries each path in order and returns the first
non-null result. The last argument is the fallback default.
DOC
function jq_get() {
  local json="$1"
  shift

  if [[ $# -eq 1 ]]; then
    local query
    query="$(__json_api__normalize_path "$1")"
    jq -r "$query" <<< "$json"
    return
  fi

  local args=("$@")
  local default="${args[-1]}"
  unset 'args[-1]'

  for path in "${args[@]}"; do
    local query value
    query="$(__json_api__normalize_path "$path")"
    value="$(jq -r "$query" <<< "$json")"
    if jq_defined "$value"; then
      echo "$value"
      return
    fi
  done

  echo "$default"
}

:<<'DOC'
Decodes a single JSON value to raw shell text (same as jq_get on root: jq -r '.').

Typical use: lines from jq_foreach / jq_reduce over jq -c '.[]' output, where
each string element is still JSON-quoted (e.g. "repo-name" → repo-name).

Usage: jq_unwrap "$json_fragment"
DOC
function jq_unwrap() {
  jq_get "$1" '.'
}

:<<'DOC'
Encodes a bash string as a JSON string value. Same as jq_encode_string;
pairs with jq_unwrap for string round-trips.

Usage: jq_wrap "$bash_string"
DOC
function jq_wrap() {
  jq_encode_string "$1"
}


:<<'DOC'
Variadic setter function for jq

Usage: jq_set "$json" '.<path1>' "val1" '.<path2>' "val2" ...
DOC
function jq_set() {
  local json="$1"
  local expected_type="^(null|object|array)$"
  shift

  local filter=""
  local args=()
  local arg_num=0

  # Validate the variadic paths against original $json object and
  # build up arguments and query one key/value pair at a time.
  # Note: This runs all set operations at once, so key/value pairs cannot
  # depend on one-another.
  while [[ $# -ge 2 ]]; do
    local unexpected_type
    local path
    path="$(__json_api__normalize_path "$1")"
    local value="$2"

    local parent
    parent="$(__json_api__parent_path "$path")"
    unexpected_type=$(__json_api__validate_path_type "$json" "$parent" "$expected_type")
    if [[ -n "$unexpected_type" ]]; then
      fatal "put: cannot set field on '$unexpected_type' at '$path'"
      return 1
    fi

    filter+="${filter:+" | "}${path} = \$v${arg_num}"
    __json_api__arg_flag args "v${arg_num}" "$value"
    shift 2
    ((arg_num++))
  done

  jq "${args[@]}" "$filter" <<< "$json"
}

:<<'DOC'
Construct a JSON object from variadic key/value pairs.

Usage: jq_obj '.<path1>' "val1" '.<path2>' "val2" ...
DOC
function jq_obj() {
  jq_set '{}' "$@"
}

:<<'DOC'
Construct a JSON array from variadic elements.

Usage: jq_arr "val1" "val2" ...
DOC
function jq_arr() {
  jq_push '[]' '.' "$@"
}

:<<'DOC'
Variadic append function for jq arrays.

Usage: jq_push "$json" '{"a":1}' '{"b":2}' ...
       jq_push "$json" '.<path>' '{"a":1}' '{"b":2}' ...

When called with 2 args, appends to the root array.
When called with 3+ args, the second arg is the jq path.
DOC
function jq_push() {
  local json="$1"
  local path unexpected_type

  if [[ $# -eq 2 ]]; then
    path="."
    shift 1
  else
    path="$(__json_api__normalize_path "$2")"
    shift 2
  fi

  unexpected_type=$(__json_api__validate_path_type "$json" "$path" "^array$")
  if [[ -n "$unexpected_type" ]]; then
    fatal "append: expected array at '${path}', got '$unexpected_type'"
    return 1
  fi

  local filter="" args=() i=0
  while [[ $# -ge 1 ]]; do
    filter+="${filter:+" | "}${path} += [\$v${i}]"
    __json_api__arg_flag args "v${i}" "$1"
    shift
    ((i++))
  done

  jq "${args[@]}" "$filter" <<< "$json"
}

:<<'DOC'
Returns the last element of a JSON array.

Usage: jq_last "$json_array"
DOC
function jq_last() {
  local json_array="$1"
  jq -c '.[-1]' <<< "$json_array"
}

:<<'DOC'
Returns the first element of a JSON array.

Usage: jq_first "$json_array"
DOC
function jq_first() {
  local json_array="$1"
  jq -c '.[0]' <<< "$json_array"
}

:<<'DOC'
Removes and returns the last element of a JSON array.

Outputs two lines: the removed element, then the remaining array.
Capture with: read -r element && read -r rest

Usage:
  { read -r elem; read -r rest; } < <(jq_pop "$json_array")
DOC
function jq_pop() {
  local json_array="$1"
  jq -c '.[-1]' <<< "$json_array"
  jq -c '.[:-1]' <<< "$json_array"
}

:<<'DOC'
Removes and returns the first element of a JSON array.

Outputs two lines: the removed element, then the remaining array.

Usage:
  { read -r elem; read -r rest; } < <(jq_shift "$json_array")
DOC
function jq_shift() {
  local json_array="$1"
  jq -c '.[0]' <<< "$json_array"
  jq -c '.[1:]' <<< "$json_array"
}

:<<'DOC'
Returns the first N elements of a JSON array.

If N exceeds the array length, returns the whole array. If N is 0, returns [].

Usage: jq_take "$json_array" "$n"
DOC
function jq_take() {
  local json_array="$1" n="$2"
  local unexpected_type

  if [[ ! "$n" =~ ^[0-9]+$ ]]; then
    fatal "jq_take: n must be a non-negative integer, got '$n'"
    return 1
  fi

  unexpected_type=$(__json_api__validate_path_type "$json_array" "." "^array$")
  if [[ -n "$unexpected_type" ]]; then
    fatal "jq_take: expected array, got '$unexpected_type'"
    return 1
  fi

  local length
  length="$(jq -r 'length' <<< "$json_array")"
  if (( n > length )); then
    warn "jq_take: requested $n elements but array only has $length"
  fi

  jq -c --argjson _end "$n" '.[0:$_end]' <<< "$json_array"
}

function __json_api__arg_flag() {
  local -n _arr="$1"
  local name="$2" value="$3"

  if [[ -n "$value" ]] && jq '.' <<< "$value" &>/dev/null; then
    _arr+=(--argjson "$name" "$value")
  else
    _arr+=(--arg "$name" "$value")
  fi
}

function __json_api__normalize_path() {
  local path="$1"
  if [[ "$path" != .* && "$path" != \[* ]]; then
    echo ".\"$path\""
  else
    echo "$path"
  fi
}

function __json_api__parent_path() {
  local path="$1"
  local parent="${path%.*}"
  echo "${parent:-.}"
}

function __json_api__validate_path_type() {
  local json="$1" path type_rgx="$3"
  path="$(__json_api__normalize_path "$2")"
  local target_type
  target_type="$(jq -r "${path} | type" <<< "$json" 2>/dev/null)"
  if [[ "$target_type" =~ $type_rgx ]]; then
    return 0
  else
    echo "$target_type"
    return 1
  fi
}

:<<'DOC'
  Helper function to dump a json object. Optionally accepts a filter to include/exclude keys as needed.

  Usage: jq_dump <object> [filter]
DOC
function jq_dump() {
  local object="$1"
  local filter="${2:-.}"
  jq "$filter" <<< "$object"
}

:<<'DOC'
Returns compact single-line JSON (jq -c '.'). Fails if the input is not valid JSON.

Usage: jq_compact "$json"
DOC
function jq_compact() {
  local json="$1"

  if ! jq_is_valid "$json"; then
    fatal "jq_compact: invalid JSON"
    return 1
  fi

  jq -c '.' <<< "$json"
}

:<<'DOC'
Maps a bash function over each element of a JSON array, returning a new JSON array.

Usage: jq_map callback_func "$json_array" [extra_args...]

  Callback signature:
    callback "$element" [extra_args...] -> transformed element JSON
DOC
function jq_map() {
  local func="$1" json_array="$2"
  shift 2
  local extra=("$@")
  local result="[]"

  while IFS= read -r element; do
    local transformed
    transformed=$("$func" "$element" "${extra[@]}")
    result=$(jq_push "$result" '.' "$transformed")
  done < <(jq -c '.[]' <<< "$json_array")

  echo "$result"
}

:<<'DOC'
Like jq_map, but passes the element index to the callback.

Usage: jq_mapi callback_func "$json_array" [extra_args...]

  Callback signature:
    callback "$element" "$index" [extra_args...] -> transformed element JSON
DOC
function jq_mapi() {
  local func="$1" json_array="$2"
  shift 2
  local extra=("$@")
  local result="[]"
  local index=0

  while IFS= read -r element; do
    local transformed
    transformed=$("$func" "$element" "$index" "${extra[@]}")
    result=$(jq_push "$result" '.' "$transformed")
    ((index++))
  done < <(jq -c '.[]' <<< "$json_array")

  echo "$result"
}

:<<'DOC'
Reduces a JSON array through a bash function, threading an accumulator.

Usage: jq_reduce callback_func "$json_array" initial_accumulator [extra_args...]

  Callback signature:
    callback "$accumulator" "$element" [extra_args...] -> new accumulator JSON
DOC
function jq_reduce() {
  local func="$1" json_array="$2" accumulator="${3:-'{}'}"
  shift 3 2>/dev/null || shift $#
  local extra=("$@")

  while IFS= read -r element; do
    accumulator=$("$func" "$accumulator" "$element" "${extra[@]}")
  done < <(jq -c '.[]' <<< "$json_array")

  echo "$accumulator"
}

:<<'DOC'
Like jq_reduce, but passes the element index to the callback.

Usage: jq_reducei callback_func "$json_array" initial_accumulator [extra_args...]

  Callback signature:
    callback "$accumulator" "$element" "$index" [extra_args...] -> new accumulator JSON
DOC
function jq_reducei() {
  local func="$1" json_array="$2" accumulator="${3:-'{}'}"
  shift 3 2>/dev/null || shift $#
  local extra=("$@")
  local index=0

  while IFS= read -r element; do
    accumulator=$("$func" "$accumulator" "$element" "$index" "${extra[@]}")
    ((index++))
  done < <(jq -c '.[]' <<< "$json_array")

  echo "$accumulator"
}

:<<'DOC'
Iterates a JSON array, calling a bash function for each element.

Unlike jq_map, this does not collect results -- use for side-effectful operations.

Usage: jq_foreach callback_func "$json_array" [extra_args...]

  Callback signature:
    callback "$element" [extra_args...]
DOC
function jq_foreach() {
  local func="$1" json_array="$2"
  shift 2
  local extra=("$@")

  while IFS= read -r element; do
    "$func" "$element" "${extra[@]}" || return $?
  done < <(jq -c '.[]' <<< "$json_array")
}

:<<'DOC'
Like jq_foreach, but passes the element index to the callback.

Usage: jq_foreachi callback_func "$json_array" [extra_args...]

  Callback signature:
    callback "$element" "$index" [extra_args...]
DOC
function jq_foreachi() {
  local func="$1" json_array="$2"
  shift 2
  local extra=("$@")
  local index=0

  while IFS= read -r element; do
    "$func" "$element" "$index" "${extra[@]}"
    ((index++))
  done < <(jq -c '.[]' <<< "$json_array")
}

:<<'DOC'
Returns the first element of a JSON array where the callback returns 0 (true).

Usage: jq_find callback_func "$json_array" [extra_args...]

  Callback signature:
    callback "$element" [extra_args...] -> return 0 for match, non-zero to skip

  Returns the matching element, or "null" if none matched.
DOC
function jq_find() {
  local func="$1" json_array="$2"
  shift 2
  local extra=("$@")

  while IFS= read -r element; do
    if "$func" "$element" "${extra[@]}"; then
      echo "$element"
      return
    fi
  done < <(jq -c '.[]' <<< "$json_array")

  echo "null"
}

:<<'DOC'
Like jq_find, but passes the element index to the callback.

Usage: jq_findi callback_func "$json_array" [extra_args...]

  Callback signature:
    callback "$element" "$index" [extra_args...] -> return 0 for match, non-zero to skip

  Returns the matching element, or "null" if none matched.
DOC
function jq_findi() {
  local func="$1" json_array="$2"
  shift 2
  local extra=("$@")
  local index=0

  while IFS= read -r element; do
    if "$func" "$element" "$index" "${extra[@]}"; then
      echo "$element"
      return
    fi
    ((index++))
  done < <(jq -c '.[]' <<< "$json_array")

  echo "null"
}

:<<'DOC'
Extracts a single field from each element of a JSON array.

Usage: jq_pluck "$json_array" '.name'
       jq_pluck "$json_array" '.user.id'
DOC
function jq_pluck() {
  local json_array="$1"
  local field
  field="$(__json_api__normalize_path "$2")"
  jq -c "[.[] | $field]" <<< "$json_array"
}

:<<'DOC'
Projects each element of a JSON array through an object constructor.

Usage: jq_project "$json_array" "{name, color, description}"
       jq_project "$json_array" "{label: .name, hex: .color}"
DOC
function jq_project() {
  local json_array="$1" projection="$2"

  if [[ ! "$projection" =~ ^\{.+\}$ ]]; then
    fatal "jq_project: expected object constructor (e.g. {name, color}), got '$projection'"
    return 1
  fi

  local unexpected_type
  unexpected_type=$(__json_api__validate_path_type "$json_array" "." "^array$")
  if [[ -n "$unexpected_type" ]]; then
    fatal "jq_project: expected array, got '$unexpected_type'"
    return 1
  fi

  jq -c "[.[] | $projection]" <<< "$json_array"
}

:<<'DOC'
Sums numeric values in a JSON array. Returns 0 for empty arrays.

Usage: jq_sum "$json_array"
       jq_sum "$(jq_pluck "$objects" '.score')"
DOC
function jq_sum() {
  local json_array="$1"
  local unexpected_type
  unexpected_type=$(__json_api__validate_path_type "$json_array" "." "^array$")
  if [[ -n "$unexpected_type" ]]; then
    fatal "jq_sum: expected array, got '$unexpected_type'"
    return 1
  fi

  jq 'add // 0' <<< "$json_array"
}

:<<'DOC'
Filters a JSON array, keeping only elements that match a jq condition.

Usage: jq_filter "$json_array" '. != null'
       jq_filter "$json_array" '.age > 18'
DOC
function jq_filter() {
  local json_array="$1" condition="$2"
  jq -c "[.[] | select($condition)]" <<< "$json_array"
}

:<<'DOC'
Returns elements present in both JSON arrays.

Usage: jq_intersect "$array_a" "$array_b"
DOC
function jq_intersect() {
  local a="$1" b="$2"
  jq -c --argjson b "$b" '[.[] | select(. as $l | $b | index($l))]' <<< "$a"
}

:<<'DOC'
Returns elements in the first JSON array that are not in the second.

Usage: jq_difference "$array_a" "$array_b"
DOC
function jq_difference() {
  local a="$1" b="$2"
  jq -c --argjson b "$b" '[.[] | select(. as $l | $b | index($l) | not)]' <<< "$a"
}

:<<'DOC'
Concatenates two JSON arrays.

Usage: jq_concat "$array_a" "$array_b"
DOC
function jq_concat() {
  local a="$1" b="$2"
  jq -c --argjson b "$b" '. + $b' <<< "$a"
}

:<<'DOC'
Shallow-merges two JSON objects. Right-side wins on key conflicts.

Usage: jq_merge "$object_a" "$object_b"
DOC
function jq_merge() {
  local a="$1" b="$2"
  jq -cn --argjson a "$a" --argjson b "$b" '$a + $b'
}

:<<'DOC'
Converts a JSON object into an array of {key, value} entries.

Usage: jq_entries "$json_object"

Returns: [{"key":"k1","value":"v1"}, ...]
DOC
function jq_entries() {
  local json="$1"
  local unexpected_type
  unexpected_type=$(__json_api__validate_path_type "$json" "." "^object$")
  if [[ -n "$unexpected_type" ]]; then
    fatal "jq_entries: expected object, got '$unexpected_type'"
    return 1
  fi

  jq -c '[to_entries[]]' <<< "$json"
}

:<<'DOC'
Joins a JSON string array into a single string.

Usage: jq_join "\$json_array" ", "
       jq_join "\$json_array" " " '"\`\(.)\`"'

Optional third arg is a jq expression applied to each element before joining.
DOC
function jq_join() {
  local json_array="$1" separator="${2:-}" template="${3:-.}"
  jq -r --arg sep "$separator" "map($template) | join(\$sep)" <<< "$json_array"
}

:<<'DOC'
Splits a string on a delimiter and returns a JSON string array.
Defaults to newline if no delimiter is given.

Usage: jq_split "$lines"
       jq_split "a,b,c" ","
DOC
function jq_split() {
  local input="$1"
  local delimiter="${2-}"

  if [[ -z "$delimiter" ]]; then
    delimiter=$'\n'
  fi

  jq -c -R -s --arg d "$delimiter" '[rtrimstr("\n") | split($d) | .[] | select(length > 0)]' <<< "$input"
}

:<<'DOC'
Returns the JSON type of a value at the given path.

Usage: jq_typeof "$json"
       jq_typeof "$json" '.field'

Returns one of: object, array, string, number, boolean, null
DOC
function jq_typeof() {
  local json="$1"
  local path
  path="$(__json_api__normalize_path "${2:-.}")"
  jq -r "$path | type" <<< "$json"
}

:<<'DOC'
Returns 0 if the string parses as a single JSON value (object, array, string,
number, boolean, or null). Returns 1 on parse failure.

Unlike `jq -e .`, root `null` is accepted — `jq -e` treats a `null` output as
failure.

Usage: jq_is_valid "$json_string"
DOC
function jq_is_valid() {
  jq -e 'true' <<< "$1" >/dev/null 2>&1
}

:<<'DOC'
Parses JSON from text that may be wrapped in a markdown code fence: trims
whitespace, tries `fromjson`, then strips one fence (``` or ```lang …) and
parses the inner body. Prints compact JSON on success.

Inverse of jq_to_codeblock for well-formed inputs.

Fails if no valid JSON value remains after normalization.

Usage: jq_from_codeblock "$text"
DOC
function jq_from_codeblock() {
  local text="$1"

  jq -c -Rs '
    def trim: gsub("^\\s+";"") | gsub("\\s+$";"");
    def strip_fence:
      trim
      | if test("^```") then
          sub("^```[A-Za-z0-9]*[[:space:]]*\n";"")
          | sub("\n[[:space:]]*```[[:space:]]*$";"")
        else . end
      | trim;
    . as $raw
    | try ($raw | trim | fromjson) catch ($raw | strip_fence | fromjson)
  ' <<< "$text"
}

:<<'DOC'
Wraps valid JSON in a markdown fenced block. Pretty-prints with jq (default
indentation) before wrapping. Fails if the input is not valid JSON.

Optional second argument is the fence language tag (default when omitted:
json). Pass "" as the second argument for an opening fence with no language
(``` with no info string).

Usage: jq_to_codeblock "$json"
       jq_to_codeblock "$json" "yaml"
       jq_to_codeblock "$json" ""
DOC
function jq_to_codeblock() {
  local json="$1"
  local lang="${2-json}"
  local formatted

  formatted="$(jq_dump "$json")" || return 1

  if [[ -n "$lang" ]]; then
    # shellcheck disable=SC2016
    printf '```%s\n%s\n```\n' "$lang" "$formatted"
  else
    # shellcheck disable=SC2016
    printf '```\n%s\n```\n' "$formatted"
  fi
}

:<<'DOC'
Validates a JSON value against a JSON Schema file. Returns 0 on success, 1 on failure with diagnostic on stderr.

Usage: jq_validate "$json" "$schema_file"
DOC
function jq_validate() {
  local json="$1" schema_file="$2"

  if [[ ! -f "$schema_file" ]]; then
    echo "jq_validate: schema file not found: $schema_file" >&2
    return 1
  fi

  local schema
  schema=$(<"$schema_file")

  jq -n --argjson data "$json" --argjson schema "$schema" \
    -f "$SHELLOPS_DIR/lib/jsonschema.jq" >/dev/null
}

:<<'DOC'
Encodes a bash string as a JSON string value (quoted, escaped). jq_wrap
delegates here. Use with --argjson when building jq payloads from untrusted
shell text.

Usage: jq_encode_string "$bash_string"
DOC
function jq_encode_string() {
  jq -n --arg v "$1" '$v'
}

:<<'DOC'
Returns true (0) if the value is neither "null" nor empty.

Usage: jq_defined "$value"
DOC
function jq_defined() {
  [[ "$1" != "null" && -n "$1" ]]
}

:<<'DOC'
Returns the length of a JSON array, object (key count), or string.

Usage: jq_len "$json"
DOC
function jq_len() {
  jq 'length' <<< "$1"
}

:<<'DOC'
Runs a .jq script file against input JSON, returning compact output.
Supports extra jq flags (e.g. --arg, --argjson) for parameterized scripts.

Usage:
  jq_run "$script_file" "$json"
  jq_run "$script_file" "$json" --arg name "value"
DOC
function jq_run() {
  local script_file="$1" json="$2"
  shift 2
  jq -c "$@" -f "$script_file" <<< "$json"
}
