#!/bin/bash
################################################################################
# Script Name: /usr/local/bin/hitechcloudcli
# Description: Makes all HiTechCloudCLI commands available on the terminal.
# Usage: hitechcloudcli <COMMAND-NAME> <ATTRIBUTES>
# Created: 15.11.2023
# Last Modified: 20.04.2026
# Company/Brand: HiTechCloud by Pho Tue SoftWare Solutions JSC
# Copyright (c) HiTechCloud by Pho Tue SoftWare Solutions JSC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
################################################################################

readonly HITECHCLOUDCLI_VERSION="202.609.027"
readonly LOG_FILE="/var/log/hitechcloud/admin/hitechcloudcli.log"
readonly SCRIPTS_DIR="/usr/local/hitechcloudcli"
readonly HOOKS_DIR="/etc/hitechcloud/hitechcloud/hooks"
debug_flag=""

show_about() {
    cat <<EOF
About:
  Documentation:                    https://hitechcloud.org/docs/category/hitechcloudcli/
  Version:                          ${HITECHCLOUDCLI_VERSION}
  License:                          MIT

EOF
}

show_history() {
    [[ -f "$LOG_FILE" ]] || { echo "Warning: log file $LOG_FILE not found."; return; }

    echo "Recently used commands:"
    tail -n 100 "$LOG_FILE" | grep -o 'hitechcloudcli [^ ]*' | sort | uniq | tail -n 5 | sed 's/^/  /'
    echo ""

    echo "Most commonly used commands:"
    awk -F ' \\| ' 'NF>=2 {print $2}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -n 5 | awk '{$1=""; print "  " $0}'
    echo ""
}

show_usage() {
    cat <<EOF

Usage: hitechcloudcli <COMMAND> [additional_arguments]

Suggested commands:
  hitechcloudcli faq                       Display frequently asked questions and answers.
  hitechcloudcli --help                    List all available HiTechCloudCLI commands and their usage.

EOF
}

log_command() {
    [[ -n "$LOG_FILE" ]] || return 0
    echo "$(date +"%Y-%m-%d %H:%M:%S") | hitechcloudcli $1" >> "$LOG_FILE" 2>/dev/null
}

run_hook() {
    local hook_type="$1"
    local command="$2"
    shift 2

    local hook_file_base="${HOOKS_DIR}/${hook_type}_${command}"
    local hook_file=""

    if [[ -f "${hook_file_base}.sh" ]]; then
        hook_file="${hook_file_base}.sh"
    elif [[ -f "${hook_file_base}" ]]; then
        hook_file="${hook_file_base}"   # legacy pre-1.3
    else
        return 0
    fi

    case "$hook_type" in
        pre) bash ${debug_flag:+"$debug_flag"} "$hook_file" "$@" </dev/null >/dev/null 2>&1 ;;
        post) nohup bash "$hook_file" "$@" </dev/null >/dev/null 2>&1 & ;;
    esac
}

# Special cases only. `locale` used to be one - it downloaded a script from the
# translations repository and piped it into bash - and is now an ordinary
# locale.sh like every other command, which is what makes it possible to fix,
# test and ship it with the rest of the CLI.
handle_special_commands() {
    local command="$1"; shift
    case "$command" in
        error) python3 "${SCRIPTS_DIR}/error.py" "$@"; exit 0 ;;
    esac
}

# Commands that have moved to the compiled CLI. It lists the ones it
# implements completely (`hitechcloudcli --direct ported`); those run there,
# everything else runs its .sh - and on a server running this script, nothing
# runs there, because the two are installed at the same path. A
# command is only listed once its output and exit codes match the script's,
# so nothing changes for a caller - and the .sh is deleted a release later.
# There is no second program to hand a command to any more.
#
# This script and the compiled CLI are installed at the same path,
# /usr/local/bin/hitechcloudcli, so exactly one of them is there: this one only
# when the compiled binary could not be installed at all. It used to look for a
# second name, and because that name was a symlink to this very path, writing
# this script there made it ask *itself* which commands it handled - one bash
# deeper each time, with no bottom. The name is gone and so is the question.

