#!/bin/sh
# SPDX-License-Identifier: MIT
# enne2-cpufreq-task — launcher di task con profilo CPU dedicato.
#
# Gira SEMPRE come utente: il comando del task non viene mai eseguito come root.
# Solo il cambio di profilo passa dall'helper privilegiato via pkexec.
#
# Flusso di "run":
#   1. helper task-begin <profilo>  → salva la baseline (una sola volta) e applica
#   2. il comando parte staccato (setsid) e scrive il proprio pid nel file di stato
#   3. un watcher staccato attende l'uscita del pid e chiama helper task-end, che
#      ripristina la baseline quando non ci sono altri task attivi
#
# Il plasmoide invoca "add-hex" con gli argomenti codificati in esadecimale
# (UTF-8): così nomi, comandi con spazi, virgolette o newline attraversano
# l'exec engine di Plasma senza dipendere dal parsing delle virgolette.
set -u

PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
export PATH

SELF_DIR=$(dirname "$0")
HELPER=/usr/local/libexec/enne2-cpufreq/cpufreq-helper
CONF_DIR=${XDG_CONFIG_HOME:-$HOME/.config}/enne2-cpufreq
TASKS=$CONF_DIR/tasks.json
RUN=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/enne2-cpufreq

mkdir -p "$CONF_DIR" "$RUN" 2>/dev/null

fail() {
    printf 'ok=0\nerror=%s\n' "$*"
    exit 2
}

readf() {
    [ -r "$1" ] || return 1
    _val=
    IFS= read -r _val < "$1" 2>/dev/null || [ -n "$_val" ] || return 1
    printf '%s\n' "$_val"
}

# Decodifica esadecimale UTF-8 senza dipendenze (xxd/od non sempre presenti).
hex_decode() {
    [ -n "${1:-}" ] || return 0
    case "$1" in *[!0-9a-fA-F]*) fail "payload esadecimale non valido" ;; esac
    _esc=$(printf '%s' "$1" | sed 's/\(..\)/\\x\1/g')
    printf '%b' "$_esc"
}

need_jq() {
    command -v jq >/dev/null 2>&1 || fail "jq non installato (serve per gestire tasks.json)"
}

init_tasks() {
    [ -r "$TASKS" ] && return 0
    printf '{"version":1,"tasks":[]}\n' > "$TASKS" || fail "impossibile creare $TASKS"
}

slugify() {
    printf '%s' "$1" | tr 'A-Z' 'a-z' | tr -c 'A-Za-z0-9._-' '-' \
        | sed -e 's/--*/-/g' -e 's/^-//' -e 's/-$//'
}

# Accetta indifferentemente il nome visualizzato o lo slug.
resolve_slug() {
    jq -r --arg q "$1" '.tasks[] | select(.name == $q or .slug == $q) | .slug' \
        "$TASKS" 2>/dev/null | head -n 1
}

field_of_slug() {                           # field_of_slug <slug> <campo>
    jq -r --arg s "$1" --arg f "$2" \
        '.tasks[] | select(.slug == $s) | .[$f] // empty' "$TASKS" 2>/dev/null | head -n 1
}

is_running() {                              # is_running <slug>
    _pid=$(readf "$RUN/$1.pid") || return 1
    case "$_pid" in ''|*[!0-9]*) return 1 ;; esac
    kill -0 "$_pid" 2>/dev/null
}

helper_ready() {
    [ -x "$HELPER" ] || fail "helper non installato in $HELPER (eseguire install.sh)"
    command -v pkexec >/dev/null 2>&1 || fail "pkexec non disponibile (installare polkit)"
}

do_add() {                                  # do_add <nome> <profilo> <comando>
    init_tasks
    need_jq
    _name=${1:-}
    _prof=${2:-}
    _cmd=${3:-}
    [ -n "$_name" ] || fail "nome del task mancante"
    [ -n "$_cmd" ] || fail "comando del task mancante"
    case $_prof in
        quiet|powersave|balanced|performance) ;;
        *) fail "profilo non valido: $_prof (quiet|powersave|balanced|performance)" ;;
    esac
    _slug=$(slugify "$_name")
    [ -n "$_slug" ] || fail "nome non utilizzabile come slug: $_name"
    if [ -n "$(resolve_slug "$_name")" ]; then
        fail "esiste già un task chiamato '$_name'"
    fi
    _tmp=$TASKS.tmp.$$
    jq --arg n "$_name" --arg p "$_prof" --arg c "$_cmd" --arg s "$_slug" \
        '.tasks += [{name: $n, profile: $p, command: $c, slug: $s}]' "$TASKS" > "$_tmp" \
        && mv "$_tmp" "$TASKS" || { rm -f "$_tmp"; fail "scrittura tasks.json fallita"; }
    printf 'ok=1\nname=%s\nprofile=%s\nslug=%s\n' "$_name" "$_prof" "$_slug"
}

