#!/usr/bin/env bash
# audit_logger.sh — SCLS Node-Side Encrypted Audit Logger
# Sourced by common.sh. Provides audit_log_event() and audit_wrap().
#
# Encrypts tool execution statements with per-node AES-256-CBC keys,
# POSTs to Engine audit API (forge tunnel primary, WG fallback),
# spools on failure for later retry via audit_flush_spool().
#
# HMAC format: ts.nonce.METHOD.path.body (canonical dot-separated)

[[ -n "${_AUDIT_LOGGER_LOADED:-}" ]] && return 0
readonly _AUDIT_LOGGER_LOADED=1

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
readonly AUDIT_API_URL_PRIMARY="https://172.28.50.1:8301/api/audit/log"
readonly AUDIT_API_URL_FALLBACK="https://10.69.69.6:8301/api/audit/log"
readonly AUDIT_KEY_FILE="/opt/binary/config/.k7x9m2/audit.key"
readonly AUDIT_SPOOL_DIR="/opt/binary/data/audit-spool"

# Node identity — prefer env, then on-disk file
AUDIT_NODE_ID="${NODE_ID:-}"
if [[ -z "$AUDIT_NODE_ID" ]] && [[ -f /etc/binary/node_id ]]; then
    AUDIT_NODE_ID="$(< /etc/binary/node_id)"
fi
readonly AUDIT_NODE_ID

# Ensure spool directory exists
[[ -d "$AUDIT_SPOOL_DIR" ]] || mkdir -p "$AUDIT_SPOOL_DIR" 2>/dev/null || true

# ---------------------------------------------------------------------------
# _audit_ts — ISO-8601 timestamp with nanosecond precision
# ---------------------------------------------------------------------------
_audit_ts() {
    date -u '+%Y-%m-%dT%H:%M:%S.%NZ' 2>/dev/null || date -u '+%Y-%m-%dT%H:%M:%SZ'
}

# ---------------------------------------------------------------------------
# _audit_log — internal logger (stderr, does NOT recurse into audit)
# ---------------------------------------------------------------------------
_audit_log() {
    local level="$1"; shift
    printf '[%s] [audit_logger] [%s] %s\n' "$(_audit_ts)" "$level" "$*" >&2
}

