#!/bin/bash
# SecuBox MetaBlogizer Controller
# Static site publisher for Debian
# Three-fold architecture: Components, Status, Access

VERSION="1.2.0"
CONFIG_FILE="/etc/secubox/metablogizer.toml"
SITES_ROOT="/srv/metablogizer/sites"
DATA_PATH="/srv/metablogizer"
NGINX_VHOST_DIR="/etc/nginx/sites-available"
NGINX_ENABLED_DIR="/etc/nginx/sites-enabled"

# Public-vhost wiring. Set to "1" to inject HAProxy + mitmproxy route on publish.
# Triggered automatically when site_publish is called with --public-domain.
# The sync script + service may live at non-standard paths on different hosts;
# we try systemctl first, fall back to `command -v`.
HAPROXYCTL="${HAPROXYCTL:-haproxyctl}"
SYNC_SCRIPT="${SYNC_SCRIPT:-sync-mitmproxy-routes.sh}"
SYNC_SERVICE="${SYNC_SERVICE:-sync-mitmproxy-routes.service}"

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

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

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"
}

RUNTIME=$(config_get "runtime" "nginx")
DEFAULT_DOMAIN=$(config_get "default_domain" "secubox.local")

init_dirs() {
    mkdir -p "$SITES_ROOT" "$DATA_PATH/templates" "$DATA_PATH/backups"
}

# ============================================================================
# Site Management
# ============================================================================

