#!/bin/bash

# Routes a CLI command through the existing libreportal task processor and
# follows its log in the foreground, so the terminal sees the same output
# the WebUI sees and Ctrl-C detaches cleanly without killing the task.
#
# Usage:
#   cliTaskRun "libreportal app install dashy" install dashy
#   cliTaskRun "libreportal app install dashy" install dashy --detach
#
# Sets up a task file that's identical to what the WebUI's "install" button
# produces — same format, same task dir, same processor. The CLI just adds
# itself as a viewer.
#
# How the command bypass works: the recursive command in the task file is
# prefixed with `LIBREPORTAL_TASK_EXEC=1`, and the caller (the app install
# handler) honours that by running inline instead of re-enqueueing. No
# processor changes required — the bypass travels with the task.

# Resolve task dir, FIFO, the manifest of tasks dir/log paths. Mirrors what
# crontab_task_processor.sh and webui_task_create.sh use; if those move, both
# this file and they have to update together.
_taskDir() {
    echo "${containers_dir}libreportal/frontend/data/tasks"
}

_genTaskId() {
    local rand
    rand=$(openssl rand -hex 4 2>/dev/null || printf '%08x' $RANDOM)
    echo "task_$(date +%s)_${rand}"
}

# Write the task file + append to queue.json. Identical shape to
# createTaskFile in webui_task_create.sh, but inlined here so the CLI doesn't
# have to source the WebUI generators when LP_LAZY=1 hasn't pulled them in.
_writeTaskFile() {
    local task_id="$1" command="$2" task_type="$3" app_name="$4"
    local task_dir; task_dir=$(_taskDir)
    local task_file="$task_dir/${task_id}.json"
    # ISO-8601 UTC with milliseconds. Matches the format the WebUI's
    # tasks-manager.js emits (Date.now() → toISOString()), so the tasks
    # panel's `new Date(task.createdAt).toLocaleString()` renders correctly.
    # Falls back to seconds-precision if %3N isn't supported (busybox).
    local created_at
    created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ" 2>/dev/null) \
        || created_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

    [[ -d "$task_dir" ]] || runFileOp mkdir -p "$task_dir"

    # CamelCase initial fields match the WebUI's task shape; processor adds
    # snake_case status fields on top as it runs. Both task-manager.js and
    # tasks-manager.js read camelCase directly (no alias needed for createdAt).
    local task_json
    if command -v jq >/dev/null 2>&1; then
        task_json=$(jq -n \
            --arg id "$task_id" \
            --arg command "$command" \
            --arg status queued \
            --arg createdAt "$created_at" \
            --arg type "${task_type:-}" \
            --arg app "${app_name:-}" \
            '{id:$id, command:$command, status:$status, createdAt:$createdAt,
              startedAt:null, completedAt:null, heartbeatAt:null,
              exitCode:null, errorMessage:null}
             + (if $type != "" then {type:$type} else {} end)
             + (if $app  != "" then {app:$app}   else {} end)')
    else
        # Plain string interpolation — command is the only field that needs
        # escaping. Strip control chars; the dispatcher uses ascii only so
        # this is sufficient without pulling jq in.
        local esc="${command//\\/\\\\}"; esc="${esc//\"/\\\"}"
        task_json="{\"id\":\"$task_id\",\"command\":\"$esc\",\"status\":\"queued\",\"createdAt\":\"$created_at\""
        task_json+=",\"startedAt\":null,\"completedAt\":null,\"heartbeatAt\":null"
        task_json+=",\"exitCode\":null,\"errorMessage\":null"
        [[ -n "$task_type" ]] && task_json+=",\"type\":\"$task_type\""
        [[ -n "$app_name"  ]] && task_json+=",\"app\":\"$app_name\""
        task_json+="}"
    fi

    printf '%s' "$task_json" | runFileWrite "$task_file"
    runFileOp chmod 644 "$task_file" 2>/dev/null

    local queue="$task_dir/queue.json"
    [[ -f "$queue" ]] || printf '[]' | runFileWrite "$queue"
    if command -v jq >/dev/null 2>&1; then
        local updated; updated=$(jq --arg id "$task_id" '. + [$id]' "$queue" 2>/dev/null || echo "[\"$task_id\"]")
        printf '%s' "$updated" | runFileWrite "$queue"
    else
        # Best-effort fallback.
        local cur; cur=$(cat "$queue" 2>/dev/null)
        if [[ "$cur" == "[]" || -z "$cur" ]]; then
            printf '["%s"]' "$task_id" | runFileWrite "$queue"
        else
            printf '%s' "${cur%]}, \"$task_id\"]" | runFileWrite "$queue"
        fi
    fi

    # Poke the FIFO so the processor dispatches immediately instead of
    # waiting up to IDLE_POLL_SECS (3s) for its next read timeout.
    local fifo="$task_dir/.queue.fifo"
    [[ -p "$fifo" ]] && printf '%s\n' "$task_id" >> "$fifo" 2>/dev/null || true
}

