#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# SecuBox-Deb :: yacyctl
# CyberMind — https://cybermind.fr
#
# YaCy host-side controller — opens the SEARCH layer of the SecuBox CTL
# grammar. Three-fold introspection + peer/index/query/blacklist/crawler
# nouns. See docs/grammar.md and docs/MODULE-GUIDELINES.md.

set -u

readonly VERSION="1.0.0"
readonly CONFIG_FILE="${SECUBOX_YACY_CONFIG:-/etc/secubox/yacy.toml}"
readonly STATE_DIR="${SECUBOX_YACY_STATE:-/var/lib/secubox/yacy}"
readonly SECRETS_DIR="${SECUBOX_SECRETS_DIR:-/etc/secubox/secrets}"
readonly INSTALL_LIB="${SECUBOX_INSTALL_LIB:-/usr/share/secubox/lib/yacy/install-lxc.sh}"
readonly YACY_CONF="/opt/yacy/DATA/SETTINGS/yacy.conf"

readonly RED='\033[0;31m'; readonly GREEN='\033[0;32m'; readonly YELLOW='\033[1;33m'; readonly NC='\033[0m'
log()  { printf '%b[yacy]%b %s\n' "$GREEN" "$NC" "$*"; }
warn() { printf '%b[warn]%b %s\n' "$YELLOW" "$NC" "$*" >&2; }
err()  { printf '%b[error]%b %s\n' "$RED" "$NC" "$*" >&2; }