site_create() {
    local name="$1"
    local domain="${2:-${name}.${DEFAULT_DOMAIN}}"
    local template="${3:-default}"

    if [ -z "$name" ]; then
        echo "Usage: metablogizerctl site create <name> [domain] [template]"
        return 1
    fi

    init_dirs

    local site_dir="$SITES_ROOT/$name"
    if [ -d "$site_dir" ]; then
        error "Site already exists: $name"
        return 1
    fi

    log "Creating site: $name"
    mkdir -p "$site_dir/public"

    # Create default index.html
    cat > "$site_dir/public/index.html" << EOF
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>$name - SecuBox MetaBlogizer</title>
    <style>
        body { font-family: -apple-system, sans-serif; background: #0d1117; color: #c9d1d9; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
        .container { text-align: center; }
        h1 { color: #58a6ff; }
        p { color: #8b949e; }
    </style>
</head>
<body>
    <div class="container">
        <h1>$name</h1>
        <p>Your static site is ready!</p>
        <p>Edit files in: $site_dir/public/</p>
    </div>
</body>
</html>
EOF

    # Create site config
    cat > "$site_dir/site.json" << EOF
{
    "name": "$name",
    "domain": "$domain",
    "created": "$(date -Iseconds)",
    "template": "$template",
    "enabled": true
}
EOF

    # Set permissions
    chmod -R 755 "$site_dir"

    log "Site created: $name at $site_dir"
    log "Domain: $domain"
}

site_delete() {
    local name="$1"

    if [ -z "$name" ]; then
        echo "Usage: metablogizerctl site delete <name>"
        return 1
    fi

    local site_dir="$SITES_ROOT/$name"
    if [ ! -d "$site_dir" ]; then
        error "Site not found: $name"
        return 1
    fi

    # Remove nginx vhost
    rm -f "$NGINX_VHOST_DIR/${name}.conf"
    rm -f "$NGINX_ENABLED_DIR/${name}.conf"

    # Remove site directory
    rm -rf "$site_dir"

    log "Site deleted: $name"
}

site_publish() {
    local name="" public_domain=""

    # Parse args: positional <name> then optional --public-domain <fqdn>.
    while [ $# -gt 0 ]; do
        case "$1" in
            --public-domain)
                public_domain="$2"
                shift 2
                ;;
            --public-domain=*)
                public_domain="${1#--public-domain=}"
                shift
                ;;
            -*)
                error "Unknown option: $1"
                return 1
                ;;
            *)
                if [ -z "$name" ]; then
                    name="$1"
                else
                    error "Unexpected argument: $1"
                    return 1
                fi
                shift
                ;;
        esac
    done

    if [ -z "$name" ]; then
        echo "Usage: metablogizerctl site publish <name> [--public-domain <fqdn>]"
        return 1
    fi

    # Validate --public-domain syntactically (defence-in-depth: blocks shell/TOML injection).
    if [ -n "$public_domain" ]; then
        if ! printf '%s' "$public_domain" | grep -qE '^[a-zA-Z0-9.-]+$'; then
            error "Invalid --public-domain '$public_domain'"
            return 1
        fi
    fi

    local site_dir="$SITES_ROOT/$name"
    if [ ! -d "$site_dir" ]; then
        error "Site not found: $name"
        return 1
    fi

    # Read existing internal domain from site.json (or fall back to <name>.<default>).
    local domain
    if [ -f "$site_dir/site.json" ]; then
        domain=$(grep '"domain"' "$site_dir/site.json" | cut -d'"' -f4)
    fi
    domain="${domain:-${name}.${DEFAULT_DOMAIN}}"

    # The nginx vhost responds to BOTH the internal .secubox.local name and the
    # public FQDN (so the same nginx instance answers Host: smoke.gk2.secubox.in
    # coming back through HAProxy → mitmproxy).
    local server_names="$domain"
    if [ -n "$public_domain" ] && [ "$public_domain" != "$domain" ]; then
        server_names="$domain $public_domain"
    fi

    log "Publishing site: $name to $domain${public_domain:+ (public: $public_domain)}"

    # Generate nginx config
    cat > "$NGINX_VHOST_DIR/${name}.conf" << EOF
# MetaBlogizer site: $name
# Generated by SecuBox MetaBlogizer

server {
    listen 80;
    server_name $server_names;
    root $site_dir/public;
    index index.html index.htm;

    location / {
        try_files \$uri \$uri/ =404;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    access_log /var/log/nginx/${name}_access.log;
    error_log /var/log/nginx/${name}_error.log;
}
EOF

    # Enable site
    ln -sf "$NGINX_VHOST_DIR/${name}.conf" "$NGINX_ENABLED_DIR/${name}.conf"

    # Reload nginx
    nginx -t && systemctl reload nginx

    log "Site published: http://$domain"

    # Public-vhost wiring (HAProxy + mitmproxy route sync).
    # Only runs when --public-domain was passed AND the helper CLIs are present.
    # Failures are tolerated so the nginx publish still counts as success.
    if [ -n "$public_domain" ]; then
        site_persist_public_domain "$name" "$public_domain"
        site_wire_public_vhost "$public_domain"
    fi
}

# Persist public_domain in site.json so site_unpublish can reverse the wiring.
# Naïve TOML-style key/value: we keep the file as a flat JSON object with
# "domain" + "public_domain" keys. Uses python3 if available for safe rewrite;
# falls back to a grep/sed approach.
site_persist_public_domain() {
    local name="$1" public_domain="$2"
    local site_dir="$SITES_ROOT/$name"
    local site_json="$site_dir/site.json"

    if command -v python3 >/dev/null 2>&1; then
        python3 - "$site_json" "$public_domain" <<'PY'
import json, os, sys, tempfile
path, public_domain = sys.argv[1:]
data = {}
if os.path.exists(path):
    try:
        with open(path) as f:
            data = json.load(f)
    except (json.JSONDecodeError, OSError):
        data = {}
data["public_domain"] = public_domain
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path) or ".", prefix=".site.", suffix=".json")
os.write(fd, json.dumps(data, indent=2).encode() + b"\n")
os.close(fd)
os.replace(tmp, path)
PY
    else
        # Minimal fallback: append/replace the "public_domain" line.
        # Caller already validated the FQDN regex so quoting is safe.
        if [ -f "$site_json" ] && grep -q '"public_domain"' "$site_json"; then
            sed -i -E "s|\"public_domain\":[[:space:]]*\"[^\"]*\"|\"public_domain\": \"${public_domain}\"|" "$site_json"
        elif [ -f "$site_json" ]; then
            # Insert before the final }
            sed -i -E "s|^}|  ,\"public_domain\": \"${public_domain}\"\n}|" "$site_json"
        else
            printf '{\n  "public_domain": "%s"\n}\n' "$public_domain" > "$site_json"
        fi
    fi
}

