#!/usr/bin/env bash
#
# web_proxy — proxy de interceptacion HTTP/HTTPS liviano, siempre activo.
#
# Orquesta funciones del registry fn_registry para capturar todo el trafico de
# un navegador (estilo ZAP/Burp) con un footprint minimo basado en mitmproxy:
#
#   start_mitm_capture_bash_cybersecurity   arranca mitmdump con rotacion
#   rotate_capture_flows_py_cybersecurity   addon que trocea las capturas
#   query_mitm_flows_bash_cybersecurity     consulta capturas guardadas
#   launch_chromium_proxy_bash_browser      navegador proxeado en perfil aislado
#
# El proxy puede correr en dos modos:
#   - manual:  `web_proxy start` / `web_proxy stop` (background + pidfile)
#   - servicio: `web_proxy install-service` (systemd --user, Restart=always,
#               siempre activo, sobrevive a logout/reboot con linger)
#
# Estado y configuracion viven en $WEB_PROXY_HOME (default ~/.web_proxy).
# Las capturas .mitm viven en el directorio de salida (default ~/captures).

set -uo pipefail

# --- Resolucion de rutas -----------------------------------------------------

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# apps/web_proxy/ -> raiz del registry son dos niveles arriba.
REGISTRY_ROOT="${FN_REGISTRY_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
export FN_REGISTRY_ROOT="$REGISTRY_ROOT"

FN_BIN="$REGISTRY_ROOT/fn"
ADDON_PATH="$REGISTRY_ROOT/python/functions/cybersecurity/rotate_capture_flows.py"

START_FN="$REGISTRY_ROOT/bash/functions/cybersecurity/start_mitm_capture.sh"
QUERY_FN="$REGISTRY_ROOT/bash/functions/cybersecurity/query_mitm_flows.sh"
BROWSER_FN="$REGISTRY_ROOT/bash/functions/browser/launch_chromium_proxy.sh"
EXT_DIR="$SCRIPT_DIR/extension"

# --- Configuracion -----------------------------------------------------------

WEB_PROXY_HOME="${WEB_PROXY_HOME:-$HOME/.web_proxy}"
PIDFILE="$WEB_PROXY_HOME/web_proxy.pid"
CONFFILE="$WEB_PROXY_HOME/web_proxy.conf"
SERVICE_NAME="web_proxy.service"
SYSTEMD_USER_DIR="$HOME/.config/systemd/user"

DEFAULT_PORT=8080
DEFAULT_OUT="$HOME/captures"
DEFAULT_ROTATE=20
DEFAULT_WEB_PORT=8081
DEFAULT_MAX_MB=2048      # tope de tamaño del directorio de capturas (MB); 0 = sin limite
DEFAULT_MAX_DAYS=7       # antiguedad maxima de una captura (dias); 0 = sin limite

MITMDUMP_BIN="$(command -v mitmdump 2>/dev/null || echo "$HOME/.local/bin/mitmdump")"
MITMWEB_BIN="$(command -v mitmweb 2>/dev/null || echo "$HOME/.local/bin/mitmweb")"
CA_CERT="$HOME/.mitmproxy/mitmproxy-ca-cert.pem"

mkdir -p "$WEB_PROXY_HOME"

# --- Helpers -----------------------------------------------------------------

err()  { printf '\033[31m%s\033[0m\n' "$*" >&2; }
ok()   { printf '\033[32m%s\033[0m\n' "$*"; }
info() { printf '%s\n' "$*"; }

# Lee KEY del conffile (formato KEY=VAL), o imprime el default pasado.
conf_get() {
    local key="$1" def="${2:-}"
    if [[ -f "$CONFFILE" ]]; then
        local val
        val="$(grep -E "^${key}=" "$CONFFILE" 2>/dev/null | tail -1 | cut -d= -f2-)"
        [[ -n "$val" ]] && { printf '%s' "$val"; return; }
    fi
    printf '%s' "$def"
}

conf_write() {
    local port="$1" out="$2" rotate="$3" web_port="${4:-}" web_pass="${5:-}" max_mb="${6:-}" max_days="${7:-}"
    cat > "$CONFFILE" <<EOF
PORT=$port
OUT=$out
ROTATE=$rotate
WEB_PORT=$web_port
WEB_PASS=$web_pass
MAX_MB=$max_mb
MAX_DAYS=$max_days
EOF
}

