#!/usr/bin/bash

# heosutil-run-command  Copyright 2025-2026  Norman Carver

# This script program is not intended to be called directly
# by users, it is to be called by the heos-* scripts only!

# Script program to Send a HEOS Command to a HEOS device.
# Uses the C socket program heosutil-send-command-topaddr,
# but this is the program level that determines the IP address
# for the target HEOS device.
#
# By default, sends command to the default HEOS Master device.
# An alternative device can be the command target by using the
# MASTER_IPNUM optional argument.
#
# The network prefix and default master ipnum can be set below
# in this file (parameters 'networkprefix' and 'masteripnum'),
# else they will be loaded from the file heosinfo-network
# (in the same directory as this program is run from).
#
# This program can complete HEOS commands that involve a particular
# HEOS player or group, by using the --player or --group options.
# With --player, program determines the Player ID (PID) of the player,
# and replaces '${pid}' in the command with that value.
# With --group, program determines the Group ID (GID) of the group,
# and replaces '${gid}' in the command.
# If PID/GID is known, the --pid option can be used to supply the vales,
# avoiding calls to determine the ID (--player or --group options must
# still be used and name/ipnum given (for use in error messages).
#
# Error messages with 'Error: prefix should be printed by caller,
# while those with 'ERROR:' prefix should not, except for debugging.
# Error messages from calls to heosutil-send-command-toipaddr will be
# suppressed, unless the --debug option is given.
# (Those messages would be confusing for users, so only for debugging.)


# HEOS LAN info (leave empty to source from heosinfo-network file):
networkprefix=       #LAN network prefix (e.g.: 192.168.0)
masteripnum=         #default master device IP address host portion (e.g.: 101)


function print_usage()
{
    echo -e "Run a HEOS CLI command.  Note: not intended to be called by users!\n" >&2
    echo    "Usage: heosutil-run-command [--player=PLAYER | --group=GROUP] [--pid=PID] [--numjson=N] [--prettify] [--debug] COMMAND [MASTER_IPNUM]" >&2
    echo    "where:" >&2
    echo    "  COMMAND -- a complete HEOS CLI command (may contain '\$pid'/'\$gid' to be filled in with --player/--group options);" >&2
    echo    "  MASTER_IPNUM -- use a non-default HEOS master device;" >&2
    echo -e "  (IPNUM means the last quad of an IP address only: i.e., 0--255.)\n" >&2
    echo    "Options:" >&2
    echo    "  --player -- PLAYER is a HEOS player Name (substring) or the player's IPNUM;" >&2
    echo    "  --group -- GROUP is a group name (substring);" >&2
    echo    "  --pid -- PID is the PID/GID rather than a name/IPNUM;" >&2
    echo    "  --numjson -- N is the number of JSON objects to be read (default is 1);" >&2
    echo    "  --prettify -- JSON results from COMMAND are \"prettified\";" >&2
    echo -e "  --debug -- heosutil-send-command-toipaddr error messages are printed to stderr (instead of hidden);\n" >&2
    echo    "If --player or --group is supplied, '&' chars in COMMAND must be escaped ('\&')."
}


if [[ "$1" == --help ]]; then
    print_usage
    exit 0
fi

heosutilsdir=$(dirname "$0")

# Get and check network info from heosinfo-network file:
if [[ -z "$networkprefix" || -z "$masteripnum" ]]; then
    # Must get/source network info from file heosinfo-network:
    if [[ ! -f "$heosutilsdir"/heosinfo-network ]]; then
        echo "Error: network info file missing ('$heosutilsdir/heosinfo-network')!" >&2
        exit 1
    fi
    source "$heosutilsdir"/heosinfo-network
    if [[ -z "$networkprefix" || -z "$masteripnum" ]]; then
        echo "Error: network info variables not properly set, see documentation!" >&2
        exit 1
    fi
fi

# Options default:
playerp=false
groupp=false
pidp=false
prettyp=false
numjson=1
errors=/dev/null