# Read public_domain from site.json. Echoes the value or empty string.
site_read_public_domain() {
    local name="$1"
    local site_json="$SITES_ROOT/$name/site.json"
    [ -f "$site_json" ] || return 0
    if command -v python3 >/dev/null 2>&1; then
        python3 -c "
import json, sys
try:
    print(json.load(open('$site_json')).get('public_domain', ''))
except Exception:
    pass
"
    else
        grep '"public_domain"' "$site_json" 2>/dev/null | head -1 | cut -d'"' -f4 || true
    fi
}

# Wire the HAProxy vhost + trigger mitmproxy route sync for <public_domain>.
# Tolerant of missing tools (logs a warning and continues).
site_wire_public_vhost() {
    local public_domain="$1"
    if ! command -v "$HAPROXYCTL" >/dev/null 2>&1; then
        warn "$HAPROXYCTL not found — skipping HAProxy vhost add for $public_domain"
        return 0
    fi
    if "$HAPROXYCTL" vhost add "$public_domain" mitmproxy_inspector true 2>&1; then
        log "HAProxy vhost added: $public_domain → mitmproxy_inspector (ssl)"
    else
        warn "haproxyctl vhost add failed for $public_domain (continuing)"
    fi
    site_trigger_sync
}

# Trigger mitmproxy route sync. Prefer systemctl unit, fall back to direct script.
site_trigger_sync() {
    if command -v systemctl >/dev/null 2>&1 && \
       systemctl list-unit-files "$SYNC_SERVICE" >/dev/null 2>&1; then
        systemctl start "$SYNC_SERVICE" 2>/dev/null || warn "systemctl start $SYNC_SERVICE failed"
    elif command -v "$SYNC_SCRIPT" >/dev/null 2>&1; then
        "$SYNC_SCRIPT" 2>&1 || warn "$SYNC_SCRIPT exited non-zero"
    else
        warn "no sync mechanism found (neither $SYNC_SERVICE nor $SYNC_SCRIPT in PATH) — mitmproxy routes NOT updated"
    fi
}

# Reverse of site_wire_public_vhost: remove the HAProxy vhost + re-sync.
site_unwire_public_vhost() {
    local public_domain="$1"
    if ! command -v "$HAPROXYCTL" >/dev/null 2>&1; then
        warn "$HAPROXYCTL not found — skipping HAProxy vhost remove for $public_domain"
        return 0
    fi
    if "$HAPROXYCTL" vhost remove "$public_domain" 2>&1; then
        log "HAProxy vhost removed: $public_domain"
    else
        warn "haproxyctl vhost remove failed for $public_domain (continuing)"
    fi
    site_trigger_sync
}

site_unpublish() {
    local name="$1"

    if [ -z "$name" ]; then
        echo "Usage: metablogizerctl site unpublish <name>"
        return 1
    fi

    # Read public_domain BEFORE removing the site files (site_delete may follow).
    local public_domain
    public_domain="$(site_read_public_domain "$name")"

    rm -f "$NGINX_ENABLED_DIR/${name}.conf"
    systemctl reload nginx

    log "Site unpublished: $name"

    # Reverse the HAProxy + mitmproxy wiring if site was publicly published.
    if [ -n "$public_domain" ]; then
        site_unwire_public_vhost "$public_domain"
    fi
}

# ============================================================================
# Tor — Emancipate verb (Punk Exposure Engine, issue #184)
#
# Per CLAUDE.md the Punk Exposure Engine has three verbs (Peek/Poke/Emancipate)
# and Tor is one of the three exposure channels. `metablogizerctl tor expose`
# is the Emancipate verb at the publishing layer for static sites.
#
# Implementation: write a per-site Tor HiddenService stanza, reload tor,
# read the generated .onion hostname back. If secubox-exposure is installed,
# delegate to it for consistency with other channels; otherwise fall back
# to direct torrc manipulation.
# ============================================================================

