from __future__ import annotations

import argparse
import json
import os
import re
import socket
import sys
from datetime import datetime, timezone
from pathlib import Path
from collections.abc import Callable
from typing import Any, TypeVar, TYPE_CHECKING
from urllib.parse import urlsplit, urlunsplit

import httpx
import yaml

from cybervisor.adapters.gemini.hook_context import extract_tool_activity_from_transcript_path
from cybervisor.config import contract_artifact_path, contract_prompt_path

if TYPE_CHECKING:
    from cybervisor.config import StageContractConfig

DEFAULT_ENDPOINT = "https://api.openai.com/v1"
DEFAULT_MODEL = "auto"
HOOK_USER_AGENT = "cybervisor-hook/1"
HOOK_HTTP_TIMEOUT_SECONDS = 120.0
STRUCTURED_RETRY_ATTEMPTS = 5
VERIFIER_REQUEST_RETRY_ATTEMPTS = 3
TStructured = TypeVar("TStructured")
AUTONOMY_DENY_MESSAGE = "Proceed autonomously without asking for user input."
_PROGRESS_UPDATE_RE = re.compile(r"^(?:i will|i'll|i am going to|let me|now i will|next i will)\b", re.IGNORECASE)
class VerifierRequestError(Exception):
    pass


class VerifierResponseShapeError(VerifierRequestError):
    pass

class HookDecision:
    __slots__ = ("action", "reason", "system_message")

    def __init__(self, action: str, reason: str, system_message: str | None = None) -> None:
        self.action = action
        self.reason = reason
        self.system_message = system_message

    def as_payload(self) -> dict[str, Any]:
        return {
            "decision": self.action,
            "reason": self.reason,
        }

    def as_hook_output(self, agent_tool: str | None) -> dict[str, Any]:
        if agent_tool != "claude":
            if self.action == "approve":
                return {
                    "decision": "allow",
                    "reason": self.reason,
                }
            payload = {
                "decision": "deny",
                "reason": self.reason,
            }
            payload["systemMessage"] = self.system_message or self.reason or AUTONOMY_DENY_MESSAGE
            return payload
        if self.action == "block":
            stop_reason = self.system_message or self.reason or AUTONOMY_DENY_MESSAGE
            return {
                "decision": "block",
                "reason": stop_reason,
            }
        return {}

    @property
    def is_blocking(self) -> bool:
        return self.action == "block"

def _contract_blocking_decision(agent_tool: str | None, message: str) -> HookDecision:
    if agent_tool == "claude":
        return HookDecision(action="block", reason=message)
    return HookDecision(action="block", reason="stage_contract_invalid", system_message=message)

def _utc_now() -> str:
    return datetime.now(tz=timezone.utc).isoformat()

def _load_hook_runtime_config(config_path: Path) -> dict[str, Any]:
    if not config_path.exists():
        raise FileNotFoundError(f"Configuration file not found: {config_path}")

    content = config_path.read_text(encoding="utf-8").strip()
    if not content:
        return {}

    raw = json.loads(content)
    if not isinstance(raw, dict):
        raise ValueError("Configuration file root must be a JSON object")
    return raw

def _json_default(value: Any) -> Any:
    if isinstance(value, Path):
        return str(value)
    raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")

def _append_hook_event(log_path: Path, payload: dict[str, Any]) -> None:
    log_path.parent.mkdir(parents=True, exist_ok=True)
    with log_path.open("a", encoding="utf-8") as fh:
        fh.write(json.dumps(payload, default=_json_default) + "\n")

def _send_hook_event(socket_path: str | None, payload: dict[str, Any]) -> None:
    if not socket_path:
        return
    try:
        with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
            client.settimeout(0.2)
            client.connect(socket_path)
            client.sendall((json.dumps(payload, default=_json_default) + "\n").encode("utf-8"))
    except OSError:
        return

def _socket_path_from_config(config_path: Path) -> str | None:
    try:
        raw_socket_path = _load_hook_runtime_config(config_path).get("socket_path")
    except FileNotFoundError:
        return None
    return raw_socket_path if isinstance(raw_socket_path, str) and raw_socket_path else None