# Process supplied options:
while [[ "$1" == --* ]]; do
    if [[ "$1" == --player=* ]]; then
        if $groupp; then echo "Error: heosutil-run-command: use only --player or --group (alternatives)" >&2; fi
        playerp=true
        playername=${1#--player=}
    elif [[ "$1" = --group* ]]; then
        if $playerp; then echo "Error: heosutil-run-command: use only --player or --group (alternatives)" >&2; fi
        groupp=true
        groupname=${1#--group=}
    elif [[ "$1" == --pid=?* ]]; then
        pidp=true
        pidval=${1#--pid=}
    elif [[ "$1" == --prettify ]]; then
        prettyp=true
    elif [[ "$1" =~ ^--numjson= ]]; then
        numjson=${1#--numjson=}
    elif [[ "$1" == --debug ]]; then
        errors=/dev/stderr
    elif [[ "$1" == -- ]]; then
        shift;break
    else
        echo "Error: heosutil-run-command: invalid option '$1'" >&2
        exit 1
    fi
    shift
done

# Process command arguments:
if [[ $# == 0 || $# -gt 2 ]]; then
    print_usage
    exit 1
fi

command=$1
if [[ $# == 2 ]]; then
    masteripnum=${2}
    if [[ ! ("$masteripnum" =~ ^[0-9]+$ && "$masteripnum" -gt 0 && "$masteripnum" -lt 255) ]]; then
        echo "Error: heosutil-run-command: MASTER_IPNUM must be 1--254 (is '$masteripnum')" >&2
        exit 1
    fi
fi
master=${networkprefix}.${masteripnum}


# Validate master:
result=$("$heosutilsdir"/heosutil-send-command-toipaddr "heos://system/heart_beat" "$master" 2>"$errors")
if [[ ($? != 0) || ("$result" != *'"result": "success"'*) ]]; then
    echo "Error: HEOS MASTER invalid or not responding ('$master')" >&2
    exit 1
fi


if $playerp; then
    # --player option supplied:
    if ! $pidp; then
        # Get player PID:
        players=$("$heosutilsdir"/heosutil-send-command-toipaddr "heos://player/get_players" "$master" 2>"$errors")
        if [[ ($? != 0) || ("$players" != *'"result": "success"'*) ]]; then
            echo "ERROR: failed getting HEOS Players list" >&2
            exit 1
        fi

        if [[ "$playername" =~ ^[0-9]+$ ]]; then
            # PLAYER is an ipnum, so get matching PID:
            playeripaddr=${networkprefix}.${playername}
            matches=$(grep -Eio '\{[^{}]+"ip": "'"${playeripaddr}"'"[^{}]+\}' <<<"$players")
            if [[ $? != 0 ]]; then
                echo "Error: PLAYER is an invalid player IPNUM: '$playername'" >&2
                exit 1
            fi
            matches=${matches#*\"pid\":\ }
            pid=${matches%%[,\}]*}
        else
            # PLAYER is a player name, so get matching PID:
            matches=$(grep -Eio '\{"name": "[^"]*'"${playername}"'[^"]*"[^{}]+\}' <<<"$players")
            if [[ $? != 0 ]]; then
                echo "Error: PLAYER is an invalid player name: '$playername'" >&2
                exit 1
            fi
            matches=${matches#*\"pid\":\ }
            pid=${matches%%[,\}]*}
        fi

    else
        # PLAYER is a player PID:
        pid=$pidval

    fi

    # Change &'s to \&'s to avoid misinterpretation with eval:
    command=${command//&/\\&}
    # Complete command with ${pid}:
    eval command=$command

elif $groupp; then
    # --group option supplied:
    if ! $pidp; then
        # Get group GID:
        groups=$("$heosutilsdir"/heosutil-send-command-toipaddr "heos://group/get_groups" "$master" 2>"$errors")
        if [[ ($? != 0) || ("$groups" != *'"result": "success"'*) ]]; then
            echo "ERROR: failed getting HEOS Groups list" >&2
            exit 1
        fi

        matches=$(grep -Eio '\{"name": "[^"]*'"${groupname}"'[^"]*", "gid": -?[0-9]+' <<<"$groups")
        if [[ $? != 0 ]]; then
            echo "Error: GROUP is an invalid group name: '$groupname'" >&2
            exit 1
        fi
        gid=$(grep -Eo -m1 '"gid": [0-9-]+' <<<"$matches")
        gid=${gid#\"gid\": }

    else
        #GROUP  is a group GID:
        gid=$pidval

    fi

    #Since a GID is identical to leader's PID, below allows use of "player" commands with groups:
    pid=$gid

    # Change &'s to \&'s to avoid misinterpretation with eval:
    command=${command//&/\\&}
    # Complete command containing ${pid} or ${gid}:
    eval command=$command
fi

# Enable prettify_json if --prettify option supplied:
if $prettyp; then
    result=$("$heosutilsdir"/heosutil-send-command-toipaddr "heos://system/prettify_json_response?enable=on" "$master" 2>"$errors")
    if [[ ($? != 0) || ("$result" != *'"result": "success"'*) ]]; then
        echo "ERROR: failed enabling JSON prettify mode" >&2
        exit 1
    fi
fi

# Run COMMAND (command json result will be printed to stdout):
"$heosutilsdir"/heosutil-send-command-toipaddr "$command" "$master" "$numjson" 2>"$errors"
cmdstatus=$?

# Make sure prettify_json gets disabled, regardless of COMMAND status:
if $prettyp; then
    result=$("$heosutilsdir"/heosutil-send-command-toipaddr "heos://system/prettify_json_response?enable=off" "$master" 2>"$errors")
    prettystatus=$?
fi

# Assess the status of COMMAND and disabling of prettify_json:
if [[ $cmdstatus != 0 ]]; then
    echo "ERROR: HEOS CLI command failed: '$command'" >&2
    exit 1
fi

if $prettyp; then
    if [[ ($prettystatus != 0) || ("$result" != *'"result": "success"'*) ]]; then
        echo "ERROR: failed disabling JSON prettify mode" >&2
        exit 1
    fi
fi

exit 0

#EOF
