#!/usr/bin/env python3
"""
classify_message.py — tutorial message classifier
(netsi.dk · "Could Codex hooks filter messages before you read them?")

Reads a hook payload (or raw text) on STDIN and prints exactly one category:

    fraud | political | neutral

It runs OFFLINE by default using simple heuristics, so you can test the hook
wiring without any API key. To use a real LLM classifier instead, set
NETSI_USE_OPENAI=1 (and OPENAI_API_KEY); see classifier-prompt.txt.

Exit codes — these are what a Codex / Claude Code `UserPromptSubmit` hook uses:
    0  allow  (the category is printed to STDOUT)
    2  block  (a reason is printed to STDERR, which the agent shows the user)

Choose which categories should block with, e.g.:
    export NETSI_BLOCK="fraud,political"

This is a prototype classifier for a tutorial, not a production spam filter.
"""
import json
import os
import re
import sys

# The hook payload field that holds the submitted text can vary by agent and
# version, so we check the common names and fall back to the raw input. Check
# the official hook docs for your agent and add the exact field if needed.
_TEXT_KEYS = ("prompt", "user_prompt", "userPrompt", "message", "text", "input", "content")


def extract_message(raw: str) -> str:
    """Pull the message text out of a hook JSON payload, or return raw text."""
    raw = raw.strip()
    if not raw:
        return ""
    try:
        data = json.loads(raw)
    except Exception:
        return raw  # not JSON — treat the whole input as the message
    if isinstance(data, str):
        return data
    if isinstance(data, dict):
        for key in _TEXT_KEYS:
            value = data.get(key)
            if isinstance(value, str) and value.strip():
                return value
        for container in ("data", "payload", "hook", "event"):
            inner = data.get(container)
            if isinstance(inner, dict):
                for key in _TEXT_KEYS:
                    value = inner.get(key)
                    if isinstance(value, str) and value.strip():
                        return value
        return json.dumps(data)
    return raw


_FRAUD = [
    r"https?://", r"\bwww\.", r"\bbit\.ly\b",
    r"\bverify (your )?(account|identity)", r"\bgift ?cards?\b",
    r"\b(bank|paypal|iban|routing) (details|number|account)",
    r"\bssn\b", r"\bsocial security\b", r"\bwire (transfer|money)",
    r"\b(bitcoin|crypto|usdt|wallet)\b", r"\bpassword\b",
    r"\bone[- ]?time (code|password)\b", r"\botp\b",
    r"\bclick (here|the link|below)\b",
    r"\b(act now|urgent|immediately|asap|within \d+\s*(min|hour))\b",
    r"\byou('| ?ha)ve won\b", r"\bclaim your (prize|reward|refund)\b",
    r"\b(account|card) (suspended|locked|blocked)\b",
    r"\bunusual (activity|sign[- ]?in)\b",
    r"\bpay(ment)?\b.*\b(now|fee|fine|penalty|owed)\b",
    r"\bconfirm your (payment|details|information)\b",
    # Danish
    r"\b(nemid|mitid)\b", r"\bklik (her|på linket)\b",
    r"\bbekræft (din|dine) (konto|identitet|oplysninger|betaling)",
    r"\b(konto|kort)(et)? (er )?(spærret|låst|blokeret|lukket)\b",
    r"\b(haster|straks|omgående)\b",
    r"\bbetal\b.*\b(nu|gebyr|bøde|told)\b",
    r"\bdu har vundet\b", r"\b(din )?pakke (venter|er tilbageholdt)\b",
    r"\bkontonummer\b", r"\bcpr(-?nummer)?\b", r"\bkodeord\b",
]

_POLITICAL = [
    r"\bvote\b", r"\belection\b", r"\bballot\b", r"\bpolling\b",
    r"\bcampaign\b", r"\bcandidate\b", r"\bsenator\b", r"\bcongress\b",
    r"\bparliament\b", r"\bre-?elect\b", r"\bpolitical\b",
    r"\bdonate to (the|our) (campaign|party)\b", r"\bregister to vote\b",
    r"\bpolling (station|place)\b", r"\bvote (for|yes|no)\b",
    # Danish ("stemmer"/"valg" alone are too common: "det stemmer", "et godt valg")
    r"\bstem\b", r"\bstem (ja|nej|på|for)\b", r"\bafstemning(en)?\b",
    r"\b(folketings|kommunal|regions|europaparlaments)valg(et)?\b",
    r"\bvalgkamp(en)?\b", r"\bvalgsted(et)?\b", r"\bstemmeseddel\b",
    r"\bgenval(g|gt)\b", r"\bgenvælg(e|es)?\b", r"\bkandidat(en)?\b",
    r"\bfolketinget\b", r"\bborgmester(en)?\b", r"\bpolitisk(e)?\b",
    r"\bpartiet\b", r"\bstøt (partiet|kampagnen)\b",
]


def classify(message: str) -> str:
    """Offline heuristic classifier. fraud > political > neutral."""
    text = message.lower()
    if any(re.search(pattern, text) for pattern in _FRAUD):
        return "fraud"
    if any(re.search(pattern, text) for pattern in _POLITICAL):
        return "political"
    return "neutral"


def classify_with_openai(message: str) -> str:
    """Optional LLM classifier. Illustrative — verify the model id and SDK
    against the current OpenAI API docs for your account."""
    from openai import OpenAI  # pip install openai

    here = os.path.dirname(os.path.abspath(__file__))
    with open(os.path.join(here, "classifier-prompt.txt"), encoding="utf-8") as fh:
        instructions = fh.read()
    client = OpenAI()
    response = client.responses.create(
        model=os.environ.get("NETSI_MODEL", "gpt-5-mini"),
        instructions=instructions,
        input=message,
    )
    out = (getattr(response, "output_text", "") or "").strip().lower()
    return out if out in ("fraud", "political", "neutral") else "neutral"


def main() -> int:
    if not sys.stdin.isatty():
        raw = sys.stdin.read()
    else:
        raw = " ".join(sys.argv[1:])
    # Inside a Claude Code hook, anything printed to STDOUT on exit 0 is added
    # to the model's context, so stay silent there and only print when blocking.
    as_hook = '"hook_event_name"' in raw
    report = (lambda _category: None) if as_hook else print
    message = extract_message(raw)
    if not message:
        report("neutral")
        return 0

    try:
        if os.environ.get("NETSI_USE_OPENAI") == "1":
            category = classify_with_openai(message)
        else:
            category = classify(message)
    except Exception as exc:  # fail open: never break the turn on a classifier error
        report("neutral")
        print(f"[classifier error: {exc}]", file=sys.stderr)
        return 0

    blocking = {c.strip() for c in os.environ.get("NETSI_BLOCK", "").split(",") if c.strip()}
    if category in blocking:
        print(f"Message filter blocked this input (category: {category}).", file=sys.stderr)
        report(category)
        return 2

    report(category)
    return 0


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