def _log_hook_event(
    log_path: Path,
    event: str,
    message: str,
    *,
    agent_tool: str | None,
    config_path: Path,
    hook_input: str | None = None,
    verifier_output: str | None = None,
    decision: dict[str, Any] | None = None,
    error: str | None = None,
) -> None:
    payload: dict[str, Any] = {
        "timestamp": _utc_now(),
        "event": event,
        "message": message,
        "agent_tool": agent_tool,
        "pid": os.getpid(),
        "config_path": str(config_path.resolve()),
    }
    if hook_input is not None:
        payload["hook_input"] = hook_input
    if verifier_output is not None:
        payload["verifier_output"] = verifier_output
    if decision is not None:
        payload["decision"] = decision
    if error is not None:
        payload["error"] = error
    _append_hook_event(log_path, payload)
    _send_hook_event(_socket_path_from_config(config_path), payload)

def _resolve_verifier_settings(config_path: Path) -> tuple[str, str, str]:
    del config_path
    api_endpoint = str(
        os.getenv("CYBERVISOR_LLM_BASE_URL")
        or DEFAULT_ENDPOINT
    )
    api_key = str(os.getenv("CYBERVISOR_LLM_API_KEY") or "")
    model = str(os.getenv("CYBERVISOR_LLM_MODEL") or DEFAULT_MODEL)
    return api_endpoint, api_key, model

def _active_stage_contract(config_path: Path) -> dict[str, Any] | None:
    try:
        raw = _load_hook_runtime_config(config_path).get("active_stage_contract")
    except FileNotFoundError:
        return None
    return raw if isinstance(raw, dict) else None

def _is_final_stage_attempt(config_path: Path) -> bool:
    try:
        raw = _load_hook_runtime_config(config_path)
    except FileNotFoundError:
        return False
    attempt = raw.get("active_stage_attempt")
    max_retries = raw.get("active_stage_max_retries")
    return isinstance(attempt, int) and isinstance(max_retries, int) and attempt >= max_retries

def _resolve_artifact_path(contract: dict[str, Any]) -> Path:
    raw_path = contract.get("artifact_path")
    if not isinstance(raw_path, str) or not raw_path.strip():
        raise ValueError("contract artifact_path is missing")
    candidate = Path(raw_path)
    if candidate.is_absolute():
        raise ValueError("contract artifact_path must be relative to the working directory")
    cwd = Path.cwd().resolve()
    resolved = (cwd / candidate).resolve()
    try:
        resolved.relative_to(cwd)
    except ValueError as exc:
        raise ValueError("contract artifact_path must stay within the working directory") from exc
    return resolved

def _contract_prompt_guidance(contract: dict[str, Any]) -> str:
    prompt_contents = contract.get("prompt_contents")
    if isinstance(prompt_contents, str) and prompt_contents.strip():
        return prompt_contents.strip()
    prompt_path = contract.get("prompt_path")
    if isinstance(prompt_path, str) and prompt_path.strip():
        return f"Follow the contract prompt in {prompt_path}."
    return "Write the required contract artifact using the expected YAML structure."

def _contract_repair_message(contract: dict[str, Any], validation_message: str) -> str:
    artifact_path = _resolve_artifact_path(contract)
    allowed_statuses = contract.get("allowed_statuses", [])
    guidance = _contract_prompt_guidance(contract)
    allowed = ", ".join(allowed_statuses) if isinstance(allowed_statuses, list) else ""
    status_hint = f"Allowed statuses: {allowed}." if allowed else ""
    return (
        f"Before stopping, repair {artifact_path}. {validation_message}\n"
        "Writing prose such as 'status: approved' in the reply is not sufficient. "
        "You must write the YAML file itself.\n"
        f"{status_hint}\n"
        f"{guidance}"
    ).strip()

