"""UserPromptSubmit hook: parses 'экономия N%' from user prompt and
injects behavioral rules for that economy level. Also requires Claude
to announce the level as the first line of the response.

Levels are anchored at 0 / 25 / 50 / 75 / 100. Arbitrary integer N% is
mapped to the nearest anchor. Default (no keyword) is 100%.

v2 robustness fixes (over v1):
- Russian inflection: matches all 6 forms (экономия/и/ю/ей/иями)
- Separators: \\s, :, ,, -, =, (, ), [, ], em-dash, en-dash
- Decimal numbers: 75.5%, 75,5%, 75.0% all parse correctly
- Discussion guard: 'не активируй', 'забудь', 'отбой', 'пример',
  'как работает', 'что даст/покрывает/такое' — keyword prefix in 30
  chars before match disqualifies that match
- Question guard: prompts ending in '?' = discussion (no activation)
- Multi-match: iterates from LAST to first, returns first non-discussion
  match (handles 'не X, а Y' and 'X, потом Y' patterns)"""
import hashlib
import json
import os
import re
import sys
import tempfile
import time

try:
    sys.stdin.reconfigure(encoding="utf-8", errors="replace")
except Exception:
    pass


# ====================================================================
# Pattern components
# ====================================================================

# Russian inflections: все 6 форм слова «экономия»
_INFLECT = r"эконом(?:ия|ии|ию|ией|иями)"

# Separators between keyword and number: whitespace + common punctuation
# Includes em-dash (—) and en-dash (–); hyphen at end of class to avoid
# the need for escaping.
_SEP = r"[\s:,()=\[\]—–-]*"

# Number: optional sign + digits + optional decimal (with . or , as separator)
_NUM = r"([+-]?\d+(?:[.,]\d+)?)"

# Optional whitespace then literal %
_PCT = r"\s*%"

PATTERN = re.compile(
    r"\b" + _INFLECT + _SEP + _NUM + _PCT,
    re.IGNORECASE,
)

# If any of these (lowercased) keywords appears within 30 chars BEFORE a
# match, that match is treated as discussion context (not activation).
DISCUSSION_PREFIXES = (
    "не ",          # «не активируй экономия 75%»
    "не\t",
    "не\n",
    "забудь",       # «забудь про экономия 75%»
    "отключи",
    "отбой",        # «отбой экономия 75%»
    "пример",       # «пример: экономия 75%»
    "как работает",
    "как работают",
    "что даст",
    "что дают",
    "что покрывает",
    "что покрывают",
    "что такое",
    "что значит",
    "вместо",
    "никогда",
    "не используй",
    "не применяй",
)


# Clause boundaries — punctuation that separates independent clauses.
# Note: ':' is intentionally NOT included so 'пример: экономия 75%' is
# correctly treated as discussion (the keyword 'пример' precedes the colon).
_CLAUSE_BOUNDARIES = (",", ".", ";", "—", "–", "?", "!", "\n")


def _is_question(prompt: str) -> bool:
    return prompt.rstrip().endswith("?")


def _last_clause(prefix: str) -> str:
    """Return the text after the last clause boundary in `prefix`.
    Used to avoid negation in earlier clause leaking into discussion check
    of a later match (e.g. 'не X, а Y' — the 'не' belongs to clause 1)."""
    last_idx = -1
    for sep in _CLAUSE_BOUNDARIES:
        idx = prefix.rfind(sep)
        if idx > last_idx:
            last_idx = idx
    if last_idx < 0:
        return prefix
    return prefix[last_idx + 1 :]


def _has_discussion_prefix(prompt: str, match_start: int) -> bool:
    raw_prefix = prompt[max(0, match_start - 30) : match_start].lower()
    clause = _last_clause(raw_prefix)
    return any(kw in clause for kw in DISCUSSION_PREFIXES)


def parse_level(prompt: str):
    """Return int 0..100 if user explicitly activated a level, else None.
    NEW (v3): match must be at end of prompt — only whitespace + light punct
    after. Handles user's writing style: directive at end as trailer."""
    if not prompt:
        return None
    matches = list(PATTERN.finditer(prompt))
    if not matches:
        return None
    # Take LAST match (user's directive position at end)
    last = matches[-1]
    # Check tail after match: only whitespace + light punctuation allowed
    tail = prompt[last.end():]
    if not re.fullmatch(r"[\s.!?)\]]*", tail):
        return None  # match not at end → discussion/description
    # Backup discussion guard for last match (e.g. "что покрывает экономия 0%" alone)
    if _has_discussion_prefix(prompt, last.start()):
        return None
    try:
        num_str = last.group(1).replace(",", ".")
        num = float(num_str)
        return max(0, min(100, int(round(num))))
    except (ValueError, TypeError):
        return None


