#!/bin/bash
# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT

# =============================================================================
# OmniClaude Hooks - Shared Shell Functions
# =============================================================================
# Common utility functions for all hook scripts.
# Source this file at the top of hook scripts after setting PLUGIN_ROOT.
#
# Usage:
#   source "${HOOKS_DIR}/scripts/common.sh"
#
# Requires (must be set before sourcing):
#   - PLUGIN_ROOT: Path to the plugin root directory
#   - PROJECT_ROOT: Path to project root (used for .env loading, not for Python)
#
# Exports after sourcing:
#   - PYTHON_CMD: Resolved Python interpreter (hard fails if not found)
#   - KAFKA_ENABLED: "true" or "false"
# =============================================================================

# =============================================================================
# Python Environment Detection
# =============================================================================
# Strict priority chain with NO fallbacks. If no valid Python is found,
# hooks refuse to run. This prevents silent degradation where hooks run
# against the wrong interpreter with missing dependencies.
#
# Priority:
#   1. PLUGIN_PYTHON_BIN env var (explicit override / escape hatch)
#   2. Plugin-bundled venv at PLUGIN_ROOT/lib/.venv (marketplace runtime)
#   3. OMNICLAUDE_PROJECT_ROOT/.venv (explicit dev mode, no heuristics)
#   4. Hard failure with actionable error message

find_python() {
    # 1. Explicit override (escape hatch for custom environments)
    if [[ -n "${PLUGIN_PYTHON_BIN:-}" && -f "${PLUGIN_PYTHON_BIN}" && -x "${PLUGIN_PYTHON_BIN}" ]]; then
        echo "${PLUGIN_PYTHON_BIN}"
        return
    fi

    # 2. Plugin-bundled venv (marketplace runtime — created by deploy.sh)
    if [[ -f "${PLUGIN_ROOT}/lib/.venv/bin/python3" && -x "${PLUGIN_ROOT}/lib/.venv/bin/python3" ]]; then
        echo "${PLUGIN_ROOT}/lib/.venv/bin/python3"
        return
    fi

    # 3. Explicit dev-mode project venv (no heuristics, no CWD probing)
    if [[ -n "${OMNICLAUDE_PROJECT_ROOT:-}" && -f "${OMNICLAUDE_PROJECT_ROOT}/.venv/bin/python3" && -x "${OMNICLAUDE_PROJECT_ROOT}/.venv/bin/python3" ]]; then
        echo "${OMNICLAUDE_PROJECT_ROOT}/.venv/bin/python3"
        return
    fi

    # 4. Lite mode: accept system Python when mode is lite (or mode.sh absent)
    if command -v python3 &>/dev/null; then
        local mode_sh
        mode_sh="$(dirname "${BASH_SOURCE[0]}")/../../lib/mode.sh"
        if [[ -f "$mode_sh" ]]; then
            # shellcheck disable=SC1090
            source "$mode_sh"
            if [[ "$(omniclaude_mode)" == "lite" ]]; then
                echo "python3"
                return
            fi
        else
            # mode.sh absent (e.g., incomplete deploy, container install):
            # default to lite — accept system Python rather than hard-failing.
            # WARNING: if this is a broken full-mode deploy, hooks will run against
            # system Python which may lack omniclaude imports. Log for visibility.
            echo "WARN: mode.sh not found at ${mode_sh}; defaulting to lite mode (system python3)" >&2
            echo "python3"
            return
        fi
    fi

    # No fallback: return empty to trigger hard failure
    echo ""
}

# =============================================================================
# Venv Verification Helper (OMN-3729)
# =============================================================================
# Reusable guard for any script that copies files near or inside the venv.
# Returns 0 when the venv looks healthy, 1 with a warning on stderr otherwise.
#
# Usage:
#   verify_venv_or_warn "/path/to/.venv"  || return 1
#   verify_venv_or_warn "${PLUGIN_ROOT}/lib/.venv" || echo "skipping venv-dependent step"

verify_venv_or_warn() {
    local venv_dir="$1"
    if [[ ! -f "${venv_dir}/bin/python3" || ! -x "${venv_dir}/bin/python3" ]]; then
        echo "WARN: Venv missing or broken at ${venv_dir}. Run: deploy.sh --repair-venv" 1>&2
        return 1
    fi
    return 0
}

# =============================================================================
# Inline Venv Auto-Repair (OMN-3726)
# =============================================================================
# If find_python() returns empty, attempt to create a minimal venv at
# PLUGIN_ROOT/lib/.venv using system python3. This recovers from the common
# failure mode where deploy.sh ran but the venv was deleted or corrupted.
#
# Design:
#   - python3 -m venv is synchronous (~200ms) — creates a usable interpreter
#   - pip install of omniclaude is fully backgrounded (logs to /tmp)
#   - Rate-limited via /tmp/omniclaude-venv-repair-failed marker (5 min cooldown)
#   - Writes .omniclaude-sentinel timestamp on successful venv creation
#
# Returns: path to the newly created python3 interpreter, or empty string

