#!/usr/bin/env bash
# hooks/session-start — SessionStart hook: stack detection + always-on agent rules.

set -uo pipefail

HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
PLUGIN_ROOT="$(cd "${HOOKS_DIR}/.." && pwd)"
: "${CLAUDE_PLUGIN_ROOT:=$PLUGIN_ROOT}"
: "${CLAUDE_PLUGIN_DATA:=${XDG_DATA_HOME:-$HOME/.local/share}/claude/plugins/slop-guard}"
: "${CLAUDE_PROJECT_DIR:=${PWD}}"

# shellcheck source=../lib/hook.sh
. "${CLAUDE_PLUGIN_ROOT}/lib/hook.sh"
# shellcheck source=../lib/state.sh
. "${CLAUDE_PLUGIN_ROOT}/lib/state.sh"
# shellcheck source=../lib/tools.sh
. "${CLAUDE_PLUGIN_ROOT}/lib/tools.sh"
# Fallback: tool_config_mode is defined in lib/tools.sh (ConfigMode slice).
# This stub fires only when that slice hasn't landed yet; the contract is identical.
# Unknown CONFIG_SOURCE values → overlay (safe: a typo must not disable the guard).
if ! declare -F tool_config_mode >/dev/null 2>&1; then
    tool_config_mode() {
        local _tcm_t="$1" _tcm_pd="${2:-${CLAUDE_PROJECT_DIR:-}}"
        local _tcm_cp; _tcm_cp="$(tool_config_path "$_tcm_t" "$_tcm_pd")"
        case "$(tool_config_source "$_tcm_cp")" in
            project) printf 'project' ;;
            *)
                case "${CLAUDE_PLUGIN_OPTION_CONFIG_SOURCE:-overlay}" in
                    full)         printf 'full' ;;
                    project-only) printf 'skip' ;;
                    *)            printf 'overlay' ;;
                esac
                ;;
        esac
    }
fi
# shellcheck source=../lib/detect.sh
. "${CLAUDE_PLUGIN_ROOT}/lib/detect.sh"
# shellcheck source=../lib/config.sh
. "${CLAUDE_PLUGIN_ROOT}/lib/config.sh"
# shellcheck source=../lib/ext.sh
_sg_ext_lib="${SLOPGUARD_EXT_SH:-${CLAUDE_PLUGIN_ROOT}/lib/ext.sh}"
if [ -f "$_sg_ext_lib" ]; then
    # shellcheck disable=SC1090
    . "$_sg_ext_lib"
fi