# Follow a task's log + status until terminal state.
# Returns the task's exit_code (0 on completed, non-zero on failed/cancelled).
# Ctrl-C from the user detaches the follower without cancelling the task.
cliTaskFollow() {
    local task_id="$1"
    local task_dir; task_dir=$(_taskDir)
    local task_file="$task_dir/${task_id}.json"
    local log_file="$task_dir/${task_id}.log"

    [[ -f "$task_file" ]] || { isError "Task $task_id not found."; return 1; }

    # Ctrl-C detaches us, NOT the task. Print a hint and exit 0 — the
    # processor keeps running in the background and the WebUI continues
    # to show the task. `libreportal task log $task_id` (future) can
    # reattach.
    local detached=0
    trap 'detached=1' INT

    # tail -F (capital): retry-on-error, so we can start tailing before
    # the log file exists. Processor creates it just before exec.
    tail -n +1 -F "$log_file" 2>/dev/null &
    local tail_pid=$!

    # Poll the task file until status hits a terminal state OR the user
    # detaches. 0.5s polling — bash + jq is cheap and the UX feels live.
    local status="queued" exit_code=0
    while (( detached == 0 )); do
        if command -v jq >/dev/null 2>&1; then
            status=$(jq -r '.status // "queued"' "$task_file" 2>/dev/null)
        else
            status=$(grep -oE '"status"[[:space:]]*:[[:space:]]*"[^"]+"' "$task_file" 2>/dev/null | sed 's/.*"\([^"]*\)"$/\1/')
        fi
        case "$status" in
            completed|failed|cancelled) break ;;
        esac
        sleep 0.5
    done
    trap - INT

    # Let tail flush the final output buffered behind the log writer, then
    # kill it cleanly.
    sleep 0.3
    kill "$tail_pid" 2>/dev/null
    wait "$tail_pid" 2>/dev/null

    if (( detached )); then
        echo ""
        isNotice "Detached from $task_id. Task continues in the background; the WebUI tasks panel will show its progress."
        return 0
    fi

    if command -v jq >/dev/null 2>&1; then
        exit_code=$(jq -r '.exit_code // 0' "$task_file" 2>/dev/null)
    fi
    case "$status" in
        completed) return "${exit_code:-0}" ;;
        failed)    return "${exit_code:-1}" ;;
        cancelled) return 130 ;;
    esac
    return 0
}

# Enqueue a command and follow until terminal state (or --detach).
# Args: command_string task_type app_name [--detach]
cliTaskRun() {
    local command="$1" task_type="$2" app_name="$3" mode="$4"

    if [[ -z "$command" ]]; then
        isError "cliTaskRun: empty command."
        return 1
    fi

    local task_id; task_id=$(_genTaskId)
    _writeTaskFile "$task_id" "$command" "$task_type" "$app_name"

    if [[ "$mode" == "--detach" ]]; then
        isSuccessful "Task queued: $task_id"
        isNotice "Detached — track progress in the WebUI tasks panel or via 'libreportal task log $task_id' (future)."
        return 0
    fi

    isNotice "Queued task $task_id ($task_type${app_name:+ $app_name}). Following live output — Ctrl-C detaches without cancelling."
    echo ""
    cliTaskFollow "$task_id"
}