_try_inline_venv_repair() {
    local venv_dir="${PLUGIN_ROOT}/lib/.venv"
    local repair_failed_marker="/tmp/omniclaude-venv-repair-failed"
    local repair_log="/tmp/omniclaude-venv-repair.log"
    local sentinel="${venv_dir}/.omniclaude-sentinel"

    # Rate-limit: skip if a previous repair failed less than 5 minutes ago
    if [[ -f "$repair_failed_marker" ]]; then
        local marker_ts
        marker_ts=$(stat -f '%m' "$repair_failed_marker" 2>/dev/null \
            || stat -c '%Y' "$repair_failed_marker" 2>/dev/null \
            || echo 0)
        local now
        now=$(date +%s)
        if (( now - marker_ts < 300 )); then
            echo ""
            return 1
        fi
        # Cooldown expired, remove stale marker
        rm -f "$repair_failed_marker" 2>/dev/null || true
    fi

    # Find system python3 (must not be inside our own broken venv)
    local sys_python=""
    local candidate
    # Fast-path candidates: common locations on macOS + Linux containers.
    # The PATH fallback search below handles all other locations.
    for candidate in /usr/bin/python3 /usr/local/bin/python3 /opt/homebrew/bin/python3 /usr/bin/python3.11 /usr/bin/python3.12 /usr/bin/python3.13; do
        if [[ -x "$candidate" ]]; then
            sys_python="$candidate"
            break
        fi
    done

    # Fallback: search PATH but exclude PLUGIN_ROOT to avoid circular reference
    if [[ -z "$sys_python" ]]; then
        local IFS=':'
        local p
        for p in $PATH; do
            # Skip paths inside our own plugin tree
            [[ "$p" == "${PLUGIN_ROOT}"* ]] && continue
            if [[ -x "${p}/python3" ]]; then
                sys_python="${p}/python3"
                break
            fi
        done
        unset IFS
    fi

    if [[ -z "$sys_python" ]]; then
        # No system python3 available — mark failure and bail
        touch "$repair_failed_marker" 2>/dev/null || true
        echo ""
        return 1
    fi

    # Determine venv creation strategy: prefer python3 -m venv, fallback to uv
    local venv_strategy=""
    if "$sys_python" -m venv --help >/dev/null 2>&1; then
        venv_strategy="python-venv"
    elif command -v uv &>/dev/null; then
        venv_strategy="uv"
    else
        # Neither python3 -m venv nor uv available
        touch "$repair_failed_marker" 2>/dev/null || true
        echo ""
        return 1
    fi

    # Create the venv directory tree if needed
    mkdir -p "${PLUGIN_ROOT}/lib" 2>/dev/null || {
        touch "$repair_failed_marker" 2>/dev/null || true
        echo ""
        return 1
    }

    # Create minimal venv (~200ms synchronous)
    echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] Auto-repair: creating venv at ${venv_dir} (strategy: ${venv_strategy})" >> "$repair_log" 2>/dev/null || true
    local venv_ok=false
    if [[ "$venv_strategy" == "python-venv" ]]; then
        "$sys_python" -m venv "$venv_dir" >> "$repair_log" 2>&1 && venv_ok=true
    elif [[ "$venv_strategy" == "uv" ]]; then
        uv venv "$venv_dir" >> "$repair_log" 2>&1 && venv_ok=true
    fi
    if [[ "$venv_ok" != "true" ]]; then
        echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] Auto-repair: venv creation failed (strategy: ${venv_strategy})" >> "$repair_log" 2>/dev/null || true
        touch "$repair_failed_marker" 2>/dev/null || true
        echo ""
        return 1
    fi

    local repaired_python="${venv_dir}/bin/python3"
    if [[ ! -x "$repaired_python" ]]; then
        touch "$repair_failed_marker" 2>/dev/null || true
        echo ""
        return 1
    fi

    # Write sentinel with creation timestamp
    date -u +"%Y-%m-%dT%H:%M:%SZ" > "$sentinel" 2>/dev/null || true

    # Remove any stale failure marker (repair succeeded)
    rm -f "$repair_failed_marker" 2>/dev/null || true

    # Background: install omniclaude into the repaired venv
    # This is non-blocking — the venv python3 is already usable for hooks.
    # Uses venv_strategy (not re-probing PATH) to select the correct install tool.
    # uv-created venvs have no pip, so pip install would fail without this guard.
    (
        _bg_strategy="$venv_strategy"  # capture before any PATH changes
        _bg_python="$repaired_python"
        _bg_root="$PLUGIN_ROOT"
        _bg_log="$repair_log"
        echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] Auto-repair: background install starting (strategy: ${_bg_strategy})" >> "$_bg_log" 2>/dev/null || true

        # _bg_strategy is captured from the enclosing subshell scope — do not
        # extract this function into a separate script without passing it explicitly.
        _install_pkg() {
            local flag="$1" target="$2"
            if [[ "$_bg_strategy" == "uv" ]]; then
                uv pip install --quiet "$flag" "$target" >> "$_bg_log" 2>&1 || true
            elif "$_bg_python" -m pip --version &>/dev/null 2>&1; then
                "$_bg_python" -m pip install --quiet --disable-pip-version-check "$flag" "$target" >> "$_bg_log" 2>&1 || true
            else
                echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] Auto-repair: no install tool available" >> "$_bg_log" 2>/dev/null || true
            fi
        }

        if [[ -f "${_bg_root}/pyproject.toml" ]]; then
            _install_pkg "-e" "${_bg_root}"
        elif [[ -f "${_bg_root}/requirements.txt" ]]; then
            _install_pkg "-r" "${_bg_root}/requirements.txt"
        fi
        echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] Auto-repair: background install finished" >> "$_bg_log" 2>/dev/null || true
    ) &

    echo "$repaired_python"
    return 0
}

