#!/bin/bash
# SecuBox Gitea Controller
# Git server in Alpine LXC for Debian
# Three-fold architecture: Components, Status, Access

VERSION="1.5.0"
CONFIG_FILE="/etc/secubox/gitea.toml"
LXC_PATH=""              # auto-detected below if blank, see resolve_lxc_path
DATA_PATH="/data/gitea"
CONTAINER="gitea"
GITEA_VERSION="1.22.6"

# Gitea API hard floors
MIRROR_MIN_INTERVAL="10m0s"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

log() { echo -e "${GREEN}[GITEA]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }

# ============================================================================
# Configuration (TOML parsing)
# ============================================================================

config_get() {
    local key="$1" default="$2"
    [ -f "$CONFIG_FILE" ] && grep "^${key} *=" "$CONFIG_FILE" 2>/dev/null | head -1 | cut -d= -f2- | tr -d ' "' || echo "$default"
}

HTTP_PORT=$(config_get "http_port" "3000")
SSH_PORT=$(config_get "ssh_port" "2222")
DOMAIN=$(config_get "domain" "git.local")
APP_NAME=$(config_get "app_name" "SecuBox Git")
LXC_IP=$(config_get "lxc_ip" "10.100.0.40")
GITEA_APP_INI=$(config_get "app_ini" "/var/lib/gitea/custom/conf/app.ini")
ADMIN_USER=$(config_get "admin_user" "gandalf")

# Resolve LXC_PATH: explicit config wins, then fall back to the first
# directory containing a `<CONTAINER>/rootfs/` from the board conventions.
resolve_lxc_path() {
    local cfg
    cfg=$(config_get "lxc_path" "")
    if [ -n "$cfg" ]; then echo "$cfg"; return; fi
    for p in /data/lxc /srv/lxc /var/lib/lxc; do
        [ -d "$p/$CONTAINER/rootfs" ] && { echo "$p"; return; }
    done
    echo "/data/lxc"   # match modern board reality; install will create it
}
LXC_PATH=${LXC_PATH:-$(resolve_lxc_path)}

require_root() {
    [ "$(id -u)" -eq 0 ] || { error "Root required"; exit 1; }
}

lxc_running() { lxc-info -n "$CONTAINER" -P "$LXC_PATH" 2>/dev/null | grep -q "State:.*RUNNING"; }
lxc_exists() { [ -d "$LXC_PATH/$CONTAINER/rootfs" ]; }

lxc_attach() {
    local cmd="$1"
    lxc-attach -n "$CONTAINER" -P "$LXC_PATH" -- sh -c "$cmd"
}

# ============================================================================
# Alpine Bootstrap
# ============================================================================

bootstrap_alpine() {
    require_root
    log "Bootstrapping Alpine Linux..."

    mkdir -p "$LXC_PATH/$CONTAINER"
    cd "$LXC_PATH/$CONTAINER"

    # Download apk-tools-static
    if [ ! -f sbin/apk.static ]; then
        log "Downloading apk-tools-static..."
        local arch=$(uname -m)
        [ "$arch" = "x86_64" ] && arch="x86_64"
        [ "$arch" = "aarch64" ] && arch="aarch64"
        curl -L -o apk-tools-static.apk \
            "https://dl-cdn.alpinelinux.org/alpine/v3.21/main/${arch}/apk-tools-static-2.14.6-r3.apk"
        tar -xzf apk-tools-static.apk sbin/apk.static
        rm -f apk-tools-static.apk
    fi

    log "Installing base system..."
    ./sbin/apk.static -X https://dl-cdn.alpinelinux.org/alpine/v3.21/main \
        -U --allow-untrusted --root rootfs --initdb add \
        alpine-base alpine-baselayout busybox musl

    mkdir -p rootfs/etc/apk
    cat > rootfs/etc/apk/repositories << 'EOF'
https://dl-cdn.alpinelinux.org/alpine/v3.21/main
https://dl-cdn.alpinelinux.org/alpine/v3.21/community
EOF

    cat > rootfs/etc/resolv.conf << 'EOF'
nameserver 8.8.8.8
nameserver 1.1.1.1
EOF

    echo "gitea" > rootfs/etc/hostname
    log "Base system installed"
}

# ============================================================================
# LXC Configuration
# ============================================================================

create_lxc_config() {
    mkdir -p "$LXC_PATH/$CONTAINER"
    cat > "$LXC_PATH/$CONTAINER/config" << EOF
lxc.uts.name = gitea
lxc.rootfs.path = dir:${LXC_PATH}/${CONTAINER}/rootfs

# Host networking (no bridge required)
lxc.net.0.type = none

lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
lxc.cap.drop = sys_module mac_admin mac_override sys_time
lxc.tty.max = 0
lxc.pty.max = 256
lxc.cgroup2.memory.max = 512M
lxc.init.cmd = /opt/start-gitea.sh

# Bind mounts for persistent data
lxc.mount.entry = ${DATA_PATH}/git var/lib/gitea none bind,create=dir 0 0
lxc.mount.entry = ${DATA_PATH}/custom etc/gitea none bind,create=dir 0 0
lxc.mount.entry = ${DATA_PATH}/log var/log/gitea none bind,create=dir 0 0
EOF
    log "LXC config created"
}

create_install_init() {
    # Temporary init script for installation phase - just keeps container running
    mkdir -p "$LXC_PATH/$CONTAINER/rootfs/opt"
    cat > "$LXC_PATH/$CONTAINER/rootfs/opt/install-init.sh" << 'EOF'
#!/bin/sh
# Temporary init for installation - keeps container running
exec /bin/sleep infinity
EOF
    chmod +x "$LXC_PATH/$CONTAINER/rootfs/opt/install-init.sh"

    # Update config to use install init
    sed -i 's|lxc.init.cmd = .*|lxc.init.cmd = /opt/install-init.sh|' "$LXC_PATH/$CONTAINER/config"
}

create_startup_script() {
    mkdir -p "$LXC_PATH/$CONTAINER/rootfs/opt"
    cat > "$LXC_PATH/$CONTAINER/rootfs/opt/start-gitea.sh" << 'EOF'
#!/bin/sh
# Gitea startup script
export PATH=/usr/local/bin:/usr/bin:/bin:/sbin:/usr/sbin
export HOME=/var/lib/gitea

# Ensure directories exist
mkdir -p /var/lib/gitea /var/log/gitea /etc/gitea/conf
chown -R git:git /var/lib/gitea /var/log/gitea /etc/gitea 2>/dev/null || true

# Start Gitea as git user
cd /var/lib/gitea
exec su-exec git /usr/local/bin/gitea web --config /etc/gitea/conf/app.ini --work-path /var/lib/gitea
EOF
    chmod +x "$LXC_PATH/$CONTAINER/rootfs/opt/start-gitea.sh"

    # Update config to use gitea startup
    sed -i 's|lxc.init.cmd = .*|lxc.init.cmd = /opt/start-gitea.sh|' "$LXC_PATH/$CONTAINER/config"
}

# ============================================================================
# Install Gitea
# ============================================================================

install_gitea() {
    require_root
    log "Installing Gitea in LXC..."

    if ! lxc_running; then
        log "Starting container..."
        lxc-start -n "$CONTAINER" -P "$LXC_PATH" -d
        sleep 5
    fi

    # Install required packages
    log "Installing packages..."
    lxc_attach "apk update && apk add --no-cache git su-exec bash sqlite ca-certificates curl tzdata"

    # Create git user
    lxc_attach "adduser -D -H -s /bin/bash -h /var/lib/gitea git 2>/dev/null || true"

    # Download Gitea binary
    log "Downloading Gitea ${GITEA_VERSION}..."
    local arch=$(uname -m)
    [ "$arch" = "x86_64" ] && arch="amd64"
    [ "$arch" = "aarch64" ] && arch="arm64"

    lxc_attach "curl -fsSL -o /usr/local/bin/gitea \
        https://dl.gitea.io/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-linux-${arch} && \
        chmod +x /usr/local/bin/gitea"

    log "Gitea binary installed"

    # Create directories
    mkdir -p "$DATA_PATH"/{git,custom,log,backups}
    chown -R 1000:1000 "$DATA_PATH"

    # Create initial config
    create_gitea_config

    log "Gitea installed successfully!"
}

create_gitea_config() {
    mkdir -p "$DATA_PATH/custom/conf"
    cat > "$DATA_PATH/custom/conf/app.ini" << EOF
APP_NAME = ${APP_NAME}
RUN_USER = git
RUN_MODE = prod
WORK_PATH = /var/lib/gitea

[server]
DOMAIN = ${DOMAIN}
HTTP_PORT = ${HTTP_PORT}
ROOT_URL = http://${DOMAIN}:${HTTP_PORT}/
DISABLE_SSH = false
SSH_PORT = ${SSH_PORT}
BUILTIN_SSH_SERVER_USER = git
START_SSH_SERVER = true

[lfs]
ENABLED = true
PATH = /var/lib/gitea/lfs

[database]
DB_TYPE = sqlite3
PATH = /var/lib/gitea/gitea.db

[repository]
ROOT = /var/lib/gitea/repositories

[security]
INSTALL_LOCK = true
SECRET_KEY = $(openssl rand -hex 32)
INTERNAL_TOKEN = $(openssl rand -hex 64)

[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = false

[log]
MODE = file
LEVEL = Info
ROOT_PATH = /var/log/gitea

[oauth2]
JWT_SECRET = $(openssl rand -hex 32)
EOF
    chown -R 1000:1000 "$DATA_PATH/custom"
    log "Gitea configuration created"
}

# ============================================================================
# User Management
# ============================================================================

user_add() {
    local username="$1"
    local password="$2"
    local email="${3:-${username}@${DOMAIN}}"
    local admin="${4:-false}"

    if [ -z "$username" ] || [ -z "$password" ]; then
        echo "Usage: giteactl user add <username> <password> [email] [admin]"
        return 1
    fi

    if ! lxc_running; then
        error "Container not running"
        return 1
    fi

    local admin_flag=""
    [ "$admin" = "true" ] || [ "$admin" = "yes" ] || [ "$admin" = "1" ] && admin_flag="--admin"

    log "Creating user: $username"
    lxc_attach "su-exec git /usr/local/bin/gitea admin user create \
        --username '$username' \
        --password '$password' \
        --email '$email' \
        $admin_flag \
        --config /etc/gitea/conf/app.ini"
}

user_del() {
    local username="$1"

    if [ -z "$username" ]; then
        echo "Usage: giteactl user del <username>"
        return 1
    fi

    if ! lxc_running; then
        error "Container not running"
        return 1
    fi

    log "Deleting user: $username"
    lxc_attach "su-exec git /usr/local/bin/gitea admin user delete \
        --username '$username' \
        --config /etc/gitea/conf/app.ini"
}

user_passwd() {
    local username="$1"
    local password="$2"

    if [ -z "$username" ] || [ -z "$password" ]; then
        echo "Usage: giteactl user passwd <username> <password>"
        return 1
    fi

    if ! lxc_running; then
        error "Container not running"
        return 1
    fi

    log "Changing password for: $username"
    lxc_attach "su-exec git /usr/local/bin/gitea admin user change-password \
        --username '$username' \
        --password '$password' \
        --config /etc/gitea/conf/app.ini"
}

user_list() {
    if ! lxc_running; then
        error "Container not running"
        return 1
    fi

    echo "Gitea Users:"
    echo "============"
    lxc_attach "su-exec git /usr/local/bin/gitea admin user list --config /etc/gitea/conf/app.ini"
}

# ============================================================================
# Mirror Management
# ============================================================================

# ============================================================================
# Repo / Mirror — the third routing verb (issue #176)
# Forges `repo mirror add/remove/list/sync` parallel to mitmproxyctl route
# (issue #173) and haproxyctl vhost. Wraps the 4 quirks of the Gitea
# pull-mirror API into one atomic operation:
#   1. No "convert existing repo to mirror" -> delete + migrate
#   2. Minimum interval = 10m (enforced server-side, 500 below)
#   3. Filesystem fetches don't trigger Gitea hooks -> DB stays "empty"
#   4. Token generation needs to run inside the LXC as the gitea user
# ============================================================================

# Generate a short-lived admin token via gitea CLI inside the LXC and echo
# it. Token name embeds the PID so concurrent calls don't collide.
gitea_token_gen() {
    require_root
    if ! lxc_running; then error "Container not running"; return 1; fi
    local tname="giteactl-$$-$(date +%s)"
    local out
    out=$(lxc-attach -n "$CONTAINER" -P "$LXC_PATH" -- \
        su -s /bin/bash gitea -c \
        "gitea --config $GITEA_APP_INI admin user generate-access-token \
            --username $ADMIN_USER --token-name $tname --scopes all 2>&1" \
        | tail -1)
    # output line ends with the actual token after the last ':'
    local token
    token=$(echo "$out" | awk -F: '{print $NF}' | tr -d ' ')
    [ -z "$token" ] && { error "token gen failed: $out"; return 1; }
    echo "$token" "$tname"
}

gitea_token_revoke() {
    local tname="$1"
    [ -z "$tname" ] && return 0
    lxc-attach -n "$CONTAINER" -P "$LXC_PATH" -- \
        su -s /bin/bash gitea -c \
        "gitea --config $GITEA_APP_INI admin user delete-access-token \
            --username $ADMIN_USER --token-name $tname 2>&1" \
        >/dev/null 2>&1 || true
}

# Run a curl against the in-LXC Gitea API. Caller passes method + path
# (without /api/v1 prefix) + body (or "" if none). Echo HTTP code on stderr,
# body on stdout.
gitea_api() {
    local token="$1" method="$2" path="$3" body="$4"
    local url="http://localhost:${HTTP_PORT}/api/v1${path}"
    local args=("-s" "-X" "$method" "-H" "Authorization: token $token" "-H" "Content-Type: application/json")
    [ -n "$body" ] && args+=("-d" "$body")
    lxc-attach -n "$CONTAINER" -P "$LXC_PATH" -- \
        curl "${args[@]}" -w "\nHTTP_CODE:%{http_code}\n" "$url"
}

# Normalize an interval string. Gitea requires ≥10m; coerce silently to the
# floor so operators can ask for "5m" and get the closest legal value.
_normalize_interval() {
    local i="$1"
    [ -z "$i" ] && { echo "$MIRROR_MIN_INTERVAL"; return; }
    # Already in the canonical "Nm0s" form
    case "$i" in
        *h*|*d*) echo "$i"; return ;;
        *m*)
            local n
            n=$(echo "$i" | sed 's/[^0-9].*//')
            if [ -n "$n" ] && [ "$n" -lt 10 ]; then
                warn "interval ${i} below Gitea floor ${MIRROR_MIN_INTERVAL}; coerced"
                echo "$MIRROR_MIN_INTERVAL"
            else
                # ensure "NmXs" canonical
                case "$i" in *s) echo "$i" ;; *) echo "${i}0s" ;; esac
            fi
            ;;
        *) echo "$i" ;;
    esac
}

