#!/bin/sh
# Copyright (c) 2017 Markus Weippert
# GNU General Public License v3.0 (see https://www.gnu.org/licenses/gpl-3.0.txt)

WANT_JSON="1"
PARAMS="
    autocommit/bool
    command=cmd/str
    config/str
    find=find_by=search/any
    keep_keys=keep/any
    key/str
    merge/bool//false
    name/str
    option/str
    replace/bool//false
    section/str
    set_find/bool//true
    type/str
    unique/bool//false
    value/any
"
RESPONSE_VARS="result=_result command config section option"

init() {
    state_path=""
    [ -z "$_ansible_check_mode" ] || state_path="$(mktemp -d)" ||
        fail "could not create state path"
    changes="$(uci_change_hash)"
    case "$_type_keep_keys" in
        object) fail "keep_keys must be list or string";;
        array) json_get_values keep_keys "$_keep_keys" || :;;
    esac
    [ -z "$key" ] &&
        key="${config:+$config${section:+.$section${option:+.$option}}}" ||
        { oIFS="$IFS"; IFS="."; set -- $key; IFS="$oIFS"
            config="$1"; section="$2"; option="$3"; }
    [ -z "$_ansible_diff" -o -z "$config" ] ||
        set_diff "$(uci export "$config")"
    [ -n "$command" ] || { [ -z "$value" ] && command="get" || command="set"; }
}

uci() {
    [ -z "$state_path" ] || set -- -P "$state_path" "$@"
    command uci "$@"
}

uci_change_hash() {
    uci changes | md5
}

uci_result_do() {
    json_set_namespace result
    "$@"
    json_set_namespace params
}

uci_get_safe() {
    local tmp opts
    while [ "${1#-}" != "$1" ]; do opts="$opts $1"; shift; done
    tmp="$(uci $opts show "$1")" || return $?
    echo "${tmp#*=}"
}

uci_check_type() {
    local key="$1"; local type="$2"; local t
    t="$(uci -q get "$key")" || return 1
    [ -n "$type" -a "$t" != "$type" ] || return 0
    fail "$key exists with $t instead of $type"
}

uci_compare_list() {
    local k="${1:-$key}"
    local match="1"
    local keys values v i
    json_get_keys keys
    ! values="$(uci_get_safe -q "$k")" || {
        eval "set -- $values"
        for i in $keys; do
            json_get_var v "$i"
            [ $# -gt 0 -a "$v" = "$1" ] || { match=""; break; }
            shift
        done
        [ $# -eq 0 ] || match=""
        [ -z "$match" ] || return 0
    }
    return 1
}

uci_add() {
    section="${section:-$value}"
    [ -n "$name" -o -z "$type" ] || name="$section"
    type="${type:-$section}"
    [ -n "$type" ] || fail "type required for $command"
    [ -n "$name" ] && {
        uci_check_type "$config.$name" "$type" || {
            try uci add "$config" "$type"
            try uci rename "$config.$_result=$name"
        }
    } || try uci add "$config" "$type"
}

uci_set_list() {
    local k="${1:-$key}"
    local keys v i
    ! uci_compare_list "$k" || return 0
    uci -q delete "$k" || :
    json_get_keys keys
    for i in $keys; do
        json_get_var v "$i"
        try uci add_list "$k=$v"
    done
}

uci_set_dict() {
    local keys k v t
    json_get_keys keys
    keep_keys="$keep_keys $keys"
    for k in $keys; do
        json_get_type t "$k"
        case "$t" in
            array)
                json_select "$k"
                uci_set_list "$key.$k"
                json_select ..;;
            object) fail "cannot set $k to dict";;
            *)
                json_get_var v "$k"
                try uci set "$key.$k=$v";;
        esac
    done
}