# Resolve Python — hard fail if not found (unless advisory hook)
# NOTE: This exit 1 intentionally violates the "hooks exit 0" invariant (CLAUDE.md).
# Rationale: running hooks against the wrong Python produces non-reproducible bugs
# that are far worse than a visible, actionable error. See OMN-2051.
#
# OMN-3725: Advisory hooks (session-end, stop, pre-compact, post-tool-use-quality)
# exit 0 gracefully when Python is missing. Critical hooks still hard-fail.
# The advisory allowlist is checked via BASH_SOURCE[1] (the sourcing script)
# to prevent env-var-only spoofing of OMNICLAUDE_HOOK_CRITICALITY.
PYTHON_CMD="$(find_python)"
if [[ -z "${PYTHON_CMD}" ]]; then
    # Attempt inline venv repair before hard-failing (OMN-3726)
    PYTHON_CMD="$(_try_inline_venv_repair 2>/dev/null)" || PYTHON_CMD=""
fi
if [[ -z "${PYTHON_CMD}" ]]; then
    # OMN-3725: Advisory hooks exit gracefully when Python is missing
    _hook_base="$(basename "${BASH_SOURCE[1]:-}" 2>/dev/null || echo "")"
    _advisory_ok=false
    case "$_hook_base" in
        session-end.sh|stop.sh|pre-compact.sh|post-tool-use-quality.sh) _advisory_ok=true ;;
    esac

    if [[ "${OMNICLAUDE_HOOK_CRITICALITY:-critical}" == "advisory" && "$_advisory_ok" == "true" ]]; then
        echo "WARN: No Python found. Advisory hook exiting gracefully." 1>&2
        cat > /dev/null 2>/dev/null || true
        exit 0
    fi
    # Critical hook: hard-fail with actionable error
    echo "ERROR: No valid Python found for ONEX hooks." 1>&2
    echo "  Expected one of:" 1>&2
    echo "    - PLUGIN_PYTHON_BIN=/path/to/python3 (explicit override)" 1>&2
    echo "    - ${PLUGIN_ROOT}/lib/.venv/bin/python3 (deploy the plugin)" 1>&2
    echo "    - OMNICLAUDE_PROJECT_ROOT=/path/to/repo with .venv (dev mode)" 1>&2
    echo "" 1>&2
    echo "  Auto-repair was attempted but failed. Check /tmp/omniclaude-venv-repair.log" 1>&2
    echo "" 1>&2
    echo "  Quick fix: run the deploy skill with --repair-venv to build lib/.venv" 1>&2
    echo "  in the active cache version without a full redeploy:" 1>&2
    echo "    \${CLAUDE_PLUGIN_ROOT}/skills/deploy-local-plugin/deploy.sh --repair-venv" 1>&2
    exit 1
fi
export PYTHON_CMD

# =============================================================================
# Venv Sentinel Check (OMN-3727)
# =============================================================================
# After PYTHON_CMD is resolved, check for .omniclaude-sentinel in the venv.
# If missing, write one and trigger background integrity verification.
# The sentinel is a single-line ISO 8601 timestamp written by deploy.sh or
# auto-repair. Its absence indicates the venv was not created through a
# normal deploy path and may need verification.
#
# Timing: Single stat = ~0.1ms. Background verification adds zero to the
# synchronous path.

_bg_verify_venv() {
    local venv_dir="$1"
    local repair_log="/tmp/omniclaude-venv-repair.log"
    local python_bin="${venv_dir}/bin/python3"

    # Quick import check — if omniclaude imports cleanly, venv is healthy
    if "$python_bin" -c "import omniclaude" >/dev/null 2>&1; then
        return 0
    fi

    # Import failed — attempt background pip install to repair
    echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] Sentinel verify: import omniclaude failed, attempting background repair" >> "$repair_log" 2>/dev/null || true

    local plugin_root_dir="${PLUGIN_ROOT:-}"
    if [[ -n "$plugin_root_dir" && -f "${plugin_root_dir}/pyproject.toml" ]]; then
        "$python_bin" -m pip install --quiet --disable-pip-version-check \
            -e "${plugin_root_dir}" >> "$repair_log" 2>&1 || true
    elif [[ -n "$plugin_root_dir" && -f "${plugin_root_dir}/requirements.txt" ]]; then
        "$python_bin" -m pip install --quiet --disable-pip-version-check \
            -r "${plugin_root_dir}/requirements.txt" >> "$repair_log" 2>&1 || true
    fi

    echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] Sentinel verify: background repair finished" >> "$repair_log" 2>/dev/null || true
}

if [[ "${PYTHON_CMD}" == *"/.venv/bin/python3" ]]; then
    _SENTINEL="${PYTHON_CMD%/bin/python3}/.omniclaude-sentinel"
    if [[ ! -f "${_SENTINEL}" ]]; then
        date -u +"%Y-%m-%dT%H:%M:%SZ" > "${_SENTINEL}" 2>/dev/null || true
        ( _bg_verify_venv "${PYTHON_CMD%/bin/python3}" ) &
    fi
    unset _SENTINEL
fi

# Log resolved interpreter for debugging (only if LOG_FILE is available)
# Uses inline printf instead of log() which is defined later in this file
if [[ -n "${LOG_FILE:-}" ]]; then
    printf "[%s] Resolved python: %s\n" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "${PYTHON_CMD}" >> "$LOG_FILE"
fi