# Parse "owner/name" or "name" (default owner = ADMIN_USER)
_parse_repo_ref() {
    local ref="$1"
    case "$ref" in
        */*) echo "${ref%/*}" "${ref#*/}" ;;
        *)   echo "$ADMIN_USER" "$ref" ;;
    esac
}

cmd_repo() {
    local action="${1:-}"; shift || true
    case "$action" in
        create)      cmd_repo_create "$@" ;;
        delete|del)  cmd_repo_delete "$@" ;;
        mirror)      cmd_repo_mirror "$@" ;;
        list|ls)     cmd_repo_list "$@" ;;
        *)
            cat <<EOF
Repo commands:
  repo create OWNER/NAME [--private] [--default-branch BR]
  repo delete OWNER/NAME [--force]
  repo list   [OWNER]
  repo mirror add OWNER/NAME GITHUB_URL [--interval 10m] [--force]
                                          (--force allows recreating an
                                           existing non-mirror repo)
  repo mirror remove OWNER/NAME            (turns off mirror, repo stays)
  repo mirror sync   OWNER/NAME            (trigger an immediate pull)
  repo mirror list                         (all mirror repos, JSON)
EOF
            ;;
    esac
}

cmd_repo_create() {
    local ref="$1"; shift || true
    [ -z "$ref" ] && { error "repo create: OWNER/NAME required"; return 1; }
    local owner name; read -r owner name <<<"$(_parse_repo_ref "$ref")"
    local private=false default_branch="master"
    while [ $# -gt 0 ]; do
        case "$1" in
            --private) private=true ;;
            --default-branch) default_branch="$2"; shift ;;
            *) error "unknown flag: $1"; return 1 ;;
        esac
        shift
    done
    local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1
    local body
    body=$(printf '{"name":"%s","description":"Created by giteactl","private":%s,"default_branch":"%s"}' \
        "$name" "$private" "$default_branch")
    local out
    out=$(gitea_api "$tok" POST "/admin/users/${owner}/repos" "$body")
    gitea_token_revoke "$tname"
    local code
    code=$(echo "$out" | grep HTTP_CODE | cut -d: -f2)
    case "$code" in
        201) log "created ${owner}/${name}"; return 0 ;;
        409) warn "${owner}/${name} already exists"; return 0 ;;
        *)   error "create failed (HTTP $code): $(echo "$out" | head -1)"; return 1 ;;
    esac
}

