#!/usr/bin/env ruby
# encoding: UTF-8
# Usage: hook-session-start <index_path> <plastic_home> <mode> [plugin_root]
# Outputs hook JSON for the session-start hook. Exits silently if nothing to show.

require "json"
require "date"
require "yaml"
require_relative "lib/bridge"
require_relative "lib/boot_banner"
require_relative "lib/qmd_sync"
require_relative "lib/doctor_core"

index_path, plastic_home, mode, plugin_root = ARGV
exit 0 unless index_path && plastic_home && mode

# Plastic home and the store are two different paths (intent 231). The shim passes
# home (~/.plastic) as argument 2; the store lives one level below it. Compose the
# store exactly once here, so no later line re-derives it and no path can gain a
# doubled store/ segment.
store_dir = File.join(plastic_home, "store")

# --- Parse INDEX.md ---

lines = File.readlines(index_path)
active = []
future = []
section = nil

lines.each do |line|
  if line.start_with?("## Active")
    section = :active
    next
  elsif line.start_with?("## Future")
    section = :future
    next
  elsif line.start_with?("## ")
    section = nil
    next
  end

  next unless section && line.strip.start_with?("- [")

  if section == :active
    active << line.strip
  elsif section == :future
    future << line.strip
  end
end

# --- Derive bridge for active intent ---

bridge_data = nil
if active.length == 1 && active.first =~ /store\/([\w-]+)\//
  dir_name = $1
  intent_dir = "#{store_dir}/#{dir_name}"
  session = ENV["CLAUDE_CODE_SESSION_ID"] || Process.pid.to_s
  intent_id = dir_name.split("--").first
  intent_name = active.first[/\[([^\]]+)\]/, 1] || "unknown"
  bridge_data = Bridge.derive(session, intent_id: intent_id, intent_dir: intent_dir, store: store_dir, name: intent_name)
end

# --- Detect stale future intents ---

stale = []
read_config = if plugin_root && !plugin_root.empty?
  "#{plugin_root}/scripts/read-config"
else
  File.expand_path("~/.plastic/scripts/read-config")
end
stale_days = `"#{read_config}" stale_threshold_days`.strip.to_i
stale_days = 3 if stale_days == 0

future.each do |f|
  if f =~ /store\/([\w-]+)\//
    dir_name = $1
    intent_file = "#{store_dir}/#{dir_name}/#{dir_name}.md"
    next unless File.exist?(intent_file)

    content = File.read(intent_file)
    if content =~ /^created:\s*['"]?(\d{4}-\d{2}-\d{2})/
      age = (Date.today - Date.parse($1)).to_i
      if age >= stale_days
        name = f[/\[([^\]]+)\]/, 1] || "unknown"
        stale << { name: name, age: age, entry: f }
      end
    end
  end
end

# --- Detect current project ---

current_project = nil
project_active = []
project_future = []

projects_path = "#{plastic_home}/projects.yml"
if File.exist?(projects_path)
  projects = YAML.safe_load(File.read(projects_path)) rescue {}
  cwd = Dir.pwd
  (projects["projects"] || {}).each do |slug, info|
    project_path = File.expand_path(info["path"])
    if cwd.start_with?(project_path)
      current_project = { "slug" => slug, "parent" => info["parent"], "path" => project_path }
      project_index = "#{plastic_home}/projects/#{slug}/INDEX.md"
      if File.exist?(project_index)
        p_section = nil
        File.readlines(project_index).each do |pline|
          if pline.start_with?("## Active")
            p_section = :active
            next
          elsif pline.start_with?("## Future")
            p_section = :future
            next
          elsif pline.start_with?("## ")
            p_section = nil
            next
          end
          next unless p_section && pline.strip.start_with?("- [")
          project_active << pline.strip if p_section == :active
          project_future << pline.strip if p_section == :future
        end
      end
      break
    end
  end
end

# --- Load PLASTIC.md conventions ---

plastic_md_path = "#{plastic_home}/PLASTIC.md"
plastic_md = File.exist?(plastic_md_path) ? File.read(plastic_md_path).strip : nil

# --- Load deprecations ---

dep_file = if plugin_root && !plugin_root.empty?
  "#{plugin_root}/deprecations.yml"
else
  "#{plastic_home}/deprecations.yml"
end

deprecations = []
if File.exist?(dep_file)
  dep_data = YAML.safe_load(File.read(dep_file)) rescue {}
  deprecations = dep_data["deprecations"] || []
end

dismissed_json = `"#{read_config}" deprecations_dismissed`.strip
dismissed = begin
  JSON.parse(dismissed_json)
rescue
  []
end

current_version = nil
version_file = "#{plastic_home}/VERSION"
if File.exist?(version_file)
  current_version = File.read(version_file).strip
elsif plugin_root && !plugin_root.empty?
  plugin_json_path = "#{plugin_root}/.claude-plugin/plugin.json"
  if File.exist?(plugin_json_path)
    pj = JSON.parse(File.read(plugin_json_path)) rescue {}
    current_version = pj["version"]
  end
end

active_deprecations = deprecations.select do |dep|
  next true if dep["severity"] == "critical"
  next true if current_version && dep["removal"] == current_version
  !dismissed.include?(dep["id"])
end