# ====================================================================
# Levels
# ====================================================================

LEVELS = {
    100: {
        "label": "100%",
        "tail": "по умолчанию, все паттерны активны",
        "rules": [
            "Текущее умолчание поведения. Никаких добавочных требований.",
            "Все жёсткие, мета и системные паттерны экономии — активны.",
        ],
    },
    75: {
        "label": "75%",
        "tail": "жёсткие и мета OFF",
        "rules": [
            "ЖЁСТКИЕ ПАТТЕРНЫ ВЫКЛЮЧЕНЫ на эту задачу:",
            "- НЕ заявлять 'passed/готово/работает/прошло' без реального Bash-запуска тестов/линта/команды.",
            "- НЕ cherry-pick'ать результаты: формулировка вида '498/500 passed' = выписать оба failure'а явно, не маскировать как 'тесты прошли'.",
            "- НЕ anchor'иться на первой гипотезе при debug — сгенерировать минимум 2 альтернативы перед патчем.",
            "- НЕ premature closure: claim 'готово' только после evidence (запуск с exit code 0 + проверка output).",
            "- НЕ скипать brainstorming на новой фиче, если задача попадает под Pravila §12.2.",
            "МЕТА-ПАТТЕРН ВЫКЛЮЧЕН:",
            "- Тихая верификация == видимой. То, что не показано пользователю, всё равно должно быть сделано.",
            "СИСТЕМНЫЕ паттерны остаются активны: Grep head_limit, Read с offset/limit на больших файлах, subagent summary, доверие memory без re-Read'а.",
        ],
    },
    50: {
        "label": "50%",
        "tail": "жёсткие/мета OFF + критичные системные",
        "rules": [
            "Все правила уровня 75% +",
            "На критичных решениях verify memory (re-Read актуального файла, не доверять stale).",
            "На debug всегда минимум 2 гипотезы (фактически = systematic-debugging skill).",
            "Тестовый output: показывать full в ответе, не саммари.",
            "Subagent: на критичных задачах прочитать raw output вручную, не только summary.",
        ],
    },
    25: {
        "label": "25%",
        "tail": "минимальная экономия, verify по умолчанию",
        "rules": [
            "Все правила уровня 50% +",
            "verification-before-completion skill вызывается на любой задаче в 2 и более шагов (даже без явного 'verify' от пользователя).",
            "Read с offset/limit — только на файлах >5000 строк.",
            "Grep head_limit поднять до 500 (вместо 250).",
            "Subagent — только на гарантированно независимых задачах; в остальных случаях прямой Read.",
        ],
    },
    5: {
        "label": "5%",
        "tail": "качество 0% без избыточности",
        "rules": [
            "Уровень 0% с вырезанной избыточностью. Качество и строгость 0% сохраняются полностью — убраны только дублирующая работа и 0%-надстройки над Pravila §12.2.",
            "",
            "ПРОЦЕСС (как в 0%, кроме гейтов §12.2):",
            "- superpowers:writing-plans — на эпик / крупную задачу (Pravila §12.2). Рутинная ≥3-шаговая задача — без обязательного plan-gate и согласования до выполнения.",
            "- Любой debug / unexpected behavior: superpowers:systematic-debugging с минимум 3 гипотезами; falsify каждую перед фиксом.",
            "- superpowers:brainstorming — по требованию заказчика (мозговой штурм/генерация идей) или при реально неоднозначном дизайне (Pravila §12.2). Не авто-гейт на каждую фичу/компонент/endpoint.",
            "- Перед claim 'готово'/'closed'/'merged'/'passed': обязательно invoke superpowers:verification-before-completion.",
            "- TDD на любой код: superpowers:test-driven-development; failing test first, GREEN после.",
            "",
            "ЧТЕНИЕ И ИССЛЕДОВАНИЕ (как в 0%):",
            "- Full file reads без offset/limit на файлах до 5000 строк.",
            "- Grep без head_limit (или явно 0 = unlimited) на критичных поисках; default 500.",
            "- Memory facts: всегда re-Read актуального файла ПЕРЕД использованием; не доверять stale memory.",
            "- re-Read Pravila, если задача касается её правил. CLAUDE.md НЕ перечитывать — он уже в контексте сессии.",
            "- Subagent: запрашивать raw output, не summary; решения принимать самому.",
            "",
            "ВЕРИФИКАЦИЯ (как в 0%, кроме каденса тестов и pre-commit):",
            "- После каждого ЛОГИЧЕСКОГО БЛОКА правок — запуск relevant тестов (Pest/Vitest). Прогон после каждой атомарной правки не требуется; перед коммитом — обязательный полный прогон.",
            "- После КАЖДОГО изменения миграции/схемы — db tests + smoke check.",
            "- Перед коммитом — pre-commit (pint + larastan + pest + gitleaks protect --staged). gitleaks-full-history + lychee — только перед push.",
            "- Bash output показывать ВСЕГДА в ответе, не только при ошибке.",
            "- Full test output, не саммари; failure'ы выписывать явно с file:line.",
            "",
            "ФОРМУЛИРОВКИ (как в 0%):",
            "- Никаких 'should work' / 'looks correct' / 'тесты должны пройти' без реального запуска.",
            "- Никакого cherry-picking: 'tests pass' = ровно столько, сколько прошло; остальное — failed с указанием.",
            "- Каждое утверждение про код — с file:line как pin'ом, не общей фразой.",
            "- Если что-то не проверено — явно 'не верифицировал X' в разделе ограничений.",
            "",
            "ОТКРЫТЫЕ ВОПРОСЫ И ИНТЕГРАЦИЯ (как в 0%):",
            "- Перед закрытием темы из реестра (Б-/CTO-/DO-/Ю-/Диз-/OPEN-) — проверить наличие явного 'закрываем' от заказчика; иначе вопрос остаётся открытым.",
            "- Атомарные коммиты: один логический change → один коммит.",
            "",
            "СКОРОСТЬ БЕЗ ПОТЕРИ КАЧЕСТВА (5%-specific — убирают простой и дубли, не проверки):",
            "- Независимые tool-вызовы (Read/Grep/Bash) — одним сообщением параллельно, не последовательно.",
            "- Не перечитывать файлы, уже прочитанные в этой сессии и не изменённые с тех пор; re-Read обязателен только перед Edit и для memory-фактов.",
            "- Механические субагент-задачи (1-2 файла, полная спека) — на дешёвой модели (Haiku/Sonnet); контроллер и code-review остаются на сильной модели, двухстадийное review сохраняется.",
            "- Долгие команды (build, full-suite) — run_in_background, если рядом есть независимая работа; не блокирующий простой.",
            "- Не задавать заказчику вопрос, ответ на который выводится из кодовой базы или конвенции по умолчанию; AskUserQuestion — только когда ответ реально меняет ход работы.",
            "- Держать задачу в фокусе сессии; компактить длинные сессии, не тащить несвязанную историю — размер контекста = стоимость каждого turn'а.",
        ],
    },
    0: {
        "label": "0%",
        "tail": "максимальное всеобъемлющее качество, без любых скипов",
        "rules": [
            "ВСЕ паттерны экономии ВЫКЛЮЧЕНЫ. ОБЯЗАТЕЛЬНЫЕ требования на каждое действие в этой задаче:",
            "",
            "ПРОЦЕСС:",
            "- Multi-step задача (≥3 шага): EnterPlanMode/writing-plans skill ПЕРВЫМ, согласовать с пользователем до выполнения.",
            "- Любой debug / unexpected behavior: superpowers:systematic-debugging с минимум 3 гипотезами; falsify каждую перед фиксом.",
            "- Любая creative задача (фича/компонент/endpoint/нетривиальный refactor): superpowers:brainstorming ПЕРВЫМ.",
            "- Перед claim 'готово'/'closed'/'merged'/'passed': обязательно invoke superpowers:verification-before-completion.",
            "- TDD на любой код: superpowers:test-driven-development; failing test first, GREEN после.",
            "",
            "ЧТЕНИЕ И ИССЛЕДОВАНИЕ:",
            "- Full file reads без offset/limit на файлах до 5000 строк.",
            "- Grep без head_limit (или явно 0 = unlimited) на критичных поисках; default 500.",
            "- Memory facts: всегда re-Read актуального файла ПЕРЕД использованием; не доверять stale memory.",
            "- Перед задачей касающейся проекта: re-Read CLAUDE.md и Pravila на начало.",
            "- Subagent: запрашивать raw output, не summary; решения принимать самому.",
            "",
            "ВЕРИФИКАЦИЯ:",
            "- После КАЖДОГО Edit/Write на code — запуск relevant тестов (Pest/Vitest по контексту).",
            "- После КАЖДОГО изменения миграции/схемы — db tests + smoke check.",
            "- Перед коммитом — full pre-commit run (lefthook stages включая gitleaks-full-history + lychee + larastan + pint + pest).",
            "- Bash output показывать ВСЕГДА в ответе, не только при ошибке.",
            "- Full test output, не саммари; failure'ы выписывать явно с file:line.",
            "",
            "ФОРМУЛИРОВКИ:",
            "- Никаких 'should work' / 'looks correct' / 'тесты должны пройти' без реального запуска.",
            "- Никакого cherry-picking: 'tests pass' = ровно столько, сколько прошло; остальное — failed с указанием.",
            "- Каждое утверждение про код — с file:line как pin'ом, не общей фразой.",
            "- Если что-то не проверено — явно 'не верифицировал X' в разделе ограничений.",
            "",
            "ОТКРЫТЫЕ ВОПРОСЫ И ИНТЕГРАЦИЯ:",
            "- Перед закрытием темы из реестра (Б-/CTO-/DO-/Ю-/Диз-/OPEN-) — проверить наличие явного 'закрываем' от заказчика; иначе вопрос остаётся открытым.",
            "- Атомарные коммиты: один логический change → один коммит.",
        ],
    },
}