# ---------------------------------------------------------------------------
# audit_encrypt(plaintext, key_file)
# Outputs: IV_HEX:ENCRYPTED_B64
# ---------------------------------------------------------------------------
audit_encrypt() {
    local plaintext="$1"
    local key_file="${2:-$AUDIT_KEY_FILE}"

    if [[ ! -f "$key_file" ]]; then
        _audit_log "ERROR" "Encryption key file not found: $key_file"
        return 1
    fi

    local KEY_HEX
    KEY_HEX="$(tr -d '[:space:]' < "$key_file")"
    if [[ ${#KEY_HEX} -ne 64 ]]; then
        _audit_log "ERROR" "Key must be 64 hex chars (256-bit), got ${#KEY_HEX}"
        return 1
    fi

    local IV_HEX
    IV_HEX="$(openssl rand -hex 16)" || {
        _audit_log "ERROR" "Failed to generate IV"
        return 1
    }

    local ENCRYPTED_B64
    ENCRYPTED_B64="$(printf '%s' "$plaintext" \
        | gzip -c -9 \
        | openssl enc -aes-256-cbc -K "$KEY_HEX" -iv "$IV_HEX" -nosalt 2>/dev/null \
        | base64 -w 0)" || {
        _audit_log "ERROR" "Encryption pipeline failed"
        return 1
    }

    echo "${IV_HEX}:${ENCRYPTED_B64}"
}

# ---------------------------------------------------------------------------
# audit_hmac(ts, nonce, method, path, body, key_file)
# Outputs: 64-char hex HMAC-SHA256
# Canonical format: ts.nonce.METHOD.path.body
# ---------------------------------------------------------------------------
audit_hmac() {
    local ts="$1"
    local nonce="$2"
    local method="$3"
    local path="$4"
    local body="$5"
    local key_file="${6:-$AUDIT_KEY_FILE}"

    if [[ ! -f "$key_file" ]]; then
        _audit_log "ERROR" "HMAC key file not found: $key_file"
        return 1
    fi

    local KEY_HEX
    KEY_HEX="$(tr -d '[:space:]' < "$key_file")"

    # Canonical dot-separated HMAC input
    local input="${ts}.${nonce}.${method}.${path}.${body}"

    local hmac_out
    hmac_out="$(printf '%s' "$input" \
        | openssl dgst -sha256 -hmac "$KEY_HEX" -hex 2>/dev/null \
        | awk '{print $NF}')" || {
        _audit_log "ERROR" "HMAC computation failed"
        return 1
    }

    echo "$hmac_out"
}

# ---------------------------------------------------------------------------
# _audit_post(url, payload_json, ts, nonce, sig)
# Returns 0 on HTTP 2xx, 1 otherwise
# ---------------------------------------------------------------------------
_audit_post() {
    local url="$1"
    local payload="$2"
    local ts="$3"
    local nonce="$4"
    local sig="$5"

    local http_code
    http_code="$(curl -s -o /dev/null -w '%{http_code}' \
        --connect-timeout 5 \
        --max-time 15 \
        -X POST \
        -H "Content-Type: application/json" \
        -H "x-engine-ts: ${ts}" \
        -H "x-engine-nonce: ${nonce}" \
        -H "x-engine-sig: ${sig}" \
        -H "x-engine-node: ${AUDIT_NODE_ID}" \
        -d "$payload" \
        --cacert /opt/binary/config/tls/ca.pem \
        "$url" 2>/dev/null)" || return 1

    [[ "$http_code" =~ ^2[0-9]{2}$ ]]
}

# ---------------------------------------------------------------------------
# _audit_spool(payload_json)
# Writes failed payload to spool directory for later retry
# ---------------------------------------------------------------------------
_audit_spool() {
    local payload="$1"
    local spool_file="${AUDIT_SPOOL_DIR}/audit_$(date -u '+%s%N')_$$.json"

    if printf '%s\n' "$payload" > "$spool_file" 2>/dev/null; then
        chmod 600 "$spool_file" 2>/dev/null
        _audit_log "WARN" "Spooled audit event to ${spool_file}"
    else
        _audit_log "ERROR" "Failed to spool audit event"
    fi
}

# ---------------------------------------------------------------------------
# audit_log_event(tool_id, tool_name, statement_json, session_id,
#                 trust_score, exit_code, duration_ms,
#                 started_at, completed_at)
# Encrypts statement, POSTs async (forge primary, WG fallback), spools on fail
# ---------------------------------------------------------------------------
audit_log_event() {
    local tool_id="${1:-unknown}"
    local tool_name="${2:-unknown}"
    local statement_json="${3:-{}}"
    local session_id="${4:-}"
    local trust_score="${5:-0}"
    local exit_code="${6:-0}"
    local duration_ms="${7:-0}"
    local started_at="${8:-}"
    local completed_at="${9:-}"

    # Run the entire submission in a subshell, backgrounded
    (
        # Generate event UUID
        local event_uuid
        if [[ -f /proc/sys/kernel/random/uuid ]]; then
            event_uuid="$(< /proc/sys/kernel/random/uuid)"
        else
            local raw_hex
            raw_hex="$(openssl rand -hex 16)"
            event_uuid="${raw_hex:0:8}-${raw_hex:8:4}-${raw_hex:12:4}-${raw_hex:16:4}-${raw_hex:20:12}"
        fi

        # Encrypt the statement
        local encrypted_blob
        encrypted_blob="$(audit_encrypt "$statement_json")" || {
            _audit_log "ERROR" "Encryption failed for event ${event_uuid}"
            return 1
        }

        local encryption_iv="${encrypted_blob%%:*}"
        local encrypted_statement="${encrypted_blob#*:}"

        # Build JSON payload
        local payload
        payload="$(cat <<EOJSON
{
  "event_uuid": "${event_uuid}",
  "node_id": "${AUDIT_NODE_ID}",
  "tool_id": "${tool_id}",
  "tool_name": "${tool_name}",
  "encrypted_statement": "${encrypted_statement}",
  "encryption_iv": "${encryption_iv}",
  "session_id": "${session_id}",
  "trust_score": ${trust_score},
  "exit_code": ${exit_code},
  "duration_ms": ${duration_ms},
  "started_at": "${started_at}",
  "completed_at": "${completed_at}"
}
EOJSON
)"

        # Generate HMAC signature
        local ts nonce sig
        ts="$(date -u '+%s')"
        nonce="$(openssl rand -hex 16)"

        sig="$(audit_hmac "$ts" "$nonce" "POST" "/api/audit/log" "$payload")" || {
            _audit_log "ERROR" "HMAC generation failed for event ${event_uuid}"
            _audit_spool "$payload"
            return 1
        }

        # Try forge tunnel first, then WG fallback
        if _audit_post "$AUDIT_API_URL_PRIMARY" "$payload" "$ts" "$nonce" "$sig"; then
            _audit_log "INFO" "Audit event ${event_uuid} sent via forge tunnel"
            return 0
        fi

        _audit_log "WARN" "Forge tunnel failed for ${event_uuid}, trying WG fallback"

        if _audit_post "$AUDIT_API_URL_FALLBACK" "$payload" "$ts" "$nonce" "$sig"; then
            _audit_log "INFO" "Audit event ${event_uuid} sent via WG fallback"
            return 0
        fi

        _audit_log "ERROR" "All endpoints failed for event ${event_uuid}, spooling"
        _audit_spool "$payload"
    ) &
    disown 2>/dev/null
}

# ---------------------------------------------------------------------------
# audit_wrap(func_label, function_to_call, args...)
# Transparent wrapper: captures timing/exit, logs encrypted audit event,
# also calls log_to_db() for plaintext supplement. Returns original exit code.
# ---------------------------------------------------------------------------
audit_wrap() {
    local func_label="$1"; shift
    local function_to_call="$1"; shift

    local started_at duration_ms completed_at
    local stdout_file stderr_file
    local rc=0

    started_at="$(_audit_ts)"
    local start_ns
    start_ns="$(date +%s%N 2>/dev/null || echo 0)"

    # Temp files for capture
    stdout_file="$(mktemp /tmp/audit_stdout.XXXXXX)"
    stderr_file="$(mktemp /tmp/audit_stderr.XXXXXX)"

    # Execute the wrapped function
    "$function_to_call" "$@" >"$stdout_file" 2>"$stderr_file" || rc=$?

    completed_at="$(_audit_ts)"
    local end_ns
    end_ns="$(date +%s%N 2>/dev/null || echo 0)"

    # Calculate duration in milliseconds
    if [[ "$start_ns" != "0" ]] && [[ "$end_ns" != "0" ]]; then
        duration_ms="$(( (end_ns - start_ns) / 1000000 ))"
    else
        duration_ms=0
    fi

    # Capture output snippets (first 500 chars)
    local stdout_snippet stderr_snippet
    stdout_snippet="$(head -c 500 "$stdout_file" 2>/dev/null | tr -d '\000' || echo '')"
    stderr_snippet="$(head -c 500 "$stderr_file" 2>/dev/null | tr -d '\000' || echo '')"

    # Replay captured output to real stdout/stderr
    cat "$stdout_file" 2>/dev/null
    cat "$stderr_file" >&2 2>/dev/null

    # Build statement JSON — escape for JSON safety
    local escaped_args escaped_stdout escaped_stderr escaped_dir
    escaped_args="$(printf '%s ' "$@" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | head -c 500)"
    escaped_stdout="$(printf '%s' "$stdout_snippet" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\n/\\n/g')"
    escaped_stderr="$(printf '%s' "$stderr_snippet" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\n/\\n/g')"
    escaped_dir="$(pwd | sed 's/\\/\\\\/g; s/"/\\"/g')"

    local statement_json
    statement_json="$(cat <<EOSTMT
{
  "tool_id": "${func_label}",
  "command": "${function_to_call}",
  "args": "${escaped_args}",
  "stdout_snippet": "${escaped_stdout}",
  "stderr": "${escaped_stderr}",
  "working_dir": "${escaped_dir}",
  "started_at": "${started_at}",
  "completed_at": "${completed_at}"
}
EOSTMT
)"

    # Generate a tool-id hash
    local tool_id
    tool_id="$(printf '%s' "${func_label}" | openssl dgst -sha256 -hex 2>/dev/null | awk '{print $NF}' | head -c 12)"

    # Submit encrypted audit event (async)
    audit_log_event \
        "$tool_id" \
        "$func_label" \
        "$statement_json" \
        "${SESSION_ID:-}" \
        "${TRUST_SCORE:-0}" \
        "$rc" \
        "$duration_ms" \
        "$started_at" \
        "$completed_at"

    # Plaintext supplement via log_to_db (if available)
    if declare -f log_to_db &>/dev/null; then
        log_to_db "audit_wrap" "${func_label}: exit=${rc} duration=${duration_ms}ms" 2>/dev/null || true
    fi

    # Cleanup temp files
    rm -f "$stdout_file" "$stderr_file" 2>/dev/null

    return "$rc"
}

# ---------------------------------------------------------------------------
# audit_flush_spool()
# Retries spooled audit events. Called by telemetry-cron.sh.
# ---------------------------------------------------------------------------
audit_flush_spool() {
    if [[ ! -d "$AUDIT_SPOOL_DIR" ]]; then
        _audit_log "INFO" "No spool directory, nothing to flush"
        return 0
    fi

    local spool_files
    spool_files=( "$AUDIT_SPOOL_DIR"/*.json )

    # Check if glob matched anything
    if [[ ${#spool_files[@]} -eq 0 ]] || [[ ! -e "${spool_files[0]}" ]]; then
        _audit_log "INFO" "No spooled events to flush"
        return 0
    fi

    local flushed=0 failed=0

    for spool_file in "${spool_files[@]}"; do
        [[ -f "$spool_file" ]] || continue

        local payload
        payload="$(< "$spool_file")" || {
            _audit_log "ERROR" "Failed to read spool file: ${spool_file}"
            (( failed++ ))
            continue
        }

        # Generate fresh HMAC for retry
        local ts nonce sig
        ts="$(date -u '+%s')"
        nonce="$(openssl rand -hex 16)"

        sig="$(audit_hmac "$ts" "$nonce" "POST" "/api/audit/log" "$payload" 2>/dev/null)" || {
            _audit_log "ERROR" "HMAC failed for spooled event: ${spool_file}"
            (( failed++ ))
            continue
        }

        # Try forge, then WG
        if _audit_post "$AUDIT_API_URL_PRIMARY" "$payload" "$ts" "$nonce" "$sig" ||
           _audit_post "$AUDIT_API_URL_FALLBACK" "$payload" "$ts" "$nonce" "$sig"; then
            rm -f "$spool_file" 2>/dev/null
            (( flushed++ ))
            _audit_log "INFO" "Flushed spooled event: $(basename "$spool_file")"
        else
            (( failed++ ))
            _audit_log "WARN" "Still failing for spooled event: $(basename "$spool_file")"
        fi
    done

    _audit_log "INFO" "Spool flush complete: ${flushed} flushed, ${failed} failed"
}