TOR_DROPIN_DIR="${TOR_DROPIN_DIR:-/etc/tor/secubox-metablogizer.d}"
TOR_DATA_DIR="${TOR_DATA_DIR:-/var/lib/tor/secubox-metablogizer}"

_tor_drop_path() { echo "$TOR_DROPIN_DIR/${1}.conf"; }
_tor_data_path() { echo "$TOR_DATA_DIR/${1}"; }

_have_exposure() { command -v secubox-exposure >/dev/null 2>&1; }
_have_tor() { command -v tor >/dev/null 2>&1; }

tor_expose() {
    local name="$1"
    [ -z "$name" ] && { error "Usage: metablogizerctl tor expose <site>"; return 1; }

    local site_dir="$SITES_ROOT/$name"
    [ -d "$site_dir" ] || { error "site not found: $name (run: metablogizerctl site create $name first)"; return 1; }

    # Prefer secubox-exposure when available so all 3 exposure channels share
    # the same orchestration (Tor / DNS+SSL / Mesh) and Peek shows it.
    if _have_exposure; then
        log "delegating to secubox-exposure emancipate (tor channel)"
        # The static site is served via nginx on port 80 (per site_publish);
        # secubox-exposure handles the HiddenService wiring and revocation.
        secubox-exposure emancipate "metablogizer-${name}" 80 --tor
        return $?
    fi

    # Fallback: write a tor drop-in directly.
    _have_tor || { error "tor not installed and secubox-exposure unavailable"; return 1; }
    mkdir -p "$TOR_DROPIN_DIR"
    mkdir -p "$TOR_DATA_DIR"
    local data; data=$(_tor_data_path "$name")
    local drop; drop=$(_tor_drop_path "$name")
    log "writing Tor HiddenService stanza for $name"
    cat > "$drop" <<EOF
# metablogizer site: $name (#184 — Emancipate via Tor)
HiddenServiceDir $data
HiddenServicePort 80 127.0.0.1:80
EOF
    chown -R debian-tor:debian-tor "$TOR_DATA_DIR" 2>/dev/null || true
    chmod 700 "$data" 2>/dev/null || true
    log "reloading tor"
    systemctl reload tor 2>/dev/null || systemctl restart tor
    # Wait briefly for tor to publish the hostname file
    local i=0
    while [ $i -lt 10 ] && [ ! -f "$data/hostname" ]; do sleep 1; i=$((i+1)); done
    if [ -f "$data/hostname" ]; then
        local onion; onion=$(cat "$data/hostname")
        log "site emancipated via Tor: $onion"
        # Persist the address back into site.json for future Peek calls
        if [ -f "$site_dir/site.json" ] && command -v python3 >/dev/null 2>&1; then
            python3 -c "
import json, sys
p='$site_dir/site.json'
d=json.load(open(p))
d.setdefault('exposure',{})['tor']='$onion'
json.dump(d, open(p,'w'), indent=2)
" 2>/dev/null || true
        fi
    else
        warn "tor reload OK but hostname not yet written; check 'metablogizerctl tor status $name'"
    fi
}

tor_revoke() {
    local name="$1"
    [ -z "$name" ] && { error "Usage: metablogizerctl tor revoke <site>"; return 1; }
    if _have_exposure; then
        log "delegating to secubox-exposure revoke"
        secubox-exposure revoke "metablogizer-${name}" --tor
        return $?
    fi
    local drop; drop=$(_tor_drop_path "$name")
    [ -f "$drop" ] || { warn "no Tor stanza for $name"; return 0; }
    rm -f "$drop"
    systemctl reload tor 2>/dev/null || systemctl restart tor
    log "tor stanza removed for $name (data dir kept under $TOR_DATA_DIR/$name — delete manually if desired)"
}

