#!/usr/bin/env bash
# tk: run the whole Thinkera stack (site + CMS, platform, WhatsApp gateway) from one place.
#   ./tk help
set -euo pipefail

# Git Bash on Windows rewrites /container/paths into C:\ paths before docker sees them; Linux ignores this.
export MSYS_NO_PATHCONV=1
cd "$(dirname "$0")"
ROOT=$(pwd)

dc() { docker compose "$@"; }
log() { printf '%s  %s\n' "$(date '+%H:%M:%S')" "$*"; }
die() { printf 'tk: %s\n' "$*" >&2; exit 1; }

# a value from an env file (Windows line endings stripped)
envval() { { grep -E "^$1=" "${2:-.env}" 2>/dev/null || true; } | head -1 | cut -d= -f2- | tr -d '\r'; }

# a host folder as docker understands it (D:/... under Git Bash, the plain path on Linux)
hostpath() { (cd "$1" && { pwd -W 2>/dev/null || pwd; }); }

[ -f .env ] || [ "${1:-help}" = help ] || die "no .env here: copy .env.example to .env and env/*.env.example to env/*.env, then fill them"

PROJECT=$(envval COMPOSE_PROJECT_NAME); PROJECT=${PROJECT:-thinkera}
PG_ADMIN=$(envval PG_ADMIN_USER); PG_ADMIN=${PG_ADMIN:-postgres}
PHP="dc exec -T -u www-data -w /var/www/html moodle php"

# ── the platform's own steps ─────────────────────────────────────────────────────────────────────────────────
# While the database is being upgraded Moodle answers every request by throwing «upgraderunning», and the
# student gets a bare grey box that reads like a fault. Maintenance mode instead serves one file straight
# from disk — before the database, before the theme — so this puts our own page there first and takes the
# site down for the seconds the upgrade needs. Reported from the live site, 29 September 2026.
platform_maintenance() {   # on | off
    if [ "$1" = on ]; then
        if [ -f infra/maintenance/climaintenance.html ]; then
            dc cp infra/maintenance/climaintenance.html moodle:/tmp/climaintenance.html >/dev/null 2>&1
        fi
        $PHP admin/cli/maintenance.php --enable >/dev/null 2>&1
        # Moodle writes its own plain page when maintenance goes on; ours replaces it right after
        dc exec -T moodle sh -lc '[ -f /tmp/climaintenance.html ] && cp /tmp/climaintenance.html \
            /var/www/moodledata/climaintenance.html && chown www-data:www-data \
            /var/www/moodledata/climaintenance.html' >/dev/null 2>&1 || true
    else
        $PHP admin/cli/maintenance.php --disable >/dev/null 2>&1
    fi
}

platform_upgrade() {
    log "platform: plugins into moodle and cron, upgrade, caches"
    dc exec -T moodle sync-plugins --live >/dev/null
    dc exec -T cron sync-plugins --live >/dev/null
    platform_maintenance on
    $PHP admin/cli/upgrade.php --non-interactive | grep -E '^-->|^\+\+|upgrade|ترقية' || true
    $PHP admin/cli/purge_caches.php
    platform_maintenance off
}

platform_configure() {
    log "platform: settings from env/platform.env (mail, live sessions, WhatsApp, analytics, wording)"
    local s
    for s in local/thinkera/cli/set_mail.php local/thinkera/cli/set_bbb.php local/thinkera/cli/set_whatsapp.php \
             local/thinkera/cli/set_analytics.php theme/thinkera/cli/rebrand_strings.php; do
        $PHP "public/$s" >/dev/null 2>&1 && echo "  ok  $s" || echo "  --  $s (skipped)"
    done
    $PHP admin/cli/purge_caches.php
}

wait_healthy() {   # wait_healthy <service>
    local i
    for i in $(seq 1 60); do
        [ "$(docker inspect -f '{{.State.Health.Status}}' "$(dc ps -q "$1")" 2>/dev/null)" = healthy ] && return 0
        sleep 2
    done
    die "$1 did not become healthy"
}

