#!/usr/bin/env ruby
# frozen_string_literal: true

# ==============================================================================
# SanePrompt - UserPromptSubmit Hook
# ==============================================================================
# Classifies prompts, injects context, detects patterns.
#
# Exit codes:
#   0 = allow (context injected via stdout if needed)
#   2 = block (rare - only for truly dangerous prompts)
#
# What this does:
#   1. Classifies: passthrough, question, task, big_task
#   2. Detects task types: bug_fix, new_feature, refactor, etc.
#   3. Shows applicable rules
#   4. Detects pattern triggers (words that predict rule violations)
#   5. Updates state for other hooks
# ==============================================================================

require 'json'
require 'fileutils'
require 'time'
require_relative 'core/mandatory_workflows'
require_relative 'core/state_manager'
require_relative 'saneprompt_intelligence'
require_relative 'saneprompt_commands'

include SanePromptIntelligence
include SanePromptCommands

LOG_FILE = File.expand_path('../../.claude/saneprompt.log', __dir__)

# === CONFIGURATION ===

# Pre-compiled regex unions for O(1) matching instead of O(n) array iteration
PASSTHROUGH_PATTERN = Regexp.union(
  /^(y|yes|n|no|ok|done|continue|approved|cancel|sure|thanks|thx)$/i,
  /^\/\w+/,  # slash commands
  /^[^a-zA-Z]*$/  # no letters (just symbols/numbers)
).freeze

# === SAFEMODE COMMANDS ===
# Extracted to saneprompt_commands.rb per Rule #10

QUESTION_PATTERN = Regexp.union(
  /^(what|where|when|why|how|which|who|can you explain|tell me about)\b/i,
  /\?$/,  # ends with question mark
  /^(does|is|are|do|should|could|would)\s+(this|it|the|that)\b/i
).freeze

TASK_INDICATOR = Regexp.union(
  /\b(fix|add|create|implement|build|rebuild|refactor|update|change|modify|delete|remove|move)\b/i,
  /\b(bug|error|issue|problem|broken|failing|crash)\b/i,
  /\b(feature|functionality|capability)\b/i,
  /\b(write|make|generate|set ?up|rewrite|overhaul|redesign)\b/i
).freeze

BIG_TASK_INDICATOR = Regexp.union(
  /\b(everything|all|entire|whole|complete|full)\b/i,
  /\b(rebuild|rewrite|overhaul|redesign|architecture)\b/i,
  /\b(system|framework|infrastructure)\b/i,
  /\bmultiple (files|components|modules)\b/i
).freeze

SANEUI_SOURCE_OF_TRUTH_PATTERN = Regexp.union(
  /\b(settings|preferences|about|license|support|software updates?)\b/i,
  /\b(button|toggle|font|text color|typography|layout|design system)\b/i,
  /\bSaneUI\b/i
).freeze

# Trigger words that predict rule violations (learned from patterns)
PATTERN_TRIGGERS = {
  'quick' => { rules: ['#3'], warning: 'quick often leads to skipped research' },
  'just' => { rules: ['#3', '#2'], warning: '"just" suggests underestimating complexity' },
  'simple' => { rules: ['#2'], warning: '"simple" changes often need API verification' },
  'fast' => { rules: ['#3'], warning: 'rushing predicts #3 violations' },
  'easy' => { rules: ['#3', '#2'], warning: '"easy" often means skipped due diligence' },
  'minor' => { rules: ['#7'], warning: '"minor" changes still need tests' },
  'small' => { rules: ['#7'], warning: '"small" fixes still need tests' },
  'tiny' => { rules: ['#7'], warning: '"tiny" fixes still need tests' },
  'trivial' => { rules: ['#3', '#7'], warning: '"trivial" often means skipped verification' },
}.freeze

# === INTELLIGENCE: Frustration Detection (in saneprompt_intelligence.rb) ===

# === BUTT-KICKER: Catch ignored warnings ===
def check_ignored_warning(prompt)
  return nil unless File.exist?(LOG_FILE)

  # Get last non-passthrough entry
  last_entry = nil
  File.readlines(LOG_FILE).reverse_each do |line|
    entry = JSON.parse(line) rescue next
    next if entry['type'] == 'passthrough'
    last_entry = entry
    break
  end

  return nil unless last_entry
  return nil if last_entry['triggers'].nil? || last_entry['triggers'].empty?

  # Check if current prompt has same triggers
  current_triggers = detect_triggers(prompt).map { |t| t[:word] }
  repeated = last_entry['triggers'] & current_triggers

  return nil if repeated.empty?

  # BUTT-KICK
  "STOP. You were just warned about: #{repeated.join(', ')}. READ THE WARNING."