cmd_repo_delete() {
    local ref="$1"; shift || true
    [ -z "$ref" ] && { error "repo delete: OWNER/NAME required"; return 1; }
    local force=false
    [ "${1:-}" = "--force" ] && force=true
    local owner name; read -r owner name <<<"$(_parse_repo_ref "$ref")"
    if ! $force; then
        warn "repo delete is destructive — pass --force to proceed (will delete ${owner}/${name})"
        return 1
    fi
    local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1
    local out
    out=$(gitea_api "$tok" DELETE "/repos/${owner}/${name}" "")
    gitea_token_revoke "$tname"
    local code
    code=$(echo "$out" | grep HTTP_CODE | cut -d: -f2)
    case "$code" in
        204) log "deleted ${owner}/${name}"; return 0 ;;
        404) warn "${owner}/${name} did not exist"; return 0 ;;
        *)   error "delete failed (HTTP $code): $(echo "$out" | head -1)"; return 1 ;;
    esac
}

cmd_repo_list() {
    local owner="${1:-}"
    local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1
    local path
    if [ -n "$owner" ]; then path="/users/${owner}/repos?limit=100"
    else path="/repos/search?limit=100"; fi
    local out
    out=$(gitea_api "$tok" GET "$path" "")
    gitea_token_revoke "$tname"
    echo "$out" | sed '/^HTTP_CODE/d'
}

cmd_repo_mirror() {
    local action="${1:-}"; shift || true
    case "$action" in
        add)    cmd_repo_mirror_add "$@" ;;
        remove|rm) cmd_repo_mirror_remove "$@" ;;
        sync)   cmd_repo_mirror_sync "$@" ;;
        list|ls) cmd_repo_mirror_list "$@" ;;
        *)
            cat <<EOF