# =============================================================================
# Boolean Normalization
# =============================================================================
# Normalizes various boolean representations to "true" or "false".
# Accepts: true, 1, yes, on (case-insensitive) -> "true"
# Everything else -> "false"

_normalize_bool() {
    # Use tr for lowercase conversion (compatible with bash 3.2 on macOS)
    # Accepted truthy values mirror Python's _TRUTHY frozenset in
    # local_delegation_handler.py: true, 1, yes, on
    local val
    val=$(echo "$1" | tr '[:upper:]' '[:lower:]')
    case "$val" in
        true|1|yes|on) echo "true" ;;
        *) echo "false" ;;
    esac
}

# =============================================================================
# Timing Functions
# =============================================================================
# Get current time in milliseconds.
# Uses native bash date if available (GNU date supports %N), falls back to Python.
# macOS date doesn't support %N, so we detect and fall back appropriately.

# Detect if native millisecond timing is available (GNU date supports %N).
# IMPORTANT: This check runs ONCE at script load time and caches the result.
# We intentionally cache rather than checking per-call because:
#   1. Performance: Avoid subprocess overhead on every timing call
#   2. Consistency: All timestamps in a session use the same method
#   3. Reliability: No race conditions from method changing mid-execution
if date +%s%3N 2>/dev/null | grep -qE '^[0-9]+$'; then
    _USE_NATIVE_TIME=true
else
    _USE_NATIVE_TIME=false
fi

get_time_ms() {
    if [[ "$_USE_NATIVE_TIME" == "true" ]]; then
        date +%s%3N
    else
        $PYTHON_CMD -c "import time; print(int(time.time() * 1000))"
    fi
}

# =============================================================================
# Environment File Loading
# =============================================================================
# Source project .env file if present to pick up KAFKA_BOOTSTRAP_SERVERS and
# other configuration. This enables hooks to use project-specific settings.
#
# Order of precedence:
# 1. Project .env file (highest priority - overrides existing env vars)
# 2. Already-set environment variables
# 3. Default values (lowest priority)
#
# SECURITY NOTE: Using `set -a` exports ALL variables from .env to the environment.
# This means secrets in .env (API keys, passwords, tokens) will be visible to ALL
# subprocesses spawned by hooks. This is standard shell behavior for local dev
# environments but be aware of the implications for sensitive credentials.

# Load global ~/.omnibase/.env first (lowest priority — project .env overrides below).
# This ensures LLM routing, Kafka, and other shared vars are always available even
# when the hook runs from a non-project CWD (e.g. home dir on dock launch).
_CLAUDE_GLOBAL_ENV="${HOME}/.omnibase/.env"
if [[ -f "${_CLAUDE_GLOBAL_ENV}" ]]; then
    set -a
    # shellcheck disable=SC1090
    if ! source "${_CLAUDE_GLOBAL_ENV}" 2>/dev/null; then
        if [[ -n "${LOG_FILE:-}" ]]; then
            # Use printf instead of log() — log() function is defined later in this file
            # and may not be available at this point in the source order.
            printf "[%s] WARN: Failed to source %s - check file syntax\n" \
                "$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo 'unknown')" \
                "${_CLAUDE_GLOBAL_ENV}" >> "${LOG_FILE}" 2>/dev/null || true
        fi
    fi
    set +a
fi
unset _CLAUDE_GLOBAL_ENV

if [[ -f "${PROJECT_ROOT}/.env" ]]; then
    # Source .env - note this WILL override already-set variables
    # Using set -a to export all variables, then set +a to stop
    set -a
    # shellcheck disable=SC1091
    # Note: We use 2>/dev/null because .env files may contain comments or blank
    # lines that produce benign warnings. Syntax errors are rare in .env files.
    if ! source "${PROJECT_ROOT}/.env" 2>/dev/null; then
        # Only log if LOG_FILE is set (caller script responsibility)
        if [[ -n "${LOG_FILE:-}" ]]; then
            # Use printf instead of log() — log() function is defined later in this file
            # and may not be available at this point in the source order.
            printf "[%s] WARN: Failed to source %s - check file syntax\n" \
                "$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo 'unknown')" \
                "${PROJECT_ROOT}/.env" >> "${LOG_FILE}" 2>/dev/null || true
        fi
    fi
    set +a
fi

# =============================================================================
# Kafka Configuration
# =============================================================================
# Kafka is REQUIRED for OmniClaude intelligence gathering.
# The entire architecture is event-driven via Kafka - without it, hooks have no purpose.
# Set KAFKA_BOOTSTRAP_SERVERS in .env (e.g., KAFKA_BOOTSTRAP_SERVERS=<kafka-bootstrap-servers>:9092).
# SessionStart hook will fail fast if Kafka is not configured.

KAFKA_ENABLED="false"
if [[ -n "${KAFKA_BOOTSTRAP_SERVERS:-}" ]]; then
    KAFKA_ENABLED="true"
    # Export KAFKA_BROKERS for legacy compatibility with Python scripts
    # that use shared_lib/kafka_config.py's get_kafka_bootstrap_servers()
    # fallback chain: KAFKA_BOOTSTRAP_SERVERS -> KAFKA_INTELLIGENCE_BOOTSTRAP_SERVERS -> KAFKA_BROKERS
    export KAFKA_BROKERS="${KAFKA_BROKERS:-${KAFKA_BOOTSTRAP_SERVERS:-}}"
fi
export KAFKA_ENABLED

