#!/usr/bin/env bash
# =============================================================================
# onx-file-tree — Per-account directory listing with stat metadata
#
# Purpose:
#   `ls -la` equivalent emitting JSON. Used by File Manager UI to populate
#   directory listings with mode, owner, group, size, mtime and symlink target
#   information. Optional shallow recursion up to MAX_DEPTH levels.
#
# Input (stdin JSON):
#   {
#     "username":  "onx_xxxx",         -- required; ^onx_[a-z0-9]{4,12}$
#     "path":      "public_html",      -- required; relative to /home/<user>
#     "depth":     1,                  -- optional; 1..6 (default 1)
#     "show_hidden": true,             -- optional; default true
#     "follow_symlinks": false         -- optional; default false (security)
#   }
#
# Output (stdout JSON):
#   {
#     "path":   "/home/onx_xxxx/public_html",
#     "depth":  1,
#     "count":  17,
#     "items":  [
#       {
#         "name":  "index.html",
#         "rel":   "public_html/index.html",
#         "type":  "file",       # file | dir | symlink
#         "size":  1247,
#         "mode_octal":    "0644",
#         "mode_symbolic": "-rw-r--r--",
#         "owner": "onx_xxxx",
#         "group": "apache",
#         "uid":   10001,
#         "gid":   10001,
#         "mtime": "2026-05-14T18:32:11Z",
#         "atime": "2026-05-15T08:01:33Z",
#         "target": null         # symlink target (null otherwise)
#       },
#       ...
#     ]
#   }
#
# Exit codes: 0=ok 1=invalid-input 2=preflight-fail 3=execution-fail
# Deployed to: /usr/local/onoxsoft/bin/onx-file-tree
# =============================================================================

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 find

onx_json_input

USERNAME="$(onx_json_field username)"
REL_PATH="$(onx_json_field path)"
DEPTH="$(onx_json_field depth 1)"
SHOW_HIDDEN="$(onx_json_get_bool "$INPUT" show_hidden true)"
FOLLOW_SYMLINKS="$(onx_json_get_bool "$INPUT" follow_symlinks false)"

onx_validate_username "$USERNAME"

[[ "$DEPTH" =~ ^[0-9]+$ ]] || onx_die 1 "depth must be integer"
(( DEPTH < 1 ))  && DEPTH=1
(( DEPTH > 6 ))  && DEPTH=6

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

# ── Resolve target path (must stay under HOME_DIR) ──────────────────────────
# realpath -m allows non-existent paths (so callers can validate "is it safe?"
# before mkdir). We then re-verify existence afterwards.
TARGET_INPUT="${HOME_DIR}/${REL_PATH#/}"
[[ -z "${REL_PATH}" ]] && TARGET_INPUT="${HOME_DIR}"

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

[[ -e "${TARGET}" ]] || onx_die 2 "path not found: ${REL_PATH}"
[[ -d "${TARGET}" ]] || onx_die 1 "path is not a directory: ${REL_PATH}"

# ── Build find expression ──────────────────────────────────────────────────
# We avoid -L (follow symlinks) unless explicitly requested. For a flat
# listing (depth=1) we want mindepth=1 maxdepth=1; for shallow recursion we
# keep the user-supplied DEPTH but cap at 6 already.
FIND_FLAGS=()
[[ "$FOLLOW_SYMLINKS" == "true" ]] || FIND_FLAGS+=("-P")
FIND_ARGS=("${TARGET}" -mindepth 1 -maxdepth "${DEPTH}")
[[ "$SHOW_HIDDEN" == "true" ]] || FIND_ARGS+=( '!' -name '.*' )

# ── Collect entries ─────────────────────────────────────────────────────────
TMP_OUT="$(mktemp -t onx-tree.XXXXXX)"
trap '[[ -f "${TMP_OUT}" ]] && rm -f "${TMP_OUT}"' EXIT

# Use NUL separator to survive filenames with newlines or spaces.
# stat format: "%n\0%F\0%s\0%a\0%A\0%U\0%G\0%u\0%g\0%Y\0%X"
# Then we resolve symlink target separately when type=symbolic link.
if ! find "${FIND_FLAGS[@]}" "${FIND_ARGS[@]}" -print0 2>/dev/null \
    | xargs -0 -I {} stat --format='%n|%F|%s|%a|%A|%U|%G|%u|%g|%Y|%X' '{}' \
    > "${TMP_OUT}" 2>/dev/null; then
    : # tolerate partial failures (permission denied on subdirs)
fi

# ── Emit JSON ───────────────────────────────────────────────────────────────
ITEMS_JSON="[]"
if [[ -s "${TMP_OUT}" ]]; then
    LINES=()
    while IFS='|' read -r PNAME PTYPE PSIZE PMODE_O PMODE_S PUSER PGROUP PUID PGID PMTIME PATIME; do
        [[ -z "${PNAME}" ]] && continue

        case "${PTYPE}" in
            "regular file"|"regular empty file") TYPE_STR="file" ;;
            "directory")                          TYPE_STR="dir" ;;
            "symbolic link")                      TYPE_STR="symlink" ;;
            *)                                    TYPE_STR="other" ;;
        esac

        # Symlink target — readlink -m to get the canonical path even if broken.
        TARGET_STR=""
        if [[ "${TYPE_STR}" == "symlink" ]]; then
            TARGET_STR="$(readlink -m "${PNAME}" 2>/dev/null || true)"
        fi

        # Relative path under /home/<user>
        REL="${PNAME#${HOME_DIR}/}"

        # ISO 8601 mtime/atime
        MTIME_ISO="$(date -u -d "@${PMTIME}" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")"
        ATIME_ISO="$(date -u -d "@${PATIME}" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")"

        # Pad mode_octal to 4 digits
        MODE_PADDED="0$(printf '%s' "${PMODE_O}" | tail -c 3)"

        LINES+=("$(jq -nc \
            --arg name   "$(basename "${PNAME}")" \
            --arg rel    "${REL}" \
            --arg type   "${TYPE_STR}" \
            --argjson size "${PSIZE:-0}" \
            --arg mode_octal    "${MODE_PADDED}" \
            --arg mode_symbolic "${PMODE_S}" \
            --arg owner  "${PUSER}" \
            --arg group  "${PGROUP}" \
            --argjson uid "${PUID:-0}" \
            --argjson gid "${PGID:-0}" \
            --arg mtime  "${MTIME_ISO}" \
            --arg atime  "${ATIME_ISO}" \
            --arg target "${TARGET_STR}" \
            '{
                name: $name,
                rel: $rel,
                type: $type,
                size: $size,
                mode_octal: $mode_octal,
                mode_symbolic: $mode_symbolic,
                owner: $owner,
                group: $group,
                uid: $uid,
                gid: $gid,
                mtime: $mtime,
                atime: $atime,
                target: (if $target == "" then null else $target end)
             }')")
    done < "${TMP_OUT}"

    if [[ ${#LINES[@]} -gt 0 ]]; then
        ITEMS_JSON="$(printf '%s\n' "${LINES[@]}" | jq -s '.')"
    fi
fi

COUNT="$(printf '%s' "${ITEMS_JSON}" | jq 'length')"

onx_log "file-tree: user=${USERNAME} path='${REL_PATH}' depth=${DEPTH} count=${COUNT}"

jq -nc \
    --arg path  "${TARGET}" \
    --argjson depth "${DEPTH}" \
    --argjson count "${COUNT}" \
    --argjson items "${ITEMS_JSON}" \
    '{ path: $path, depth: $depth, count: $count, items: $items }'