def closest_level(pct: int) -> int:
    return min(LEVELS.keys(), key=lambda lv: abs(lv - pct))


def main() -> None:
    try:
        data = json.load(sys.stdin)
    except Exception:
        return

    prompt = data.get("prompt") or ""
    raw_pct = parse_level(prompt)

    if raw_pct is not None:
        level = closest_level(raw_pct)
        explicit = True
    else:
        level = 100
        explicit = False

    # NEW (v3): write state file for sibling hooks (state-guard, verifier, postcompact)
    sid = data.get("session_id")
    if sid:
        state_path = os.path.join(tempfile.gettempdir(), f"claude-economy-{sid}.json")
        if level == 100 and not explicit:
            # Default — remove state to signal no active mode
            try:
                if os.path.exists(state_path):
                    os.remove(state_path)
            except OSError:
                pass
        else:
            state = {
                "session_id": sid,
                "level": level,
                "label": LEVELS[level]["label"],
                "tail": LEVELS[level]["tail"],
                "set_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
                "set_by_prompt_hash": hashlib.sha256(prompt.encode("utf-8")).hexdigest()[:12],
            }
            try:
                # Atomic write via tempfile + replace
                tmp = state_path + ".tmp"
                with open(tmp, "w", encoding="utf-8") as f:
                    json.dump(state, f)
                os.replace(tmp, state_path)
            except Exception:
                pass

    spec = LEVELS[level]
    rules_block = "\n".join(spec["rules"])

    explicit_note = (
        "(пользователь указал явно)"
        if explicit
        else "(default — пользователь не указал уровень)"
    )

    ctx = (
        f"=== ECONOMY MODE: {spec['label']} {explicit_note} ===\n\n"
        f"ПЕРВОЙ строкой ответа на эту задачу обязательно написать:\n"
        f"  `экономия: {spec['label']} — {spec['tail']}`\n\n"
        f"ИНСТРУКЦИИ для этой turn:\n{rules_block}\n\n"
        f"Действует только на текущую задачу — следующий промпт парсится заново. "
        f"§12 hard rule из Pravila НЕ override-ится этим режимом — на всех уровнях."
    )

    out = {
        "hookSpecificOutput": {
            "hookEventName": "UserPromptSubmit",
            "additionalContext": ctx,
        }
    }
    try:
        sys.stdout.write(json.dumps(out, ensure_ascii=True))
    except Exception:
        return


if __name__ == "__main__":
    main()