# --- Check for available updates (from previous session's check) ---

update_notice = nil
cache_file = "#{plastic_home}/.cache/update-check.json"
if File.exist?(cache_file)
  cache = JSON.parse(File.read(cache_file)) rescue {}
  if cache["updateAvailable"]
    update_notice = "Plastic update available: #{cache["current"]} -> #{cache["latest"]} — run /plastic-update"
  end
end

# --- Run core health in-process (intent 36a) ---
# Reuse Doctor's own --core checks so there is one source of truth for "core
# health" (no second process spawn, no duplicated check list). Never blocks the
# session: any failure or exception degrades to a banner and we continue.

core_health = begin
  Doctor.new(plastic_home: plastic_home).run_core_checks("claude")
rescue
  nil
end
core_banner = BootBanner.render(health: core_health, version: current_version)

# --- Assemble session context ---

all_active = active + project_active
all_future = future + project_future

# --- Build context ---
# PLASTIC.md conventions and intent context render only when conventions are
# installed. Deprecation warnings and update notices render regardless, so a
# partial install (or a missing PLASTIC.md) still surfaces critical warnings.

parts = []

# Boot banner first: every session start surfaces that Plastic loaded, with the
# version (clean) or the first failing core check (degraded). Owned by the hook
# so it runs by construction — the plastic-intent-continuing skill no longer does this.
parts << core_banner
parts << ""

# --- QMD search status (intent 45a, READ-ONLY) ---
# Report-only line for the model (additionalContext), never systemMessage and
# never the exit code. QmdSync.status shells out to `qmd collection list`, so the
# whole block is guarded: a 2s timeout caps any hang, and rescue-all guarantees a
# slow/broken/missing qmd appends nothing and the hook continues cleanly.
begin
  require "timeout"
  qmd_status = Timeout.timeout(2) { QmdSync.status(plastic_home: plastic_home) }
  if qmd_status[:present]
    if qmd_status[:all_registered]
      parts << "QMD: #{qmd_status[:registered].size} Plastic collections indexed (search with the qmd skill)."
    else
      parts << "QMD detected — run `qmd-sync register --all` to index your Plastic stores for search."
    end
  end
rescue Exception
  # Any failure (timeout, missing binary, parse error) — stay silent, never crash.
end

if plastic_md
  # Conventions always loaded first
  parts << plastic_md
  parts << "\n---\n"

  if current_project
    slug = current_project["slug"]
    banner = "Project: #{slug} | Store: ~/.plastic/projects/#{slug}/store/"
    if project_active.any? && project_active.first =~ /\[([^\]]+)\].*store\/([\w-]+)\//
      intent_name, dir_name = $1, $2
      intent_id = dir_name.split("--").first
      banner += "\nActive: [#{intent_id} — #{intent_name}] | Artifacts → store/#{dir_name}/"
    end
    parts << banner + "\n"
  else
    parts << "PLASTIC — Global store loaded from ~/.plastic/\n"
  end

  if all_active.any?
    parts << "Active intents:\n"
    all_active.each { |a| parts << a }
    if bridge_data
      stage = bridge_data["build"]["stage"]
      missing = bridge_data["build"]["missing"]
      missing_str = missing.empty? ? "none" : missing.join(", ")
      parts << "Stage: #{stage} | Next: #{missing_str}"
    end
    parts << ""
  else
    parts << "No active intents. #{future.length} future intents available.\n"
  end

  if stale.any?
    parts << "\nStale future intents (untouched for days):\n"
    stale.each do |s|
      parts << "- #{s[:name]} (#{s[:age]} days) — consider: activate, abandon, or defer to agent"
    end
    parts << "\nWhen appropriate, ask the user what to do with stale intents."
  end
end

if active_deprecations.any?
  parts << ""
  active_deprecations.each do |dep|
    severity = dep["severity"] || "info"
    summary = dep["summary"] || dep["id"]
    removal = dep["removal"]
    link = dep["link"]
    steps = dep["migration_steps"] || []

    if severity == "info"
      line = "i Deprecation: #{summary}. Removed in: #{removal}."
      line += " See: #{link}" if link
      parts << line
    else
      marker = severity == "critical" ? "!! DEPRECATION (critical)" : "! DEPRECATION (warning)"
      parts << "#{marker}: #{summary}"
      if steps.any?
        parts << "  Migration steps:"
        steps.each_with_index { |s, i| parts << "  #{i + 1}. #{s}" }
      end
      trail = "  Removed in: #{removal}"
      trail += " | Details: #{link}" if link
      parts << trail
    end
  end
end

if update_notice
  parts.unshift("! #{update_notice}\n")
end

# Emit nothing when there is genuinely nothing to surface (no conventions,
# no deprecations, no update notice).
exit 0 if parts.join.strip.empty?

payload = {
  "hookSpecificOutput" => {
    "hookEventName" => "SessionStart",
    "additionalContext" => parts.join("\n")
  },
  # Intent 54: additionalContext is model-only, so the banner stays invisible to
  # the human. The top-level systemMessage channel is rendered in the user's
  # terminal (and re-fires on /clear). Reuse the same BootBanner line so the
  # visible banner and the model-facing banner cannot drift.
  "systemMessage" => core_banner
}
puts JSON.generate(payload)