execute_command() {
    local command="$1"; shift
    local binary_path="${SCRIPTS_DIR}/${command//-//}.sh"

    # An unknown command used to print the error, then the usage, then the
    # command history, then the about screen. Anything reading the output to
    # find out what went wrong - the admin's backup page reports the last line
    # of it - got "License: MIT", the last line of the about screen, and told
    # the administrator "Backup failed: License: MIT".
    #
    # The error is the answer. The suggestion goes with it, on stderr, where an
    # error belongs; a caller that shows the last line now shows a sentence
    # about the command that was not found.
    if [[ ! -f "$binary_path" ]]; then
        # Since 3.4.0 a ported command has no script any more: it exists only
        # in the compiled CLI. A server running this script instead is told
        # that, not "command not found".
        if [[ -f "${SCRIPTS_DIR}/${command//-//}.rs" ]]; then
            echo "Error: '$command' runs in the compiled hitechcloudcli, which is not installed on this server." >&2
            echo "Install it with: hitechcloudcli update   (or fetch hitechcloudcli-linux-<arch> from the release)" >&2
            exit 127
        fi
        echo "Error: Command '$command' not found." >&2
        echo "Run 'hitechcloudcli --help' for the list of commands." >&2
        exit 127
    fi

    # A command with a .rs beside its .sh has a Rust implementation, and that
    # one is the product; this script is the fallback for a server whose
    # compiled binary could not be installed. They are not the same age, and
    # the difference has been material: native/install.sh has no
    # /etc/cron.d/hitechcloud step at all, so a server installed by it came up
    # with eighteen scheduled jobs that never ran and nothing else out of
    # place. Running the older one is allowed. Running it in silence is what
    # made that cost fifteen hours to find.
    #
    # On stderr, because a caller that reads the last line of stdout to report
    # what happened must keep reading the command's own output.
    if [[ -f "${SCRIPTS_DIR}/${command//-//}.rs" ]]; then
        echo "Note: ${command} is running from its shell implementation; the compiled CLI is not installed on this server and the two are not always the same age. Install it with: hitechcloudcli update" >&2
    fi

    log_command "$command"
    run_hook "pre"  "$command" "$@"

    # Keep the script's own exit status. It used to be discarded by the post
    # hook that ran after it, so hitechcloudcli reported success for everything -
    # including commands that had printed an error and exited 1. Both panels
    # shell out to hitechcloudcli and check the exit status, so a failed command
    # came back to them as a success whose output was the error message: the
    # admin's webmail link, for one, sent users to https://<error text>/.
    bash ${debug_flag:+"$debug_flag"} "$binary_path" "$@"
    local status=$?

    run_hook "post" "$command" "$@"
    return "$status"
}

