#!/usr/bin/env bash
# =============================================================================
# onx-file-edit — Read or write a small text file under /home/<user>
#
# Purpose:
#   File Manager inline editor backend. Two modes:
#     - mode=read  : cat the file (max 5 MiB), return base64 if binary
#     - mode=write : replace file content from stdin "content" field;
#                    creates a .bak-<ts> backup of the previous contents
#
# Input (stdin JSON):
#   {
#     "username":  "onx_xxxx",
#     "path":      "public_html/index.html",
#     "mode":      "read",          -- "read" | "write"
#     "content":   "<new body>",    -- required for mode=write (base64 OK)
#     "encoding":  "utf8",          -- "utf8" | "base64" (write payload)
#     "diff":      false            -- optional; emit diff in write response
#   }
#
# Output (mode=read):
#   {
#     "path":      "/home/onx_xxxx/public_html/index.html",
#     "size":      1247,
#     "encoding":  "utf8",          # or "base64" for binary
#     "binary":    false,
#     "content":   "<file body>",
#     "sha256":    "...",
#     "mtime":     "2026-05-15T12:00:00Z"
#   }
#
# Output (mode=write):
#   {
#     "path":          "/home/onx_xxxx/public_html/index.html",
#     "bytes_written": 1310,
#     "backup_path":   "/home/onx_xxxx/public_html/index.html.bak-1715784000",
#     "sha256":        "...",
#     "diff":          null    # populated when diff=true
#   }
#
# Exit codes: 0=ok 1=invalid-input 2=preflight-fail 3=execution-fail
# Deployed to: /usr/local/onoxsoft/bin/onx-file-edit
# =============================================================================

set -euo pipefail

SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=_lib/common.sh
source "${SCRIPT_DIR}/_lib/common.sh"

require_cmd jq
require_cmd stat
require_cmd sha256sum

MAX_BYTES=5242880   # 5 MiB

onx_json_input

USERNAME="$(onx_json_field username)"
REL_PATH="$(onx_json_field path)"
MODE="$(onx_json_field mode read)"
ENCODING="$(onx_json_field encoding utf8)"
DIFF="$(onx_json_get_bool "$INPUT" diff false)"

onx_validate_username "$USERNAME"
[[ -z "${REL_PATH}" ]] && onx_die 1 "path is required"
[[ "${MODE}" =~ ^(read|write)$ ]] || onx_die 1 "mode must be read|write"
[[ "${ENCODING}" =~ ^(utf8|base64)$ ]] || onx_die 1 "encoding must be utf8|base64"

HOME_DIR="/home/${USERNAME}"
[[ -d "$HOME_DIR" ]] || onx_die 2 "home directory missing: ${HOME_DIR}"