tor_list() {
    if _have_exposure; then
        log "(delegate) secubox-exposure list --tor"
        secubox-exposure list --tor 2>/dev/null && return
    fi
    if [ ! -d "$TOR_DROPIN_DIR" ]; then
        echo "(no Tor-exposed sites)"
        return
    fi
    local any=0
    for d in "$TOR_DROPIN_DIR"/*.conf; do
        [ -f "$d" ] || continue
        any=1
        local n; n=$(basename "$d" .conf)
        local h="$TOR_DATA_DIR/$n/hostname"
        if [ -f "$h" ]; then
            printf "  %-30s  ->  %s\n" "$n" "$(cat "$h")"
        else
            printf "  %-30s  ->  (publishing...)\n" "$n"
        fi
    done
    [ $any = 0 ] && echo "(no Tor-exposed sites)"
}

tor_status() {
    local name="$1"
    [ -z "$name" ] && { error "Usage: metablogizerctl tor status <site>"; return 1; }
    local data; data=$(_tor_data_path "$name")
    local drop; drop=$(_tor_drop_path "$name")
    echo "site:           $name"
    echo "stanza present: $([ -f "$drop" ] && echo yes || echo no)"
    if [ -f "$data/hostname" ]; then
        echo "onion:          $(cat "$data/hostname")"
    else
        echo "onion:          (not yet published)"
    fi
    systemctl is-active tor >/dev/null 2>&1 && echo "tor service:    active" || echo "tor service:    inactive"
}

cmd_tor() {
    local action="${1:-}"; shift || true
    case "$action" in
        expose)         tor_expose "$@" ;;
        revoke|remove)  tor_revoke "$@" ;;
        list|ls)        tor_list ;;
        status)         tor_status "$@" ;;
        *)
            cat <<EOF
Tor commands (Punk Exposure / Emancipate verb, issue #184):
  tor expose <site>   - publish site via Tor hidden service
  tor revoke <site>   - stop publishing via Tor
  tor list            - list Tor-exposed sites + their onion addresses
  tor status <site>   - show stanza presence + onion + tor service state
EOF
            ;;
    esac
}

site_list() {
    echo "MetaBlogizer Sites:"
    echo "==================="

    if [ ! -d "$SITES_ROOT" ]; then
        echo "  No sites found"
        return 0
    fi

    for site_dir in "$SITES_ROOT"/*/; do
        [ -d "$site_dir" ] || continue
        local name=$(basename "$site_dir")
        local domain=""
        local enabled="no"

        if [ -f "$site_dir/site.json" ]; then
            domain=$(grep '"domain"' "$site_dir/site.json" | cut -d'"' -f4)
        fi

        [ -L "$NGINX_ENABLED_DIR/${name}.conf" ] && enabled="yes"

        printf "  %-20s %-30s published: %s\n" "$name" "${domain:-N/A}" "$enabled"
    done
}

# ============================================================================
# Migration
# ============================================================================

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

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

    if ! ssh -o ConnectTimeout=5 "root@$source" "echo ok" >/dev/null 2>&1; then
        error "Cannot connect to $source via SSH"
        return 1
    fi

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

    # Copy sites
    log "Copying sites..."
    rsync -avz --progress "root@$source:/srv/metablogizer/sites/" "$SITES_ROOT/"

    # Export UCI config
    log "Exporting UCI config..."
    ssh "root@$source" "uci export metablogizer" > "$backup_dir/metablogizer.uci" 2>/dev/null || true

    # Publish migrated sites
    for site_dir in "$SITES_ROOT"/*/; do
        [ -d "$site_dir" ] || continue
        local name=$(basename "$site_dir")
        site_publish "$name"
    done

    log "Migration complete!"
    log "Backup saved to: $backup_dir"
}

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

cmd_status() {
    echo ""
    echo "MetaBlogizer v$VERSION"
    echo "====================="
    echo ""

    echo "Runtime: $RUNTIME"
    echo "Sites root: $SITES_ROOT"
    echo ""

    # Count sites
    local site_count=0
    local published_count=0
    if [ -d "$SITES_ROOT" ]; then
        site_count=$(find "$SITES_ROOT" -maxdepth 1 -type d | wc -l)
        site_count=$((site_count - 1))
        for site_dir in "$SITES_ROOT"/*/; do
            [ -d "$site_dir" ] || continue
            local name=$(basename "$site_dir")
            [ -L "$NGINX_ENABLED_DIR/${name}.conf" ] && published_count=$((published_count + 1))
        done
    fi

    echo "Sites: $site_count"
    echo "Published: $published_count"
    echo ""
}

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