status() {
    dc ps --format 'table {{.Service}}\t{{.Status}}\t{{.Ports}}'
    echo
    local bind site plat gw code u
    bind=$(envval BIND_ADDRESS); bind=${bind:-127.0.0.1}
    site=$(envval SITE_PORT); plat=$(envval PLATFORM_PORT); gw=$(envval GATEWAY_PORT)
    for u in "site+CMS  http://$bind:${site:-8890}/" "platform  http://$bind:${plat:-8891}/login/index.php" \
             "gateway   http://$bind:${gw:-8892}/openapi.json"; do
        code=$(curl -s -o /dev/null -m 10 -w '%{http_code}' "${u##* }" 2>/dev/null || true)
        printf '%-10s %s  HTTP %s\n' "${u%% *}" "${u##* }" "${code:-down}"
    done
}

# ── backup / restore ─────────────────────────────────────────────────────────────────────────────────────────
# A backup folder holds:  db-<name>.dump (pg_dump custom format) for each database, moodledata.tgz,
# cms-media.tgz, evolution.tgz and SHA256SUMS. The same layout comes out of `tk migrate`.
vol_platform=moodledata; vol_cms=cmsmedia; vol_whatsapp=evolution
tgz_platform=moodledata.tgz; tgz_cms=cms-media.tgz; tgz_whatsapp=evolution.tgz

dbname() {   # dbname <part> -> the database names of that part
    case $1 in
        platform) envval MOODLE_DB_NAME ;;
        cms) envval CMS_DB_NAME ;;
        whatsapp) printf 'evolution\ngateway\n' ;;
    esac
}
dbowner() {
    case $1 in
        platform) envval MOODLE_DB_USER ;;
        cms) envval CMS_DB_USER ;;
        whatsapp) envval WA_DB_USER ;;
    esac
}

volume_tar() {   # volume_tar <volume> > file.tgz
    docker run --rm -v "${PROJECT}_$1:/data:ro" alpine tar czf - -C /data .
}

backup() {
    local base keep day work final part db
    base=${BACKUP_DIR:-$(envval BACKUP_DIR)}; base=${base:-$ROOT/backups}
    keep=${BACKUP_KEEP:-$(envval BACKUP_KEEP)}; keep=${keep:-14}
    day=$(date +%Y-%m-%d_%H%M)
    work="$base/$day.partial"; final="$base/$day"
    mkdir -p "$work"
    trap 'rm -rf "$work"' ERR
    log "backup into $final"
    dc up -d db >/dev/null 2>&1; wait_healthy db
    for part in platform cms whatsapp; do
        for db in $(dbname $part); do
            dc exec -T db pg_dump -U "$PG_ADMIN" -d "$db" -Fc > "$work/db-$db.dump"
            log "  database $db  $(du -h "$work/db-$db.dump" | cut -f1)"
        done
        local vol tgz; eval "vol=\$vol_$part tgz=\$tgz_$part"
        volume_tar "$vol" > "$work/$tgz"
        log "  files    $tgz  $(du -h "$work/$tgz" | cut -f1)"
    done
    (cd "$work" && sha256sum -- * > SHA256SUMS && sha256sum -c SHA256SUMS >/dev/null) || die "checksums do not match"
    trap - ERR
    mv "$work" "$final"
    log "kept $(du -sh "$final" | cut -f1) in $final"
    ls -1d "$base"/20??-??-??_???? 2>/dev/null | sort -r | tail -n +"$((keep + 1))" | while read -r d; do
        rm -rf "$d"; log "removed old $d"
    done
}

