#!/usr/bin/env bash
# gstack-distill-apply — apply a single distillation proposal after user Y.
#
# Plan-tune cathedral T11. Reads distillation-proposals.json, applies the
# Nth proposal to the right surface:
#
#   preference     → gstack-question-preference --write
#   declared-nudge → atomic update to ~/.gstack/developer-profile.json declared
#   memory-nugget  → append to ~/.gstack/free-text-memory.json (local fallback)
#
# Always confirm before calling this from the skill — the bin assumes the user
# already approved (Codex #15 trust boundary). The skill template (/plan-tune
# distill review section) handles the confirm UX.
#
# gbrain integration: when gbrain is configured, the skill template ALSO
# invokes mcp__gbrain__put_page / extract_facts / add_tag in the same turn
# (those are MCP tools, not CLI-callable). Pass --gbrain-published true to
# mark the proposal as mirrored to gbrain. The local file always gets the
# write so it's the durable source-of-truth even on machines without gbrain.
#
# Usage:
#   gstack-distill-apply --proposal <N>                # apply Nth proposal
#   gstack-distill-apply --proposal <N> --gbrain-published true
#   gstack-distill-apply --list                        # show pending proposals
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
GSTACK_HOME="${GSTACK_STATE_ROOT:-${GSTACK_HOME:-$HOME/.gstack}}"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null || true)"
SLUG="${SLUG:-unknown}"
PROJECT_DIR="$GSTACK_HOME/projects/$SLUG"
PROPOSAL_FILE="$PROJECT_DIR/distillation-proposals.json"
MEMORY_FILE="$GSTACK_HOME/free-text-memory.json"
PROFILE_FILE="$GSTACK_HOME/developer-profile.json"

ACTION="apply"
PROPOSAL_IDX=""
GBRAIN_PUBLISHED="false"

while [ $# -gt 0 ]; do
  case "$1" in
    --proposal) PROPOSAL_IDX="$2"; shift 2 ;;
    --gbrain-published) GBRAIN_PUBLISHED="$2"; shift 2 ;;
    --list) ACTION="list"; shift ;;
    --help|-h)
      sed -n '1,/^set -euo/p' "$0" | sed 's|^# \?||'
      exit 0
      ;;
    *) echo "unknown arg: $1" >&2; exit 1 ;;
  esac
done

if [ ! -f "$PROPOSAL_FILE" ]; then
  echo "NO_PROPOSALS: $PROPOSAL_FILE missing — run gstack-distill-free-text first"
  exit 0
fi

if [ "$ACTION" = "list" ]; then
  PROPOSAL_FILE_PATH="$PROPOSAL_FILE" bun -e '
    const fs = require("fs");
    const p = JSON.parse(fs.readFileSync(process.env.PROPOSAL_FILE_PATH, "utf-8"));
    const proposals = p.proposals || [];
    if (proposals.length === 0) { console.log("(no proposals)"); process.exit(0); }
    console.log("GENERATED: " + p.generated_at);
    console.log("SOURCE_EVENTS: " + (p.source_event_count || 0));
    proposals.forEach((pr, i) => {
      console.log("");
      console.log("[" + i + "] " + (pr.kind || "?") + " (confidence: " + (pr.confidence || "?") + ")");
      if (pr.rationale) console.log("    rationale: " + pr.rationale);
      if (pr.kind === "preference") {
        console.log("    question_id: " + pr.question_id);
        console.log("    preference: " + pr.preference);
      } else if (pr.kind === "declared-nudge") {
        console.log("    dimension: " + pr.dimension);
        console.log("    direction: " + pr.direction + " (" + (pr.magnitude || "?") + ")");
      } else if (pr.kind === "memory-nugget") {
        console.log("    nugget: " + pr.nugget);
        console.log("    signal_keys: " + JSON.stringify(pr.applies_to_signal_keys || []));
      }
      if (pr.source_quotes && pr.source_quotes.length) {
        console.log("    quotes:");
        pr.source_quotes.forEach((q) => console.log("      - \"" + q + "\""));
      }
    });
  '
  exit 0
fi

if [ -z "$PROPOSAL_IDX" ]; then
  echo "--proposal <N> required" >&2
  exit 1
fi