def _validate_stage_contract_artifact(config_path: Path) -> tuple[dict[str, Any] | None, str | None]:
    contract = _active_stage_contract(config_path)
    if contract is None:
        return None, None

    try:
        artifact_path = _resolve_artifact_path(contract)
    except ValueError as exc:
        return None, str(exc)

    if not artifact_path.exists():
        return None, f"write the required stage artifact at {artifact_path}"

    try:
        payload = yaml.safe_load(artifact_path.read_text(encoding="utf-8"))
    except yaml.YAMLError as exc:
        return None, f"rewrite {artifact_path} as valid YAML ({exc})"

    if not isinstance(payload, dict):
        return None, f"rewrite {artifact_path} so the top-level value is a YAML mapping"

    allowed_statuses = contract.get("allowed_statuses", ["approved", "changes_requested"])
    if not isinstance(allowed_statuses, list) or not all(isinstance(item, str) for item in allowed_statuses):
        return None, "stage contract allowed_statuses is invalid"

    status = payload.get("status")
    if not isinstance(status, str) or status not in allowed_statuses:
        return None, f"set {artifact_path} status to one of: {', '.join(allowed_statuses)}"

    summary = payload.get("summary")
    if not isinstance(summary, str) or not summary.strip():
        return None, f"set {artifact_path} summary to a non-empty string"

    details = payload.get("details")
    if not isinstance(details, dict):
        return None, f"set {artifact_path} details to a YAML mapping"

    actions = payload.get("actions")
    if not isinstance(actions, dict):
        return None, f"set {artifact_path} actions to a YAML mapping"

    return payload, None

def _strip_fences(text: str) -> str:
    stripped = text.strip()
    if stripped.startswith("```") and stripped.endswith("```"):
        lines = stripped.splitlines()
        if len(lines) >= 3:
            return "\n".join(lines[1:-1]).strip()
    return stripped

def _parse_decision(text: str) -> HookDecision:
    payload = json.loads(_strip_fences(text))
    if not isinstance(payload, dict):
        raise ValueError("decision output must be a JSON object")
    raw_decision = payload.get("decision")
    reason = payload.get("reason", payload.get("explanation"))
    if not isinstance(raw_decision, str):
        raise ValueError("decision must be a string")
    normalized_decision = raw_decision.strip().lower()
    action_aliases = {
        "approve": "approve",
        "allow": "approve",
        "block": "block",
        "deny": "block",
    }
    action = action_aliases.get(normalized_decision)
    if action is None:
        raise ValueError("decision must be 'approve' or 'block'")
    if not isinstance(reason, str) or not reason.strip():
        raise ValueError("reason must be a non-empty string")
    return HookDecision(action=action, reason=reason.strip())


def _parse_hook_payload(output: str) -> dict[str, Any] | None:
    try:
        payload = json.loads(output)
    except json.JSONDecodeError:
        return None
    return payload if isinstance(payload, dict) else None


def _collapse_whitespace(text: str) -> str:
    return re.sub(r"\s+", " ", text).strip()


def _extract_json_string_field(output: str, key: str) -> str | None:
    match = re.search(rf'"{re.escape(key)}"\s*:\s*', output)
    if match is None:
        return None
    suffix = output[match.end():].lstrip()
    if not suffix.startswith('"'):
        return None
    try:
        decoded, _ = json.JSONDecoder().raw_decode(suffix)
    except json.JSONDecodeError:
        return None
    return decoded if isinstance(decoded, str) and decoded.strip() else None


def _dedupe_subsumed_segments(segments: list[str]) -> list[str]:
    unique: list[str] = []
    normalized_seen: list[str] = []
    for segment in segments:
        normalized = _collapse_whitespace(segment)
        if not normalized:
            continue
        if any(
            normalized == seen
            or (len(normalized) >= 24 and normalized in seen)
            or (len(seen) >= 24 and seen in normalized)
            for seen in normalized_seen
        ):
            continue
        unique.append(segment)
        normalized_seen.append(normalized)
    return unique


