#!/bin/bash

# Generic per-app install/uninstall/start/stop/restart/edit driver.
#
# The 31 containers/<app>/<app>.sh files used to each define their own
# install<App>() with the SAME 10-step sequence. ~4,000 lines of duplicated
# boilerplate. This is the one place that sequence lives now; per-app
# customisation lands in declarative hooks in containers/<app>/tools/
# <app>_tools.sh (or wherever the app's tools.sh lives — auto-sourced).
#
# Dispatch is driven by the `$<slug>` global variable (set by dockerInstallApp
# in scripts/docker/app/functions/function_install_app.sh — `declare $app=i`).
# Same convention the per-app .sh files used; nothing changes for the caller.
# Actions are letters: c (config edit), u (uninstall), s (stop), r (restart),
# i (install), t (treated like c — legacy alias).
#
# Hook surface — all are `declare -f`-gated, silent no-op when absent:
#
#   <slug>_install_pre              before any install work
#   <slug>_install_post_setup       after dockerConfigSetupToContainer
#                                   (install folder + .config exist; compose
#                                   file not yet written)
#   <slug>_install_post_compose     after dockerComposeSetupFile
#                                   (docker-compose.yml has been written +
#                                   tag-substituted; container not yet up)
#   <slug>_install_post_start       after dockerComposeUpdateAndStartApp
#                                   (container is up; the place for
#                                   wait-for-ready + post-up API calls)
#   <slug>_install_message_data     echoes extra args for menuShowFinalMessages
#                                   (typically credentials / URLs)
#   <slug>_install_post             very last thing, after the final message
#
#   <slug>_uninstall_pre / _post    around dockerUninstallApp
#   <slug>_stop_post                after dockerComposeDown
#   <slug>_restart_post             after dockerComposeRestart
#
# Hooks receive $app_name as $1 (and stay un-namespaced — they're already
# slug-prefixed). Return code is ignored unless they isError; the install
# continues regardless. Use that escape hatch for non-fatal app-specific
# refinements (rotate a key, patch a yaml after start, etc.).

_appCallHook()
{
    local hook_name="$1"; shift
    if declare -F "$hook_name" >/dev/null 2>&1; then
        "$hook_name" "$@"
    fi
}

# Standard "post-start integration" steps. Same for every app. Lives in a
# helper so the generic install body stays readable; safe for apps that
# don't tag for monitoring (the helpers no-op gracefully).
_appPostStartIntegrations()
{
    local app_name="$1"
    appUpdateSpecifics "$app_name"
    setupHeadscale "$app_name"
    databaseInstallApp "$app_name"
    webuiContainerSetup "$app_name" install

    # Scrape-target + dashboard re-gather. The compose-level toggle ran
    # already (post-compose, so the running container reflects it).
    # monitoringRefreshAll is self-correcting and no-ops when Prometheus
    # / Grafana aren't installed.
    if declare -F monitoringRefreshAll >/dev/null 2>&1; then
        monitoringRefreshAll 2>/dev/null || true
    fi
}

installApp()
{
    local app_slug="$1"
    local config_variables="$2"

    # APP_NAME comes from the app's CFG_<APP>_APP_NAME (the user's chosen
    # subdomain / install name). Fall back to the slug if unset.
    local app_name_var="CFG_${app_slug^^}_APP_NAME"
    local app_name="${!app_name_var:-$app_slug}"

    # Dispatch flags live in the $<slug> global, e.g. linkding=i. Default to
    # install if nothing set — installApp called directly without flag = install.
    local actions="${!app_slug:-i}"

    # Setup phase shared by every action (folder + variables).
    if [[ "$actions" == *[cCtTuUsSrRiI]* ]]; then
        dockerConfigSetupToContainer silent "$app_slug"
        initializeAppVariables "$app_name"
    fi

    if [[ "$actions" == *[cCtT]* ]]; then
        editAppConfig "$app_name"
    fi

    if [[ "$actions" == *[uU]* ]]; then
        _appCallHook "${app_slug}_uninstall_pre" "$app_name"
        dockerUninstallApp "$app_name"
        _appCallHook "${app_slug}_uninstall_post" "$app_name"
    fi

    if [[ "$actions" == *[sS]* ]]; then
        dockerComposeDown "$app_name"
        _appCallHook "${app_slug}_stop_post" "$app_name"
    fi

    if [[ "$actions" == *[rR]* ]]; then
        dockerComposeRestart "$app_name"
        _appCallHook "${app_slug}_restart_post" "$app_name"
    fi

    if [[ "$actions" == *[iI]* ]]; then
        isHeader "Install $app_name"
        _appCallHook "${app_slug}_install_pre" "$app_name"

        ((menu_number++))
        echo ""
        echo "---- $menu_number. Setting up install folder and config for $app_name."
        echo ""
        dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables"
        isSuccessful "Install folders and Config files set up for $app_name."
        _appCallHook "${app_slug}_install_post_setup" "$app_name"

        ((menu_number++))
        echo ""
        echo "---- $menu_number. Setting up the $app_name docker-compose.yml."
        echo ""
        dockerComposeSetupFile "$app_name"

        # Compose-level monitoring toggle MUST run before docker-compose up
        # — the compose file is the source of truth for the running
        # container, so editing it post-start wouldn't take effect until
        # the next restart. Idempotent + no-op for apps without a marker
        # block; apps that toggle additional files (authelia config.yml,
        # traefik traefik.yml, unbound unbound.conf …) call it again from
        # their _install_post_compose hook.
        if declare -F monitoringToggleAppConfig >/dev/null 2>&1; then
            monitoringToggleAppConfig "$app_name" "docker-compose.yml" 2>/dev/null || true
        fi

        _appCallHook "${app_slug}_install_post_compose" "$app_name"

        # Optional .env handling — apps that ship a .env in their template
        # dir get it copied + tag-substituted. No-op for apps without one.
        if [[ -f "${install_containers_dir}${app_slug}/.env" ]]; then
            local result
            result=$(copyResource "$app_name" ".env" "")
            checkSuccess "Copying .env for $app_name"
            configSetupFileWithData "$app_name" ".env"
        fi

        ((menu_number++))
        echo ""
        echo "---- $menu_number. Updating file permissions before starting."
        echo ""
        fixPermissionsBeforeStart "$app_name"

        ((menu_number++))
        echo ""
        echo "---- $menu_number. Running docker-compose to install + start $app_name."
        echo ""
        dockerComposeUpdateAndStartApp "$app_name" install
        _appCallHook "${app_slug}_install_post_start" "$app_name"

        ((menu_number++))
        echo ""
        echo "---- $menu_number. Running post-install integrations."
        echo ""
        _appPostStartIntegrations "$app_name"

        ((menu_number++))
        echo ""
        echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name"
        echo ""

        # Final-message data — apps that want extra args (creds, URLs, etc.)
        # printed in the menu output echo them from their hook. Word-split is
        # intentional: each space-separated token becomes a positional arg.
        local msg_data=""
        if declare -F "${app_slug}_install_message_data" >/dev/null 2>&1; then
            msg_data=$("${app_slug}_install_message_data" "$app_name")
        fi
        # shellcheck disable=SC2086  # intentional split — hook returns "u p" etc.
        menuShowFinalMessages "$app_name" $msg_data

        _appCallHook "${app_slug}_install_post" "$app_name"
        menu_number=0
    fi

    # Reset the dispatch flag so a stale value doesn't trip a later call.
    eval "$app_slug=n"
}
