#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.

# SecuBox-Deb :: dropletctl — droplet content publisher (port from OpenWrt)
set -euo pipefail

readonly MODULE="dropletctl"
readonly VERSION="1.1.0"

# Environment overrides for tests (default to production paths if unset).
: "${SITES_DIR:=/srv/metablogizer/sites}"
: "${TOML_PATH:=/etc/secubox/droplet.toml}"
: "${LOG_TAG:=droplet}"

log_info()  { logger -t "$LOG_TAG" -p user.info  "$*" 2>/dev/null || true; echo "[INFO] $*"; }
log_error() { logger -t "$LOG_TAG" -p user.error "$*" 2>/dev/null || true; echo "[ERROR] $*" >&2; }
log_ok()    { echo "[OK] $*"; }

# Lowercase + replace non-[a-z0-9_-] with '_'. Empty result -> caller errors.
sanitize_name() {
    printf '%s' "$1" | awk '{print tolower($0)}' | sed 's/[^a-z0-9_-]/_/g'
}

# Read default_domain from droplet.toml. Falls back to gk2.secubox.in.
default_domain() {
    [[ -r "$TOML_PATH" ]] || { echo "gk2.secubox.in"; return; }
    local v
    v=$(grep -E '^default_domain\s*=' "$TOML_PATH" 2>/dev/null \
        | head -1 | sed -E 's/^default_domain\s*=\s*"?([^"]*)"?\s*$/\1/')
    echo "${v:-gk2.secubox.in}"
}

# Stage <src> into a fresh temp dir. Echoes the staging dir path.
stage_content() {
    local src="$1"
    local staging
    staging="$(mktemp -d /tmp/droplet.XXXXXX)"

    local ext
    if [[ -d "$src" ]]; then
        ext="dir"
    else
        ext="${src##*.}"; ext="${ext,,}"
        [[ "$src" == *.tar.gz ]] && ext="tar.gz"
    fi

    case "$ext" in
        html|htm)
            cp "$src" "$staging/index.html"
            ;;
        zip)
            if ! unzip -q "$src" -d "$staging"; then
                log_error "Failed to extract zip: $src"
                rm -rf "$staging"
                exit 3
            fi
            unwrap_single_nested "$staging"
            ;;
        tgz|tar.gz)
            if ! tar -xzf "$src" -C "$staging"; then
                log_error "Failed to extract tarball: $src"
                rm -rf "$staging"
                exit 3
            fi
            unwrap_single_nested "$staging"
            ;;
        dir)
            # Caller passed a directory — rsync its contents verbatim.
            rsync -a "$src"/ "$staging"/
            ;;
    esac

    echo "$staging"
}