def _strip_prompt_prefix(text: str, stage_prompt: str | None) -> str:
    stripped = text.strip()
    if not stage_prompt:
        return stripped
    prompt = stage_prompt.strip()
    if prompt and stripped.startswith(prompt):
        stripped = stripped[len(prompt):].lstrip()
    return stripped


def _clean_gemini_final_response(text: str, stage_prompt: str | None) -> str:
    stripped = _strip_prompt_prefix(text, stage_prompt)
    if not stripped:
        return ""

    raw_segments = [
        _collapse_whitespace(segment)
        for segment in re.split(r"\n\s*\n+", stripped)
        if _collapse_whitespace(segment)
    ]
    cleaned_segments = [
        segment
        for segment in _dedupe_subsumed_segments(raw_segments)
        if not _PROGRESS_UPDATE_RE.match(segment)
    ]
    if cleaned_segments:
        return "\n\n".join(cleaned_segments)
    return _collapse_whitespace(stripped)


def _format_verifier_payload(*, agent_tool: str | None, stage_prompt: str | None, final_response: str) -> str:
    lines = [f"Agent tool: {agent_tool or 'unknown'}"]
    if stage_prompt and stage_prompt.strip():
        lines.extend(["Stage prompt:", stage_prompt.strip()])
    lines.extend(["", "", "Final assistant response:", final_response.strip()])
    return "\n".join(lines)


def _format_contract_verifier_payload(
    *,
    agent_tool: str | None,
    stage_prompt: str | None,
    artifact_path: Path,
    artifact_payload: dict[str, Any],
    tool_context: str | None,
) -> str:
    lines = [f"Agent tool: {agent_tool or 'unknown'}"]
    if stage_prompt and stage_prompt.strip():
        lines.extend(["Stage prompt:", stage_prompt.strip()])
    lines.extend(
        [
            "Contract artifact path:",
            str(artifact_path),
            "Contract artifact payload:",
            yaml.safe_dump(artifact_payload, sort_keys=False).rstrip(),
        ]
    )
    if tool_context:
        lines.extend(["Relevant tool activity:", tool_context])
    return "\n".join(lines)


def _normalize_hook_verifier_input(output: str, *, agent_tool: str | None, config_path: Path) -> str:
    try:
        runtime_config = _load_hook_runtime_config(config_path)
    except FileNotFoundError:
        runtime_config = {}
    stage_prompt_raw = runtime_config.get("active_stage_prompt")
    stage_prompt = stage_prompt_raw.strip() if isinstance(stage_prompt_raw, str) and stage_prompt_raw.strip() else None
    payload = _parse_hook_payload(output)

    contract = _active_stage_contract(config_path)
    if contract is not None:
        artifact_payload, artifact_error = _validate_stage_contract_artifact(config_path)
        if artifact_error is None and artifact_payload is not None:
            tool_context = extract_tool_activity_from_transcript_path(
                payload.get("transcript_path") if payload is not None else None
            )
            return _format_contract_verifier_payload(
                agent_tool=agent_tool,
                stage_prompt=stage_prompt,
                artifact_path=_resolve_artifact_path(contract),
                artifact_payload=artifact_payload,
                tool_context=tool_context,
            )

    if agent_tool == "claude":
        final_response = None
        if payload is not None:
            response = payload.get("last_assistant_message")
            if isinstance(response, str) and response.strip():
                final_response = response.strip()
        if final_response is None:
            final_response = _collapse_whitespace(output)
        return _format_verifier_payload(
            agent_tool=agent_tool,
            stage_prompt=stage_prompt,
            final_response=final_response,
        )

    final_candidates: list[str] = []
    if payload is not None:
        for key in ("prompt_response", "response", "final_response", "assistant_response"):
            value = payload.get(key)
            if isinstance(value, str) and value.strip():
                final_candidates.append(value)
    else:
        for key in ("prompt_response", "response", "final_response", "assistant_response", "last_assistant_message"):
            value = _extract_json_string_field(output, key)
            if value is not None:
                final_candidates.append(value)
    if not final_candidates:
        final_candidates.append(output)

    cleaned_candidates = [
        _clean_gemini_final_response(candidate, stage_prompt)
        for candidate in final_candidates
    ]
    final_response = next((candidate for candidate in cleaned_candidates if candidate.strip()), _collapse_whitespace(output))
    return _format_verifier_payload(
        agent_tool=agent_tool,
        stage_prompt=stage_prompt,
        final_response=final_response,
    )

