#!/bin/bash
#
# microsoft-connected-cache - public command dispatcher for Microsoft Connected Cache.
#
# Installed as /usr/bin/microsoft-connected-cache by the microsoft-connected-cache
# package. It is the only supported entry point; the implementation scripts under
# PAYLOAD_DIR are package-private and are not a public interface.
#
# Exit codes (public contract):
#   0  success, or status found a healthy provisioned node
#   1  failure, or status found a degraded provisioned node
#   2  invalid command or arguments
#   3  MCC is not provisioned
#   4  root required, or required read permissions are missing

set -u
set -o pipefail

PAYLOAD_DIR="/opt/microsoft/microsoft-connected-cache"
PACKAGE_NAME="microsoft-connected-cache"
MCC_CONTAINER="MCC"

EXIT_OK=0
EXIT_FAILED=1
EXIT_USAGE=2
EXIT_NOT_PROVISIONED=3
EXIT_PERMISSION=4

err() { printf '%s\n' "$*" >&2; }

usage() {
    cat <<EOF
Microsoft Connected Cache

Usage: ${PACKAGE_NAME} <command> [arguments]

Commands:
  provision <key=value>...        Provision this host using portal-generated parameters.
  status                          Report provisioning, service, and container state.
  diagnostics                     Collect a diagnostic support bundle.
  configure-proxy <url> [cert]    Configure the outbound proxy, with optional TLS certificate.
  import-cert [--certName FILE]   Import a certificate.
  version                         Print the installed package version.
  uninstall                       Interactively remove the MCC deployment from this host.
  help                            Show this help.

Commands requiring root: provision, diagnostics, configure-proxy, import-cert, uninstall.

Exit codes:
  0 success / healthy    1 failure / degraded    2 invalid usage
  3 not provisioned      4 permission denied

Removing the OS package does not remove an active MCC deployment. Run
'${PACKAGE_NAME} uninstall' first, then remove the package.
EOF
}

require_root() {
    if [ "$(id -u)" -ne 0 ]; then
        err "Error: '$1' must be run as root. Try: sudo ${PACKAGE_NAME} $1"
        exit "${EXIT_PERMISSION}"
    fi
}

require_payload() {
    if [ ! -d "${PAYLOAD_DIR}" ]; then
        err "Error: MCC payload directory not found at ${PAYLOAD_DIR}."
        err "The ${PACKAGE_NAME} package appears to be damaged. Reinstall it."
        exit "${EXIT_FAILED}"
    fi
    if [ ! -x "${PAYLOAD_DIR}/$1" ]; then
        err "Error: required script '$1' is missing or not executable in ${PAYLOAD_DIR}."
        err "The ${PACKAGE_NAME} package appears to be damaged. Reinstall it."
        exit "${EXIT_FAILED}"
    fi
}

# Runs a payload script from inside PAYLOAD_DIR.
#
# deploymcc.sh and its peers resolve dpsTemplate.txt, packageVer.txt,
# installiotedge.sh, and registermcc.sh relative to the current working
# directory, not to the script location, so the working directory must be the
# payload directory. Underlying exit codes are deliberately normalized to
# EXIT_FAILED; the implementation scripts' internal codes are not a public API.
run_payload() {
    local script="$1"
    shift
    require_payload "${script}"
    ( cd "${PAYLOAD_DIR}" && "./${script}" "$@" )
    local rc=$?
    if [ "${rc}" -ne 0 ]; then
        return "${EXIT_FAILED}"
    fi
    return "${EXIT_OK}"
}

have() { command -v "$1" >/dev/null 2>&1; }

# Reports whether the caller can query container state at all, so that a
# permission failure is never reported as "not provisioned".
docker_readable() {
    have docker || return 1
    docker ps -a >/dev/null 2>&1
}

container_exists() {
    docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qx "$1"
}

container_running() {
    docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$1"
}

service_active() {
    have systemctl || return 1
    systemctl is-active --quiet "$1" 2>/dev/null
}

service_enabled_or_active() {
    have systemctl || return 1
    systemctl is-active --quiet "$1" 2>/dev/null && return 0
    systemctl is-enabled --quiet "$1" 2>/dev/null
}

# Provisioned detection is runtime-state based by design. The existence of an
# empty /etc directory does not imply provisioning, and the package creates
# those directories at install time.
#
# Returns 0 provisioned, 1 not provisioned, 4 undeterminable (fail closed).
is_provisioned() {
    local can_check=1

    if have systemctl; then
        can_check=0
        if service_enabled_or_active aziot-edged || service_enabled_or_active aziot-identityd; then
            return 0
        fi
    fi

    if have docker; then
        if docker_readable; then
            can_check=0
            if container_exists "${MCC_CONTAINER}"; then
                return 0
            fi
        else
            # Docker is present but unreadable by this caller. Never downgrade
            # this to "not provisioned".
            return 4
        fi
    fi

    if [ "${can_check}" -ne 0 ]; then
        return 4
    fi
    return 1
}