# =============================================================================
# Slack Webhook Alerting
# =============================================================================
# Send a Slack notification for hook/daemon failures.
# Self-protecting: curl timeouts guarantee max 2s delay even on DNS hangs.
# Rate-limited per category (5-min window) to prevent alert spam.
# Always call from a backgrounded subshell: ( slack_notify "cat" "msg" ) &
#
# Requires:
#   - SLACK_WEBHOOK_URL: Webhook URL (no-op if unset)
#
# Usage: ( slack_notify "daemon_startup" "Emit daemon failed to start..." ) &

# Cache hostname once at source time
_SLACK_HOST="${HOSTNAME:-$(hostname -s 2>/dev/null || echo unknown)}"

slack_notify() {
    local category="$1"
    local message="$2"
    local webhook_url="${SLACK_WEBHOOK_URL:-}"

    # No-op if webhook not configured
    [[ -z "$webhook_url" ]] && return 0

    # Rate limiting: 5-minute window per category
    local rate_dir="/tmp/omniclaude-slack-rate"
    mkdir -p "$rate_dir" 2>/dev/null || true
    # Sanitize category for safe filename (alphanumeric + dash + underscore only)
    local safe_cat
    safe_cat=$(printf '%s' "$category" | tr -cd 'a-zA-Z0-9_-')
    [[ -z "$safe_cat" ]] && safe_cat="unknown"
    local rate_file="${rate_dir}/${safe_cat}.last"

    if [[ -f "$rate_file" ]]; then
        local last_sent
        last_sent=$(cat "$rate_file" 2>/dev/null) || last_sent=0
        [[ "$last_sent" =~ ^[0-9]+$ ]] || last_sent=0
        local now
        now=$(date -u +%s)
        if (( now - last_sent < 300 )); then
            return 0  # Rate limited, skip
        fi
    fi

    # JSON-escape the message: backslashes first, then quotes, then control chars
    local escaped
    escaped=$(printf '%s' "$message" \
        | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' \
        | tr '\n' ' ' | tr '\r' ' ' | tr '\t' ' ')

    # Send with strict timeouts (connect 1s, total 2s).
    # Use --url flag instead of positional argument so the webhook URL does not
    # appear in process list output (ps aux) on multi-user systems.
    if curl -s -S --connect-timeout 1 --max-time 2 \
        -H 'Content-Type: application/json' \
        -d "{\"text\": \"${escaped}\"}" \
        --url "$webhook_url" >/dev/null 2>&1; then
        # Record send time for rate limiting
        date -u +%s > "$rate_file" 2>/dev/null || true
    fi

    return 0
}

# =============================================================================
# Degraded Hook Notification (OMN-6567)
# =============================================================================
# Send a Slack notification when a hook exits 0 but ran in degraded mode
# (e.g. Python subprocess hit ModuleNotFoundError/ImportError). Complements
# error-guard.sh which only catches non-zero exits.
#
# Debounce key: {hook_name}:{sha256_of_first_error_line} — different errors
# from the same hook are tracked separately.
# Debounce window: 15 minutes (longer than error-guard's 5 min because
# degraded errors repeat on every tool call).
#
# Dependencies: curl (best-effort). No Python, no jq.
# Must not write to stdout (hooks use stdout for Claude Code communication).
# Always call from a backgrounded subshell: ( notify_hook_degraded "foo" "msg" ) &
#
# Usage: ( notify_hook_degraded "$_OMNICLAUDE_HOOK_NAME" "$(head -1 "$_stderr_tmp")" ) &

notify_hook_degraded() {
    local hook_name="$1"
    local error_message="$2"
    local webhook_url="${SLACK_WEBHOOK_URL:-}"

    # No-op if webhook not configured
    [[ -z "$webhook_url" ]] && return 0

    # Build debounce key: hook_name:sha256(first_line_of_error)
    # Use shasum (macOS) or sha256sum (Linux) for the hash
    local error_hash
    if command -v shasum >/dev/null 2>&1; then
        error_hash=$(printf '%s' "$error_message" | shasum -a 256 2>/dev/null | cut -c1-16)
    elif command -v sha256sum >/dev/null 2>&1; then
        error_hash=$(printf '%s' "$error_message" | sha256sum 2>/dev/null | cut -c1-16)
    else
        # Fallback: use a simple tr-based sanitization as key
        error_hash=$(printf '%s' "$error_message" | tr -cd 'a-zA-Z0-9' | cut -c1-32)
    fi
    [[ -z "$error_hash" ]] && error_hash="unknown"

    # Sanitize hook name for safe filename
    local safe_hook
    safe_hook=$(printf '%s' "$hook_name" | tr -cd 'a-zA-Z0-9_-')
    [[ -z "$safe_hook" ]] && safe_hook="unknown"

    local safe_key="${safe_hook}_${error_hash}"

    # Rate limiting: 15-minute window per debounce key
    local rate_dir="/tmp/omniclaude-slack-rate"
    mkdir -p "$rate_dir" 2>/dev/null || true
    local rate_file="${rate_dir}/degraded-${safe_key}.last"

    if [[ -f "$rate_file" ]]; then
        local last_sent
        last_sent=$(cat "$rate_file" 2>/dev/null) || last_sent=0
        [[ "$last_sent" =~ ^[0-9]+$ ]] || last_sent=0
        local now
        now=$(date -u +%s)
        if (( now - last_sent < 900 )); then
            return 0  # Rate limited, skip
        fi
    fi

    # JSON-escape the message: backslashes first, then quotes, then control chars
    local escaped
    escaped=$(printf '%s' "[hook-degraded][${_SLACK_HOST}] Hook '${hook_name}' running degraded: ${error_message}" \
        | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' \
        | tr '\n' ' ' | tr '\r' ' ' | tr '\t' ' ')

    # Send with strict timeouts (connect 1s, total 2s).
    if curl -s -S --connect-timeout 1 --max-time 2 \
        -H 'Content-Type: application/json' \
        -d "{\"text\": \"${escaped}\"}" \
        --url "$webhook_url" >/dev/null 2>&1; then
        # Record send time for rate limiting
        date -u +%s > "$rate_file" 2>/dev/null || true
    fi

    return 0
}

# =============================================================================
# Emit Daemon Helper (OMN-1631, OMN-1632)
# =============================================================================
# Emit event via emit daemon for fast, non-blocking Kafka emission.
# Single call - daemon handles fan-out to multiple topics.
#
# Requires (must be set before calling):
#   - PYTHON_CMD: Path to Python interpreter (provided by common.sh)
#   - HOOKS_LIB: Path to hooks lib directory (set by caller script)
#   - LOG_FILE: Path to log file (set by caller script)
#
# Usage: emit_via_daemon <event_type> <payload_json> [timeout_ms]
# Returns: 0 on success, 1 on failure (non-fatal)

emit_via_daemon() {
    local event_type="$1"
    local payload="$2"
    local timeout_ms="${3:-50}"
    local health_dir="${HOOKS_DIR}/logs/emit-health"
    # Status file is keyed per event_type so failure counters are isolated.
    # A sanitized form of event_type is used to produce a safe filename.
    local _safe_event_type
    _safe_event_type=$(printf '%s' "$event_type" | tr -cd 'a-zA-Z0-9_-')
    [[ -z "$_safe_event_type" ]] && _safe_event_type="unknown"
    local status_file="${health_dir}/status-${_safe_event_type}"

    mkdir -p "$health_dir" 2>/dev/null || true

    if "$PYTHON_CMD" "${HOOKS_LIB}/emit_client_wrapper.py" emit \
        --event-type "$event_type" --payload "$payload" --timeout "$timeout_ms" \
        >> "$LOG_FILE" 2>&1; then
        # Success: reset failure count, record success timestamp
        local _now
        _now=$(date -u +%s)
        local _tmp="${status_file}.tmp.$$"
        echo "0 0 $_now $event_type" > "$_tmp" && mv -f "$_tmp" "$status_file" 2>/dev/null || rm -f "$_tmp"
        return 0
    else
        echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] Emit daemon failed for ${event_type}" >> "$LOG_FILE"
        # Increment failure count, preserve last_success_ts
        # Single read splits all fields from the status file in one shot (no TOCTOU)
        # Format: <fail_count> <fail_timestamp> <success_timestamp> <event_type>
        local _prev_failures=0 _prev_success_ts=0
        if [[ -f "$status_file" ]]; then
            local _prev_fail_ts=0 _prev_evt=""
            read -r _prev_failures _prev_fail_ts _prev_success_ts _prev_evt < "$status_file" 2>/dev/null \
                || { _prev_failures=0; _prev_success_ts=0; }
            [[ "$_prev_failures" =~ ^[0-9]+$ ]] || _prev_failures=0
            [[ "$_prev_success_ts" =~ ^[0-9]+$ ]] || _prev_success_ts=0
        fi
        local _now
        _now=$(date -u +%s)
        local _tmp="${status_file}.tmp.$$"
        echo "$((_prev_failures + 1)) $_now $_prev_success_ts $event_type" > "$_tmp" \
            && mv -f "$_tmp" "$status_file" 2>/dev/null || rm -f "$_tmp"
        # Milestone-based Slack alerts for sustained failures (avoid spam)
        local n=$((_prev_failures + 1))
        if (( n == 5 )); then
            # Attempt daemon auto-restart after 5 consecutive failures (OMN-3647)
            _try_restart_emit_daemon &
        fi
        if (( n == 10 || n == 25 || n == 50 || n == 100 )); then
            local _last_ok
            if [[ -z "$_prev_success_ts" || "$_prev_success_ts" -eq 0 ]]; then
                _last_ok="never"
            else
                _last_ok=$(date -u -r "${_prev_success_ts}" +"%Y-%m-%d %H:%M:%S UTC" 2>/dev/null || echo "never")
            fi
            ( slack_notify "emit_sustained" "[omniclaude][${_SLACK_HOST}] ${n} consecutive emit failures for '${event_type}'. Last success: ${_last_ok}. Daemon may be unhealthy." ) &
        fi
        return 1
    fi
}

# =============================================================================
# Emit Daemon Self-Healing (OMN-3647)
# =============================================================================
# Auto-restart emit daemon after 5 consecutive failures with 4-layer idempotency guards.
# Called by emit_via_daemon when fail counter reaches 5.
#
# Design:
#   Guard 1: Atomic mkdir lock (prevents concurrent restarts)
#   Guard 2: Socket ping check (maybe daemon recovered)
#   Guard 3: Kafka reachability probe with 1s timeout + 10min cooldown
#   Guard 4: Stale process cleanup with tight pattern matching
#
# Returns: 0 on restart attempt (whether successful or not), 1 if skipped

_try_restart_emit_daemon() {
    local socket_path="${OMNICLAUDE_EMIT_SOCKET:-${TMPDIR:-/tmp}/omniclaude-emit.sock}"
    local lock_dir="/tmp/omniclaude-emit-restart.lock"
    local fail_count_file="/tmp/omniclaude-emit-fail-count"
    local restart_ts_file="/tmp/omniclaude-emit-restart-last-at"
    local kafka_unreachable_file="/tmp/omniclaude-emit-kafka-unreachable"

    local now
    now=$(date +%s)

    # Guard 1: Atomic mkdir lock (prevents concurrent restarts)
    # Use mkdir's atomicity on POSIX systems to create exclusive lock
    if ! mkdir "$lock_dir" 2>/dev/null; then
        log "Emit daemon restart already in progress (lock exists)"
        return 1
    fi

    # Ensure trap cleanup runs even if the function exits early
    trap "rmdir '$lock_dir' 2>/dev/null || true" RETURN

    # Guard 2: Ping socket before restart (exit cleanly if already responsive)
    if [[ -S "$socket_path" ]]; then
        if "$PYTHON_CMD" "${HOOKS_LIB}/emit_client_wrapper.py" ping \
            >> "$LOG_FILE" 2>&1; then
            log "Emit daemon socket responsive, skipping restart"
            echo "0 0 $now unknown" > "$fail_count_file" 2>/dev/null || true
            return 0
        fi
    fi

    # Guard 3: Kafka reachability probe with 1s timeout + 10min cooldown
    # Use nc (netcat) for TCP check if available, fall back to Python check
    local kafka_reachable=false
    if [[ -n "${KAFKA_BOOTSTRAP_SERVERS:-}" ]]; then
        # Parse first broker from comma-separated list
        local first_broker
        first_broker=$(echo "$KAFKA_BOOTSTRAP_SERVERS" | cut -d',' -f1)
        local broker_host broker_port
        broker_host="${first_broker%:*}"
        broker_port="${first_broker##*:}"

        if command -v nc &>/dev/null; then
            # Use nc with 1s timeout (macOS compatible)
            if nc -z -w 1 "$broker_host" "$broker_port" >/dev/null 2>&1; then
                kafka_reachable=true
            fi
        else
            # Fallback: Python socket check
            if "$PYTHON_CMD" -c "import socket; s=socket.socket(); s.settimeout(1); s.connect(('$broker_host', $broker_port)); s.close()" >/dev/null 2>&1; then
                kafka_reachable=true
            fi
        fi
    else
        # Kafka not configured, treat as reachable to allow restart attempt
        kafka_reachable=true
    fi

    if ! $kafka_reachable; then
        # Kafka unreachable: check cooldown timer
        local kafka_unreachable_ts=0
        if [[ -f "$kafka_unreachable_file" ]]; then
            read -r kafka_unreachable_ts < "$kafka_unreachable_file" 2>/dev/null || kafka_unreachable_ts=0
        fi

        local cooldown_elapsed=$(( now - kafka_unreachable_ts ))
        local cooldown_seconds=600  # 10 minutes

        if (( cooldown_elapsed < cooldown_seconds )); then
            local remaining=$(( cooldown_seconds - cooldown_elapsed ))
            log "Emit daemon: Kafka unreachable, restart suppressed (cooldown: ${remaining}s remaining)"
            return 1
        fi

        # Cooldown expired, record new timestamp and proceed with restart attempt
        echo "$now" > "$kafka_unreachable_file" 2>/dev/null || true
    fi

    # Guard 4: Restart cooldown (60s minimum between attempts)
    local restart_ts=0
    if [[ -f "$restart_ts_file" ]]; then
        read -r restart_ts < "$restart_ts_file" 2>/dev/null || restart_ts=0
    fi

    local restart_cooldown_seconds=60
    local restart_elapsed=$(( now - restart_ts ))
    if (( restart_elapsed < restart_cooldown_seconds )); then
        local remaining=$(( restart_cooldown_seconds - restart_elapsed ))
        log "Emit daemon: restart attempt suppressed (cooldown: ${remaining}s remaining)"
        return 1
    fi

    # All guards passed - attempt restart
    log "Emit daemon: Attempting restart after 5 consecutive failures"

    # Record restart attempt timestamp before cleanup
    echo "$now" > "$restart_ts_file" 2>/dev/null || true

    # Clean up stale socket and processes
    rm -f "$socket_path" 2>/dev/null || true

    # Kill stale daemon process with tight pattern matching
    # Match process with full socket path to avoid false positives
    local stale_pids
    stale_pids=$(pgrep -f "omniclaude\.publisher start.*--socket-path $(printf '%s\n' "$socket_path" | sed 's/[[\.*^$/]/\\&/g')" 2>/dev/null) || true

    if [[ -n "$stale_pids" ]]; then
        log "Emit daemon: Killing stale processes: $stale_pids"
        echo "$stale_pids" | xargs -r kill -9 2>/dev/null || true
        sleep 0.1  # Brief pause for kernel to clean up resources
    fi

    # Respawn daemon using same startup sequence as session-start.sh
    # This requires KAFKA_BOOTSTRAP_SERVERS and PYTHON_CMD to be set
    if [[ -z "${KAFKA_BOOTSTRAP_SERVERS:-}" ]]; then
        log "Emit daemon: Cannot restart, KAFKA_BOOTSTRAP_SERVERS not set"
        return 1
    fi

    nohup "$PYTHON_CMD" -m omniclaude.publisher start \
        --kafka-servers "$KAFKA_BOOTSTRAP_SERVERS" \
        --socket-path "$socket_path" \
        >> "${HOOKS_DIR}/logs/emit-daemon.log" 2>&1 &

    local daemon_pid=$!
    log "Emit daemon: Respawned with PID $daemon_pid, socket: $socket_path"

    # Wait briefly for socket to be created (max 200ms in 20ms increments)
    local wait_count=0
    local max_wait=10
    while [[ ! -S "$socket_path" && $wait_count -lt $max_wait ]]; do
        sleep 0.02
        ((wait_count++)) || true
    done

    if [[ -S "$socket_path" ]]; then
        log "Emit daemon: Socket created successfully, resetting failure counter"
        echo "0 0 $now unknown" > "$fail_count_file" 2>/dev/null || true
        return 0
    else
        log "Emit daemon: Socket not created after restart (PID: $daemon_pid)"
        return 1
    fi
}

# =============================================================================
# Tab Activity Helper (Statusline Integration)
# =============================================================================
# Updates the tab activity for the statusline tab bar.
# Writes a lightweight file read by statusline.sh on each render.
# Activity persists until the next prompt clears or replaces it.
#
# The activity file stores an ANSI 256-color code (integer). The statusline
# renders a colored dot (●) using that code. Each skill gets a deterministic
# color via skill_dot_color(). Skills can override by setting `dot_color: NNN`
# in their SKILL.md frontmatter.
#
# Usage: update_tab_activity "ticket-work"    # Set activity (auto-color)
#        update_tab_activity ""               # Clear activity

# Curated palette of 16 visually distinct 256-colors for skill dots
_DOT_PALETTE=(196 208 220 82 46 49 39 27 129 165 205 214 117 156 183 209)

# Map a skill name to a deterministic 256-color code from the palette.
# Checks SKILL.md frontmatter for `dot_color:` override first.
skill_dot_color() {
    local skill="$1"

    # Check for frontmatter override (dot_color: NNN)
    local plugin_root="${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}"
    if [ -n "$plugin_root" ]; then
        local skill_md="${plugin_root}/skills/${skill}/SKILL.md"
        if [ -f "$skill_md" ]; then
            local override
            override=$(sed -n '/^---$/,/^---$/{ /^dot_color:/{ s/^dot_color:[[:space:]]*//; s/[^0-9]//g; p; q; } }' "$skill_md" 2>/dev/null)
            if [ -n "$override" ]; then
                echo "$override"
                return 0
            fi
        fi
    fi

    # Hash skill name to palette index
    # Multiplier 37 chosen for better distribution: 37 mod 16 = 5 (coprime to 16),
    # whereas 31 mod 16 = 15 which biases lower bits toward last few chars.
    local hash=0 i char_val
    for ((i=0; i<${#skill}; i++)); do
        printf -v char_val '%d' "'${skill:$i:1}"
        hash=$(( (hash * 37 + char_val) % ${#_DOT_PALETTE[@]} ))
    done
    echo "${_DOT_PALETTE[$hash]}"
}

update_tab_activity() {
    local activity="$1"
    local iterm_guid="${ITERM_SESSION_ID:-}"
    [ -z "$iterm_guid" ] && return 0
    local guid="${iterm_guid#*:}"
    local activity_file="/tmp/omniclaude-tabs/${guid}.activity"
    mkdir -p "/tmp/omniclaude-tabs" 2>/dev/null || true
    if [ -n "$activity" ]; then
        local color
        color=$(skill_dot_color "$activity")
        printf '%s' "$color" > "$activity_file" 2>/dev/null || true
    else
        : > "$activity_file" 2>/dev/null || true
    fi
}

# =============================================================================
# Secret Redaction
# =============================================================================
# Redacts known secret patterns from stdin. Used by hook scripts before writing
# to trace logs or Kafka payloads. Covers API keys, tokens, PEM keys, and
# bearer tokens. Reads from stdin, writes redacted output to stdout.
#
# Usage: echo "$sensitive_text" | redact_secrets

redact_secrets() {
    sed -E \
        -e 's/sk-[a-zA-Z0-9]{20,}/sk-***REDACTED***/g' \
        -e 's/AKIA[A-Z0-9]{16}/AKIA***REDACTED***/g' \
        -e 's/ghp_[a-zA-Z0-9]{36}/ghp_***REDACTED***/g' \
        -e 's/gho_[a-zA-Z0-9]{36}/gho_***REDACTED***/g' \
        -e 's/xox[baprs]-[a-zA-Z0-9-]+/xox*-***REDACTED***/g' \
        -e 's/Bearer [a-zA-Z0-9._-]{20,}/Bearer ***REDACTED***/g' \
        -e 's/:\/\/[^:]+:[^@]+@/:\/\/***:***@/g' \
    | perl -0777 -pe 's/-----BEGIN [A-Z ]*(?:PRIVATE|RSA|EC|DSA) KEY-----[\s\S]*?-----END [A-Z ]*(?:PRIVATE|RSA|EC|DSA) KEY-----/[REDACTED PEM KEY]/g'
}

# =============================================================================
# Logging Helper
# =============================================================================
# Simple timestamped logging to a file.
#
# Requires (must be set before calling):
#   - LOG_FILE: Path to log file (set by caller script)
#
# Usage: log "message to log"

log() {
    printf "[%s] %s\n" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$*" >> "$LOG_FILE"
}