def _blocking_decision(agent_tool: str | None, reason: str) -> HookDecision:
    if agent_tool == "claude":
        return HookDecision(action="block", reason=AUTONOMY_DENY_MESSAGE)
    return HookDecision(action="block", reason=reason, system_message=AUTONOMY_DENY_MESSAGE)

def _chat_completions_url(base_url: str) -> str:
    stripped = base_url.strip()
    if not stripped:
        raise ValueError("Verifier API endpoint must not be empty")

    parsed = urlsplit(stripped)
    path = parsed.path.rstrip("/")
    if path.endswith("/chat/completions"):
        normalized_path = path
    else:
        normalized_path = f"{path}/chat/completions" if path else "/chat/completions"
    return urlunsplit((parsed.scheme, parsed.netloc, normalized_path, parsed.query, parsed.fragment))

def _post(api_endpoint: str, api_key: str, model: str, messages: list[dict[str, str]]) -> str:
    headers = {
        "Content-Type": "application/json",
        "User-Agent": HOOK_USER_AGENT,
    }
    if api_key:
        headers["Authorization"] = f"Bearer {api_key}"
    try:
        with httpx.Client(timeout=HOOK_HTTP_TIMEOUT_SECONDS) as client:
            response = client.post(
                _chat_completions_url(api_endpoint),
                headers=headers,
                json={"model": model, "messages": messages},
            )
        response.raise_for_status()
    except httpx.HTTPStatusError as exc:
        if exc.response.status_code >= 500:
            raise VerifierRequestError(f"verifier server error ({exc.response.status_code})") from exc
        raise
    except httpx.TimeoutException as exc:
        raise VerifierRequestError("verifier request timed out") from exc
    except httpx.NetworkError as exc:
        raise VerifierRequestError(f"verifier network error: {exc}") from exc

    try:
        payload = response.json()
    except json.JSONDecodeError as exc:
        raise VerifierResponseShapeError("invalid JSON in LLM response") from exc
    choices = payload.get("choices", [])
    if not choices:
        raise VerifierResponseShapeError("missing choices in LLM response")
    message = choices[0].get("message", {})
    content = message.get("content", "")
    if not isinstance(content, str) or not content.strip():
        raise VerifierResponseShapeError("missing content in LLM response")
    return content


def _request_verifier_text(
    *,
    api_endpoint: str,
    api_key: str,
    model: str,
    messages: list[dict[str, str]],
    log_path: Path,
    agent_tool: str | None,
    config_path: Path,
    hook_input: str,
    event_name: str,
) -> str:
    last_error: Exception | None = None
    for attempt in range(1, VERIFIER_REQUEST_RETRY_ATTEMPTS + 1):
        try:
            text = _post(api_endpoint, api_key, model, messages)
        except VerifierRequestError as exc:
            last_error = exc
            if attempt >= VERIFIER_REQUEST_RETRY_ATTEMPTS:
                break
            _log_hook_event(
                log_path,
                event_name,
                "Verifier request failed; retrying.",
                agent_tool=agent_tool,
                config_path=config_path,
                hook_input=hook_input,
                error=f"attempt={attempt + 1}/{VERIFIER_REQUEST_RETRY_ATTEMPTS}: {exc}",
            )
            continue

        _log_hook_event(
            log_path,
            "verifier_response_received",
            "Received verifier response.",
            agent_tool=agent_tool,
            config_path=config_path,
            hook_input=hook_input,
            verifier_output=text,
        )
        return text

    if last_error is None:
        raise VerifierRequestError("verifier request failed")
    raise VerifierRequestError(
        "Verifier request failed after "
        f"{VERIFIER_REQUEST_RETRY_ATTEMPTS} attempts: {last_error}"
    ) from last_error

