# === Hook I/O - Policy Enforcement Decision Engine (src_hook.rs) v1 ===
#  Dependencies: serde, serde_json, std::io, std::process, std::time, wait_timeout, regex, crate::policy, crate::vault
# Reads Claude Code PreToolUse JSON from stdin, evaluates tool calls against compiled policy and preflight constraints, executes ensure check scripts when needed, and returns allow/deny/ask decisions on stdout. Implements two-pass enforcement: hard policy rules followed by soft preflight constraints, with escalation context generation and violation logging.

# Module invariants:
#   - run_hook always returns 0 regardless of decision outcome
#   - Exactly one JSON decision is emitted to stdout per run_hook invocation
#   - Disabled file bypasses all policy and preflight evaluation
#   - Paused mode only enforces locked rules; non-locked rules are allowed
#   - Hard deny decisions (from policy evaluation) short-circuit preflight constraint checks
#   - Escalated preflights always result in Decision::Ask with violation context
#   - Ensure timeouts are clamped to [1, 30] seconds
#   - Vault logging truncates parameter detail to 500 characters
#   - Violation parameters_summary is truncated to 200 characters
#   - Escalation context shows up to 20 violations grouped by constraint
#   - Tool parameter detail is truncated to 60 characters per violation in escalation context
#   - Amount is only logged if final_decision == Decision::Allow, otherwise 0.0
#   - hook_event_name is always 'PreToolUse' in emitted responses
#   - Child processes for ensure checks are killed if they exceed timeout

class HookInput:
    """Deserialized JSON input from Claude Code PreToolUse hook"""
    tool_name: String                        # required, Name of the tool being invoked
    parameters: Option<serde_json::Value> = None # optional, Tool parameters as JSON (aliased from tool_input)

class HookResponse:
    """Top-level response wrapper expected by Claude Code"""
    hook_specific_output: HookOutput         # required, Nested output structure (serialized as hookSpecificOutput)

class HookOutput:
    """Actual hook decision payload with reason and context"""
    hook_event_name: String                  # required, Always 'PreToolUse'
    permission_decision: String              # required, One of: allow, deny, ask
    reason: Option<String> = None            # optional, Human-readable reason for deny/ask decisions
    additional_context: Option<String> = None # optional, Rich context for Claude (e.g., escalation messages, violation details)

def evaluate_preflight_constraint(
    call: &ToolCall,
    preflight: &Preflight,
    vault: &Vault,
) -> Option<(SoftConstraint, String)>:
    """
    Evaluates a tool call against active preflight soft constraints. Returns the first matching constraint with constructed reason message, or None if no constraints match. Uses regex matching on tool name and AND logic for all conditions.

    Preconditions:
      - preflight contains valid constraints with tool_pattern and conditions
      - vault is accessible for condition evaluation

    Postconditions:
      - Returns Some((constraint, reason)) for first matching constraint
      - Reason string is formatted as '{constraint.reason} Instead: {constraint.alternative}'
      - Returns None if no constraint matches tool_name or all conditions fail

    Side effects: Compiles regex patterns from constraint.tool_pattern (invalid patterns are silently skipped), Calls policy::evaluate_condition for each constraint condition
    Idempotent: no
    """
    ...

def run_hook(
    policy: &CompiledPolicy,
    vault: Option<&Vault> = None,
) -> i32:
    """
    Main hook entry point. Reads JSON from stdin, evaluates tool call through two-pass enforcement (hard policy + soft preflight), resolves ensure checks, logs violations and actions to vault, emits decision to stdout. Returns 0 always.

    Preconditions:
      - stdin contains valid JSON matching HookInput schema or is readable
      - policy is compiled and valid

    Postconditions:
      - Always returns 0
      - Exactly one decision emitted to stdout as JSON HookResponse
      - If vault present, action logged with decision, tool name, category, amount, and parameter detail (truncated to 500 chars)
      - If disabled file exists, emits allow without evaluation
      - If paused file exists, only enforces locked rules; non-locked calls are allowed
      - Hard deny decisions short-circuit preflight evaluation
      - Escalated preflights generate Decision::Ask with rich context detailing up to 20 violations
      - Non-escalated preflight violations are logged to vault and decision determined by constraint action (DENY or ASK)

    Errors:
      - stdin_read_failure (recoverable): io::stdin().read_to_string fails
          decision: deny
          reason: Failed to read stdin
      - json_parse_failure (recoverable): serde_json::from_str fails on input
          decision: deny
          reason: Malformed hook input

    Side effects: Reads entire stdin to string, Writes JSON decision to stdout via println!, Calls crate::vault::is_disabled_file(), Calls crate::vault::is_paused_file(), Calls policy::evaluate(), May call resolve_ensure_result() which spawns child processes, May log preflight violations to vault via vault.log_preflight_violation(), Logs action to vault via vault.log_action() if vault is Some, Retrieves violation history via vault.preflight_violations(), Retrieves active preflight via vault.active_preflight()
    Idempotent: no
    """
    ...

