#!/usr/bin/env ruby
# encoding: UTF-8
# Usage: hook-gate-check <file_path> [session_id]
# PostToolUse gate enforcement for Plastic intent lifecycle.
# Checks if a written file triggers a stage-transition gate.
# Exit 0 = allow (optionally with transition context)
# Exit 2 = block (gate violation)

require "json"
require_relative "lib/bridge"
require_relative "lib/intent_validator"
require_relative "lib/lock"

file_path = ARGV[0]
exit 0 unless file_path && !file_path.empty?

session = (ARGV[1] unless ARGV[1].to_s.empty?) || ENV["CLAUDE_CODE_SESSION_ID"]

file_path_abs = File.expand_path(file_path)

# --- Decoupled savepoint ledger (intent 34, hardened in intent 52) ---
# The savepoint is derived purely from the file path's intent directory, BEFORE
# any bridge resolution. A missing bridge must never skip the savepoint.
intent_dir_abs = Bridge.intent_dir_for(file_path_abs)
if intent_dir_abs
  begin
    Bridge.append_savepoint(intent_dir_abs, file_path_abs)
    # When checklist.md lands, How ends and Exec begins: emit the `Exec started`
    # companion in the same event (intent 81). Guard on a real (non-placeholder)
    # checklist so a scaffold sentinel does not trip it.
    if File.basename(file_path_abs) == "checklist.md" && Bridge.stage_file_present?(file_path_abs)
      Bridge.append_exec_started(intent_dir_abs)
    end
  rescue StandardError
    # ignore — rebuildable from disk
  end
end

# --- Artifact-validity backstop (intent 4a1c1) ---
# When the written file IS the intent file itself (the `<id>--<slug>.md` directly
# inside `store/<id>--<slug>/`), run IntentValidator on it. This is headless-safe:
# it depends only on the file path and disk, never on a bridge or session. In the
# PostToolUse model the write has already happened, so we cannot prevent it; the
# loud non-zero exit + stderr is the rejection signal. NOT for spec.md/plan.md/
# checklist.md/outcome.md/savepoint.md — those are validated by the stage gates.
if intent_dir_abs && File.basename(file_path_abs) == File.basename(Bridge.intent_file(intent_dir_abs))
  result = IntentValidator.validate(intent_dir_abs)
  unless result[:ok]
    warn "PLASTIC ARTIFACT INVALID — #{File.basename(file_path_abs)} is not born complete:"
    result[:missing].each { |field| warn "  missing required field: #{field}" }
    result[:errors].each { |error| warn "  #{error}" }
    warn "Fix the frontmatter; the intent is not valid until every required field is present and sources/chain are well-formed."
    exit 1
  end
end

# --- Find bridge file ---
bridge_data = Bridge.discover_bridge(session: session, cwd: Dir.pwd)

# No bridge = no active intent = no stage enforcement. Savepoint already handled.
exit 0 unless bridge_data
session = bridge_data["session"]

# --- Resolve intent directory ---

intent_info = bridge_data["intent"]
exit 0 unless intent_info

store = intent_info["store"]
dir = intent_info["dir"]
intent_dir = "#{store}/#{dir}"

# Normalize both paths for comparison
bridge_intent_dir_abs = File.expand_path(intent_dir)

# Lease heartbeat (intent 108, D1): every write by the session that owns (or
# delegates under) the lock refreshes the delivery.lock mtime. Best-effort:
# a heartbeat failure must never break the gate hook.
begin
  Lock.heartbeat(bridge_intent_dir_abs, session: session)
rescue StandardError
  # ignore
end

# Not inside intent dir = not our business
exit 0 unless file_path_abs.start_with?("#{bridge_intent_dir_abs}/")

# --- Determine if this is a stage-transition file ---

relative = file_path_abs.sub("#{bridge_intent_dir_abs}/", "")
basename = File.basename(file_path)

stage_files = %w[spec.md plan.md checklist.md outcome.md]
is_stage_file = stage_files.include?(basename)
is_action_file = relative.start_with?("actions/")

if is_stage_file || is_action_file
  # Run gate check
  error = Bridge.check_gate(bridge_intent_dir_abs, file_path_abs)

  if error
    # Block log (intent 229): same hand-rolled best-effort write as
    # hook-bash-gate's; this script keeps its current requires.
    begin
      require "fileutils"
      require "time"
      blocklog = File.join(Dir.home, ".plastic", ".cache", "gate-blocks.log")
      FileUtils.mkdir_p(File.dirname(blocklog))
      File.open(blocklog, "a") do |io|
        io.puts([Time.now.utc.iso8601, "gate-check", session.to_s,
                 Bridge.intent_id_from_dir(bridge_intent_dir_abs).to_s,
                 file_path_abs.to_s.gsub(/\s+/, " ").strip,
                 error.to_s.gsub(/\s+/, " ").strip].join("\t"))
      end
    rescue StandardError
      # the block still applies; logging is best-effort
    end

    # Gate violation — block the write
    bridge_data["build"]["gate_failures"] = (bridge_data["build"]["gate_failures"] || 0) + 1
    Bridge.write(session, bridge_data)

    payload = {
      "decision" => "block",
      "reason" => "PLASTIC GATE — #{error}"
    }
    puts JSON.generate(payload)
    exit 2
  end

  # Gate passed — update bridge with new stage
  old_stage = bridge_data["build"]["stage"]
  new_stage = Bridge.derive_stage(bridge_intent_dir_abs)
  new_missing = Bridge.missing_for_stage(new_stage) - Bridge.has_files(bridge_intent_dir_abs)

  bridge_data["build"]["stage"] = new_stage
  bridge_data["build"]["has"] = Bridge.has_files(bridge_intent_dir_abs)
  bridge_data["build"]["missing"] = new_missing
  bridge_data["build"]["gate_failures"] = 0
  bridge_data["build"]["last_activity"] = Time.now.utc.iso8601

  if old_stage != new_stage
    bridge_data["observe"]["last_transition"] = "#{old_stage} → #{new_stage}"
  end

  Bridge.write(session, bridge_data)

  # Build transition context — ONE concise sentence (intent 84, Lever 1),
  # preserving the `Next: ...` hint. Formatting is pure in Bridge.gate_narration.
  context = Bridge.gate_narration(
    old_stage: old_stage, new_stage: new_stage,
    basename: basename, new_missing: new_missing
  )

  payload = {
    "hookSpecificOutput" => {
      "hookEventName" => "PostToolUse",
      "additionalContext" => context
    }
  }
  puts JSON.generate(payload)
  exit 0
else
  # Regular file in intent dir — just update last_activity. The savepoint for
  # the What milestone was already handled above by the decoupled ledger write.
  bridge_data["build"]["last_activity"] = Time.now.utc.iso8601
  Bridge.write(session, bridge_data)
  exit 0
end