Mirror commands:
  repo mirror add OWNER/NAME GITHUB_URL [--interval 10m] [--force]
  repo mirror remove OWNER/NAME
  repo mirror sync OWNER/NAME
  repo mirror list
EOF
            ;;
    esac
}

cmd_repo_mirror_add() {
    local ref="$1" url="$2"; shift 2 || { error "repo mirror add: OWNER/NAME URL required"; return 1; }
    local interval="$MIRROR_MIN_INTERVAL" force=false
    while [ $# -gt 0 ]; do
        case "$1" in
            --interval) interval="$2"; shift ;;
            --force) force=true ;;
            *) error "unknown flag: $1"; return 1 ;;
        esac
        shift
    done
    interval=$(_normalize_interval "$interval")
    local owner name; read -r owner name <<<"$(_parse_repo_ref "$ref")"
    local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1

    # 1. Probe existing state
    local probe
    probe=$(gitea_api "$tok" GET "/repos/${owner}/${name}" "")
    local probe_code
    probe_code=$(echo "$probe" | grep HTTP_CODE | cut -d: -f2)
    local is_mirror=false
    if [ "$probe_code" = "200" ]; then
        echo "$probe" | grep -q '"mirror":true' && is_mirror=true
        if $is_mirror; then
            log "${owner}/${name} is already a mirror; updating interval to ${interval}"
            local body
            body=$(printf '{"mirror_interval":"%s"}' "$interval")
            gitea_api "$tok" PATCH "/repos/${owner}/${name}" "$body" >/dev/null
            cmd_repo_mirror_sync_inner "$tok" "$owner" "$name"
            gitea_token_revoke "$tname"; return 0
        else
            if ! $force; then
                error "${owner}/${name} exists and is NOT a mirror; pass --force to delete+recreate"
                gitea_token_revoke "$tname"; return 1
            fi
            log "deleting existing non-mirror ${owner}/${name} (force)"
            gitea_api "$tok" DELETE "/repos/${owner}/${name}" "" >/dev/null
            sleep 2
        fi
    fi

    # 2. Migrate as mirror
    log "creating pull mirror ${owner}/${name} <- ${url} (interval ${interval})"
    local body
    body=$(printf '{"clone_addr":"%s","repo_name":"%s","repo_owner":"%s","mirror":true,"mirror_interval":"%s","service":"github","private":false,"description":"Mirror of %s"}' \
        "$url" "$name" "$owner" "$interval" "$url")
    local out
    out=$(gitea_api "$tok" POST "/repos/migrate" "$body")
    local code
    code=$(echo "$out" | grep HTTP_CODE | cut -d: -f2)
    case "$code" in
        201) log "mirror created" ;;
        500)
            # Gitea returns 500 for "interval below minimum" — surface message
            local msg
            msg=$(echo "$out" | grep -o '"message":"[^"]*"' | head -1)
            error "migrate failed (500): $msg"
            gitea_token_revoke "$tname"; return 1
            ;;
        *)   error "migrate failed (HTTP $code): $(echo "$out" | head -3)"
             gitea_token_revoke "$tname"; return 1 ;;
    esac

    # 3. Wait for initial clone to settle (avoid race with DB rescan)
    sleep 5
    cmd_repo_mirror_sync_inner "$tok" "$owner" "$name"
    gitea_token_revoke "$tname"
    log "OK — ${owner}/${name} mirrors ${url} every ${interval}"
    return 0
}

cmd_repo_mirror_remove() {
    local ref="$1"
    [ -z "$ref" ] && { error "repo mirror remove: OWNER/NAME required"; return 1; }
    local owner name; read -r owner name <<<"$(_parse_repo_ref "$ref")"
    local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1
    # Setting mirror_interval="" disables auto-sync; repo stays as non-mirror.
    # Per Gitea docs there is no true "demote-to-non-mirror" — the flag stays.
    # Cleanest: just set interval to 0 which disables sync.
    local out
    out=$(gitea_api "$tok" PATCH "/repos/${owner}/${name}" '{"mirror_interval":"0s"}')
    gitea_token_revoke "$tname"
    local code
    code=$(echo "$out" | grep HTTP_CODE | cut -d: -f2)
    case "$code" in
        200) log "${owner}/${name} mirror sync disabled (interval=0); repo content preserved" ;;
        *)   error "remove failed (HTTP $code): $(echo "$out" | head -1)"; return 1 ;;
    esac
}

cmd_repo_mirror_sync_inner() {
    local tok="$1" owner="$2" name="$3"
    log "triggering mirror-sync on ${owner}/${name}"
    local out
    out=$(gitea_api "$tok" POST "/repos/${owner}/${name}/mirror-sync" "")
    local code
    code=$(echo "$out" | grep HTTP_CODE | cut -d: -f2)
    case "$code" in
        200) log "sync triggered" ;;
        400) warn "sync rejected (HTTP 400) — repo may still be in initial clone state" ;;
        *)   warn "sync returned HTTP $code" ;;
    esac
}

cmd_repo_mirror_sync() {
    local ref="$1"
    [ -z "$ref" ] && { error "repo mirror sync: OWNER/NAME required"; return 1; }
    local owner name; read -r owner name <<<"$(_parse_repo_ref "$ref")"
    local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1
    cmd_repo_mirror_sync_inner "$tok" "$owner" "$name"
    gitea_token_revoke "$tname"
}

cmd_repo_mirror_list() {
    local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1
    local out
    out=$(gitea_api "$tok" GET "/repos/search?mirror=true&limit=100" "")
    gitea_token_revoke "$tname"
    echo "$out" | sed '/^HTTP_CODE/d' | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)
    for r in d.get('data', []):
        print(f\"  {r['full_name']:40s}  <-  {r.get('original_url','?')}   (every {r.get('mirror_interval','?')})\")
    if not d.get('data'):
        print('(no mirror repos)')
except Exception as e:
    print(f'parse error: {e}', file=sys.stderr)
" 2>&1
}

# ============================================================================
# Runner — the CI execution verb (issue #190)
#
# Parallel to `repo mirror` (#176) and `user` (existing). Bootstraps a
# dedicated LXC for each gitea-runner instance — operator decree: LXC only,
# no docker on host, no insecure host-mode.
#
# Subcommands:
#   runner token gen         Generate a one-shot registration token
#   runner add NAME --labels L1,L2,... [--arch arm64|amd64] [--memory 1G]
#   runner remove NAME [--keep-data]
#   runner list
#   runner logs NAME [--lines N]
#   runner restart NAME
# ============================================================================