# URL base de la UI de registros en vivo (sin token; la auth es por password).
web_ui_url() {
    local web_port
    web_port="$(conf_get WEB_PORT "")"
    [[ -z "$web_port" ]] && return 1
    printf 'http://127.0.0.1:%s/' "$web_port"
}

# PID del proxy manual, si vive. Imprime el PID o nada.
running_pid() {
    [[ -f "$PIDFILE" ]] || return 1
    local pid
    pid="$(cat "$PIDFILE" 2>/dev/null)"
    [[ -n "$pid" ]] || return 1
    if kill -0 "$pid" 2>/dev/null; then
        printf '%s' "$pid"
        return 0
    fi
    return 1
}

service_active() {
    systemctl --user is-active --quiet "$SERVICE_NAME" 2>/dev/null
}

# --- Subcomandos -------------------------------------------------------------

cmd_start() {
    local port out rotate
    port="$DEFAULT_PORT"; out="$DEFAULT_OUT"; rotate="$DEFAULT_ROTATE"
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --port)       port="$2"; shift 2 ;;
            --out)        out="$2"; shift 2 ;;
            --rotate-min) rotate="$2"; shift 2 ;;
            *) err "start: flag desconocido: $1"; return 2 ;;
        esac
    done

    if service_active; then
        err "El servicio systemd ($SERVICE_NAME) ya esta activo. Usa 'web_proxy stop-service' o gestiona el servicio en su lugar."
        return 1
    fi
    if running_pid >/dev/null; then
        err "Ya hay un proxy manual corriendo (PID $(running_pid)). Para con 'web_proxy stop'."
        return 1
    fi

    local json
    json="$(bash "$START_FN" --port "$port" --out "$out" --rotate-min "$rotate")" || {
        err "Fallo al arrancar mitmdump."
        return 1
    }
    local pid
    pid="$(printf '%s' "$json" | python3 -c 'import sys,json;print(json.load(sys.stdin)["pid"])' 2>/dev/null)"
    if [[ -z "$pid" ]]; then
        err "No se pudo extraer el PID del arranque. Salida: $json"
        return 1
    fi
    printf '%s' "$pid" > "$PIDFILE"
    conf_write "$port" "$out" "$rotate"
    ok "Proxy activo en 127.0.0.1:$port (PID $pid)"
    info "  capturas -> $out (rotacion cada ${rotate} min)"
    info "  navegador -> web_proxy browser"
    info "  consultar -> web_proxy query \"~m POST\""
}

cmd_stop() {
    local pid
    if pid="$(running_pid)"; then
        kill -TERM "$pid" 2>/dev/null
        # Esperar cierre limpio; SIGKILL si se resiste.
        for _ in 1 2 3 4 5; do
            kill -0 "$pid" 2>/dev/null || break
            sleep 0.3
        done
        kill -9 "$pid" 2>/dev/null
        rm -f "$PIDFILE"
        ok "Proxy manual detenido (PID $pid)."
    else
        rm -f "$PIDFILE"
        info "No habia proxy manual corriendo."
    fi
}

cmd_restart() {
    cmd_stop
    sleep 1
    cmd_start "$@"
}