config_get() {
    # Strips TOML inline `# comment`, surrounding whitespace + quotes.
    local key="$1" default="${2:-}" raw=""
    if [ -f "$CONFIG_FILE" ]; then
        raw=$(grep -E "^[[:space:]]*${key}[[:space:]]*=" "$CONFIG_FILE" 2>/dev/null | head -1)
    fi
    if [ -z "$raw" ]; then
        echo "$default"
        return
    fi
    local val
    val=$(echo "$raw" | cut -d= -f2- | sed 's/[[:space:]]*#.*$//' \
        | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' \
        | tr -d '"' | tr -d "'")
    if [ -z "$val" ]; then echo "$default"; else echo "$val"; fi
}

LXC_NAME=$(config_get "name" "yacy")
LXC_IP=$(config_get "ip" "10.100.0.80")
LXC_PATH=$(config_get "path" "/data/lxc")
HTTP_PORT=$(config_get "http_port" "8090")
PEER_MODE=$(config_get "mode" "standalone")
PUBLIC_HOSTNAME=$(config_get "public_hostname" "$(secubox-domaine yacy)")

# Introspection must work from the host FastAPI (secubox-yacy.service),
# which runs as User=secubox with NoNewPrivileges=true. lxc-info / lxc-attach
# on the unprivileged /data/lxc/yacy container need root, and NNP blocks sudo
# from elevating — so liveness is PROBED directly over the br-lxc network
# (the host routes to $LXC_IP), which needs no privilege and is a truer
# "is YaCy actually serving" signal. lxc-info is kept only as a best-effort
# enrichment when yacyctl runs as root (CLI / reload). Before this,
# component/status wrongly reported the LXC "absent" + daemon "stopped"
# while both were running (overall red).
if [ "$(id -u)" -eq 0 ]; then SUDO=""; else SUDO="sudo -n"; fi
lxc_info()   { $SUDO lxc-info   -n "$LXC_NAME" -P "$LXC_PATH" "$@"; }
lxc_attach() { $SUDO lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" "$@"; }

# Privilege-free liveness probe: does the YaCy daemon answer on its port?
daemon_reachable() {
    curl -s -o /dev/null --max-time 3 "http://$LXC_IP:$HTTP_PORT/" 2>/dev/null
}

lxc_state() {
    lxc_info 2>/dev/null \
        | awk -F: '/^State:/ { gsub(/ /,"",$2); print tolower($2) }'
}
# A reachable daemon implies the container is up; fall back to lxc-info when
# privileged (covers the rare container-up / daemon-down case).
lxc_running() { daemon_reachable || [ "$(lxc_state)" = "running" ]; }
lxc_exists()  { daemon_reachable || lxc_info >/dev/null 2>&1 || [ -d "$LXC_PATH/$LXC_NAME/rootfs" ]; }
daemon_running() {
    daemon_reachable && return 0
    # Privileged fallback (root / CLI): ask systemd inside the container.
    [ "$(lxc_state)" = "running" ] && lxc_attach -- systemctl is-active --quiet yacy 2>/dev/null
}
host_api_running() { systemctl is-active --quiet secubox-yacy.service 2>/dev/null; }

# Probe-aware lxc cell: running (probe or lxc-info), else stopped if defined,
# else absent — without requiring root.
lxc_display_state() {
    if lxc_running; then echo "running"
    elif lxc_exists; then local s; s=$(lxc_state); echo "${s:-stopped}"
    else echo "absent"; fi
}

emit_components_json() {
    local lxc_st daemon_st api_st
    lxc_st=$(lxc_display_state)
    daemon_running && daemon_st="running" || daemon_st="stopped"
    host_api_running && api_st="running" || api_st="stopped"
    cat <<EOF
{
  "module": "yacy",
  "version": "$VERSION",
  "components": [
    {"name": "lxc",      "state": "$lxc_st",      "detail": "$LXC_NAME @ $LXC_IP on br-lxc"},
    {"name": "daemon",   "state": "$daemon_st",   "detail": "yacy ($PEER_MODE), port $HTTP_PORT"},
    {"name": "host-api", "state": "$api_st",      "detail": "secubox-yacy.service (uvicorn @ /run/secubox/yacy.sock)"}
  ]
}
EOF
}

emit_components_text() {
    printf '%-12s %-12s %s\n' "COMPONENT" "STATE" "DETAIL"
    local s; s=$(lxc_display_state)
    if [ "$s" = "absent" ]; then printf '%-12s %-12s %s\n' "lxc" "absent" "$LXC_NAME — run 'yacyctl install'"
    else printf '%-12s %-12s %s\n' "lxc" "$s" "$LXC_NAME @ $LXC_IP on br-lxc"; fi
    daemon_running && printf '%-12s %-12s %s\n' "daemon" "running" "yacy ($PEER_MODE), port $HTTP_PORT" \
                   || printf '%-12s %-12s %s\n' "daemon" "stopped" "yacy ($PEER_MODE), port $HTTP_PORT"
    host_api_running && printf '%-12s %-12s %s\n' "host-api" "running" "secubox-yacy.service (uvicorn)" \
                     || printf '%-12s %-12s %s\n' "host-api" "stopped" "secubox-yacy.service (uvicorn)"
}

emit_status_json() {
    local overall
    if lxc_running && daemon_running && host_api_running; then overall="green"
    elif lxc_exists && (daemon_running || host_api_running); then overall="yellow"
    else overall="red"; fi
    cat <<EOF
{"module": "yacy", "version": "$VERSION", "overall": "$overall"}
EOF
}

emit_access_json() {
    # Real YaCy admin URLs — NOT the /yacy/ admin subpath. The admin
    # webui's "Open YaCy" button reads from this; single source of truth.
    local lan_url public_url
    lan_url="http://${LXC_IP}:${HTTP_PORT}/"
    [ -n "$PUBLIC_HOSTNAME" ] && public_url="https://${PUBLIC_HOSTNAME}/" || public_url=""
    cat <<EOF
{
  "module": "yacy",
  "access": [
    {"url": "$lan_url", "auth": "lan-only", "scope": "lan"}$( [ -n "$public_url" ] && echo ',' )
    $( [ -n "$public_url" ] && printf '{"url": "%s", "auth": "LAN-gated", "scope": "public"}\n' "$public_url" )
  ]
}
EOF
}

cmd_components() { case "${1:-}" in --json) emit_components_json ;; *) emit_components_text ;; esac; }
cmd_status()     { case "${1:-}" in --json) emit_status_json ;; *) emit_components_text; echo; emit_status_json | python3 -c 'import json,sys; d=json.load(sys.stdin); print("overall:", d["overall"])' ;; esac; }
cmd_access()     { case "${1:-}" in --json) emit_access_json ;; *) emit_access_json | python3 -m json.tool ;; esac; }

cmd_install() {
    [ -x "$INSTALL_LIB" ] || { err "install script not found at $INSTALL_LIB"; exit 2; }
    log "Running LXC bootstrap from $INSTALL_LIB ..."
    SECUBOX_LXC_NAME="$LXC_NAME" SECUBOX_LXC_IP="$LXC_IP" SECUBOX_LXC_PATH="$LXC_PATH" \
        bash "$INSTALL_LIB" || { echo "[yacy] ERREUR : l'installation a échoué (voir ci-dessus) — rien n'est « fait » (#1729)" >&2; exit 1; }
    systemctl start secubox-yacy.service 2>/dev/null || true
    log "Done. Try 'yacyctl status' to verify."
}