cmd_version() {
    local v=""
    if have dpkg-query; then
        v=$(dpkg-query -W -f='${Version}' "${PACKAGE_NAME}" 2>/dev/null)
    fi
    if [ -z "${v}" ] && have rpm; then
        v=$(rpm -q --qf '%{VERSION}-%{RELEASE}' "${PACKAGE_NAME}" 2>/dev/null)
    fi
    if [ -z "${v}" ] && [ -r "${PAYLOAD_DIR}/packageVer.txt" ]; then
        # Fallback for an uninstalled or relocated payload.
        v="$(tr -d '[:space:]' < "${PAYLOAD_DIR}/packageVer.txt") (payload)"
    fi
    if [ -z "${v}" ]; then
        err "Error: unable to determine the installed ${PACKAGE_NAME} version."
        return "${EXIT_FAILED}"
    fi
    printf '%s %s\n' "${PACKAGE_NAME}" "${v}"
    return "${EXIT_OK}"
}

report() { printf '  %-22s %s\n' "$1" "$2"; }

cmd_status() {
    is_provisioned
    local prov=$?

    if [ "${prov}" -eq 4 ]; then
        err "Error: unable to determine MCC state. Required services or the container"
        err "runtime could not be queried. Re-run as root, or ensure this user can"
        err "access the Docker daemon and systemd."
        return "${EXIT_PERMISSION}"
    fi

    if [ "${prov}" -ne 0 ]; then
        echo "Microsoft Connected Cache: not provisioned"
        echo
        echo "The package is installed but this host has not been provisioned."
        echo "Run: sudo ${PACKAGE_NAME} provision <portal-generated parameters>"
        return "${EXIT_NOT_PROVISIONED}"
    fi

    local degraded=0

    echo "Microsoft Connected Cache: provisioned"
    echo
    echo "Services:"
    local svc
    for svc in aziot-edged aziot-identityd docker; do
        if ! have systemctl; then
            report "${svc}" "unknown (systemctl unavailable)"
            degraded=1
        elif service_active "${svc}"; then
            report "${svc}" "active"
        else
            report "${svc}" "INACTIVE"
            degraded=1
        fi
    done

    echo
    echo "Containers:"
    if ! docker_readable; then
        report "docker" "UNREADABLE (permission denied or daemon down)"
        degraded=1
    else
        local c
        for c in edgeAgent edgeHub "${MCC_CONTAINER}"; do
            if container_running "${c}"; then
                report "${c}" "running"
            elif container_exists "${c}"; then
                report "${c}" "STOPPED"
                degraded=1
            else
                report "${c}" "MISSING"
                degraded=1
            fi
        done
    fi

    echo
    if [ "${degraded}" -ne 0 ]; then
        echo "Overall: DEGRADED"
        echo "Collect diagnostics with: sudo ${PACKAGE_NAME} diagnostics"
        return "${EXIT_FAILED}"
    fi
    echo "Overall: healthy"
    return "${EXIT_OK}"
}

cmd_provision() {
    require_root provision
    if [ "$#" -eq 0 ]; then
        err "Error: 'provision' requires the portal-generated parameters."
        err "Usage: sudo ${PACKAGE_NAME} provision <key=value>..."
        return "${EXIT_USAGE}"
    fi
    run_payload deploymcc.sh "$@"
}

cmd_diagnostics() {
    require_root diagnostics
    run_payload collectmccdiagnostics.sh "$@"
}

cmd_configure_proxy() {
    require_root configure-proxy
    if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
        err "Error: 'configure-proxy' takes a proxy URL and an optional TLS certificate path."
        err "Usage: sudo ${PACKAGE_NAME} configure-proxy <url> [tls-certificate-path]"
        return "${EXIT_USAGE}"
    fi
    run_payload configureproxy.sh "$@"
}

cmd_import_cert() {
    require_root import-cert
    run_payload importCert.sh "$@"
}

cmd_uninstall() {
    require_root uninstall
    if [ "$#" -ne 0 ]; then
        err "Error: 'uninstall' takes no arguments."
        return "${EXIT_USAGE}"
    fi
    echo "This removes the MCC deployment from this host, including IoT Edge and the"
    echo "Moby container runtime. It does not remove the ${PACKAGE_NAME} OS package."
    echo
    run_payload uninstallmcc.sh
}

main() {
    if [ "$#" -eq 0 ]; then
        usage >&2
        exit "${EXIT_USAGE}"
    fi

    local command="$1"
    shift

    case "${command}" in
        provision)       cmd_provision "$@" ;;
        status)          cmd_status "$@" ;;
        diagnostics)     cmd_diagnostics "$@" ;;
        configure-proxy) cmd_configure_proxy "$@" ;;
        import-cert)     cmd_import_cert "$@" ;;
        uninstall)       cmd_uninstall "$@" ;;
        version|--version|-v) cmd_version ;;
        help|--help|-h)  usage ;;
        *)
            err "Error: unknown command '${command}'."
            err
            usage >&2
            exit "${EXIT_USAGE}"
            ;;
    esac
    exit $?
}

main "$@"