def _get_stop_verifier_prompt(config_path: Path) -> str:
    """Read the stop-hook verifier prompt from bundled assets."""
    del config_path # Use static prompt from package
    prompt_path = Path(__file__).resolve().parent.parent / "assets" / "hooks" / "stop-hook-verifier.md"
    if not prompt_path.exists():
        # Fallback if package structure is unexpected
        return "Decide if the agent can stop. Respond in valid JSON only with 'decision' and 'reason'."
    return prompt_path.read_text(encoding="utf-8").strip()

def _recover_structured_output(
    *,
    api_endpoint: str,
    api_key: str,
    model: str,
    initial_messages: list[dict[str, str]],
    parser: Callable[[str], TStructured],
    regeneration_system_prompt: str,
    log_path: Path,
    agent_tool: str | None,
    config_path: Path,
    hook_input: str,
) -> tuple[TStructured, str]:
    text = _request_verifier_text(
        api_endpoint=api_endpoint,
        api_key=api_key,
        model=model,
        messages=initial_messages,
        log_path=log_path,
        agent_tool=agent_tool,
        config_path=config_path,
        hook_input=hook_input,
        event_name="verifier_request_retry",
    )

    last_error: Exception | None = None
    for attempt in range(1, STRUCTURED_RETRY_ATTEMPTS + 1):
        try:
            return parser(text), text
        except Exception as exc:
            last_error = exc
            if attempt >= STRUCTURED_RETRY_ATTEMPTS:
                break
            _log_hook_event(
                log_path,
                "verifier_regeneration_requested",
                "Verifier response was invalid; requesting JSON regeneration.",
                agent_tool=agent_tool,
                config_path=config_path,
                hook_input=hook_input,
                verifier_output=text,
                error=f"attempt={attempt + 1}/{STRUCTURED_RETRY_ATTEMPTS}: {exc}",
            )
            text = _request_verifier_text(
                api_endpoint=api_endpoint,
                api_key=api_key,
                model=model,
                messages=[
                    {"role": "system", "content": regeneration_system_prompt},
                    {
                        "role": "user",
                        "content": (
                            f"Previous invalid output:\n{text}\n\n"
                            f"Error:\n{exc}\n\n"
                            f"Attempt: {attempt + 1}/{STRUCTURED_RETRY_ATTEMPTS}"
                        ),
                    },
                ],
                log_path=log_path,
                agent_tool=agent_tool,
                config_path=config_path,
                hook_input=hook_input,
                event_name="verifier_request_retry",
            )

    raise ValueError(
        "Verifier returned invalid structured output after "
        f"{STRUCTURED_RETRY_ATTEMPTS} attempts: {last_error}"
    )