cmd_reload() {
    systemctl restart secubox-yacy.service 2>/dev/null || true
    lxc_running && lxc_attach -- systemctl restart yacy 2>/dev/null || true
    log "Reloaded."
}

cmd_set_admin_password() {
    # Push a new plaintext admin password into YaCy's single admin account.
    #
    # The password is read from STDIN (never argv) so it cannot leak via `ps`,
    # the shell history, or sudo's command log. This is the sync target invoked
    # by secubox-users when the master 'admin' user's password changes (over
    # `sudo -n yacyctl set-admin-password`), and it is safe to run by hand.
    #
    # Requires root: it writes the root-owned secret file and drives the
    # in-container daemon via lxc-attach. When invoked through sudo, id -u == 0
    # so lxc_attach() runs lxc-attach directly.
    if [ "$(id -u)" -ne 0 ]; then err "set-admin-password requires root"; exit 2; fi

    local pw; pw=$(cat)
    [ -n "$pw" ] || { err "empty password on stdin"; exit 2; }

    install -d -m 0700 "$SECRETS_DIR"
    printf '%s\n' "$pw" > "$SECRETS_DIR/yacy-admin"
    chmod 600 "$SECRETS_DIR/yacy-admin"

    # YaCy HTTP Digest hash: md5("admin:<adminRealm>:<password>"). Mirrors YaCy's
    # own bin/passwd.sh and configure_yacy_admin() in install-lxc.sh — keep the
    # three in lock-step if the format ever changes.
    local realm hash
    realm=$(lxc_attach -- sh -c "grep '^adminRealm=' $YACY_CONF | cut -d= -f2-" 2>/dev/null || true)
    [ -n "$realm" ] || realm="YaCy"
    hash=$(printf 'admin:%s:%s' "$realm" "$pw" | md5sum | cut -d' ' -f1)
    [ -n "$hash" ] || { err "hash computation failed"; exit 2; }

    # Idempotent: skip the daemon restart when the hash is already current.
    if lxc_attach -- grep -q "^adminAccountBase64MD5=MD5:${hash}$" "$YACY_CONF" 2>/dev/null; then
        log "YaCy admin password already up to date — no change."
        return 0
    fi

    # YaCy rewrites yacy.conf on shutdown, so it must be stopped before editing.
    log "Updating YaCy admin password (restarting daemon) ..."
    lxc_attach -- systemctl stop yacy 2>/dev/null || true
    lxc_attach -- sed -i \
        "s|^adminAccountBase64MD5=.*|adminAccountBase64MD5=MD5:${hash}|" "$YACY_CONF"
    lxc_attach -- systemctl start yacy 2>/dev/null || true
    log "YaCy admin password updated."
}

cmd_help() {
    cat <<'HELP'
yacyctl — SecuBox YaCy controller (SEARCH layer)

Three-fold introspection:
  yacyctl components [--json]
  yacyctl status     [--json]
  yacyctl access     [list|show <name>] [--json]

Lifecycle:
  yacyctl install                  # idempotent LXC + JVM + YaCy bootstrap
  yacyctl reload                   # restart host FastAPI + yacy daemon
  yacyctl set-admin-password       # read new admin password from STDIN, apply + restart

Search-engine nouns:
  yacyctl peer       list|add <url>|remove <hash>|status <hash>
  yacyctl index      status|build <urls.txt>|clear|optimize
  yacyctl query      test <terms>|count <terms>
  yacyctl blacklist  list|add <pattern>|remove <pattern>
  yacyctl crawler    list|start <profile>|stop <id>|schedule <profile> <cron>

  (--json on any verb returns machine-readable output)

For grammar details: docs/grammar.md (SEARCH layer)
HELP
}

main() {
    local noun="${1:-help}"
    shift || true
    case "$noun" in
        components|status|access|install|reload) "cmd_${noun}" "$@" ;;
        set-admin-password) cmd_set_admin_password "$@" ;;
        peer|index|query|blacklist|crawler)
            err "noun '$noun' not yet implemented in v$VERSION — see #232 follow-up task"
            exit 2 ;;
        --help|-h|help|"") cmd_help ;;
        --version|-V) echo "yacyctl $VERSION" ;;
        *) err "unknown noun: $noun"; cmd_help; exit 1 ;;
    esac
}

main "$@"