# Apply via bun. Each kind has its own surface.
mkdir -p "$PROJECT_DIR"
PROPOSAL_IDX="$PROPOSAL_IDX" \
PROPOSAL_FILE_PATH="$PROPOSAL_FILE" \
MEMORY_FILE_PATH="$MEMORY_FILE" \
PROFILE_FILE_PATH="$PROFILE_FILE" \
PREF_BIN="$SCRIPT_DIR/gstack-question-preference" \
GBRAIN_PUBLISHED="$GBRAIN_PUBLISHED" \
bun -e '
  const fs = require("fs");
  const { spawnSync } = require("child_process");
  const idx = parseInt(process.env.PROPOSAL_IDX, 10);
  const p = JSON.parse(fs.readFileSync(process.env.PROPOSAL_FILE_PATH, "utf-8"));
  const proposals = p.proposals || [];
  if (!Number.isInteger(idx) || idx < 0 || idx >= proposals.length) {
    process.stderr.write("invalid --proposal index " + idx + " (have " + proposals.length + ")\n");
    process.exit(1);
  }
  const pr = proposals[idx];

  const stamp = new Date().toISOString();

  // Memory-nugget: always write to local file (durable source-of-truth even
  // when gbrain is configured — gbrain is mirror, file is canon for the
  // PreToolUse hook injection path in Layer 8).
  if (pr.kind === "memory-nugget") {
    const memPath = process.env.MEMORY_FILE_PATH;
    let mem = { nuggets: [] };
    try { mem = JSON.parse(fs.readFileSync(memPath, "utf-8")); } catch {}
    if (!Array.isArray(mem.nuggets)) mem.nuggets = [];
    mem.nuggets.push({
      nugget: pr.nugget,
      applies_to_signal_keys: pr.applies_to_signal_keys || [],
      applied_at: stamp,
      gbrain_published: process.env.GBRAIN_PUBLISHED === "true",
      source_quotes: pr.source_quotes || [],
    });
    const tmp = memPath + ".tmp";
    fs.writeFileSync(tmp, JSON.stringify(mem, null, 2));
    fs.renameSync(tmp, memPath);
    console.log("APPLIED: memory-nugget appended to " + memPath);
  }

  // Preference: route through gstack-question-preference for the user-origin
  // gate + event audit trail. source=plan-tune is the allowed value since
  // the user opt-in came from inside /plan-tune.
  if (pr.kind === "preference") {
    const res = spawnSync(process.env.PREF_BIN, [
      "--write",
      JSON.stringify({
        question_id: pr.question_id,
        preference: pr.preference,
        source: "plan-tune",
        free_text: (pr.source_quotes || []).join(" | ").slice(0, 300),
      }),
    ], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000 });
    if (res.status !== 0) {
      process.stderr.write("preference apply failed: " + (res.stderr || res.stdout) + "\n");
      process.exit(1);
    }
    console.log("APPLIED: preference " + pr.question_id + " → " + pr.preference);
  }

  // Declared-nudge: atomic update to developer-profile.json declared. Magnitude
  // tiers: small=0.05, medium=0.10, large=0.15. Clamp to [0, 1].
  if (pr.kind === "declared-nudge") {
    const mag = { small: 0.05, medium: 0.10, large: 0.15 }[pr.magnitude || "small"] || 0.05;
    const delta = pr.direction === "down" ? -mag : mag;
    const profilePath = process.env.PROFILE_FILE_PATH;
    let profile = {};
    try { profile = JSON.parse(fs.readFileSync(profilePath, "utf-8")); } catch {}
    profile.declared = profile.declared || {};
    const cur = typeof profile.declared[pr.dimension] === "number" ? profile.declared[pr.dimension] : 0.5;
    const next = Math.max(0, Math.min(1, cur + delta));
    profile.declared[pr.dimension] = +next.toFixed(3);
    profile.declared_at = stamp;
    const tmp = profilePath + ".tmp";
    fs.writeFileSync(tmp, JSON.stringify(profile, null, 2));
    fs.renameSync(tmp, profilePath);
    console.log("APPLIED: declared." + pr.dimension + " " + cur + " → " + profile.declared[pr.dimension]);
  }

  // Mark the proposal as applied so /plan-tune list shows it consumed.
  pr.applied_at = stamp;
  pr.gbrain_published = process.env.GBRAIN_PUBLISHED === "true";
  const tmp = process.env.PROPOSAL_FILE_PATH + ".tmp";
  fs.writeFileSync(tmp, JSON.stringify(p, null, 2));
  fs.renameSync(tmp, process.env.PROPOSAL_FILE_PATH);
'