restore() {   # restore <folder> [platform] [cms] [whatsapp] [-y] [--old-wwwroot=URL]
    local src="" yes=0 oldroot="" parts=() a
    for a in "$@"; do
        case $a in
            -y|--yes) yes=1 ;;
            --old-wwwroot=*) oldroot=${a#*=} ;;
            platform|cms|whatsapp) parts+=("$a") ;;
            *) src=$a ;;
        esac
    done
    [ -d "$src" ] || die "usage: tk restore <backup folder> [platform|cms|whatsapp ...] [-y] [--old-wwwroot=URL]"
    [ ${#parts[@]} -gt 0 ] || parts=(platform cms whatsapp)
    src=$(cd "$src" && pwd)
    [ ! -f "$src/SHA256SUMS" ] || (cd "$src" && sha256sum -c --quiet SHA256SUMS) || die "the backup's checksums do not match"
    echo "This REPLACES the data of: ${parts[*]}  on this machine, from $src"
    if [ $yes = 0 ]; then printf 'type yes to go on: '; read -r a; [ "$a" = yes ] || exit 1; fi

    dc up -d db redis >/dev/null 2>&1; wait_healthy db
    local part db owner vol tgz svcs
    for part in "${parts[@]}"; do
        case $part in
            platform) svcs="moodle cron platform-web" ;;
            cms) svcs="cms" ;;
            whatsapp) svcs="evolution gateway worker" ;;
        esac
        log "$part: stopping $svcs"
        dc stop $svcs >/dev/null 2>&1 || true
        owner=$(dbowner $part)
        for db in $(dbname $part); do
            [ -f "$src/db-$db.dump" ] || { log "  no db-$db.dump, database $db left as it is"; continue; }
            log "  database $db"
            dc exec -T db psql -q -U "$PG_ADMIN" -d postgres -v ON_ERROR_STOP=1 \
                -c "DROP DATABASE IF EXISTS \"$db\" WITH (FORCE)" \
                -c "CREATE DATABASE \"$db\" OWNER \"$owner\" ENCODING 'UTF8' TEMPLATE template0"
            dc exec -T db pg_restore -U "$PG_ADMIN" -d "$db" --no-owner --no-privileges --role="$owner" \
                < "$src/db-$db.dump" || log "  (pg_restore reported warnings; usually harmless extension comments)"
        done
        eval "vol=\$vol_$part tgz=\$tgz_$part"
        if [ -f "$src/$tgz" ]; then
            log "  files $tgz"
            dc create $svcs >/dev/null 2>&1 || true   # makes sure the volume exists
            docker run --rm -v "${PROJECT}_$vol:/data" -v "$(hostpath "$src"):/backup:ro" alpine \
                sh -c 'rm -rf /data/* /data/.[!.]* 2>/dev/null; tar xzf /backup/'"$tgz"' -C /data'
        fi
    done

    log "starting"
    dc up -d
    if printf '%s\n' "${parts[@]}" | grep -qx platform; then
        sleep 8
        platform_upgrade
        if [ -n "$oldroot" ]; then
            local wwwroot; wwwroot=$(envval MOODLE_WWWROOT env/platform.env)
            if [ "$oldroot" != "$wwwroot" ]; then
                log "platform: addresses in the content  $oldroot -> $wwwroot"
                $PHP public/admin/tool/replace/cli/replace.php --search="$oldroot" --replace="$wwwroot" --non-interactive
            fi
        fi
        platform_configure
    fi
    status
}