cmd_add() {                                 # add <nome> <profilo> <comando...>
    _name=${1:-}
    _prof=${2:-}
    [ "$#" -ge 3 ] || fail "servono nome, profilo e comando"
    shift 2
    do_add "$_name" "$_prof" "$*"
}

cmd_add_hex() {                             # add-hex <nome_hex> <profilo_hex> <comando_hex>
    [ "$#" -ge 3 ] || fail "servono tre argomenti esadecimali"
    do_add "$(hex_decode "$1")" "$(hex_decode "$2")" "$(hex_decode "$3")"
}

cmd_list() {
    init_tasks
    need_jq
    printf 'ok=1\n'
    [ -x "$HELPER" ] && printf 'helper=ok\n' || printf 'helper=missing\n'
    printf 'count=%s\n' "$(jq -r '.tasks | length' "$TASKS" 2>/dev/null || echo 0)"
    _i=0
    jq -c '.tasks[] | {name: .name, profile: .profile, command: .command, slug: .slug}' \
        "$TASKS" 2>/dev/null |
    while IFS= read -r _line; do
        [ -n "$_line" ] || continue
        _slug=$(printf '%s' "$_line" | jq -r '.slug')
        _run=0
        is_running "$_slug" && _run=1
        printf 'task=%s\t%s\t%s\n' "$_i" "$_run" "$_line"
        _i=$((_i + 1))
    done
}

cmd_remove() {                              # remove <nome-o-slug>
    init_tasks
    need_jq
    _slug=$(resolve_slug "${1:-}")
    [ -n "$_slug" ] || fail "task '${1:-}' inesistente"
    is_running "$_slug" && fail "il task è in esecuzione: fermarlo prima di rimuoverlo"
    _tmp=$TASKS.tmp.$$
    jq --arg s "$_slug" '.tasks |= map(select(.slug != $s))' "$TASKS" > "$_tmp" \
        && mv "$_tmp" "$TASKS" || { rm -f "$_tmp"; fail "scrittura tasks.json fallita"; }
    rm -f "$RUN/$_slug.pid" "$RUN/$_slug.info"
    printf 'ok=1\nremoved=%s\n' "$_slug"
}

cmd_run() {                                 # run <nome-o-slug>
    init_tasks
    need_jq
    helper_ready
    _slug=$(resolve_slug "${1:-}")
    [ -n "$_slug" ] || fail "task '${1:-}' inesistente"
    _name=$(field_of_slug "$_slug" name)
    _prof=$(field_of_slug "$_slug" profile)
    _cmd=$(field_of_slug "$_slug" command)
    [ -n "$_cmd" ] || fail "comando vuoto per '$_name'"
    is_running "$_slug" && fail "il task '$_name' è già in esecuzione"

    _out=$(pkexec "$HELPER" task-begin "$_prof" 2>&1) \
        || fail "profilo '$_prof' non applicato: $(printf '%s' "$_out" | tr '\n' ' ')"
    case "$_out" in
        *"ok=0"*) fail "profilo '$_prof' non applicato: $(printf '%s' "$_out" | tr '\n' ' ')" ;;
    esac

    rm -f "$RUN/$_slug.pid" "$RUN/$_slug.info"
    # Il figlio scrive il proprio pid e poi exec il comando: il pid resta valido,
    # così il watcher può attenderne davvero la fine.
    setsid sh -c 'printf "%s\n" "$$" > "$1"; shift; exec sh -c "$1"' \
        sh "$RUN/$_slug.pid" "$_cmd" </dev/null >/dev/null 2>&1 &
    _launcher=$!

    _n=0
    while [ ! -s "$RUN/$_slug.pid" ] && [ "$_n" -lt 25 ]; do
        sleep 0.1 2>/dev/null || sleep 1
        _n=$((_n + 1))
        kill -0 "$_launcher" 2>/dev/null || break
    done
    if [ ! -s "$RUN/$_slug.pid" ]; then
        pkexec "$HELPER" task-end >/dev/null 2>&1
        fail "avvio del comando fallito: $_cmd"
    fi
    _pid=$(readf "$RUN/$_slug.pid")
    printf 'name=%s\nprofile=%s\nslug=%s\nstarted=%s\n' \
        "$_name" "$_prof" "$_slug" "$(date +%s)" > "$RUN/$_slug.info"

    setsid "$SELF_DIR/$(basename "$0")" watch "$_slug" </dev/null >/dev/null 2>&1 &

    printf 'ok=1\nname=%s\nslug=%s\nprofile=%s\npid=%s\n' "$_name" "$_slug" "$_prof" "$_pid"
}