cmd_status() {
    local port out rotate
    port="$(conf_get PORT "$DEFAULT_PORT")"
    out="$(conf_get OUT "$DEFAULT_OUT")"
    rotate="$(conf_get ROTATE "$DEFAULT_ROTATE")"

    local max_mb max_days
    max_mb="$(conf_get MAX_MB "$DEFAULT_MAX_MB")"
    max_days="$(conf_get MAX_DAYS "$DEFAULT_MAX_DAYS")"
    info "web_proxy — estado"
    info "  home:      $WEB_PROXY_HOME"
    info "  capturas:  $out"
    info "  rotacion:  cada ${rotate} min"
    info "  retencion: max ${max_mb} MB, max ${max_days} dias"

    local pid
    if pid="$(running_pid)"; then
        ok  "  manual:    ACTIVO (PID $pid, puerto $port)"
    else
        info "  manual:    parado"
    fi

    if service_active; then
        ok  "  servicio:  ACTIVO ($SERVICE_NAME)"
    elif systemctl --user list-unit-files "$SERVICE_NAME" &>/dev/null && \
         systemctl --user cat "$SERVICE_NAME" &>/dev/null; then
        info "  servicio:  instalado, parado"
    else
        info "  servicio:  no instalado"
    fi

    local web_port web_pass
    web_port="$(conf_get WEB_PORT "")"
    web_pass="$(conf_get WEB_PASS "")"
    if [[ -n "$web_port" ]]; then
        ok   "  UI viva:   http://127.0.0.1:$web_port (registros en tiempo real)"
        [[ -n "$web_pass" ]] && info "  UI login:  password $web_pass (deja el usuario vacio)"
    fi

    if [[ -d "$out" ]]; then
        local n size
        n="$(find "$out" -maxdepth 1 -name 'traffic-*.mitm' 2>/dev/null | wc -l)"
        size="$(du -sh "$out" 2>/dev/null | cut -f1)"
        info "  archivos:  ${n} capturas (${size:-0})"
        local last
        last="$(find "$out" -maxdepth 1 -name 'traffic-*.mitm' -printf '%T@ %p\n' 2>/dev/null | sort -nr | head -1 | cut -d' ' -f2-)"
        [[ -n "$last" ]] && info "  ultima:    $(basename "$last")"
    fi

    if [[ -f "$CA_CERT" ]]; then
        info "  CA HTTPS:  $CA_CERT"
    else
        info "  CA HTTPS:  no generado todavia (arranca el proxy una vez)"
    fi
}

cmd_browser() {
    local url="" proxy_port show_ui="yes" web_port mode="ext"
    proxy_port="$(conf_get PORT "$DEFAULT_PORT")"
    web_port="$(conf_get WEB_PORT "")"
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --url)   url="$2"; shift 2 ;;
            --port)  proxy_port="$2"; shift 2 ;;
            --no-ui) show_ui="no"; shift ;;
            --fixed) mode="fixed"; shift ;;   # proxy siempre activo, sin extension
            --ext)   mode="ext"; shift ;;     # extension con toggle (default)
            *) err "browser: flag desconocido: $1"; return 2 ;;
        esac
    done

    if ! running_pid >/dev/null && ! service_active; then
        err "No hay proxy activo. Arranca primero con 'web_proxy start' o instala el servicio."
        return 1
    fi

    local args=(--profile "$WEB_PROXY_HOME/chromium-profile")
    if [[ "$mode" == "fixed" ]]; then
        # Proxy fijo: captura desde el primer request, sin toggle.
        args+=(--proxy "http://127.0.0.1:${proxy_port}")
    else
        # Modo extension: la extension web_proxy toggle controla el proxy. Se
        # carga desempaquetada y el navegador arranca SIN proxy fijo (toggle
        # OFF por defecto). Un clic en su icono activa la captura.
        args+=(--proxy none --ext "$EXT_DIR")
        info "Extension de captura cargada. Clic en su icono (barra de extensiones) para ACTIVAR el proxy."
        info "El proxy activo por defecto es 127.0.0.1:${proxy_port} (Captura web_proxy)."
    fi

    # Primera pestaña: la UI de registros en vivo (si el servicio corre en modo
    # web). La UI es loopback y no se proxea, asi que carga directa. Las pestañas
    # a sitios reales pasan por el proxy y aparecen en la UI en tiempo real.
    local ui_url=""
    [[ "$show_ui" == "yes" && -n "$web_port" ]] && ui_url="http://127.0.0.1:${web_port}"
    if [[ -n "$ui_url" ]]; then
        args+=(--url "$ui_url")
        [[ -n "$url" ]] && args+=(--extra "$url")
        local web_pass
        web_pass="$(conf_get WEB_PASS "")"
        [[ -n "$web_pass" ]] && info "UI de registros: login con password '$web_pass' (usuario vacio). Se recuerda en este perfil."
    elif [[ -n "$url" ]]; then
        args+=(--url "$url")
    fi
    bash "$BROWSER_FN" "${args[@]}"
}

# Abre solo la UI de registros en vivo en el navegador por defecto del sistema.
cmd_ui() {
    local web_port
    web_port="$(conf_get WEB_PORT "")"
    if [[ -z "$web_port" ]]; then
        err "El servicio no corre en modo web. Reinstala con: web_proxy install-service --web"
        return 1
    fi
    local ui_url="http://127.0.0.1:${web_port}"
    local web_pass
    web_pass="$(conf_get WEB_PASS "")"
    info "UI de registros en vivo: $ui_url"
    [[ -n "$web_pass" ]] && info "login con password '$web_pass' (usuario vacio)"
    if command -v xdg-open &>/dev/null; then
        xdg-open "$ui_url" >/dev/null 2>&1 &
    fi
}