uci_set() {
    local var="${1:-value}"
    local var_type
    eval "var_type=\"\$_type_$var\""
    [ -z "$option" ] || keep_keys="$keep_keys $option"
    case "$var_type" in
        array)
            [ -n "$config" -a -n "$section" -a -n "$option" ] ||
                fail "config, section and option required for $command"
            json_select_real "$var"
            uci_set_list
            json_select ..;;
        object)
            [ -n "$config" -a -n "$section" -a -z "$option" ] ||
                fail "config and section but not option required for $command"
            json_select_real "$var"
            uci_set_dict
            json_select ..;;
        *) try "uci set \"\$key=\$$var\"";;
    esac
}

uci_get() {
    local entry
    try uci get "$key"
    eval "set -- $(uci_get_safe -q "$key")"
    json_set_namespace result
    json_add_array result_list
    for entry; do json_add_string . "$entry"; done
    json_close_array
    json_set_namespace params
}

uci_find() {
    local keys i c v k tmp
    case "$_type_find" in
        array|object)
            [ -n "$config" -a -n "$type" ] ||
                fail "config and type required for $command"
            json_select_real find
            json_get_keys keys;;
        *)
            [ -n "$config" -a -n "$type" ] &&
                [ -n "$option" -o "$command" = find_all ] ||
                fail "config, type and option required for $command";;
    esac
    [ "$command" != "find_all" ] || uci_result_do json_add_array result
    type="${type:-$section}"
    section=""; i=0
    while [ -n "$(uci -q get "$config.@$type[$i]")" ]; do
        c="@$type[$((i++))]"
        case "$_type_find" in
            array)
                [ -z "$option" ] && {
                    for k in $keys; do
                        json_get_var v "$k"
                        uci -q get "$config.$c.$v" >/dev/null || continue 2
                    done
                } || {
                    uci_compare_list "$config.$c.$option" || continue
                };;
            object)
                for k in $keys; do
                    json_get_type tmp "$k"
                    case "$tmp" in
                        array)
                            json_select "$k"
                            uci_compare_list "$config.$c.$k" &&
                                tmp="1" || tmp=""
                            json_select ..
                            [ -n "$tmp" ] || continue 2;;
                        object)
                            fail "cannot compare $k with dict";;
                        *)
                            json_get_var v "$k"
                            tmp="$(uci -q get "$config.$c.$k")" &&
                                [ "$tmp" = "$v" ] || continue 2
                    esac
                done;;
            *)
                [ -z "$option" ] || {
                    v="$(uci -q get "$config.$c.$option")" &&
                        [ -z "$find" -o "$find" = "$v" ] || continue
                };;
        esac
        [ "$command" = "find_all" ] || { section="$c"; break; }
        uci_result_do json_add_string . "$c"
    done
    case "$_type_find" in
        array|object) json_select ..;;
    esac
    case "$command" in
        find_all)
            uci_result_do json_close_array
            _result="";;
        *)
            _result="$section"
            [ -n "$section" ] && return 0 || return 1;;
    esac
}

uci_ensure() {
    local keys k v
    [ -n "$name" -o -z "$type" ] || name="$section"
    type="${type:-$section}"
    [ -n "$config" -a -n "$type" ] ||
        fail "config, type and name required for $command"
    [ -n "$name" ] && uci_check_type "$config.$name" "$type" || {
        [ "$_type_find" = "object" -o -n "$option" ] && uci_find && {
            [ -z "$name" ] || try uci rename "$config.$section=$name"
        } || {
            [ "$command" = absent ] && return 0 || uci_add
        }
    }
    section="${name:-$_result}"
    key="$config.$section${option:+.$option}"
    [ "$command" = "absent" ] && {
        [ -z "$_defined_value" ] &&
            { uci -q delete "$key" || :; } ||
            case "$_type_value" in
                array|object)
                    json_select_real value
                    json_get_keys keys
                    for k in $keys; do
                        json_get_var v "$k"
                        case "$_type_value" in
                            array) uci -q delete "$config.$section.$v";;
                            object) uci -q delete "$config.$section.$k=$v";;
                        esac
                    done
                    json_select ..;;
                *) uci -q delete "$config.$section.$value";;
            esac
        return 0
    }
    [ -z "$set_find" -o "$_type_find" != "object" -a -z "$option" ] || {
        uci_set find
        [ "$_type_value" != "object" ] || {
            key="$config.$section"
            option=""
        }
    }
    [ -z "$_defined_value" ] || uci_set
    _result="$section"
}