# ── migrate from the three separate stacks ───────────────────────────────────────────────────────────────────
# Exports from the old folders (each still with its own docker-compose.yml and .env) into one backup folder,
# stops the old stacks (their data stays in their own volumes, untouched) and restores it all here.
migrate() {
    local old_platform=../Thinkera/platform/moodle old_cms=../Thinkera/thinkera.academy old_wa=../Thinkera/wa-gateway yes="" a
    for a in "$@"; do
        case $a in
            --platform=*) old_platform=${a#*=} ;;
            --cms=*) old_cms=${a#*=} ;;
            --gateway=*) old_wa=${a#*=} ;;
            -y|--yes) yes=-y ;;
            *) die "usage: tk migrate [--platform=DIR] [--cms=DIR] [--gateway=DIR|none] [-y]" ;;
        esac
    done
    local out="$ROOT/backups/migrate-$(date +%Y-%m-%d_%H%M)"
    mkdir -p "$out"
    local parts=()

    if [ -f "$old_platform/.env" ]; then
        log "export platform from $old_platform"
        ( cd "$old_platform"
          docker compose exec -T db pg_dump -U "$(envval MOODLE_DB_USER)" -d "$(envval MOODLE_DB_NAME)" -Fc ) \
            > "$out/db-$(envval MOODLE_DB_NAME).dump"
        ( cd "$old_platform" && docker compose exec -T moodle tar czf - -C /var/www/moodledata . ) > "$out/moodledata.tgz"
        parts+=(platform)
    fi
    if [ -f "$old_cms/.env" ]; then
        log "export site + CMS from $old_cms"
        ( cd "$old_cms"
          docker compose exec -T db pg_dump -U "$(envval POSTGRES_USER)" -d "$(envval POSTGRES_DB)" -Fc ) \
            > "$out/db-$(envval CMS_DB_NAME).dump"
        ( cd "$old_cms" && docker compose exec -T web tar czf - -C /app/media . ) > "$out/cms-media.tgz"
        parts+=(cms)
    fi
    if [ "$old_wa" != none ] && [ -f "$old_wa/.env" ]; then
        log "export WhatsApp gateway from $old_wa"
        local u; u=$(envval POSTGRES_USER "$old_wa/.env")
        ( cd "$old_wa" && docker compose exec -T postgres pg_dump -U "$u" -d evolution -Fc ) > "$out/db-evolution.dump"
        ( cd "$old_wa" && docker compose exec -T postgres pg_dump -U "$u" -d gateway -Fc ) > "$out/db-gateway.dump"
        ( cd "$old_wa" && docker compose exec -T evolution tar czf - -C /evolution/instances . ) > "$out/evolution.tgz"
        parts+=(whatsapp)
    fi
    [ ${#parts[@]} -gt 0 ] || die "none of the old folders has a .env"
    (cd "$out" && sha256sum -- * > SHA256SUMS)
    ls -lh "$out"

    log "stopping the old stacks (their volumes are kept)"
    [ -f "$old_platform/.env" ] && (cd "$old_platform" && docker compose stop) || true
    [ -f "$old_cms/.env" ] && (cd "$old_cms" && docker compose stop) || true
    [ "$old_wa" != none ] && [ -f "$old_wa/.env" ] && (cd "$old_wa" && docker compose stop) || true

    log "building and restoring here"
    dc build
    restore "$out" "${parts[@]}" $yes
}

# ── AI models — their own project now (../LLM-Gate) ──────────────────────────────────────────────────────────
llm() {
    cat <<EOF
The models are not in this stack any more. They are their own project — LLM-Gate — so that they can be copied
to another machine, with the models they have already downloaded, and run there without any of this.

  the models        cd ../LLM-Gate && ./gate status | pull <model> | list | keys | test
  this platform     ./tk moodle local/tkai/cli/active.php          which connection and model each task uses
                    ./tk moodle local/tkai/cli/set_gate.php --help point the platform at a gate (URL + key)

An administrator does the same thing on the page: Site administration > Plugins > Local plugins > Thinkera AI,
with a connection of type «متوافق مع OpenAI» whose address is the gate's /v1/ and whose key is one of its own.
EOF
}

deploy() {
    [ -f env/deploy.env ] || die "env/deploy.env is missing (see env/deploy.env.example)"
    local host user path key
    host=$(envval DEPLOY_HOST env/deploy.env); user=$(envval DEPLOY_USER env/deploy.env)
    path=$(envval DEPLOY_PATH env/deploy.env); key=$(envval DEPLOY_SSH_KEY env/deploy.env)
    [ -n "$host" ] && [ -n "$user" ] && [ -n "$path" ] || die "DEPLOY_HOST, DEPLOY_USER and DEPLOY_PATH must be set"
    local ssh_opts=(-o BatchMode=yes -o IdentitiesOnly=yes)
    [ -n "$key" ] && ssh_opts+=(-i "$key")
    local pkg; pkg=$(mktemp -t thinkera-deploy.XXXXXX).tgz
    log "packaging code (no .env, no env/*.env, no backups)"
    tar -czf "$pkg" --exclude='.env' --exclude='env/*.env' --exclude='backups' --exclude='node_modules' \
        --exclude='.next' --exclude='*.zip' docker-compose.yml tk tk.cmd README.md .env.example env infra apps
    log "uploading $(du -h "$pkg" | cut -f1) to $user@$host:$path"
    scp -q "${ssh_opts[@]}" "$pkg" "$user@$host:/tmp/thinkera-deploy.tgz"
    rm -f "$pkg"
    ssh "${ssh_opts[@]}" "$user@$host" "set -e; cd '$path'
        [ -f .env ] || { echo 'the server has no .env in $path'; exit 1; }
        [ -x tk ] && ./tk backup || echo 'first deploy: no backup taken'
        tar -xzf /tmp/thinkera-deploy.tgz && rm -f /tmp/thinkera-deploy.tgz && chmod +x tk
        ./tk up && sleep 8 && ./tk upgrade && ./tk status"
}

# ── commands ─────────────────────────────────────────────────────────────────────────────────────────────────
cmd=${1:-help}; shift || true
case $cmd in
    up)        dc up -d --build "$@"; status ;;
    down)      dc down ;;
    stop)      dc stop "$@" ;;
    start)     dc start "$@" ;;
    restart)   dc restart "$@" ;;
    build)     dc build "$@" ;;
    rebuild)   dc build --pull "$@"; dc up -d --force-recreate "$@"
               if [ $# -eq 0 ] || printf '%s\n' "$@" | grep -qx moodle; then sleep 8; platform_upgrade; fi
               status ;;
    ps|status) status ;;
    logs)      dc logs -f --tail=200 "$@" ;;
    sh)        dc exec "${1:?service}" sh ;;
    exec)      dc exec "$@" ;;
    psql)      dc exec db psql -U "$PG_ADMIN" -d "${1:-postgres}" ;;
    upgrade)   platform_upgrade ;;
    configure) platform_configure ;;
    maintenance) platform_maintenance "${1:-on}"; echo "maintenance ${1:-on}" ;;
    moodle)    $PHP "$@" ;;
    # NODE_ENV=production is not a detail: without it Payload runs in dev mode, pushes the schema instead of
    # migrating it, and writes a «dev» marker that makes the site ask at every boot whether to migrate with
    # data loss — a question a container has no terminal to answer, so the site stops starting.
    cms-tools) dc run --rm -e NODE_ENV=production cms-tools "$@" ;;
    backup)    backup ;;
    restore)   restore "$@" ;;
    migrate)   migrate "$@" ;;
    deploy)    deploy ;;
    llm)       llm "$@" ;;
    rebrand)   (cd apps/platform && sh rebrand.sh) ;;
    harden)    (cd apps/platform && sh harden.sh) ;;
    config)    dc config ;;
    help|*)    cat <<'EOF'