# Resuelve la lista de archivos a consultar: por defecto la ultima captura.
resolve_capture_files() {
    local out scope="$1"
    out="$(conf_get OUT "$DEFAULT_OUT")"
    if [[ "$scope" == "all" ]]; then
        find "$out" -maxdepth 1 -name 'traffic-*.mitm' 2>/dev/null | sort
    else
        find "$out" -maxdepth 1 -name 'traffic-*.mitm' -printf '%T@ %p\n' 2>/dev/null \
            | sort -nr | head -1 | cut -d' ' -f2-
    fi
}

cmd_query() {
    local filter="" scope="last"
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --all)  scope="all"; shift ;;
            --last) scope="last"; shift ;;
            *)      filter="$1"; shift ;;
        esac
    done
    local files
    mapfile -t files < <(resolve_capture_files "$scope")
    if [[ ${#files[@]} -eq 0 || -z "${files[0]}" ]]; then
        err "No hay capturas en $(conf_get OUT "$DEFAULT_OUT")."
        return 1
    fi
    if [[ -n "$filter" ]]; then
        bash "$QUERY_FN" "${files[@]}" --filter "$filter"
    else
        bash "$QUERY_FN" "${files[@]}"
    fi
}

cmd_har() {
    local outhar="${1:-$HOME/captures/export.har}" scope="last"
    [[ "${2:-}" == "--all" ]] && scope="all"
    local files
    mapfile -t files < <(resolve_capture_files "$scope")
    if [[ ${#files[@]} -eq 0 || -z "${files[0]}" ]]; then
        err "No hay capturas para exportar."
        return 1
    fi
    bash "$QUERY_FN" "${files[@]}" --har "$outhar"
    ok "HAR exportado a $outhar"
}

cmd_inspect() {
    local file="${1:-}"
    if [[ -z "$file" ]]; then
        file="$(resolve_capture_files last)"
    fi
    if [[ -z "$file" || ! -f "$file" ]]; then
        err "No hay captura para inspeccionar. Pasa una ruta o arranca el proxy."
        return 1
    fi
    info "Abriendo mitmweb sobre $(basename "$file") -> http://127.0.0.1:8081"
    info "(Ctrl-C para cerrar la UI; no afecta al proxy activo)"
    "$MITMWEB_BIN" -r "$file" --no-web-open-browser
}

cmd_ca() {
    info "HTTPS via proxy de interceptacion requiere confiar en el CA de mitmproxy."
    info ""
    if [[ ! -f "$CA_CERT" ]]; then
        err "El CA aun no existe ($CA_CERT)."
        info "Arranca el proxy una vez ('web_proxy start') para que mitmproxy lo genere."
        return 1
    fi
    info "CA generado en: $CA_CERT"
    info ""
    info "Opciones:"
    info "  1. Confianza a nivel sistema (recomendado):"
    info "       sudo cp $CA_CERT /usr/local/share/ca-certificates/mitmproxy.crt"
    info "       sudo update-ca-certificates"
    info "  2. Solo para el navegador proxeado: importa el .pem en"
    info "     chrome://settings/certificates (pestaña Autoridades)."
    info "  3. Rapido y sucio (menos seguro): el navegador de 'web_proxy browser'"
    info "     ya usa --ignore-certificate-errors si no instalas el CA."
}

# Genera e instala el unit systemd --user. El servicio corre en foreground
# (systemd gestiona el proceso) con Restart=always. Con --web usa mitmweb, que
# expone una UI web en vivo (estilo Burp/ZAP) ademas de capturar a disco; sin
# --web usa mitmdump headless.
cmd_install_service() {
    local port out rotate enable_linger="no" web="no" web_port web_pass max_mb max_days
    port="$(conf_get PORT "$DEFAULT_PORT")"
    out="$(conf_get OUT "$DEFAULT_OUT")"
    rotate="$(conf_get ROTATE "$DEFAULT_ROTATE")"
    web_port="$(conf_get WEB_PORT "$DEFAULT_WEB_PORT")"
    web_pass="$(conf_get WEB_PASS "")"
    max_mb="$(conf_get MAX_MB "$DEFAULT_MAX_MB")"
    max_days="$(conf_get MAX_DAYS "$DEFAULT_MAX_DAYS")"
    [[ -n "$(conf_get WEB_PORT "")" ]] && web="yes"
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --port)         port="$2"; shift 2 ;;
            --out)          out="$2"; shift 2 ;;
            --rotate-min)   rotate="$2"; shift 2 ;;
            --max-mb)       max_mb="$2"; shift 2 ;;
            --max-days)     max_days="$2"; shift 2 ;;
            --web)          web="yes"; shift ;;
            --web-port)     web="yes"; web_port="$2"; shift 2 ;;
            --web-password) web="yes"; web_pass="$2"; shift 2 ;;
            --headless)     web="no"; shift ;;
            --linger)       enable_linger="yes"; shift ;;
            *) err "install-service: flag desconocido: $1"; return 2 ;;
        esac
    done
    [[ "$web" == "yes" && -z "$web_port" ]] && web_port="$DEFAULT_WEB_PORT"
    # Password estable para la UI (la auth por token de mitmweb cambia en cada
    # arranque; un password fijo da URL estable y cookie persistente en el perfil
    # del navegador). Se genera uno aleatorio la primera vez.
    if [[ "$web" == "yes" && -z "$web_pass" ]]; then
        web_pass="$(tr -dc 'a-z0-9' </dev/urandom 2>/dev/null | head -c 10)"
        [[ -z "$web_pass" ]] && web_pass="webproxy"
    fi

    local retention="--set max_total_mb=$max_mb --set max_age_days=$max_days"
    local bin="$MITMDUMP_BIN" exec_line
    if [[ "$web" == "yes" ]]; then
        bin="$MITMWEB_BIN"
        exec_line="$MITMWEB_BIN --no-web-open-browser --web-host 127.0.0.1 --web-port $web_port --set web_password=$web_pass -s $ADDON_PATH --set rotate_min=$rotate --set capture_dir=$out --set exclude_hosts=127.0.0.1:$web_port,localhost:$web_port $retention --listen-port $port"
    else
        exec_line="$MITMDUMP_BIN -s $ADDON_PATH --set rotate_min=$rotate --set capture_dir=$out $retention --listen-port $port"
    fi
    if [[ ! -x "$bin" ]]; then
        err "$(basename "$bin") no encontrado. Instala con: uv tool install mitmproxy"
        return 1
    fi

    # Si hay un proxy manual corriendo, pararlo para no chocar de puerto.
    if running_pid >/dev/null; then
        info "Parando el proxy manual antes de instalar el servicio..."
        cmd_stop
    fi

    mkdir -p "$SYSTEMD_USER_DIR" "$out"
    conf_write "$port" "$out" "$rotate" \
        "$([[ "$web" == "yes" ]] && echo "$web_port")" \
        "$([[ "$web" == "yes" ]] && echo "$web_pass")" \
        "$max_mb" "$max_days"

    cat > "$SYSTEMD_USER_DIR/$SERVICE_NAME" <<EOF