cmd_stop() {                                # stop <nome-o-slug>
    init_tasks
    need_jq
    _slug=$(resolve_slug "${1:-}")
    [ -n "$_slug" ] || fail "task '${1:-}' inesistente"
    is_running "$_slug" || fail "il task '$_slug' non è in esecuzione"
    _pid=$(readf "$RUN/$_slug.pid")
    kill -TERM -- "-$_pid" 2>/dev/null || kill -TERM "$_pid" 2>/dev/null
    printf 'ok=1\nstopped=%s\npid=%s\n' "$_slug" "$_pid"
}

cmd_watch() {                               # watch <slug>   (interno)
    _slug=${1:-}
    [ -n "$_slug" ] || exit 2
    _pid=$(readf "$RUN/$_slug.pid" 2>/dev/null) || exit 0
    while kill -0 "$_pid" 2>/dev/null; do
        sleep 2
    done
    _name=$(sed -n 's/^name=//p' "$RUN/$_slug.info" 2>/dev/null)
    _prof=$(sed -n 's/^profile=//p' "$RUN/$_slug.info" 2>/dev/null)
    rm -f "$RUN/$_slug.pid" "$RUN/$_slug.info"
    if [ -x "$HELPER" ]; then
        pkexec "$HELPER" task-end >/dev/null 2>&1
    fi
    if command -v notify-send >/dev/null 2>&1; then
        notify-send -a "CPUFreq" -i cpu \
            "Task concluso: ${_name:-$_slug}" \
            "Profilo CPU '${_prof:-?}' chiuso, stato precedente ripristinato"
    fi
}

cmd_active() {
    printf 'ok=1\n'
    _found=0
    for _f in "$RUN"/*.pid; do
        [ -r "$_f" ] || continue
        _pid=$(readf "$_f")
        case "$_pid" in ''|*[!0-9]*) continue ;; esac
        kill -0 "$_pid" 2>/dev/null || continue
        _slug=$(basename "$_f" .pid)
        _name=$(sed -n 's/^name=//p' "$RUN/$_slug.info" 2>/dev/null)
        _prof=$(sed -n 's/^profile=//p' "$RUN/$_slug.info" 2>/dev/null)
        printf 'act=%s\t%s\t%s\t%s\n' "$_slug" "${_name:-$_slug}" "${_prof:-?}" "$_pid"
        _found=$((_found + 1))
    done
    printf 'active=%s\n' "$_found"
}

usage() {
    cat <<'EOF'
uso: enne2-cpufreq-task <comando> [argomenti]
  list                          elenco dei task configurati
  add <nome> <profilo> <comando...>
  add-hex <nome> <profilo> <comando>   argomenti in esadecimale UTF-8 (usato dal widget)
  remove <nome-o-slug>
  run <nome-o-slug>             salva lo stato, applica il profilo, esegue, ripristina
  stop <nome-o-slug>            termina il task (il watcher ripristina il profilo)
  active                        task attualmente in esecuzione
EOF
}

[ "$#" -gt 0 ] || { usage; exit 2; }
_CMD=$1
shift
case "$_CMD" in
    list) cmd_list ;;
    add) cmd_add "$@" ;;
    add-hex) cmd_add_hex "$@" ;;
    remove|rm) cmd_remove "$@" ;;
    run|start) cmd_run "$@" ;;
    stop) cmd_stop "$@" ;;
    watch) cmd_watch "$@" ;;
    active) cmd_active ;;
    -h|--help|help) usage ;;
    *) printf 'ok=0\nerror=comando sconosciuto: %s\n' "$_CMD"; usage; exit 2 ;;
esac