generate_commands() {
    readonly ALIAS_FILE="${SCRIPTS_DIR}/aliases.txt"
    readonly GREEN='\033[0;32m'
    readonly RESET='\033[0m'

    readonly SEARCH_QUERY="${1:-}"
    readonly EXCLUDE_PATTERNS=( ".git/*" ".github/*" "ftp/users.sh" "db.sh" "dev.sh" "enterprise.sh" "progress_bar.sh" "lib/*" )

    declare -a exclude_args=()

    [[ -d "$SCRIPTS_DIR" ]] || { echo "Error: Scripts directory '$SCRIPTS_DIR' not found." >&2; exit 1; }
    [[ -w "$ALIAS_FILE" ]] || touch "$ALIAS_FILE" 2>/dev/null || { echo "Error: Cannot write to alias file '$ALIAS_FILE'." >&2; exit 1; }
    [[ -z "$SEARCH_QUERY" ]] && : > "$ALIAS_FILE"

    extract_script_info() {
        local script="$1"
        local -n info_ref="$2"
        info_ref[description]=$(grep -E "^# Description:" "$script" 2>/dev/null | sed 's/^# Description: //' || true)
        info_ref[usage]=$(grep -E "^# Usage:" "$script" 2>/dev/null | sed 's/^# Usage: //' || true)
        info_ref[docs]=$(grep -E "^# Docs:" "$script" 2>/dev/null | sed 's/^# Docs: //' || true)
    }

    generate_alias_name() {
        local script="$1"
        local script_name dir_name alias_name

        script_name=$(basename "$script")
        script_name="${script_name%.sh}"
        script_name="${script_name%.py}"

        dir_name=$(dirname "$script")
        dir_name="${dir_name##*/}"

        if [[ "$dir_name" == "hitechcloudcli" ]]; then
            alias_name="$script_name"
        else
            alias_name="${dir_name}-${script_name}"
        fi

        echo "$alias_name"
    }

    display_command_info() {
        local full_alias="$1"
        local script="$2"

        if [[ -n "$SEARCH_QUERY" && "$full_alias" != *"$SEARCH_QUERY"* ]]; then
            return
        fi

        local -A script_info
        extract_script_info "$script" script_info

        echo -e "${GREEN}${full_alias}${RESET}"
        if [[ -n "${script_info[description]:-}" ]]; then
            echo "Description: ${script_info[description]}"
        fi
        if [[ -n "${script_info[usage]:-}" ]]; then
            echo "Usage: ${script_info[usage]}"
        fi
        if [[ -n "${script_info[docs]:-}" ]]; then
            echo "Docs: ${script_info[docs]}"
        fi
        echo "------------------------"
    }

    exclude_args=()
    for pattern in "${EXCLUDE_PATTERNS[@]}"; do
        exclude_args+=("!" "-path" "${SCRIPTS_DIR}/${pattern}")
    done

    declare -a commands_list=()

    while IFS= read -r -d '' script; do
        [[ -r "$script" ]] || continue

        alias_name=$(generate_alias_name "$script")
        full_alias="hitechcloudcli ${alias_name}"

        display_command_info "$full_alias" "$script"
        commands_list+=("$full_alias")

    done < <(find "$SCRIPTS_DIR" -type f -name "*.sh" "${exclude_args[@]}" -print0)

    # locale is not listed here any more: it has a locale.sh now, so the loop
    # above finds it and it would otherwise appear in the help twice.
    special_commands=("hitechcloudcli error|Displays information for specific error ID received in HiTechCloudPanel UI.|hitechcloudcli error <ID_HERE>")

    # The compiled CLI used to be asked here for the commands that have no
    # script to read a header from. It cannot be: it is installed at this
    # script's own path, so if it were there this script would not be running.
    # A server in that state lists the scripts it has, and `--help` says so.

    for cmd in "${special_commands[@]}"; do
        IFS="|" read -r name desc usage <<< "$cmd"
        if [[ -z "$SEARCH_QUERY" || "$name" == *"$SEARCH_QUERY"* ]]; then
            echo -e "${GREEN}${name}${RESET}"
            echo "Description: ${desc}"
            echo "Usage: ${usage}"
            echo "------------------------"
        fi
        commands_list+=("$name")
    done

    if [[ -z "$SEARCH_QUERY" && ${#commands_list[@]} -gt 0 ]]; then
        printf '%s\n' "${commands_list[@]}" | sort > "$ALIAS_FILE"
    fi
}

main() {
    # validation
    [[ $EUID -ne 0 ]] && { echo "HiTechCloudCLI scripts must be run as root user." >&2; exit 1; }
    [[ $# -lt 1 ]] && { show_usage; show_history; show_about; exit 1; }

    # debug
    [[ "$1" == "-x" ]] && { debug_flag="-x"; shift; }

    # help
    case "$1" in help|-h|--help) generate_commands; exit 0 ;; esac
    case "$2" in help|-h|--help) generate_commands "$1"; exit 0 ;; esac

    # run
    local command="$1"; shift
    [[ "$command" == "-v" ]] && command="--version"
    handle_special_commands "$command" "$@"
    execute_command "$command" "$@"
}

main "$@"