TARGET_INPUT="${HOME_DIR}/${REL_PATH#/}"
TARGET="$(realpath -m "${TARGET_INPUT}" 2>/dev/null || printf '%s' "${TARGET_INPUT}")"
case "${TARGET}" in
    "${HOME_DIR}"|"${HOME_DIR}"/*) ;;
    *) onx_die 1 "path escapes /home/${USERNAME}: ${REL_PATH}" ;;
esac

# ── Read ─────────────────────────────────────────────────────────────────────
if [[ "${MODE}" == "read" ]]; then
    [[ -e "${TARGET}" ]] || onx_die 2 "file not found: ${REL_PATH}"
    [[ -f "${TARGET}" ]] || onx_die 1 "path is not a regular file: ${REL_PATH}"

    SIZE="$(stat -c '%s' "${TARGET}" 2>/dev/null || echo 0)"
    (( SIZE > MAX_BYTES )) && onx_die 1 "file too large for inline edit (${SIZE} > ${MAX_BYTES})"

    MTIME_RAW="$(stat -c '%Y' "${TARGET}" 2>/dev/null || echo 0)"
    MTIME_ISO="$(date -u -d "@${MTIME_RAW}" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")"
    SHA256="$(sha256sum "${TARGET}" 2>/dev/null | awk '{print $1}')"

    # Binary detection: look at first 4 KiB for NUL byte.
    BINARY="false"
    if head -c 4096 "${TARGET}" 2>/dev/null | grep -qP '\x00'; then
        BINARY="true"
    fi

    OUT_ENCODING="utf8"
    if [[ "${BINARY}" == "true" ]]; then
        OUT_ENCODING="base64"
        CONTENT="$(base64 -w0 < "${TARGET}" 2>/dev/null)"
    else
        # Read as-is; jq -Rs will JSON-escape.
        CONTENT="$(cat "${TARGET}")"
    fi

    onx_log "file-edit: read user=${USERNAME} path='${REL_PATH}' size=${SIZE} binary=${BINARY}"

    jq -n \
        --arg path "${TARGET}" \
        --argjson size "${SIZE}" \
        --arg encoding "${OUT_ENCODING}" \
        --argjson binary "$([[ "${BINARY}" == "true" ]] && echo true || echo false)" \
        --arg content "${CONTENT}" \
        --arg sha256 "${SHA256}" \
        --arg mtime "${MTIME_ISO}" \
        '{
            path: $path,
            size: $size,
            encoding: $encoding,
            binary: $binary,
            content: $content,
            sha256: $sha256,
            mtime: $mtime
         }'
    exit 0
fi

# ── Write ────────────────────────────────────────────────────────────────────
# Parse content from input JSON. We allow callers to pass it as either a JSON
# string field (mode=utf8) or base64 (for binary writes).
NEW_CONTENT="$(printf '%s' "$INPUT" | jq -r 'if has("content") then .content else "" end')"
[[ "${NEW_CONTENT}" == "null" ]] && NEW_CONTENT=""

# Refuse to write into a non-file (would clobber directories).
if [[ -e "${TARGET}" && ! -f "${TARGET}" ]]; then
    onx_die 1 "target exists and is not a regular file: ${REL_PATH}"
fi

# Pre-create parent dir if missing.
PARENT="$(dirname "${TARGET}")"
[[ -d "${PARENT}" ]] || mkdir -p "${PARENT}"

# Backup existing file (skip when creating new file).
TS="$(date +%s)"
BACKUP_PATH=""
if [[ -f "${TARGET}" ]]; then
    BACKUP_PATH="${TARGET}.bak-${TS}"
    cp -a -- "${TARGET}" "${BACKUP_PATH}" 2>/dev/null \
        || onx_die 3 "backup failed: ${REL_PATH}"
fi

# Stage new content into a temp file so the rename is atomic.
TMP_NEW="$(mktemp -t onx-edit-new.XXXXXX)"
trap '[[ -f "${TMP_NEW}" ]] && rm -f "${TMP_NEW}"' EXIT

if [[ "${ENCODING}" == "base64" ]]; then
    printf '%s' "${NEW_CONTENT}" | base64 -d > "${TMP_NEW}" 2>/dev/null \
        || onx_die 1 "invalid base64 payload"
else
    printf '%s' "${NEW_CONTENT}" > "${TMP_NEW}"
fi

NEW_SIZE="$(stat -c '%s' "${TMP_NEW}" 2>/dev/null || echo 0)"
(( NEW_SIZE > MAX_BYTES )) && onx_die 1 "payload too large (${NEW_SIZE} > ${MAX_BYTES})"

# Optional diff (only when previous file existed).
DIFF_JSON="null"
if [[ "${DIFF}" == "true" && -n "${BACKUP_PATH}" ]] && command -v diff >/dev/null 2>&1; then
    DIFF_TEXT="$(diff -u "${BACKUP_PATH}" "${TMP_NEW}" 2>/dev/null || true)"
    # Cap at 64 KiB to keep response sane.
    DIFF_TEXT="${DIFF_TEXT:0:65536}"
    DIFF_JSON="$(printf '%s' "${DIFF_TEXT}" | jq -Rs '.')"
fi

# Atomic replace.
mv -f -- "${TMP_NEW}" "${TARGET}" 2>/dev/null \
    || onx_die 3 "atomic write failed: ${REL_PATH}"
trap - EXIT  # tmp file already consumed by mv

# Preserve owner (chown to home owner) — only effective when running as root.
HOME_UID="$(stat -c '%u' "${HOME_DIR}" 2>/dev/null || echo "")"
HOME_GID="$(stat -c '%g' "${HOME_DIR}" 2>/dev/null || echo "")"
if [[ -n "${HOME_UID}" && -n "${HOME_GID}" ]]; then
    chown "${HOME_UID}:${HOME_GID}" "${TARGET}" 2>/dev/null || true
fi
chmod 0644 "${TARGET}" 2>/dev/null || true

SHA256="$(sha256sum "${TARGET}" 2>/dev/null | awk '{print $1}')"

onx_log "file-edit: write user=${USERNAME} path='${REL_PATH}' bytes=${NEW_SIZE} backup='${BACKUP_PATH}'"

jq -nc \
    --arg path "${TARGET}" \
    --argjson bytes_written "${NEW_SIZE}" \
    --arg backup_path "${BACKUP_PATH}" \
    --arg sha256 "${SHA256}" \
    --argjson diff "${DIFF_JSON}" \
    '{
        path: $path,
        bytes_written: $bytes_written,
        backup_path: (if $backup_path == "" then null else $backup_path end),
        sha256: $sha256,
        diff: $diff
     }'
