#!/usr/bin/env python3
"""Fail-open, one-pass Stop-hook reflections (REQ-090).

When reflection and on_stop are enabled, the first Stop emits a bounded list of
enabled questions and writes an ephemeral pending marker.  The immediately
following Stop consumes that marker and succeeds silently, preventing an
infinite reflection loop.  Answers stay in the host transcript/harness; this
hook never writes subjective reflection into canonical project state.
"""

import hashlib
import json
import os
import re
import sys
import tempfile
from pathlib import Path


OPT_OUT_VALUES = {"off", "0", "false", "disable", "disabled"}
QUESTION_ID_RE = re.compile(r"^RFQ-\d+$")
CATEGORY_ID_RE = re.compile(r"^[a-z][a-z0-9_-]{0,63}$")


def _opted_out():
    if os.environ.get("PRD_REFLECTIONS", "").strip().lower() in OPT_OUT_VALUES:
        return True
    return os.environ.get("PRD_WORKER_SESSION", "").strip().lower() in {"1", "true", "yes"}


def _load_reflection(root):
    path = Path(root) / ".prd_plugin" / "config.json"
    data = json.loads(path.read_text(encoding="utf-8-sig"))
    reflection = data.get("reflection") if isinstance(data, dict) else None
    return reflection if isinstance(reflection, dict) else {}


def select_questions(reflection):
    """Return enabled questions in config order, bounded by the configured cap.

    This reader is deliberately strict-and-silent: malformed direct JSON edits
    produce no questions, and therefore cannot trap Stop.
    """
    try:
        if reflection.get("enabled") is not True or reflection.get("on_stop") is not True:
            return []
        cap = reflection.get("max_questions_per_stop")
        if isinstance(cap, bool) or not isinstance(cap, int) or not 1 <= cap <= 50:
            return []
        categories = reflection.get("categories")
        if not isinstance(categories, list):
            return []
        selected = []
        seen_ids = set()
        seen_text = set()
        for category in categories:
            if not isinstance(category, dict) or category.get("enabled") is not True:
                continue
            category_id = category.get("id")
            category_name = category.get("name")
            questions = category.get("questions")
            if (not isinstance(category_id, str) or not CATEGORY_ID_RE.fullmatch(category_id)
                    or not isinstance(category_name, str) or not category_name.strip()
                    or not isinstance(questions, list)):
                return []
            for question in questions:
                if not isinstance(question, dict) or question.get("enabled") is not True:
                    continue
                question_id = question.get("id")
                text = question.get("text")
                normalized = " ".join(str(text).split()).lower() if isinstance(text, str) else ""
                if (not isinstance(question_id, str) or not QUESTION_ID_RE.fullmatch(question_id)
                        or not normalized or question_id in seen_ids or normalized in seen_text):
                    return []
                seen_ids.add(question_id)
                seen_text.add(normalized)
                selected.append({"id": question_id, "text": text.strip(),
                                 "category_id": category_id, "category_name": category_name.strip()})
                if len(selected) >= cap:
                    return selected
        return selected
    except Exception:
        return []


def _marker_path(root, session_id):
    safe = hashlib.sha256(str(session_id).encode("utf-8", errors="replace")).hexdigest()[:32]
    return Path(root) / ".prd_plugin" / "local" / "reflection" / f"{safe}.pending"


def _consume_pending(path):
    if not path.is_file():
        return False
    try:
        path.unlink()
    except OSError:
        pass
    return True


def _mark_pending(path):
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent,
                                         prefix=path.name + ".", suffix=".tmp", delete=False) as handle:
            temp = Path(handle.name)
            handle.write("pending\n")
        os.replace(temp, path)
        return True
    except OSError:
        try:
            if "temp" in locals() and temp.exists():
                temp.unlink()
        except OSError:
            pass
        return False


def _prompt(questions):
    lines = [
        "PRD Plugin Stop reflection: answer the enabled questions below before stopping.",
        "Give concise conclusions using observable behavior or outcomes. Do not provide chain-of-thought, private reasoning, secrets, or credentials.",
        "For yes/no questions, answer yes or no first. Limit improvement lists to the number requested.",
        "Reflection is diagnostic only: it does not replace tests, evidence, review, or completion gates.",
        "",
    ]
    for index, question in enumerate(questions, 1):
        lines.append(f"{index}. [{question['id']}] ({question['category_name']}) {question['text']}")
    return "\n".join(lines)


def main():
    if _opted_out():
        return 0
    try:
        raw = sys.stdin.read()
        payload = json.loads(raw) if raw.strip() else {}
        if not isinstance(payload, dict):
            return 0
        root = payload.get("cwd") or str(Path.cwd())
        session_id = payload.get("session_id") or "default"
        marker = _marker_path(root, session_id)
        if _consume_pending(marker):
            return 0
        questions = select_questions(_load_reflection(root))
        if not questions:
            return 0
        if not _mark_pending(marker):
            return 0
        print(json.dumps({"decision": "block", "reason": _prompt(questions)}))
    except Exception:
        return 0
    return 0


if __name__ == "__main__":
    sys.exit(main())