RUNNER_VERSION="${RUNNER_VERSION:-1.0.3}"
RUNNER_DL_BASE="https://gitea.com/gitea/runner/releases/download/v${RUNNER_VERSION}"
RUNNER_LXC_PREFIX="act-runner-"
RUNNER_GITEA_URL="${RUNNER_GITEA_URL:-http://${LXC_IP}:${HTTP_PORT}}"
RUNNER_BRIDGE="${RUNNER_BRIDGE:-br-lxc}"
RUNNER_SUBNET="${RUNNER_SUBNET:-10.100.0.0/24}"
RUNNER_GATEWAY="${RUNNER_GATEWAY:-10.100.0.1}"
RUNNER_IP_POOL_START="${RUNNER_IP_POOL_START:-50}"   # 10.100.0.50..200
RUNNER_IP_POOL_END="${RUNNER_IP_POOL_END:-200}"
RUNNER_DNS="${RUNNER_DNS:-1.1.1.1 8.8.8.8}"

# Find the next free IP in the runner pool (10.100.0.50..200), avoiding
# IPs already declared in existing LXC configs.
_runner_next_ip() {
    local octet ip used
    used=$(grep -h "lxc.net.0.ipv4.address" "$LXC_PATH"/*/config 2>/dev/null \
           | sed 's|.*= *\([0-9.]*\)/.*|\1|' \
           | grep "^10\.100\.0\." | sed 's|.*\.||' | sort -un)
    for octet in $(seq "$RUNNER_IP_POOL_START" "$RUNNER_IP_POOL_END"); do
        echo "$used" | grep -qx "$octet" || { echo "10.100.0.$octet"; return 0; }
    done
    return 1
}

_runner_lxc_name() { echo "${RUNNER_LXC_PREFIX}${1}"; }
_runner_lxc_exists() { [ -d "$LXC_PATH/$(_runner_lxc_name "$1")/rootfs" ]; }
_runner_lxc_running() {
    lxc-info -n "$(_runner_lxc_name "$1")" -P "$LXC_PATH" 2>/dev/null | grep -q "State:.*RUNNING"
}

# Generate a runner registration token via gitea CLI inside the Gitea LXC.
# Output is the raw token on the last line.
cmd_runner_token_gen() {
    require_root
    if ! lxc_running; then error "gitea LXC not running"; return 1; fi
    local out
    out=$(lxc-attach -n "$CONTAINER" -P "$LXC_PATH" -- \
        su -s /bin/bash gitea -c \
        "gitea --config $GITEA_APP_INI actions generate-runner-token 2>&1" \
        | tail -1)
    [ -z "$out" ] && { error "token gen failed"; return 1; }
    echo "$out"
}

cmd_runner_add() {
    local name="$1"; shift || { error "runner add NAME required"; return 1; }
    local labels="" arch="" memory="1G" ip=""
    while [ $# -gt 0 ]; do
        case "$1" in
            --labels) labels="$2"; shift ;;
            --arch)   arch="$2"; shift ;;
            --memory) memory="$2"; shift ;;
            --ip)     ip="$2"; shift ;;
            *) error "unknown flag: $1"; return 1 ;;
        esac; shift
    done
    [ -z "$labels" ] && { error "--labels L1,L2,... required"; return 1; }
    [ -z "$arch" ] && arch=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
    [ -z "$ip" ] && ip=$(_runner_next_ip)
    [ -z "$ip" ] && { error "no free IP in pool 10.100.0.${RUNNER_IP_POOL_START}..${RUNNER_IP_POOL_END}"; return 1; }

    require_root
    local lxc_name; lxc_name=$(_runner_lxc_name "$name")
    if _runner_lxc_exists "$name"; then
        error "LXC $lxc_name already exists (use 'runner remove $name' first)"
        return 1
    fi

    log "creating LXC $lxc_name (arch $arch, memory $memory, ip $ip)"
    lxc-create -n "$lxc_name" -P "$LXC_PATH" -t download -- \
        -d debian -r bookworm -a "$arch"
    # Static IP on br-lxc + memory limit (no DHCP on this bridge)
    cat >> "$LXC_PATH/$lxc_name/config" <<EOF

# act-runner LXC — set via giteactl runner add (#190)
lxc.net.0.ipv4.address = ${ip}/24
lxc.net.0.ipv4.gateway = ${RUNNER_GATEWAY}
lxc.cgroup2.memory.max = $((${memory%[GgMm]} * (1024**$( [ "${memory: -1}" = "G" ] && echo 3 || echo 2 ))))
EOF

    log "starting $lxc_name for bootstrap"
    lxc-start -n "$lxc_name" -P "$LXC_PATH"
    sleep 4  # let networking settle

    # Fix DNS — Debian template's resolv.conf symlinks systemd-resolved
    # stub which is empty until configured; write static nameservers.
    lxc-attach -n "$lxc_name" -P "$LXC_PATH" -- bash -c "
for ns in $RUNNER_DNS; do echo \"nameserver \$ns\"; done > /etc/resolv.conf
"

    log "installing gitea-runner v$RUNNER_VERSION inside"
    local dl_url="${RUNNER_DL_BASE}/gitea-runner-${RUNNER_VERSION}-linux-${arch}"
    lxc-attach -n "$lxc_name" -P "$LXC_PATH" -- bash -c "
set -e
apt-get update -qq
apt-get install -y -qq curl ca-certificates git
curl -sL -o /usr/local/bin/act_runner '$dl_url'
chmod 755 /usr/local/bin/act_runner
useradd -r -s /usr/sbin/nologin -m -d /var/lib/act_runner act_runner 2>/dev/null || true
mkdir -p /etc/act_runner /var/lib/act_runner
chown -R act_runner:act_runner /var/lib/act_runner
"

    log "generating runner registration token"
    local token
    token=$(cmd_runner_token_gen | tail -1)
    [ -z "$token" ] && { error "could not get registration token"; return 1; }

    log "registering runner $name (labels: $labels) -> $RUNNER_GITEA_URL"
    lxc-attach -n "$lxc_name" -P "$LXC_PATH" -- bash -c "
cd /var/lib/act_runner
su -s /bin/bash act_runner -c \"
cd /var/lib/act_runner
act_runner register \\
  --no-interactive \\
  --instance '$RUNNER_GITEA_URL' \\
  --token '$token' \\
  --name '$name' \\
  --labels '$labels'
\"
"

    log "installing systemd unit + starting"
    lxc-attach -n "$lxc_name" -P "$LXC_PATH" -- bash -c "
cat > /etc/systemd/system/act_runner.service <<UNIT
[Unit]
Description=Gitea act_runner
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=act_runner
Group=act_runner
WorkingDirectory=/var/lib/act_runner
ExecStart=/usr/local/bin/act_runner daemon
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable --now act_runner.service
sleep 2
systemctl is-active act_runner.service
"
    log "runner $name installed in $lxc_name"
}

cmd_runner_remove() {
    local name="$1"; shift || { error "runner remove NAME required"; return 1; }
    local keep_data=false
    [ "${1:-}" = "--keep-data" ] && keep_data=true
    require_root
    local lxc_name; lxc_name=$(_runner_lxc_name "$name")
    if ! _runner_lxc_exists "$name"; then
        warn "LXC $lxc_name does not exist; nothing to remove"
        return 0
    fi
    if _runner_lxc_running "$name"; then
        log "stopping $lxc_name"
        lxc-stop -n "$lxc_name" -P "$LXC_PATH"
    fi
    if $keep_data; then
        log "keeping LXC rootfs (--keep-data); just stopped"
    else
        log "destroying $lxc_name"
        lxc-destroy -n "$lxc_name" -P "$LXC_PATH"
    fi
    log "(note: runner remains registered in Gitea — visit Settings -> Actions -> Runners to delete the dead entry, or wait for it to expire)"
}

cmd_runner_list() {
    require_root
    echo "Local LXC runners:"
    local any=0
    for d in "$LXC_PATH"/${RUNNER_LXC_PREFIX}*; do
        [ -d "$d/rootfs" ] || continue
        any=1
        local n; n=$(basename "$d" | sed "s|^${RUNNER_LXC_PREFIX}||")
        local state="STOPPED"
        _runner_lxc_running "$n" && state="RUNNING"
        printf "  %-25s  %s\n" "$n" "$state"
    done
    [ $any = 0 ] && echo "  (none)"
    echo
    echo "Registered runners (gitea API):"
    local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1
    local out
    out=$(gitea_api "$tok" GET "/admin/runners" "")
    gitea_token_revoke "$tname"
    api_body() { echo "$1" | sed '/^HTTP_CODE:/d'; }
    echo "$out" | sed '/^HTTP_CODE:/d' | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)
    for r in d.get('runners', d if isinstance(d, list) else []):
        print(f\"  {r.get('name','?'):25s} status={r.get('status','?')} labels={','.join(r.get('labels',[]))}\")
    if not (d.get('runners') if isinstance(d, dict) else d):
        print('  (none registered)')
except Exception as e:
    print(f'  parse error: {e}')
" 2>&1
}

cmd_runner_logs() {
    local name="$1"; [ -z "$name" ] && { error "runner logs NAME required"; return 1; }
    local lines="${2:-100}"
    [ "$1" = "--lines" ] && { lines="$2"; name="$3"; }
    require_root
    _runner_lxc_running "$name" || { error "LXC $(_runner_lxc_name "$name") not running"; return 1; }
    lxc-attach -n "$(_runner_lxc_name "$name")" -P "$LXC_PATH" -- \
        journalctl -u act_runner -n "$lines" --no-pager
}

cmd_runner_restart() {
    local name="$1"; [ -z "$name" ] && { error "runner restart NAME required"; return 1; }
    require_root
    _runner_lxc_running "$name" || { error "LXC not running, start it first"; return 1; }
    lxc-attach -n "$(_runner_lxc_name "$name")" -P "$LXC_PATH" -- \
        systemctl restart act_runner
    log "act_runner restarted in $(_runner_lxc_name "$name")"
}

cmd_runner() {
    local act="${1:-}"; shift || true
    case "$act" in
        token)
            local sub="${1:-}"
            case "$sub" in
                gen|generate) cmd_runner_token_gen ;;
                *) echo "Usage: giteactl runner token gen" ;;
            esac
            ;;
        add)         cmd_runner_add "$@" ;;
        remove|rm|delete) cmd_runner_remove "$@" ;;
        list|ls)     cmd_runner_list ;;
        logs)        cmd_runner_logs "$@" ;;
        restart)     cmd_runner_restart "$@" ;;
        *)
            cat <<EOF
Runner commands (issue #190 — CI execution layer):
  runner token gen                  Generate one-shot registration token
  runner add NAME --labels L1,L2 [--arch arm64|amd64] [--memory 1G]
                                    Bootstrap LXC, install gitea-runner, register, start
  runner remove NAME [--keep-data]  Stop + destroy LXC (Gitea entry stays)
  runner list                       Local LXCs + registered runners (JSON)
  runner logs NAME [--lines N]      Tail in-LXC act_runner journal
  runner restart NAME               systemctl restart act_runner in the LXC
EOF
            ;;
    esac
}

# ============================================================================
# Backup / Restore
# ============================================================================

cmd_backup() {
    local name="${1:-$(date +%Y%m%d_%H%M%S)}"
    local backup_file="$DATA_PATH/backups/gitea_$name.tar.gz"

    log "Creating backup: $backup_file"

    mkdir -p "$DATA_PATH/backups"

    # Stop Gitea for consistent backup
    if lxc_running; then
        log "Stopping Gitea for backup..."
        lxc-stop -n "$CONTAINER" -P "$LXC_PATH"
        sleep 2
    fi

    tar -czf "$backup_file" \
        -C "$DATA_PATH" \
        git custom log \
        2>/dev/null

    # Restart
    lxc-start -n "$CONTAINER" -P "$LXC_PATH" -d

    log "Backup created: $backup_file"
    echo "$backup_file"
}

cmd_restore() {
    local backup_file="$1"

    if [ -z "$backup_file" ] || [ ! -f "$backup_file" ]; then
        echo "Usage: giteactl restore <backup_file>"
        echo ""
        echo "Available backups:"
        ls -la "$DATA_PATH/backups/"*.tar.gz 2>/dev/null || echo "  No backups found"
        return 1
    fi

    warn "This will overwrite current Gitea data!"
    read -p "Continue? (yes/no): " confirm
    [ "$confirm" != "yes" ] && { echo "Aborted"; return 1; }

    log "Stopping Gitea..."
    lxc-stop -n "$CONTAINER" -P "$LXC_PATH" 2>/dev/null

    log "Restoring from: $backup_file"
    tar -xzf "$backup_file" -C "$DATA_PATH"

    log "Starting Gitea..."
    lxc-start -n "$CONTAINER" -P "$LXC_PATH" -d

    log "Restore complete!"
}

# ============================================================================
# Migration from OpenWrt
# ============================================================================

cmd_migrate() {
    local source="${1:-192.168.255.1}"

    log "Migrating Gitea data from $source..."

    # Check SSH connectivity
    if ! ssh -o ConnectTimeout=5 "root@$source" "echo ok" >/dev/null 2>&1; then
        error "Cannot connect to $source via SSH"
        echo "Ensure SSH key is configured: ssh-copy-id root@$source"
        return 1
    fi

    # Create backup directory
    local backup_dir="$DATA_PATH/migration_$(date +%Y%m%d_%H%M%S)"
    mkdir -p "$backup_dir"

    # Stop local Gitea
    if lxc_running; then
        log "Stopping local Gitea..."
        lxc-stop -n "$CONTAINER" -P "$LXC_PATH"
        sleep 2
    fi

    # Migrate git repositories
    log "Migrating git repositories..."
    rsync -avz --progress "root@$source:/data/gitea/git/repositories/" "$DATA_PATH/git/repositories/"

    # Migrate database
    log "Migrating database..."
    rsync -avz --progress "root@$source:/data/gitea/git/gitea.db" "$DATA_PATH/git/"

    # Migrate LFS data
    log "Migrating LFS data..."
    rsync -avz --progress "root@$source:/data/gitea/git/lfs/" "$DATA_PATH/git/lfs/" 2>/dev/null || true

    # Migrate custom config
    log "Migrating configuration..."
    rsync -avz --progress "root@$source:/data/gitea/custom/" "$DATA_PATH/custom/"

    # Import UCI config
    log "Importing UCI configuration..."
    ssh "root@$source" "uci show gitea" > "$backup_dir/gitea.uci" 2>/dev/null || true

    # Convert UCI to TOML
    if [ -f "$backup_dir/gitea.uci" ]; then
        local uci_domain=$(grep "main.domain=" "$backup_dir/gitea.uci" | cut -d"'" -f2)
        local uci_port=$(grep "main.http_port=" "$backup_dir/gitea.uci" | cut -d"'" -f2)
        local uci_ssh=$(grep "main.ssh_port=" "$backup_dir/gitea.uci" | cut -d"'" -f2)

        if [ -n "$uci_domain" ]; then
            log "Setting domain: $uci_domain"
            sed -i "s/^domain = .*/domain = \"$uci_domain\"/" "$CONFIG_FILE" 2>/dev/null || \
                echo "domain = \"$uci_domain\"" >> "$CONFIG_FILE"
        fi

        if [ -n "$uci_port" ]; then
            log "Setting HTTP port: $uci_port"
            sed -i "s/^http_port = .*/http_port = $uci_port/" "$CONFIG_FILE" 2>/dev/null || \
                echo "http_port = $uci_port" >> "$CONFIG_FILE"
        fi

        if [ -n "$uci_ssh" ]; then
            log "Setting SSH port: $uci_ssh"
            sed -i "s/^ssh_port = .*/ssh_port = $uci_ssh/" "$CONFIG_FILE" 2>/dev/null || \
                echo "ssh_port = $uci_ssh" >> "$CONFIG_FILE"
        fi
    fi

    # Fix permissions
    chown -R 1000:1000 "$DATA_PATH"

    # Start Gitea
    log "Starting Gitea..."
    lxc-start -n "$CONTAINER" -P "$LXC_PATH" -d

    log "Migration complete!"
    log "Data backed up to: $backup_dir"
}

# ============================================================================
# Commands
# ============================================================================

cmd_install() {
    require_root
    log "Installing Gitea LXC..."

    mkdir -p "$DATA_PATH"/{git,custom,log,backups}

    if ! lxc_exists; then
        bootstrap_alpine
    fi

    create_lxc_config
    create_install_init  # Use temporary init for installation

    # Start for package installation
    log "Starting container for installation..."
    lxc-start -n "$CONTAINER" -P "$LXC_PATH" -d
    sleep 3

    if lxc_running; then
        install_gitea

        # Stop and switch to gitea startup script
        log "Switching to Gitea startup..."
        lxc-stop -n "$CONTAINER" -P "$LXC_PATH"
        sleep 2
        create_startup_script  # Switch to actual gitea init

        log "Gitea installed! Start with: giteactl start"
    else
        error "Failed to start container for installation"
        return 1
    fi

    log "Installation complete!"
}

cmd_uninstall() {
    require_root
    warn "This will remove Gitea container. Data in $DATA_PATH will be preserved."
    read -p "Continue? (yes/no): " confirm
    [ "$confirm" != "yes" ] && { echo "Aborted"; return 1; }

    log "Stopping container..."
    lxc-stop -n "$CONTAINER" -P "$LXC_PATH" 2>/dev/null

    log "Removing container..."
    rm -rf "$LXC_PATH/$CONTAINER"

    log "Gitea removed. Data preserved in $DATA_PATH"
}

cmd_start() {
    require_root

    if lxc_running; then
        log "Gitea already running"
        return 0
    fi

    if ! lxc_exists; then
        error "Gitea not installed. Run 'giteactl install' first"
        return 1
    fi

    create_lxc_config

    log "Starting Gitea..."
    lxc-start -n "$CONTAINER" -P "$LXC_PATH" -d
    sleep 3

    if lxc_running; then
        log "Gitea started at http://${LXC_IP}:${HTTP_PORT}"
    else
        error "Failed to start"
        return 1
    fi
}

cmd_stop() {
    require_root

    if ! lxc_running; then
        log "Gitea is not running"
        return 0
    fi

    log "Stopping Gitea..."
    lxc-stop -n "$CONTAINER" -P "$LXC_PATH"
    log "Stopped"
}

cmd_restart() {
    cmd_stop
    sleep 2
    cmd_start
}

cmd_status() {
    echo ""
    echo "Gitea Git Server v$VERSION"
    echo "=========================="
    echo ""
    echo "Container: $CONTAINER"

    if lxc_running; then
        echo -e "Status: ${GREEN}Running${NC}"
        echo "HTTP: http://${LXC_IP}:${HTTP_PORT}"
        echo "SSH: git@${LXC_IP}:${SSH_PORT}"
    elif lxc_exists; then
        echo -e "Status: ${YELLOW}Stopped${NC}"
    else
        echo -e "Status: ${RED}Not installed${NC}"
    fi

    if [ -d "$DATA_PATH/git/repositories" ]; then
        local repo_count=$(find "$DATA_PATH/git/repositories" -name "*.git" -type d 2>/dev/null | wc -l)
        echo "Repositories: $repo_count"
    fi

    if [ -d "$DATA_PATH" ]; then
        local storage=$(du -sh "$DATA_PATH" 2>/dev/null | cut -f1)
        echo "Storage: $storage"
    fi
    echo ""
}

# ============================================================================
# THREE-FOLD ARCHITECTURE: Components (What)
# ============================================================================

cmd_components() {
    local installed=false
    local running=false
    lxc_exists && installed=true
    lxc_running && running=true

    local repo_count=0
    [ -d "$DATA_PATH/git/repositories" ] && repo_count=$(find "$DATA_PATH/git/repositories" -name "*.git" -type d 2>/dev/null | wc -l)

    cat <<EOF
{
  "components": [
    {
      "name": "Gitea Container",
      "type": "lxc",
      "container": "$CONTAINER",
      "description": "Gitea Git server",
      "installed": $installed,
      "running": $running,
      "version": "$GITEA_VERSION"
    },
    {
      "name": "Git Repositories",
      "type": "data",
      "path": "$DATA_PATH/git/repositories",
      "count": $repo_count
    },
    {
      "name": "SSH Server",
      "type": "service",
      "port": $SSH_PORT,
      "description": "Git SSH access"
    },
    {
      "name": "Web Interface",
      "type": "service",
      "port": $HTTP_PORT,
      "description": "Gitea web UI and API"
    }
  ]
}
EOF
}

# ============================================================================
# THREE-FOLD ARCHITECTURE: Access (How to connect)
# ============================================================================

cmd_access() {
    cat <<EOF
{
  "domain": "$DOMAIN",
  "http": {
    "port": $HTTP_PORT,
    "url": "http://${LXC_IP}:${HTTP_PORT}",
    "public_url": "https://$DOMAIN"
  },
  "ssh": {
    "port": $SSH_PORT,
    "host": "$LXC_IP",
    "clone_url": "git@${LXC_IP}:${SSH_PORT}/<user>/<repo>.git"
  },
  "api": {
    "base_url": "http://${LXC_IP}:${HTTP_PORT}/api/v1"
  },
  "admin_panel": "/gitea/"
}
EOF
}

cmd_shell() {
    if ! lxc_running; then
        error "Container not running"
        return 1
    fi
    lxc-attach -n "$CONTAINER" -P "$LXC_PATH" -- /bin/sh
}

cmd_logs() {
    local lines="${1:-50}"
    if [ -f "$DATA_PATH/log/gitea.log" ]; then
        tail -n "$lines" "$DATA_PATH/log/gitea.log"
    else
        echo "No logs found"
    fi
}

cmd_user() {
    local action="${1:-list}"
    shift

    case "$action" in
        add) user_add "$@" ;;
        del|delete) user_del "$@" ;;
        passwd|password) user_passwd "$@" ;;
        list) user_list ;;
        *)
            echo "User commands:"
            echo "  user add <username> <password> [email] [admin]"
            echo "  user del <username>"
            echo "  user passwd <username> <password>"
            echo "  user list"
            ;;
    esac
}