[Unit]
Description=web_proxy — proxy de interceptacion HTTP/HTTPS liviano (mitmproxy)
After=network.target

[Service]
Type=simple
ExecStart=$exec_line
Restart=always
RestartSec=2
# Restart=always (no on-failure): un SIGTERM limpio es exit success y
# on-failure NO reiniciaria, dejando el proxy muerto en silencio.

[Install]
WantedBy=default.target
EOF

    systemctl --user daemon-reload
    systemctl --user enable "$SERVICE_NAME"
    # restart (no enable --now): si el servicio ya estaba activo, --now NO lo
    # reinicia y seguiria corriendo con el unit viejo (password/puerto previos).
    # restart fuerza recarga del unit actual, arrancando si estaba parado.
    systemctl --user restart "$SERVICE_NAME"

    if [[ "$enable_linger" == "yes" ]]; then
        loginctl enable-linger "$USER" 2>/dev/null \
            && ok "Linger activado: el servicio sigue vivo aunque cierres sesion." \
            || err "No se pudo activar linger (requiere permisos). El servicio para al cerrar sesion."
    fi

    sleep 1
    if service_active; then
        ok "Servicio instalado y ACTIVO en 127.0.0.1:$port."
        info "  capturas -> $out (rotacion cada ${rotate} min)"
        info "  retencion -> max ${max_mb} MB, max ${max_days} dias (borra las mas viejas al rotar)"
        if [[ "$web" == "yes" ]]; then
            ok   "  UI viva   -> http://127.0.0.1:$web_port (registros en tiempo real)"
            info "  UI login  -> deja el usuario vacio, password: $web_pass"
        fi
        info "  logs      -> web_proxy logs"
        info "  navegador -> web_proxy browser"
        [[ "$enable_linger" == "no" ]] && info "  persistir tras logout -> web_proxy install-service --linger"
    else
        err "El servicio no arranco. Revisa: web_proxy logs"
        return 1
    fi
}