def emit_decision(
    decision: &str,
    reason: Option<String> = None,
    additional_context: Option<String> = None,
) -> unit:
    """
    Serializes and prints a HookResponse JSON to stdout with specified decision, reason, and additional_context.

    Postconditions:
      - JSON printed to stdout in HookResponse format with hook_event_name='PreToolUse'
      - Serialization always succeeds (unwrap on serde_json::to_string)

    Errors:
      - serialization_panic (panic): serde_json::to_string fails (should never happen with this schema)
          message: unwrap on serialization failure

    Side effects: Writes to stdout
    Idempotent: no
    """
    ...

def emit_allow() -> unit:
    """
    Convenience wrapper that emits an allow decision with no reason or context.

    Postconditions:
      - Calls emit_decision with decision='allow', reason=None, additional_context=None

    Side effects: Calls emit_decision
    Idempotent: no
    """
    ...

def emit_deny(
    reason: &str,
) -> unit:
    """
    Convenience wrapper that emits a deny decision with a reason string and no additional context.

    Postconditions:
      - Calls emit_decision with decision='deny', reason=Some(reason.into()), additional_context=None

    Side effects: Calls emit_decision
    Idempotent: no
    """
    ...

def resolve_ensure(
    config: &EnsureConfig,
) -> (bool, String):
    """
    Executes an ensure check script with timeout, returns (passed: bool, stderr: String). Script must exist and be executable. Timeout is clamped to 1-30 seconds. Exit code 0 means passed, non-zero or timeout means failed.

    Preconditions:
      - config.check is a valid script path reference
      - config.timeout is a positive integer

    Postconditions:
      - Returns (true, "") if script exits 0 within timeout
      - Returns (false, stderr_text) if script exits non-zero, capturing up to 500 chars of stderr
      - Returns (false, error_message) if script not found, spawn fails, or timeout exceeded
      - Timeout is clamped to [1, 30] seconds
      - Child process is killed if timeout occurs

    Errors:
      - script_not_found (recoverable): policy::resolve_ensure_script_path returns Err or path does not exist
          passed: false
          stderr: Check script not found: {path}
      - spawn_failure (recoverable): Command::spawn fails
          passed: false
          stderr: Failed to spawn check script: {error}
      - timeout (recoverable): Script does not complete within timeout_secs
          passed: false
          stderr: Check script timed out after {timeout_secs}s
      - wait_error (recoverable): wait_timeout returns Err
          passed: false
          stderr: Error waiting for check script: {error}

    Side effects: Spawns child process executing check script, Inherits stdin, nulls stdout, pipes stderr, Kills child process if timeout occurs, Reads up to 500 bytes from stderr
    Idempotent: yes
    """
    ...

def resolve_ensure_result(
    result: EvaluationResult,
) -> EvaluationResult:
    """
    Transforms an EvaluationResult with Decision::Ensure by running the ensure check script. Converts to Decision::Allow if check passes, Decision::Deny if check fails. Returns modified EvaluationResult.

    Preconditions:
      - result is a valid EvaluationResult

    Postconditions:
      - If result.ensure_config is Some and check passes: returns result with decision=Allow, ensure_config=None
      - If result.ensure_config is Some and check fails: returns result with decision=Deny, reason=config.message or default message + stderr, ensure_config=None
      - If result.ensure_config is None: returns result with decision=Deny, reason='Ensure rule missing ensure config'
      - Always clears ensure_config in returned result
      - Preserves all other fields from input result (matched_locked, etc.)

    Side effects: Calls resolve_ensure which spawns child process if ensure_config is present
    Idempotent: no
    """
    ...

# ── REQUIRED EXPORTS ──────────────────────────────────
# Your implementation module MUST export ALL of these names
# with EXACTLY these spellings. Tests import them by name.
# __all__ = ['HookInput', 'HookResponse', 'HookOutput', 'evaluate_preflight_constraint', 'run_hook', 'recoverable', 'emit_decision', 'panic', 'emit_allow', 'emit_deny', 'resolve_ensure', 'resolve_ensure_result']