rescue StandardError
  nil
end

# === INTELLIGENCE: Requirement Triggers ===
# When user says these, store as REQUIREMENT that must be satisfied
REQUIREMENT_TRIGGERS = {
  saneloop: [/\bsaneloop\b/i, /\bsane ?loop\b/i, /start the loop/i],
  commit: [/\bcommit\b/i, /\bgit commit\b/i, /commit the changes/i],
  plan: [/\bplan\s*(first|before)\b/i, /\bmake a plan\b/i, /\bplan this\b/i],
  research: [/\bresearch\s*(first|this)\b/i, /\binvestigate\b/i, /\blook into\b/i]
}.freeze

# === SKILL TRIGGERS ===
# When user says these, a skill SHOULD be invoked (not manual work)
# These skills require subagents for proper execution
SKILL_TRIGGERS = MandatoryWorkflows.skill_triggers.freeze

# Research-only mode: user wants research WITHOUT any edits
# When detected, ALL mutations blocked for the session (even after research complete)
RESEARCH_ONLY_PATTERN = Regexp.union(
  /\bresearch\b(?!.*\b(then|and|after)\b.*\b(fix|add|implement|update|change)\b)/i,  # "research X" without action verbs
  /\binvestigate\b(?!.*\b(then|and)\b.*\bfix\b)/i,     # "investigate" without "then fix"
  /\blook into\b(?!.*\b(then|and)\b.*\bfix\b)/i,       # "look into" without "then fix"
  /\bexplore\b.*\b(codebase|code|project)\b/i,         # "explore the codebase"
  /\bunderstand\b.*\b(how|why|what)\b/i,               # "understand how X works"
  /\bexplain\b.*\b(how|why|what)\b/i,                  # "explain how X works"
  /\bwhat('?s| is)\b.*\b(causing|happening|wrong)\b/i, # "what's causing X"
  /\bfind out\b.*\bwhy\b/i                             # "find out why"
).freeze

# Action verbs that override research-only (user wants changes)
ACTION_VERBS_PATTERN = /\b(fix|add|create|implement|update|change|modify|edit|write|remove|delete|refactor)\b/i.freeze

# === RESEARCH-ONLY MODE DETECTION ===
# User wants investigation WITHOUT any edits
# When detected: ALL mutations blocked for entire session

def detect_research_only_mode(prompt)
  # Must match research pattern
  return false unless prompt.match?(RESEARCH_ONLY_PATTERN)

  # If action verbs present, user wants changes (not research-only)
  return false if prompt.match?(ACTION_VERBS_PATTERN)

  true
end

# Fresh start = reset requirements; Additive = add to existing
FRESH_START_TRIGGERS = %w[saneloop commit test_mode].freeze
ADDITIVE_TRIGGERS = %w[research plan explain verify].freeze

VISUAL_VERIFICATION_PROMPT_PATTERN = Regexp.union(
  /\bvisual\s+(?:audit|verification|check|review)\b/i,
  /\bscreenshots?\b/i,
  /\bcustomer[- ]facing\s+(?:surface|view|ui|screen)s?\b/i,
  /\bstrict\s+visual\s+filters?\b/i,
  /\bbalanced,\s*clear,\s*not\s+confusing\b/i,
  /\bbeautiful\b/i,
  /\bdark\s+mode\b/i,
  /\b(?:e2e|end[- ]to[- ]end)\b.*\b(?:ui|app|surface|view|screen)\b/i,
  /\b(?:run|test|verify)\b.*\bapp\b.*\b(?:ui|visual|screen|view|surface)\b/i
).freeze

RULES_BY_TASK = {
  bug_fix: ['#3 Two Strikes', '#7 No Test No Rest', '#8 Bug Found Write Down'],
  new_feature: ['#0 Name Rule First', '#2 Verify API', '#9 Gen Pile'],
  refactor: ['#4 Green Means Go', '#10 File Size'],
  file_create: ['#1 Stay in Lane', '#9 Gen Pile'],
  general: ['#0 Name Rule First', '#5 Their House Their Rules'],
}.freeze

# === PLANNING STATE MANAGEMENT ===

PLAN_APPROVAL_PATTERN = Regexp.union(
  /^(approved|go ahead|lgtm|proceed|ship it|do it|yes|ok)$/i,
  /\b(looks good|sounds good|that works|go for it|plan approved)\b/i
).freeze

PLAN_SHOWN_PATTERN = Regexp.union(
  /\b(here's my plan|my approach|my plan is|steps?:)\b/i,
  /\b(I'll|I will|approach:)\b/i
).freeze

def check_plan_approval(prompt)
  planning = StateManager.get(:planning)
  return unless planning[:required]

  # Detect plan approval from user
  if prompt.match?(PLAN_APPROVAL_PATTERN)
    StateManager.update(:planning) do |p|
      p[:plan_approved] = true
      p
    end
    warn ''
    warn 'PLAN APPROVED - edits now allowed'
    warn ''
  end
end

def set_planning_required(prompt_type)
  return unless [:task, :big_task].include?(prompt_type)

  # Don't re-require if already approved this session
  planning = StateManager.get(:planning)
  return if planning[:plan_approved]

  StateManager.update(:planning) do |p|
    p[:required] = true
    p[:forced_at] = Time.now.iso8601 unless p[:forced_at]
    p
  end
end

# === CLASSIFICATION ===

def classify_prompt(prompt)
  return :passthrough if prompt.match?(PASSTHROUGH_PATTERN)
  return :passthrough if prompt.length < 10

  # Questions don't need full enforcement
  return :question if prompt.match?(QUESTION_PATTERN)

  # Check for task indicators
  return :question unless prompt.match?(TASK_INDICATOR)

  # Research-only prompts classify as questions even with task keywords
  # e.g. "research why the login is failing" has "failing" (task keyword)
  # but intent is investigation, not changes
  return :question if detect_research_only_mode(prompt)

  # Check for big task indicators
  prompt.match?(BIG_TASK_INDICATOR) ? :big_task : :task
end

def detect_task_types(prompt)
  types = []
  types << :bug_fix if prompt.match?(/\b(fix|bug|error|broken|failing|crash)\b/i)
  types << :new_feature if prompt.match?(/\b(add|create|implement|new|feature)\b/i)
  types << :refactor if prompt.match?(/\b(refactor|reorganize|restructure|clean ?up)\b/i)
  types << :file_create if prompt.match?(/\b(create|new) (file|class|struct|view|model)\b/i)
  types << :general if types.empty?
  types
end

# === TASK CONTEXT TRACKING (fixes task-scope bug) ===
# Research for Task A should NOT unlock edits for Task B
# Extract key nouns/topics from task to detect significant changes

# Words to ignore when extracting task keywords
STOPWORDS = %w[
  the a an and or but in on at to for of with by from
  this that these those it its
  please help me i you we they
  can could would should will
  now just also still
  file files code project app
].freeze

def extract_task_keywords(prompt)
  # Normalize and tokenize
  words = prompt.downcase.gsub(/[^a-z0-9\s]/, ' ').split

  # Remove stopwords and short words
  keywords = words.reject { |w| STOPWORDS.include?(w) || w.length < 3 }

  # Take most distinctive words (longer = more specific)
  keywords.sort_by { |w| -w.length }.first(5).sort
end

def compute_task_hash(task_types, keywords)
  require 'digest'
  content = "#{task_types.sort.join(',')}:#{keywords.sort.join(',')}"
  Digest::MD5.hexdigest(content)[0..7]
end

def check_task_change_and_reset(prompt, task_types)
  return unless [:task, :big_task].include?(classify_prompt(prompt))

  keywords = extract_task_keywords(prompt)
  new_hash = compute_task_hash(task_types, keywords)

  # Get previous task context
  prev_context = StateManager.get(:task_context)
  prev_hash = prev_context[:task_hash]

  # If no previous task, just store current
  if prev_hash.nil?
    store_task_context(task_types, keywords, new_hash)
    return
  end

  # Check if task changed significantly
  if new_hash != prev_hash
    # Check how different (keyword overlap)
    prev_keywords = prev_context[:task_keywords] || []
    overlap = (keywords & prev_keywords).length
    similarity = prev_keywords.empty? ? 0 : overlap.to_f / prev_keywords.length

    # If less than 50% overlap, this is a different task
    if similarity < 0.5
      warn ''
      warn '=' * 50
      warn 'TASK CHANGE DETECTED - RESEARCH RESET'
      warn "Previous: #{prev_context[:task_type]} (#{prev_keywords.join(', ')})"
      warn "New: #{task_types.first} (#{keywords.join(', ')})"
      warn ''
      warn 'Research from previous task does NOT apply to new task.'
      warn 'Must complete all 4 research categories for THIS task.'
      warn '=' * 50
      warn ''

      # Reset research state and planning (old plan doesn't apply to new task)
      StateManager.reset(:research)
      StateManager.reset(:edit_attempts)
      StateManager.reset(:planning)
      log_reset('research+planning', "Task change: #{prev_hash} -> #{new_hash}")
    end

    # Update stored context
    store_task_context(task_types, keywords, new_hash)
  end
end

def store_task_context(task_types, keywords, task_hash)
  StateManager.update(:task_context) do |ctx|
    ctx[:task_type] = task_types.first
    ctx[:task_keywords] = keywords
    ctx[:task_hash] = task_hash
    ctx[:researched_at] = Time.now.iso8601
    ctx
  end
rescue StandardError
  # Don't fail on state errors
end

def rules_for_prompt(prompt)
  types = detect_task_types(prompt)
  types.flat_map { |t| RULES_BY_TASK[t] }.uniq
end

def detect_triggers(prompt)
  triggers = []
  prompt_lower = prompt.downcase

  PATTERN_TRIGGERS.each do |word, info|
    if prompt_lower.include?(word)
      triggers << { word: word, rules: info[:rules], warning: info[:warning] }
    end
  end

  triggers
end


# === SKILL DETECTION ===
# Detect when a skill should be invoked and set state

def detect_skill_trigger(prompt)
  SKILL_TRIGGERS.each do |skill_name, config|
    if config[:patterns].any? { |p| prompt.match?(p) }
      return {
        name: skill_name,
        requires_subagents: config[:requires_subagents],
        min_subagents: config[:min_subagents] || 0,
        description: config[:description]
      }
    end
  end
  nil
end

def set_skill_requirement(skill_info, prompt = nil)
  return unless skill_info

  StateManager.update(:skill) do |s|
    s[:required] = skill_info[:name].to_s
    s[:required_prompt] = prompt.to_s.strip[0, 240]
    s[:invoked] = false
    s[:invoked_at] = nil
    s[:subagents_spawned] = 0
    s[:runner_started] = false
    s[:runner_proved] = false
    s[:runner_used] = false
    s[:runner_attempts] = []
    s[:runner_commands] = []
    s[:runner_proof] = nil
    s[:files_read] = []
    s[:satisfied] = false
    s[:satisfaction_reason] = nil
    s
  end

  # Output skill requirement to Claude context
  warn ''
  warn '=' * 50
  warn "SKILL REQUIRED: #{skill_info[:name]}"
  warn "  #{skill_info[:description]}"
  if skill_info[:requires_subagents]
    warn ''
    warn '  This skill REQUIRES spawning Task subagents.'
    warn "  Minimum subagents: #{skill_info[:min_subagents]}"
    warn '  DO NOT do the work manually.'
  end
  warn ''
  warn '  Use the Skill tool to invoke this skill properly.'
  if skill_info[:name] == :evolve
    warn '  Required receipt: ruby scripts/SaneMaster.rb tool_discovery --query "..."'
  end
  warn '=' * 50
  warn ''
rescue StandardError => e
  debug_log("set_skill_requirement error: #{e.message}")
end

def set_visual_verification_requirement(prompt)
  return unless prompt.match?(VISUAL_VERIFICATION_PROMPT_PATTERN)

  StateManager.update(:visual_verification) do |visual|
    visual[:required] = true
    visual[:reason] = 'prompt_requested_visual_verification'
    visual
  end

  warn ''
  warn '=' * 50
  warn 'VISUAL VERIFICATION REQUIRED'
  warn '  Capture clean Mini screenshots for every customer-facing view/state.'
  warn '  Inspect each saved image for balance, clarity, copy, contrast, confusion, clipping, overlap, and dark-mode quality.'
  warn '  Record a visual audit receipt before claiming done.'
  warn '=' * 50
  warn ''
rescue StandardError => e
  debug_log("set_visual_verification_requirement error: #{e.message}")
end

# === INTELLIGENCE: Requirement Extraction ===

def extract_requirements(prompt)
  detected = []

  REQUIREMENT_TRIGGERS.each do |req_type, patterns|
    if patterns.any? { |p| prompt.match?(p) }
      detected << req_type.to_s
    end
  end

  detected
end

def update_requirements(detected_reqs)
  return if detected_reqs.empty?

  StateManager.update(:requirements) do |reqs|
    # Check if any detected requirement is a fresh start trigger
    is_fresh = detected_reqs.any? { |r| FRESH_START_TRIGGERS.include?(r) }

    if is_fresh
      # Reset and set new requirements
      reqs[:requested] = detected_reqs
      reqs[:satisfied] = []
    else
      # Additive: merge with existing
      reqs[:requested] = ((reqs[:requested] || []) + detected_reqs).uniq
    end

    reqs
  end
rescue StandardError
  # Don't fail on state errors
end

# === INTELLIGENCE: Pattern Learning (in saneprompt_intelligence.rb) ===

# === INTELLIGENCE: Pattern Display (in saneprompt_intelligence.rb) ===

# === STATE & LOGGING ===

def log_prompt(prompt_type, rules, triggers)
  FileUtils.mkdir_p(File.dirname(LOG_FILE))
  entry = {
    timestamp: Time.now.iso8601,
    type: prompt_type,
    rules: rules,
    triggers: triggers.map { |t| t[:word] },
    pid: Process.pid
  }
  File.open(LOG_FILE, 'a') { |f| f.puts(entry.to_json) }
rescue StandardError
  # Don't fail on logging errors
end

def update_state(prompt_type, is_big_task, is_research_only = false)
  StateManager.update(:requirements) do |req|
    req[:is_task] = [:task, :big_task].include?(prompt_type)
    req[:is_big_task] = prompt_type == :big_task
    req[:is_research_only] = is_research_only
    req
  end
rescue StandardError
  # Don't fail on state errors
end

# === OUTPUT ===

def output_context(prompt_type, rules, triggers, prompt, frustrations = [], detected_reqs = [], learning_warning = nil, patterns = nil, memory_staging = nil)
  lines = []

  # Only show context for tasks
  return if [:passthrough, :question].include?(prompt_type)

  lines << '---'
  lines << "Task type: #{prompt_type}"

  # AUTO-SANELOOP: Inject structured workflow for ALL tasks
  # This is the core of the unified workflow system
  # Learned from 700+ iteration failure: guardrails prevent spirals
  # User insight: "ANY code change is a big task" - no more "no big deal" syndrome
  lines << ''
  lines << 'WORKFLOW STRUCTURE (auto-injected):'
  lines << '  1. Use the smallest evidence set that makes the task defensible'
  lines << '     - local inspection before edits'
  lines << '     - docs for uncertain APIs/libraries'
  lines << '     - web for current external facts'
  lines << '     - GitHub for upstream/repo/issue state'
  lines << '  2. Define acceptance criteria: what does "done" look like?'
  lines << '  3. Edits blocked until research complete (sanetools enforces)'
  lines << '  4. If you touch hooks, tools, skills, templates, or durable docs, update handoff + memory'
  lines << '  5. Before saying a tool is missing, choosing a new canonical tool path, or using a repeated tool-deficiency workaround, run: ruby scripts/SaneMaster.rb tool_discovery --query "..."'
  lines << '  6. If a recurring tool is truly missing, install it, document it, and make it the standard path'
  lines << '  7. Self-rate SOP compliance when done'
  lines << ''
  lines << 'GUARDRAILS ACTIVE (all code tasks):'
  lines << '  - Max 3 edit attempts before mandatory research pause (ENFORCED)'
  lines << '  - If stuck after 2 tries: STOP and investigate, do not guess'
  lines << '  - Circuit breaker trips at 3 consecutive failures'
  lines << '  - Tooling/docs work is persistent work, not optional cleanup'
  if prompt.match?(SANEUI_SOURCE_OF_TRUTH_PATTERN)
    lines << '  - Settings/About/license/update UI work MUST start from SaneUI Catalog and shared components, not app-local clones'
    lines << '  - SaneUI source of truth: ~/SaneApps/infra/SaneUI/Sources/SaneUICatalog/SaneUICatalogApp.swift'
  end
  if prompt_type == :big_task
    lines << ''
    lines << 'BIG TASK - Additional guardrails:'
    lines << '  - SaneLoop iterations tracked'
    lines << '  - Max 20 iterations before human check-in'
  end

  # INTELLIGENCE: memory update needed from previous session
  memory_context = format_memory_staging_context(memory_staging)
  if memory_context
    lines << ''
    lines << memory_context
  end

  # INTELLIGENCE: Show learned patterns from previous sessions
  pattern_context = format_patterns_for_claude(patterns)
  if pattern_context
    lines << ''
    lines << pattern_context
  end

  # INTELLIGENCE: Learning pattern warning
  if learning_warning
    lines << ''
    lines << "LEARNING WARNING: #{learning_warning}"
  end

  # INTELLIGENCE: Frustration detected
  if frustrations.any?
    lines << ''
    lines << 'FRUSTRATION DETECTED:'
    frustrations.each do |f|
      lines << "  Type: #{f[:type]} - User may be correcting you. Read carefully."
    end
  end

  # INTELLIGENCE: Requirements extracted
  if detected_reqs.any?
    lines << ''
    lines << 'REQUIREMENTS DETECTED:'
    detected_reqs.each do |r|
      lines << "  #{r} - Must be satisfied before editing"
    end
  end

  if triggers.any?
    lines << ''
    lines << 'PATTERN ALERT:'
    triggers.each do |t|
      lines << "  #{t[:word]}: #{t[:warning]}"
    end
  end

  if rules.any?
    lines << ''
    lines << 'Applicable rules:'
    rules.each { |r| lines << "  #{r}" }
  end

  lines << '---'

  # Output to stdout - this becomes context for Claude
  puts lines.join("\n")
end

def output_warning(prompt_type, rules, triggers, frustrations = [], detected_reqs = [], patterns = nil)
  # Only show warnings for tasks (stderr shown to user)
  return if [:passthrough, :question].include?(prompt_type)

  warn '---'
  warn "SanePrompt: #{prompt_type.to_s.gsub('_', ' ').upcase}"

  # INTELLIGENCE: Show pattern summary to user
  pattern_summary = format_patterns_for_user(patterns)
  if pattern_summary
    warn ''
    warn pattern_summary
  end

  # INTELLIGENCE: Show frustration warning to user
  if frustrations.any?
    warn ''
    warn 'Frustration detected - Claude will read carefully'
  end

  # INTELLIGENCE: Show detected requirements
  if detected_reqs.any?
    warn ''
    warn "Requirements: #{detected_reqs.join(', ')}"
  end

  if triggers.any?
    warn ''
    warn 'Pattern triggers detected:'
    triggers.each { |t| warn "  #{t[:word]} -> #{t[:warning]}" }
  end

  if rules.any?
    warn ''
    warn 'Rules to follow:'
    rules.first(3).each { |r| warn "  #{r}" }
  end

  warn '---'
end

# === MAIN PROCESSING ===

def process_prompt(prompt)
  # Handle safemode commands first
  if handle_safemode_command(prompt)
    return 0
  end

  # === PLANNING: Check for approval BEFORE classification ===
  # "approved", "yes", "ok" are passthroughs but must still approve plans.
  # Without this, natural approval phrases never reach check_plan_approval.
  check_plan_approval(prompt)

  early_skill_trigger = detect_skill_trigger(prompt)
  prompt_type = classify_prompt(prompt)
  prompt_type = :question if prompt_type == :passthrough && early_skill_trigger

  if prompt_type == :passthrough
    log_prompt(:passthrough, [], [])
    return 0
  end

  rules = rules_for_prompt(prompt)
  triggers = detect_triggers(prompt)
  task_types = detect_task_types(prompt)

  # === PLANNING: Require plan for task prompts ===
  set_planning_required(prompt_type)

  # === TASK CONTEXT CHECK (fixes task-scope bug) ===
  # If task changed significantly, reset research - research for Task A
  # should NOT unlock edits for Task B
  check_task_change_and_reset(prompt, task_types)

  # === INTELLIGENCE ===

  # 0. BUTT-KICKER: Check if ignoring previous warning
  butt_kick = check_ignored_warning(prompt)
  if butt_kick
    warn ''
    warn '=' * 50
    warn butt_kick
    warn '=' * 50
    warn ''
  end

  # 1. Detect frustration signals
  frustrations = detect_frustration(prompt)

  # 2. Extract and store requirements
  detected_reqs = extract_requirements(prompt)
  update_requirements(detected_reqs)

  # 3. Learn from frustration
  learn_from_frustration(prompt, frustrations)

  # 4. Check past learnings for patterns
  learning_warning = check_past_learnings

  # 5. Detect research-only mode
  is_research_only = detect_research_only_mode(prompt)

  # 6. Detect skill triggers (docs-audit, evolve, status, verify, ship, check_inbox, outreach)
  skill_trigger = early_skill_trigger || detect_skill_trigger(prompt)
  set_skill_requirement(skill_trigger, prompt) if skill_trigger

  # 6b. Visual verification is a first-class acceptance criterion for UI work.
  set_visual_verification_requirement(prompt)

  # 7. Get learned patterns from previous sessions
  learned_patterns = get_learned_patterns

  # 7. Memory staging removed — learnings captured via session_learnings.jsonl
  memory_staging = nil

  # === END INTELLIGENCE ===

  log_prompt(prompt_type, rules, triggers)
  update_state(prompt_type, prompt_type == :big_task, is_research_only)

  # Output research-only warning
  if is_research_only
    warn ''
    warn '=' * 50
    warn 'RESEARCH-ONLY MODE ACTIVE'
    warn 'User wants investigation, NOT changes.'
    warn 'ALL edits blocked for this session.'
    warn '=' * 50
    warn ''
  end

  # Memory staging output removed — learnings captured via session_learnings.jsonl

  # Output context to Claude (stdout)
  output_context(prompt_type, rules, triggers, prompt, frustrations, detected_reqs, learning_warning, learned_patterns, memory_staging)

  # Output warning to user (stderr)
  output_warning(prompt_type, rules, triggers, frustrations, detected_reqs, learned_patterns)

  0  # Always allow prompts
end

# === SELF-TEST ===
# Tests extracted to saneprompt_test.rb per Rule #10

def self_test
  require_relative 'saneprompt_test'
  exit SanePromptTest.run(
    method(:classify_prompt),
    method(:rules_for_prompt),
    method(:detect_triggers),
    method(:detect_frustration),
    method(:extract_requirements),
    method(:detect_research_only_mode),
    method(:handle_safemode_command),
    method(:check_plan_approval),
    method(:detect_skill_trigger),
    method(:process_prompt)
  )
end

def check_heartbeat
  begin
    last_line = File.readlines(LOG_FILE).last
    entry = JSON.parse(last_line)
    last = Time.parse(entry['timestamp'])
    age = Time.now - last
    warn "Last prompt: #{entry['type']} at #{entry['timestamp']} (#{age.round}s ago)"
    exit(age < 300 ? 0 : 1)
  rescue StandardError => e
    warn "No heartbeat: #{e.message}"
    exit 1
  end
end

# === DEBUG LOGGING ===
DEBUG_FILE = File.expand_path('../../.claude/saneprompt_debug.log', __dir__)

def debug_log(msg)
  FileUtils.mkdir_p(File.dirname(DEBUG_FILE))
  File.open(DEBUG_FILE, 'a') { |f| f.puts("[#{Time.now.iso8601}] #{msg}") }
rescue StandardError
  # ignore
end

# === MAIN ===

if ARGV.include?('--self-test')
  require_relative 'self_test_environment'
  exit SelfTestEnvironment.run_isolated(__FILE__)
elsif ARGV.include?('--self-test-internal')
  self_test
elsif ARGV.include?('--check-heartbeat')
  check_heartbeat
else
  begin
    raw = $stdin.read
    debug_log("RAW INPUT: #{raw[0..200]}")
    input = JSON.parse(raw)
    debug_log("PARSED KEYS: #{input.keys.inspect}")
    prompt = input['prompt'] || input['user_prompt'] || ''
    debug_log("EXTRACTED PROMPT: '#{prompt}'")
    exit process_prompt(prompt)
  rescue JSON::ParserError, Errno::ENOENT => e
    debug_log("PARSE ERROR: #{e.message}")
    exit 0  # Don't block on parse errors
  end
end