cmd_uninstall_service() {
    systemctl --user disable --now "$SERVICE_NAME" 2>/dev/null
    rm -f "$SYSTEMD_USER_DIR/$SERVICE_NAME"
    systemctl --user daemon-reload
    ok "Servicio $SERVICE_NAME desinstalado."
}

cmd_logs() {
    if service_active || systemctl --user cat "$SERVICE_NAME" &>/dev/null; then
        journalctl --user -u "$SERVICE_NAME" -n "${1:-40}" --no-pager
    else
        local out
        out="$(conf_get OUT "$DEFAULT_OUT")"
        [[ -f "$out/mitmdump.log" ]] && tail -n "${1:-40}" "$out/mitmdump.log" \
            || info "No hay logs (servicio no instalado, sin log manual)."
    fi
}

usage() {
    cat <<'EOF'
web_proxy — proxy de interceptacion HTTP/HTTPS liviano, siempre activo (mitmproxy)

USO: web_proxy <comando> [opciones]

Proxy:
  start [--port N] [--out DIR] [--rotate-min N]   Arranca el proxy (background, manual)
  stop                                            Para el proxy manual
  restart [opciones de start]                     Reinicia el proxy manual
  status                                          Estado: proxy, servicio, capturas, CA

Servicio (siempre activo, systemd --user):
  install-service [--port N] [--out DIR] [--rotate-min N] [--web] [--web-port N] [--linger]
                                                  Instala + arranca como servicio
                                                  --web: UI de registros en vivo (mitmweb, estilo Burp)
  stop-service / uninstall-service                Para / desinstala el servicio
  logs [N]                                        Ultimas N lineas de log

Navegacion:
  browser [--url URL] [--port N] [--no-ui]        Lanza Chromium proxeado (perfil aislado).
                                                  Abre la UI de registros en vivo como primera pestaña.
  ui                                              Abre solo la UI de registros en el navegador del sistema
  ca                                              Instrucciones para confiar en el CA (HTTPS)

Consultar capturas:
  query [FILTRO] [--all|--last]                   Vuelca flujos (default: ultima captura)
                                                  Filtros mitmproxy: "~m POST", "~c 500", "~d host"
  har [SALIDA.har] [--all]                        Exporta a HAR
  inspect [ARCHIVO.mitm]                          Abre UI web (mitmweb) sobre una captura

Config:
  WEB_PROXY_HOME   directorio de estado (default ~/.web_proxy)
  Defaults: puerto 8080, capturas ~/captures, rotacion 20 min

Ejemplo completo:
  web_proxy install-service --port 8080 --out ~/captures --rotate-min 20
  web_proxy ca                 # confiar en el CA para HTTPS
  web_proxy browser            # navega; todo queda capturado
  web_proxy query "~m POST"    # revisa los POST de la ultima ventana
  web_proxy inspect            # UI visual de la ultima captura
EOF
}

# --- Dispatch ----------------------------------------------------------------

main() {
    local cmd="${1:-help}"
    shift || true
    case "$cmd" in
        start)              cmd_start "$@" ;;
        stop)               cmd_stop "$@" ;;
        restart)            cmd_restart "$@" ;;
        status)             cmd_status "$@" ;;
        browser)            cmd_browser "$@" ;;
        ui)                 cmd_ui "$@" ;;
        query)              cmd_query "$@" ;;
        har)                cmd_har "$@" ;;
        inspect)            cmd_inspect "$@" ;;
        ca)                 cmd_ca "$@" ;;
        install-service)    cmd_install_service "$@" ;;
        stop-service)       systemctl --user stop "$SERVICE_NAME" && ok "Servicio parado." ;;
        uninstall-service)  cmd_uninstall_service "$@" ;;
        logs)               cmd_logs "$@" ;;
        help|-h|--help)     usage ;;
        *) err "Comando desconocido: $cmd"; echo; usage; return 2 ;;
    esac
}

main "$@"