main() {
    hook_input
    SESSION_ID="$(hook_field .session_id)"
    AGENT_ID="$(hook_field .agent_id)"
    : "${SESSION_ID:="unknown-$$"}"

    state_prune
    state_init "$SESSION_ID" "${AGENT_ID:-}"

    local _d="${CLAUDE_PROJECT_DIR:-.}"
    local _php_cfg _go_cfg _python_cfg _node_cfg
    config_resolve_stacks "$_d"
    _php_cfg="$(tool_config_source "$(tool_config_path phpstan "$_d")")"
    _go_cfg="$(tool_config_source "$(tool_config_path golangci-lint "$_d")")"
    _python_cfg="$(tool_config_source "$(tool_config_path ruff "$_d")")"
    _node_cfg="$(tool_config_source "$(tool_config_path eslint-stack "$_d")")"
    if [ -f "${_d}/pyproject.toml" ] \
        && grep -Eq '^\[tool\.(mypy|pyright)\]' "${_d}/pyproject.toml"; then
        _python_cfg="project"
    fi

    local _sdir _tools_json tool resolved source version expected matches plugin_prefix
    local config_path config_source config_mode relevant
    local -a _missing_tools=()
    _sdir="$(state_dir "$SESSION_ID" "${AGENT_ID:-}")"
    _tools_json='[]'
    # Tools this project can actually use: core plus the detected stacks' own.
    # Everything else is inventoried as not-applicable and never probed — a Go
    # linter is not "missing" from a repository that holds no Go.
    local _relevant_tools
    _relevant_tools=" $(tools_for_stacks "${SG_STACKS:-}") "
    while IFS= read -r tool; do
        expected="$(lock_version "$tool")"
        config_path="$(tool_config_path "$tool" "$_d")"
        config_source="$(tool_config_source "$config_path")"
        config_mode="$(tool_config_mode "$tool" "$_d")"
        resolved=""
        source="not-applicable"
        version=""
        matches=false
        relevant=false
        case "$_relevant_tools" in
            *" ${tool} "*) relevant=true ;;
        esac
        if [ "$relevant" = true ]; then
            resolved="$(resolve_tool "$tool")"
            source="missing"
            if [ -n "$resolved" ]; then
                plugin_prefix="${CLAUDE_PLUGIN_DATA}/tools/"
                case "$resolved" in
                    "${plugin_prefix}"*) source="plugin" ;;
                    */vendor/bin/*|*/node_modules/.bin/*|*/.venv/bin/*) source="project" ;;
                    "${_d}"/*) source="project" ;;
                    *) source="PATH" ;;
                esac
                version="$(tool_version_output "$tool" "$resolved" | head -1 || true)"
                tool_version_matches "$tool" "$resolved" && matches=true
            else
                _missing_tools+=("$tool")
            fi
        fi
        _tools_json="$(jq -c \
            --arg tool "$tool" --arg source "$source" --arg path "$resolved" \
            --arg version "$version" --arg expected "$expected" \
            --arg config_path "$config_path" --arg config_source "$config_source" \
            --arg config_mode "$config_mode" \
            --argjson matches "$matches" --argjson relevant "$relevant" \
            '. + [{name:$tool,source:$source,path:$path,version:$version,
                    expected_version:$expected,version_matches:$matches,
                    config_path:$config_path,config_source:$config_source,
                    config_mode:$config_mode,relevant:$relevant}]' \
            <<< "$_tools_json")"
    done < <(lock_tools)

    # Collect installed versions for stacks that carry a context7 field in stacks.json.
    # Lockfiles are read at most once each.
    local _fw_versions _composer_lock _pkg_lock_data _pnpm_lock_data _tf_ver
    local _ctx7 _ver _tf_file _stacks_file _sg_stack
    local _needs_composer _needs_npm _needs_terraform
    local -a _fw_pending=()
    _fw_versions='{}'
    _composer_lock=""
    _pkg_lock_data=""
    _pnpm_lock_data=""
    _tf_ver=""
    _needs_composer=false
    _needs_npm=false
    _needs_terraform=false
    _stacks_file="${SLOPGUARD_STACKS_JSON:-${CLAUDE_PLUGIN_ROOT}/rules/stacks.json}"

    # shellcheck disable=SC2086
    for _sg_stack in ${SG_STACKS:-}; do
        _ctx7="$(jq -r --arg t "$_sg_stack" '.[$t].context7 // empty' \
            "$_stacks_file" 2>/dev/null || true)"
        [ -n "$_ctx7" ] || continue
        _fw_pending+=("$_sg_stack")
        case "$_sg_stack" in
            laravel|symfony|doctrine) _needs_composer=true ;;
            react|vite|express)       _needs_npm=true ;;
            terraform)                _needs_terraform=true ;;
        esac
    done

    if [ "$_needs_composer" = true ] && [ -f "${_d}/composer.lock" ]; then
        _composer_lock="$(cat "${_d}/composer.lock" 2>/dev/null || true)"
    fi
    if [ "$_needs_npm" = true ]; then
        if [ -f "${_d}/package-lock.json" ]; then
            _pkg_lock_data="$(cat "${_d}/package-lock.json" 2>/dev/null || true)"
        elif [ -f "${_d}/pnpm-lock.yaml" ]; then
            _pnpm_lock_data="$(cat "${_d}/pnpm-lock.yaml" 2>/dev/null || true)"
        fi
    fi
    if [ "$_needs_terraform" = true ]; then
        for _tf_file in "${_d}"/*.tf; do
            [ -f "$_tf_file" ] || continue
            _tf_ver="$(grep -m1 'required_version' "$_tf_file" 2>/dev/null \
                | sed 's/.*required_version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/' || true)"
            [ -n "$_tf_ver" ] && break
        done
    fi

    if [ "${#_fw_pending[@]}" -gt 0 ]; then
        for _sg_stack in "${_fw_pending[@]}"; do
            _ver=""
            case "$_sg_stack" in
                laravel)
                    [ -n "$_composer_lock" ] && \
                        _ver="$(jq -r '.packages[] | select(.name=="laravel/framework") | .version' \
                            <<< "$_composer_lock" 2>/dev/null | head -1 || true)" ;;
                symfony)
                    [ -n "$_composer_lock" ] && \
                        _ver="$(jq -r '.packages[] | select(.name=="symfony/framework-bundle") | .version' \
                            <<< "$_composer_lock" 2>/dev/null | head -1 || true)" ;;
                doctrine)
                    [ -n "$_composer_lock" ] && \
                        _ver="$(jq -r '.packages[] | select(.name=="doctrine/orm") | .version' \
                            <<< "$_composer_lock" 2>/dev/null | head -1 || true)" ;;
                react)
                    if [ -n "$_pkg_lock_data" ]; then
                        _ver="$(jq -r '.packages["node_modules/react"].version // empty' \
                            <<< "$_pkg_lock_data" 2>/dev/null || true)"
                    elif [ -n "$_pnpm_lock_data" ]; then
                        _ver="$(printf '%s' "$_pnpm_lock_data" \
                            | grep -m1 -E '^/?react@[0-9]' \
                            | sed 's|.*react@\([0-9][^:/]*\).*|\1|' || true)"
                    fi ;;
                vite)
                    if [ -n "$_pkg_lock_data" ]; then
                        _ver="$(jq -r '.packages["node_modules/vite"].version // empty' \
                            <<< "$_pkg_lock_data" 2>/dev/null || true)"
                    elif [ -n "$_pnpm_lock_data" ]; then
                        _ver="$(printf '%s' "$_pnpm_lock_data" \
                            | grep -m1 -E '^/?vite@[0-9]' \
                            | sed 's|.*vite@\([0-9][^:/]*\).*|\1|' || true)"
                    fi ;;
                express)
                    if [ -n "$_pkg_lock_data" ]; then
                        _ver="$(jq -r '.packages["node_modules/express"].version // empty' \
                            <<< "$_pkg_lock_data" 2>/dev/null || true)"
                    elif [ -n "$_pnpm_lock_data" ]; then
                        _ver="$(printf '%s' "$_pnpm_lock_data" \
                            | grep -m1 -E '^/?express@[0-9]' \
                            | sed 's|.*express@\([0-9][^:/]*\).*|\1|' || true)"
                    fi ;;
                terraform)
                    _ver="$_tf_ver" ;;
            esac
            [ -n "$_ver" ] || continue
            _fw_versions="$(jq -c --arg k "$_sg_stack" --arg v "$_ver" \
                '. + {($k): $v}' <<< "$_fw_versions")"
        done
    fi

    # Compute overlay summary and config_modes map from the completed _tools_json.
    # Only "overlay" mode is reported — "full" and "skip" are deliberate knob choices.
    # Tools the project's stacks never reach are excluded: their config mode is
    # academic and counting them made every project look unconfigured.
    local _overlay_count _overlay_example _config_modes
    _overlay_count="$(jq '[.[] | select(.relevant and .config_mode=="overlay")] | length' \
        <<< "$_tools_json" 2>/dev/null || printf '0')"
    _overlay_example="$(jq -r \
        '([.[] | select(.relevant and .config_mode=="overlay")]
          | if length > 0 then .[0].name else "" end)' \
        <<< "$_tools_json" 2>/dev/null || true)"
    _config_modes="$(jq -c '[.[] | {key:.name,value:.config_mode}] | from_entries' \
        <<< "$_tools_json" 2>/dev/null || printf '{}')"

    jq \
        --argjson stacks "$(printf '%s' "${SG_STACKS:-}" | jq -Rcs 'split(" ") | map(select(length > 0))')" \
        --arg stacks_source "${SG_STACKS_SOURCE:-auto}" \
        --argjson stacks_warnings "$(printf '%s' "${SG_STACKS_WARNINGS:-}" | jq -Rcs 'split("\n") | map(select(length > 0))')" \
        --arg php_cfg "$_php_cfg" \
        --arg go_cfg "$_go_cfg" \
        --arg python_cfg "$_python_cfg" \
        --arg node_cfg "$_node_cfg" \
        --argjson tools "$_tools_json" \
        --argjson fw_versions "$_fw_versions" \
        --argjson config_modes "$_config_modes" \
        '.stacks = $stacks
         | .stacks_source = $stacks_source
         | .stacks_warnings = $stacks_warnings
         | .tools = $tools
         | .config_sources = {php: $php_cfg, go: $go_cfg, python: $python_cfg, node: $node_cfg}
         | .config_modes = $config_modes
         | .framework_versions = $fw_versions' \
        "${_sdir}/profile.json" > "${_sdir}/profile.json.tmp" \
        && mv "${_sdir}/profile.json.tmp" "${_sdir}/profile.json"

    local _missing_marker="${_sdir}/missing-tools-reported"
    if [ "${#_missing_tools[@]}" -gt 0 ] && [ ! -f "$_missing_marker" ]; then
        local _missing_list
        _missing_list="$(IFS=', '; printf '%s' "${_missing_tools[*]}")"
        printf 'slop-guard: missing tools for this project: %s. Install: %s/bin/slopguard doctor --install\n' \
            "$_missing_list" "${CLAUDE_PLUGIN_ROOT}"
        : > "$_missing_marker"
    fi

    local _src_label _tier2="" _sg_stack _w
    case "${SG_STACKS_SOURCE:-auto}" in
        auto) _src_label="auto" ;;
        *)    _src_label=".slopguard.json" ;;
    esac
    # shellcheck disable=SC2086
    for _sg_stack in ${SG_STACKS:-}; do
        if [ "$(stack_tier "$_sg_stack" 2>/dev/null)" = "2" ]; then
            _tier2="${_tier2:+$_tier2, }$_sg_stack"
        fi
    done
    printf 'slop-guard: stacks (%s): %s\n' "$_src_label" "${SG_STACKS:-none}"
    [ -n "$_tier2" ] \
        && printf 'slop-guard: %s — tier 2: prevention + SAST only, no type analysis\n' "$_tier2"
    if [ -n "${SG_STACKS_WARNINGS:-}" ]; then
        while IFS= read -r _w; do
            [ -n "$_w" ] || continue
            printf 'slop-guard: warning: %s\n' "$_w"
        done <<< "${SG_STACKS_WARNINGS}"
    fi
    if declare -F ext_dir >/dev/null 2>&1; then
        local _ext_dir_path _ext_tools_out _ext_counts
        local _ext_total_overrides _ext_downgrades
        local _ext_total _ext_active _ext_inactive _ext_obj
        _ext_dir_path="$(ext_dir "${_d}")"
        if [ -d "$_ext_dir_path" ]; then
            _ext_tools_out="$(ext_tools "${_d}")"
            _ext_counts="$(ext_override_counts "${_d}")"
            _ext_total_overrides="${_ext_counts%% *}"
            _ext_downgrades="${_ext_counts##* }"
            _ext_total=0
            _ext_active=0
            _ext_inactive=0
            if [ -n "$_ext_tools_out" ]; then
                while IFS= read -r _ext_obj; do
                    [ -n "$_ext_obj" ] || continue
                    _ext_total=$(( _ext_total + 1 ))
                    case "$_ext_obj" in
                        *'"status":"ok"'*) _ext_active=$(( _ext_active + 1 )) ;;
                        *)                 _ext_inactive=$(( _ext_inactive + 1 )) ;;
                    esac
                done <<< "$_ext_tools_out"
            fi
            printf 'slop-guard: project extensions: %s tools (%s active, %s absent), %s rule overrides (%s severity downgrades)\n' \
                "$_ext_total" "$_ext_active" "$_ext_inactive" \
                "$_ext_total_overrides" "$_ext_downgrades"
        fi
    fi
    # Show one line when any tool is running in overlay mode (security findings only).
    # Print nothing when every tool has a project config or the knob is non-overlay.
    if [ "${_overlay_count:-0}" -gt 0 ] && [ -n "${_overlay_example:-}" ]; then
        local _ov_word
        [ "${_overlay_count}" -eq 1 ] && _ov_word="tool" || _ov_word="tools"
        printf 'slop-guard: %s %s using example configs (security findings only) — adopt one: slopguard adopt-config %s\n' \
            "$_overlay_count" "$_ov_word" "$_overlay_example"
    fi
    # The ten rules do not change between sessions. A repository that has already
    # been told gets the pointer instead — the hooks enforce them either way, and a
    # banner reprinted at every session start is a banner nobody reads.
    # SLOPGUARD_DIGEST=full forces the long form (tests, and a deliberate refresher).
    local _digest_marker
    _digest_marker="${CLAUDE_PLUGIN_DATA}/projects/$(printf '%s' "$_d" | cksum | awk '{print $1}')/digest-shown"
    if [ "${SLOPGUARD_DIGEST:-auto}" != "full" ] && [ -f "$_digest_marker" ]; then
        printf 'slop-guard active — 10 always-on rules enforced by hooks (AP-AGENT-001..009, 011); full text: skill agent-discipline\n'
        return 0
    fi
    mkdir -p "${_digest_marker%/*}" 2>/dev/null && : > "$_digest_marker"
    cat <<'DIGEST'
slop-guard active — always-on rules (full text: skill agent-discipline)
AP-AGENT-001: linter suppressions without a reason (≥10 chars) are blocked.
AP-AGENT-002: weakening a required test gate needs human review; linter thresholds are recorded, not gated.
AP-AGENT-003: skipping or deleting tests to make CI green requires confirmation.
AP-AGENT-004: curl|sh is always blocked; an install whose name resembles a popular package needs confirmation.
AP-AGENT-005: .env*, private keys, and credential files are denied for reading.
AP-AGENT-006: hard-coded credentials in written files are detected and blocked.
AP-AGENT-007: edits outside the targeted diff scope are flagged at turn end.
AP-AGENT-008: unfamiliar library or framework API — consult Context7 docs before writing code against it.
AP-AGENT-009: new dependencies enter pinned to the current stable version; never @latest, never a floating range.
AP-AGENT-011: a finding is resolved by establishing its guarantee (parse, narrow, test the failure path) — not by moving a cast or renaming a check.
DIGEST
}

main "$@"