uci_cleanup_section() {
    case "$command" in
        set|ensure|section) :;;
        *) return 0;;
    esac
    local k v
    [ -n "$replace" -a -n "$config" -a -n "$section" ] || return 0
    for k in $(uci -q show "$config.$section" |
            sed -n 's/^..*\...*\.\([^.][^.]*\)=.*$/\1/p')
            do
        for v in $keep_keys; do
            [ "$k" != "$v" ] || continue 2
        done
        uci -q delete "$config.$section.$k"
    done
}

uci_revert() {
    [ -z "$key" ] && rm -f -- "/tmp/.uci"/* 2>/dev/null || uci revert "$key"
}

uci_autocommit() {
    [ -n "$autocommit" ] || return 0
    case "$command" in
        commit|revert) return 0;;
    esac
    [ "$1" -eq 0 ] && {
        [ -n "$config" ] && uci commit "$config" || uci commit
    } || uci_revert
}

main() {
    case "$command" in
        batch|import)
            [ -n "$value" ] || fail "value required for $command";;
        add_list|del_list|rename|reorder)
            [ -n "$key" -a -n "$value" ] ||
                fail "key and value required for $command";;
        add|get|delete|ensure|absent)
            [ -n "$key" ] || fail "key required for $command";;
    esac
    case "$command" in
        batch)
            echo "$value" | final uci $command;;
        export|changes|show|commit)
            local cmd="final uci $command"
            [ -z "$key" ] && $cmd || $cmd "$key";;
        import)
            local cmd="final uci ${merge:+-m }$command"
            [ -z "$key" ] && echo "$value" | $cmd ||
                echo "$value" | $cmd "$key";;
        add)
            uci_add; exit 0;;
        add_list)
            [ -z "$unique" ] || {
                eval "set -- $(uci_get_safe -q "$key")"
                for entry; do [ "$entry" = "$value" ] && exit 0; done
            }
            final uci add_list "$key=$value";;
        del_list|reorder)
            final uci $command "$key=$value";;
        get)
            uci_get; exit 0;;
        delete)
            final uci $command "$key${value:+=$value}";;
        rename)
            final uci $command "$key=${name:-$value}";;
        revert)
            final uci_revert;;
        set)
            uci_set; exit 0;;
        find|find_all)
            uci_find; exit $?;;
        ensure|section)
            uci_ensure; exit 0;;
        absent)
            [ -n "$_defined_find" ] || {
                uci -q delete "$key${value:+=$value}"; exit 0
            }
            uci_ensure; exit 0;;
        *) fail "unknown command: $command";;
    esac
}

cleanup() {
    local ec="$1"
    [ "$ec" -ne 0 ] || uci_cleanup_section
    [ "$changes" = "$(uci_change_hash)" ] || {
        changed
        [ "$_ansible_verbosity" -lt 2 ] || {
            local _IFS line
            _IFS="$IFS"; IFS="$N"; set -- $(uci changes); IFS="$_IFS"
            json_set_namespace result
            json_add_array changes
            for line; do json_add_string . "$line"; done
            json_close_array
            json_set_namespace params
        }
        uci_autocommit "$ec"
    }
    [ -z "$_ansible_diff" -o -z "$config" ] ||
        set_diff "" "$(uci export "$config")"
    [ -z "$_ansible_check_mode" -o -z "$state_path" -o ! -d "$state_path" ] ||
        rm -rf "$state_path"
}