tk: the Thinkera stack (site + CMS, platform, WhatsApp gateway) as one compose project

  ./tk up [service...]         build what changed and start (everything, or the services named)
  ./tk rebuild [service...]    rebuild images (pulling base images) and recreate the containers
  ./tk restart [service...]    restart without rebuilding
  ./tk stop | start | down     stop / start / remove the containers (data volumes are always kept)
  ./tk status                  containers and an HTTP check of the site, platform and gateway
  ./tk logs [service...]       follow the logs
  ./tk sh <service>            a shell inside a container
  ./tk psql [database]         PostgreSQL as the admin user

  ./tk upgrade                 platform: copy plugins, run the upgrade, purge caches (after plugin changes)
  ./tk configure               platform: apply env/platform.env settings (mail, BBB, WhatsApp, analytics)
  ./tk moodle <script> [args]  run a platform CLI script, e.g. ./tk moodle admin/cli/purge_caches.php
  ./tk rebrand | harden        platform: re-apply the Thinkera branding / the security settings
  ./tk cms-tools <cmd>         site: Payload CLI, e.g. ./tk cms-tools npx payload migrate:create name

  ./tk backup                  all databases and files into backups/<date_time>/ (BACKUP_DIR, BACKUP_KEEP)
  ./tk restore <dir> [platform|cms|whatsapp] [-y] [--old-wwwroot=URL]
  ./tk migrate [--platform=DIR] [--cms=DIR] [--gateway=DIR|none] [-y]
                               one-time move from the three separate stacks into this one
  ./tk deploy                  ship the code to the server (env/deploy.env), back up there, rebuild, upgrade

  ./tk llm status              the AI model service: installed models, what is loaded, reachable from the platform
  ./tk llm pull [model...]     download LLM_MODELS from .env (or the models named) into the llmmodels volume
  ./tk llm list | rm <model>   the models installed / remove one
  ./tk llm use <model> [conn]  the model the platform grades with (same as the Thinkera AI admin page)
  ./tk llm test [model]        one short request through the platform's AI client, with its latency

services: db redis moodle cron platform-web cms evolution gateway worker llm
EOF
    ;;
esac