cmd_components() {
    local site_count=0
    local published_count=0
    if [ -d "$SITES_ROOT" ]; then
        site_count=$(find "$SITES_ROOT" -maxdepth 1 -type d 2>/dev/null | wc -l)
        site_count=$((site_count - 1))
        [ $site_count -lt 0 ] && site_count=0
        for site_dir in "$SITES_ROOT"/*/; do
            [ -d "$site_dir" ] || continue
            local name=$(basename "$site_dir")
            [ -L "$NGINX_ENABLED_DIR/${name}.conf" ] && published_count=$((published_count + 1))
        done
    fi

    cat <<EOF
{
  "components": [
    {
      "name": "Site Publisher",
      "type": "service",
      "description": "Static site publishing engine",
      "runtime": "$RUNTIME"
    },
    {
      "name": "Sites",
      "type": "content",
      "path": "$SITES_ROOT",
      "count": $site_count
    },
    {
      "name": "Published Sites",
      "type": "vhosts",
      "count": $published_count
    },
    {
      "name": "Templates",
      "type": "templates",
      "path": "$DATA_PATH/templates"
    }
  ]
}
EOF
}

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

cmd_access() {
    local sites_json="["
    local first=true

    if [ -d "$SITES_ROOT" ]; then
        for site_dir in "$SITES_ROOT"/*/; do
            [ -d "$site_dir" ] || continue
            local name=$(basename "$site_dir")
            local published=false
            local domain=""
            [ -L "$NGINX_ENABLED_DIR/${name}.conf" ] && published=true
            [ -f "$site_dir/.domain" ] && domain=$(cat "$site_dir/.domain")

            $first || sites_json+=","
            first=false
            sites_json+="{\"name\":\"$name\",\"published\":$published,\"domain\":\"$domain\"}"
        done
    fi
    sites_json+="]"

    cat <<EOF
{
  "sites_root": "$SITES_ROOT",
  "admin_panel": "/metablogizer/",
  "sites": $sites_json
}
EOF
}

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

Usage: metablogizerctl <command> [options]

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

Sites:
  site create <name> [domain] [template]  Create new site
  site delete <name>                       Delete site
  site publish <name> [--public-domain <fqdn>]
                                           Publish site (nginx vhost + optional
                                           HAProxy + mitmproxy route sync)
  site unpublish <name>                    Unpublish site (auto-reverses HAProxy
                                           if site was published with --public-domain)
  site list                                List all sites

Tor (Punk Exposure / Emancipate, issue #184):
  tor expose <site>                        Publish site via Tor hidden service
  tor revoke <site>                        Stop publishing via Tor
  tor list                                 List Tor-exposed sites + onions
  tor status <site>                        Stanza + onion + tor service state

Service:
  migrate [host]                           Migrate from OpenWrt

Examples:
  metablogizerctl components            # JSON components
  metablogizerctl site create myblog blog.example.com
  metablogizerctl site publish myblog
  metablogizerctl tor expose myblog     # Emancipate via Tor
  metablogizerctl migrate 192.168.255.1

EOF
}

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

init_dirs

case "${1:-}" in
    # Three-fold architecture
    components) cmd_components ;;
    access)     cmd_access ;;
    status)     shift; cmd_status "$@" ;;
    # Sites
    site)
        shift
        case "$1" in
            create) shift; site_create "$@" ;;
            delete) shift; site_delete "$@" ;;
            publish) shift; site_publish "$@" ;;
            unpublish) shift; site_unpublish "$@" ;;
            list) shift; site_list "$@" ;;
            *) echo "Usage: metablogizerctl site create|delete|publish|unpublish|list" ;;
        esac
        ;;
    # Tor (Emancipate, issue #184)
    tor)        shift; cmd_tor "$@" ;;
    migrate) shift; cmd_migrate "$@" ;;
    help|--help|-h|'') show_help ;;
    *) error "Unknown: $1"; exit 1 ;;
esac

exit 0