# If $1 contains exactly one entry and it's a directory, move its contents up.
unwrap_single_nested() {
    local dir="$1"
    local entries=("$dir"/*)
    if [[ ${#entries[@]} -eq 1 ]] && [[ -d "${entries[0]}" ]]; then
        local nested="${entries[0]}"
        # Use a temp move to avoid name collision when nested basename matches dir.
        local stash; stash="$(mktemp -d "$dir/.unwrap.XXXXXX")"
        mv "$nested"/* "$nested"/.[!.]* "$stash"/ 2>/dev/null || true
        rmdir "$nested"
        mv "$stash"/* "$stash"/.[!.]* "$dir"/ 2>/dev/null || true
        rmdir "$stash"
    fi
}

cmd_publish() {
    local file="${1:-}"
    local name_raw="${2:-}"
    local domain="${3:-$(default_domain)}"

    if [[ -z "$file" ]] || [[ -z "$name_raw" ]]; then
        echo "[ERROR] Usage: dropletctl publish <file> <name> [domain]" >&2
        exit 1
    fi
    if [[ ! -e "$file" ]]; then
        echo "[ERROR] not found: $file" >&2
        exit 1
    fi

    # If the path is a directory, skip extension validation entirely.
    local ext
    if [[ -d "$file" ]]; then
        ext="dir"
    else
        ext="${file##*.}"; ext="${ext,,}"
        [[ "$file" == *.tar.gz ]] && ext="tar.gz"
        case "$ext" in
            html|htm|zip|tgz|tar.gz) : ;;
            *)
                echo "[ERROR] Unsupported file type: .$ext (expected .html .htm .zip .tar.gz .tgz or directory)" >&2
                exit 1
                ;;
        esac
    fi

    # Validate domain syntactically. Allow ASCII letters/digits/dot/hyphen
    # (RFC 1035 hostname plus dots). Reject anything else to keep the TOML
    # writer safe from injection.
    if [[ ! "$domain" =~ ^[a-zA-Z0-9.-]+$ ]]; then
        echo "[ERROR] Invalid domain '$domain'" >&2
        exit 2
    fi

    local name; name="$(sanitize_name "$name_raw")"
    if [[ -z "$name" ]]; then
        echo "[ERROR] Invalid name '$name_raw'" >&2
        exit 2
    fi

    local vhost="${name}.${domain}"
    # staging is intentionally not declared local so the EXIT trap can reference it.
    staging="$(stage_content "$file")"
    trap 'rm -rf "$staging"' EXIT

    log_info "Publishing: $file as $vhost"

    # Permissions: 644 files, 755 dirs.
    find "$staging" -type f -exec chmod 644 {} +
    find "$staging" -type d -exec chmod 755 {} +

    # Deploy to /srv/metablogizer/sites/<name>/ (idempotent overwrite).
    mkdir -p "$SITES_DIR/$name"
    rsync -a --delete "$staging"/ "$SITES_DIR/$name"/

    # Upsert TOML entry. Section format: [sites.<name>] domain="<d>" type="static".
    upsert_toml_site "$name" "$domain" "static"

    # Delegate the HTTP-facing work.
    if ! command -v metablogizerctl >/dev/null 2>&1; then
        log_error "metablogizerctl not found — install secubox-metablogizer"
        exit 4
    fi
    # Pass the public vhost so metablogizerctl wires HAProxy + mitmproxy.
    # Falls back gracefully on older metablogizer (which ignores the flag
    # and only configures nginx — same behavior as droplet 1.1.0).
    if ! metablogizerctl site publish "$name" --public-domain "$vhost"; then
        log_error "metablogizerctl failed for $name"
        exit 5
    fi

    log_ok "Published: https://$vhost/"
    echo "$vhost"
}

# Upsert [sites.<name>] section. Uses python3 for safe TOML rewrite.
upsert_toml_site() {
    local name="$1" domain="$2" type="$3"
    python3 - "$TOML_PATH" "$name" "$domain" "$type" <<'PY'
import sys, re, os, tempfile

path, name, domain, type_ = sys.argv[1:]
section = f"[sites.{name}]"
new_block = f'{section}\ndomain = "{domain}"\ntype = "{type_}"\n'

if os.path.exists(path):
    with open(path) as f:
        content = f.read()
else:
    content = ""

# Match the section + its body up to the next [section] or EOF.
pattern = re.compile(
    rf'^{re.escape(section)}\s*\n(?:(?!^\[).*\n?)*',
    flags=re.MULTILINE,
)
if pattern.search(content):
    new_content = pattern.sub(new_block, content)
else:
    if content and not content.endswith('\n'):
        content += '\n'
    if content and not content.endswith('\n\n'):
        content += '\n'
    new_content = content + new_block

# Atomic write.
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path) or '.', prefix='.droplet.', suffix='.toml')
os.write(fd, new_content.encode())
os.close(fd)
os.replace(tmp, path)
PY
}

cmd_remove() {
    local name_raw="${1:-}"
    if [[ -z "$name_raw" ]]; then
        echo "[ERROR] Usage: dropletctl remove <name>" >&2
        exit 1
    fi
    local name; name="$(sanitize_name "$name_raw")"
    if [[ -z "$name" ]]; then
        echo "[ERROR] Invalid name '$name_raw'" >&2
        exit 2
    fi

    if ! command -v metablogizerctl >/dev/null 2>&1; then
        log_error "metablogizerctl not found — install secubox-metablogizer"
        exit 4
    fi
    metablogizerctl site unpublish "$name" || true
    metablogizerctl site delete "$name"    || true

    rm -rf "$SITES_DIR/$name"
    remove_toml_site "$name"
    log_ok "Removed: $name"
}

# Drop the [sites.<name>] block from droplet.toml. No-op if not present.
remove_toml_site() {
    local name="$1"
    [[ -e "$TOML_PATH" ]] || return 0
    python3 - "$TOML_PATH" "$name" <<'PY'
import sys, re, os, tempfile
path, name = sys.argv[1:]
with open(path) as f:
    content = f.read()
pattern = re.compile(
    rf'^\[sites\.{re.escape(name)}\]\s*\n(?:(?!^\[).*\n?)*',
    flags=re.MULTILINE,
)
new = pattern.sub('', content)
if new == content:
    sys.exit(0)
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path) or '.', prefix='.droplet.', suffix='.toml')
os.write(fd, new.encode())
os.close(fd)
os.replace(tmp, path)
PY
}

cmd_rename() {
    local old_raw="${1:-}" new_raw="${2:-}"
    if [[ -z "$old_raw" ]] || [[ -z "$new_raw" ]]; then
        echo "[ERROR] Usage: dropletctl rename <old> <new>" >&2
        exit 1
    fi
    local old new
    old="$(sanitize_name "$old_raw")"
    new="$(sanitize_name "$new_raw")"
    if [[ -z "$old" ]] || [[ -z "$new" ]]; then
        echo "[ERROR] Invalid name(s): '$old_raw' or '$new_raw'" >&2
        exit 2
    fi
    if [[ "$old" == "$new" ]]; then
        echo "[ERROR] old and new names are identical after sanitization" >&2
        exit 2
    fi
    if [[ ! -d "$SITES_DIR/$old" ]]; then
        echo "[ERROR] not found: $old" >&2
        exit 1
    fi

    # Read old domain so the new site keeps the same default unless overridden.
    local domain
    domain=$(grep -A2 "^\[sites\.$old\]" "$TOML_PATH" 2>/dev/null \
        | grep -E '^domain\s*=' | head -1 \
        | sed -E 's/^domain\s*=\s*"?([^"]*)"?\s*$/\1/')
    domain="${domain:-$(default_domain)}"

    if ! command -v metablogizerctl >/dev/null 2>&1; then
        log_error "metablogizerctl not found — install secubox-metablogizer"
        exit 4
    fi

    if [[ -e "$SITES_DIR/$new" ]]; then
        echo "[ERROR] target already exists: $new" >&2
        exit 1
    fi

    mv "$SITES_DIR/$old" "$SITES_DIR/$new"
    remove_toml_site "$old"
    # Preserve the BARE BASE DOMAIN in TOML. If $domain happens to be a
    # prefixed FQDN like "oldname.example.com" (from a legacy rename or
    # hand-edit), strip the prefix to recover the base. cmd_publish stores
    # the base; we maintain the same invariant here so cmd_list can always
    # compute the vhost as "$name.$domain".
    local base_domain="$domain"
    if [[ "$domain" == "${old}."* ]]; then
        base_domain="${domain#*.}"
    fi
    upsert_toml_site "$new" "$base_domain" "static"

    # Public vhost for the new name (constructed from sanitized name + base
    # domain just like cmd_publish does — keeps invariant consistent across
    # publish/rename and ensures HAProxy is wired for the renamed site).
    local new_vhost="${new}.${base_domain}"

    # delete is tolerant: old vhost may never have been registered.
    metablogizerctl site delete "$old"  || true
    # publish must succeed: failure leaves site half-renamed and unreachable.
    metablogizerctl site publish "$new" --public-domain "$new_vhost" || {
        log_error "metablogizerctl failed for $new"
        exit 5
    }
    log_ok "Renamed: $old -> $new"
}

cmd_list() {
    [[ -r "$TOML_PATH" ]] || { echo "(no droplets)"; return 0; }
    python3 - "$TOML_PATH" <<'PY'
import sys, re
path = sys.argv[1]
with open(path) as f:
    content = f.read()
# Capture [sites.<name>] then the immediate `domain = "..."` line in its block.
section_re = re.compile(r'^\[sites\.([^\]]+)\]\s*\n((?:(?!^\[).*\n?)*)', re.MULTILINE)
for m in section_re.finditer(content):
    name = m.group(1)
    body = m.group(2)
    dm = re.search(r'^domain\s*=\s*"?([^"\n]+)"?', body, re.MULTILINE)
    domain = dm.group(1).strip() if dm else "?"
    vhost = f"{name}.{domain}"
    print(f"{name:<30s} [ON]  https://{vhost}/")
PY
}

case "${1:-}" in
    "" | -h | --help)
        cat <<USAGE
Droplet Publisher — One-drop static-content publishing
Usage: dropletctl <command> [args]

Commands:
  publish <file> <name> [domain]   Publish HTML/ZIP/tarball as a site
  list                             List published droplets
  remove <name>                    Remove a droplet
  rename <old> <new>               Rename a droplet
USAGE
        exit 0
        ;;
    publish)
        shift
        cmd_publish "$@"
        ;;
    remove)
        shift
        cmd_remove "$@"
        ;;
    rename)
        shift
        cmd_rename "$@"
        ;;
    list)
        cmd_list
        ;;
    *)
        echo "[ERROR] unknown subcommand: $1" >&2
        exit 1
        ;;
esac