# ============================================================================
# Help
# ============================================================================

show_help() {
    cat << EOF
SecuBox Gitea Controller v$VERSION
Three-fold architecture: Components, Status, Access

Usage: giteactl <command> [options]

Information (Three-fold):
  components           List system components (JSON)
  status               Show status
  access               Show connection info (JSON)

Setup:
  install              Install Gitea LXC
  uninstall            Remove Gitea container

Service:
  start                Start Gitea
  stop                 Stop Gitea
  restart              Restart Gitea

Users:
  user add <name> <pw> Add user
  user del <name>      Delete user
  user passwd <n> <p>  Change password
  user list            List users

Repos & Mirrors (issue #176, parallel to mitmproxyctl route):
  repo create OWNER/NAME [--private] [--default-branch BR]
  repo delete OWNER/NAME --force
  repo list   [OWNER]
  repo mirror add OWNER/NAME GITHUB_URL [--interval 10m] [--force]
  repo mirror remove OWNER/NAME
  repo mirror sync   OWNER/NAME
  repo mirror list

Runners (issue #190 — LXC-only act_runner, CI execution layer):
  runner token gen
  runner add NAME --labels L1,L2 [--arch arm64|amd64] [--memory 1G]
  runner remove NAME [--keep-data]
  runner list
  runner logs NAME [--lines N]
  runner restart NAME

Backup:
  backup [name]        Create backup
  restore <file>       Restore from backup
  migrate [host]       Migrate from OpenWrt

Diagnostics:
  logs [lines]         View logs
  shell                Open shell in container

Examples:
  giteactl components               # JSON components
  giteactl access                   # JSON access info
  giteactl user add admin secret admin@local true
  giteactl backup
  giteactl migrate 192.168.255.1

EOF
}

# ============================================================================
# Main
# ============================================================================

case "${1:-}" in
    # Three-fold architecture
    components)  cmd_components ;;
    access)      cmd_access ;;
    status)      shift; cmd_status "$@" ;;
    # Service
    install)     shift; cmd_install "$@" ;;
    uninstall)   shift; cmd_uninstall "$@" ;;
    start)       shift; cmd_start "$@" ;;
    stop)        shift; cmd_stop "$@" ;;
    restart)     shift; cmd_restart "$@" ;;
    user)        shift; cmd_user "$@" ;;
    repo)        shift; cmd_repo "$@" ;;
    runner)      shift; cmd_runner "$@" ;;
    backup)      shift; cmd_backup "$@" ;;
    restore)     shift; cmd_restore "$@" ;;
    migrate)     shift; cmd_migrate "$@" ;;
    logs)        shift; cmd_logs "$@" ;;
    shell)       shift; cmd_shell "$@" ;;
    help|--help|-h|'') show_help ;;
    *)           error "Unknown: $1"; exit 1 ;;
esac

exit 0