def _decide(
    output: str,
    api_endpoint: str,
    api_key: str,
    model: str,
    *,
    log_path: Path,
    agent_tool: str | None,
    config_path: Path,
) -> HookDecision:
    contract = _active_stage_contract(config_path)
    if contract is not None:
        artifact_payload, artifact_error = _validate_stage_contract_artifact(config_path)
        if artifact_error is not None:
            artifact_error = _contract_repair_message(contract, artifact_error)
            contract_decision = _contract_blocking_decision(agent_tool, artifact_error)
            _log_hook_event(
                log_path,
                "contract_validation_failed",
                "Required stage artifact was missing or invalid.",
                agent_tool=agent_tool,
                config_path=config_path,
                hook_input=output,
                decision=contract_decision.as_payload(),
                error=artifact_error,
            )
            _log_hook_event(
                log_path,
                "decision_emitted",
                "Blocked completion until the required stage artifact is valid.",
                agent_tool=agent_tool,
                config_path=config_path,
                hook_input=output,
                decision=contract_decision.as_payload(),
            )
            return contract_decision
        if artifact_payload is not None:
            _log_hook_event(
                log_path,
                "contract_validation_passed",
                "Required stage artifact validated successfully.",
                agent_tool=agent_tool,
                config_path=config_path,
                hook_input=output,
            )

    verifier_input = _normalize_hook_verifier_input(output, agent_tool=agent_tool, config_path=config_path)
    if contract is None and _is_final_stage_attempt(config_path):
        decision = HookDecision(
            action="approve",
            reason="final_attempt_reached_allowing_stop_without_hook_block",
        )
        _log_hook_event(
            log_path,
            "final_attempt_bypass",
            "Final allowed attempt reached; allowing stop without hook blocking.",
            agent_tool=agent_tool,
            config_path=config_path,
            hook_input=output,
            decision=decision.as_payload(),
        )
        return decision

    if not api_key:
        decision = _blocking_decision(agent_tool, "verifier_unavailable_no_api_key")
        _log_hook_event(
            log_path,
            "decision_emitted",
            "Verifier unavailable; emitted safe deny decision.",
            agent_tool=agent_tool,
            config_path=config_path,
            hook_input=output,
            decision=decision.as_payload(),
        )
        return decision

    _log_hook_event(
        log_path,
        "verifier_request_started",
        "Submitting normalized hook payload to verifier.",
        agent_tool=agent_tool,
        config_path=config_path,
        hook_input=output,
    )

    decision, verifier_output = _recover_structured_output(
        api_endpoint=api_endpoint,
        api_key=api_key,
        model=model,
        initial_messages=[
            {
                "role": "system",
                "content": _get_stop_verifier_prompt(config_path),
            },
            {"role": "user", "content": verifier_input},
        ],
        parser=_parse_decision,
        regeneration_system_prompt="Regenerate as VALID JSON only. No prose. Keys: decision ('approve'|'block'), reason (string).",
        log_path=log_path,
        agent_tool=agent_tool,
        config_path=config_path,
        hook_input=output,
    )
    _log_hook_event(
        log_path,
        "decision_emitted",
        "Parsed verifier decision and emitted hook response.",
        agent_tool=agent_tool,
        config_path=config_path,
        hook_input=output,
        verifier_output=verifier_output,
        decision=decision.as_payload(),
    )
    if decision.is_blocking:
        return decision

    return decision

def run_hook(config_path: Path, input_data: str) -> dict[str, Any]:
    runtime_config = _load_hook_runtime_config(config_path)
    agent_tool = runtime_config.get("agent_tool")
    if not isinstance(agent_tool, str):
        agent_tool = None
    
    # Logic for log path
    resolved_config = config_path.resolve()
    if resolved_config.parent.name == "hooks" and resolved_config.parent.parent.name == ".cybervisor":
        log_path = resolved_config.parent.parent / "logs" / "hook-events.jsonl"
    else:
        log_path = resolved_config.parent / "logs" / "hook-events.jsonl"

    _log_hook_event(
        log_path,
        "hook_invocation_started",
        "Hook invocation started.",
        agent_tool=agent_tool,
        config_path=config_path,
    )
    
    _log_hook_event(
        log_path,
        "hook_input_captured",
        "Captured hook stdin payload.",
        agent_tool=agent_tool,
        config_path=config_path,
        hook_input=input_data,
    )
    
    api_endpoint, api_key, model = _resolve_verifier_settings(config_path)
    
    try:
        decision = _decide(
            output=input_data,
            api_endpoint=api_endpoint,
            api_key=api_key,
            model=model,
            log_path=log_path,
            agent_tool=agent_tool,
            config_path=config_path,
        )
        return decision.as_hook_output(agent_tool)
    except Exception as exc:
        fallback = HookDecision(
            action="block",
            reason=f"hook_error:{exc}",
            system_message=AUTONOMY_DENY_MESSAGE,
        )
        _log_hook_event(
            log_path,
            "fallback_error_emitted",
            "Hook failed; emitted safe deny fallback.",
            agent_tool=agent_tool,
            config_path=config_path,
            decision=HookDecision(action="block", reason=AUTONOMY_DENY_MESSAGE).as_payload(),
            error=str(exc),
        )
        return fallback.as_hook_output(agent_tool)
