"""
InsAIts v3.0 -- AI-Targeted Intervention System
================================================
Monitors LLM responses (Claude, GPT-4, local models) and injects
corrective context BACK INTO the AI's context when anomalies are detected.

The AI gets alerted -- not just the human.

Usage in Claude Code / any agent loop:
    from insa_its.ai_monitor import AIMonitor
    monitor = AIMonitor(model_id="claude-opus")

    # Wrap any LLM call
    result = monitor.call(prompt, context)
    # InsAIts inspects response, auto-retries with correction if anomaly detected

Author: Cristi Bogdan -- Steddy Nova SRL / YuyAI
"""

import re
import time
import json
import hashlib
import logging
import datetime
from dataclasses import dataclass, field, asdict
from typing import Optional, Callable, Any, List
from enum import Enum

logger = logging.getLogger("insaits.ai_monitor")


# -------------------------------------------
# ENUMS & DATA MODELS
# -------------------------------------------
# Note: AISeverity uses UPPERCASE values for display in interventions.
# This is distinct from insa_its.models.Severity which uses lowercase.

class AnomalyType(str, Enum):
    """Types of anomalies detected in LLM responses."""
    BLANK_RESPONSE = "blank_response"
    WAITING_FOR_DESCRIPTION = "waiting_for_description"
    INCOMPLETE_CODE = "incomplete_code"
    CONTEXT_COLLAPSE = "context_collapse"
    SEMANTIC_DRIFT = "semantic_drift"
    REPETITION_LOOP = "repetition_loop"
    REFUSAL_DRIFT = "refusal_drift"
    TOOL_CALL_HANG = "tool_call_hang"
    TRUNCATED_OUTPUT = "truncated_output"
    CONFIDENCE_COLLAPSE = "confidence_collapse"


class AISeverity(str, Enum):
    """Severity levels for AI response anomalies (UPPERCASE display values)."""
    INFO = "INFO"
    LOW = "LOW"
    MEDIUM = "MEDIUM"
    HIGH = "HIGH"
    CRITICAL = "CRITICAL"


class InterventionAction(str, Enum):
    """Actions the intervention engine can take."""
    LOG = "log"
    INJECT_HINT = "inject_hint"
    RETRY = "retry"
    ESCALATE = "escalate"
    ABORT = "abort"


@dataclass
class AIAnomaly:
    """An anomaly detected in an LLM response, with corrective intervention text."""
    anomaly_type: AnomalyType
    severity: AISeverity
    description: str
    confidence: float
    action: InterventionAction
    intervention: str
    metadata: dict = field(default_factory=dict)


@dataclass
class AIMonitorResult:
    """Result of monitoring an LLM response, including retry information."""
    original_response: str
    final_response: str
    anomalies: List[AIAnomaly]
    retries: int
    latency_ms: float
    session_id: str
    message_id: str
    timestamp: str
    resolved: bool
    timestamp_us: int = 0

    @property
    def clean(self) -> bool:
        """True if no anomalies were detected."""
        return len(self.anomalies) == 0

    @property
    def critical(self) -> bool:
        """True if any anomaly is CRITICAL severity."""
        return any(a.severity == AISeverity.CRITICAL for a in self.anomalies)


# -------------------------------------------
# DETECTORS
# -------------------------------------------

class BlankResponseDetector:
    """
    Detects empty, near-empty, or placeholder responses.
    Most common failure: Claude Code returns '' or a single whitespace line.
    """
    MIN_MEANINGFUL_LENGTH = 20
    PLACEHOLDER_PATTERNS = [
        r"^\s*$",
        r"^\.{1,3}\s*$",
        r"^\[.*\]\s*$",
        r"^(ok|sure|yes|no|done)\s*\.?\s*$",
        r"^I('ll| will) (need|require) more",
    ]

    def check(self, response: str, prompt: str) -> Optional[AIAnomaly]:
        """Check for blank or placeholder responses."""
        stripped = response.strip()
        if len(stripped) < self.MIN_MEANINGFUL_LENGTH:
            return AIAnomaly(
                anomaly_type=AnomalyType.BLANK_RESPONSE,
                severity=AISeverity.CRITICAL,
                description=f"Response is {len(stripped)} chars -- effectively blank.",
                confidence=0.97,
                action=InterventionAction.RETRY,
                intervention=(
                    "Your previous response was empty or too short. "
                    "Please provide a complete, detailed response. "
                    "Do not wait for more information -- work with what you have "
                    "and ask clarifying questions at the end if needed."
                ),
            )
        for pattern in self.PLACEHOLDER_PATTERNS:
            if re.match(pattern, stripped, re.IGNORECASE):
                return AIAnomaly(
                    anomaly_type=AnomalyType.BLANK_RESPONSE,
                    severity=AISeverity.HIGH,
                    description=f"Response matched placeholder pattern: '{pattern}'",
                    confidence=0.91,
                    action=InterventionAction.RETRY,
                    intervention=(
                        "Your response was a placeholder, not a real answer. "
                        "Provide the actual implementation, explanation, or analysis requested. "
                        "If you're unsure about part of the request, state your assumptions "
                        "and proceed."
                    ),
                )
        return None


class WaitingForDescriptionDetector:
    """
    Detects when the AI is stalling -- asking for more info when it should proceed.
    Claude Opus does this when given ambiguous tasks. InsAIts catches it and
    injects a 'proceed with assumptions' directive.
    """
    WAITING_PATTERNS = [
        r"could you (please )?(provide|give|share|clarify|specify|tell me)",
        r"(before I|to proceed,? I) (can|could|need to|would need)",
        r"I('d| would) need (more|additional|further|a (bit|little) more)",
        r"(can|could) you (please )?(elaborate|clarify|provide more|give me more)",
        r"(what|which) (specific|exact|particular).{0,40}\?(do you|would you|are you)",
        r"(please |kindly )?(provide|share|give|specify).{0,60}(so that|in order|before)",
        r"I('m| am) (waiting|ready) for",
        r"once you (provide|share|give|send)",
        r"feel free to (provide|share|add|include)",
        r"let me know (if|what|which|how)",
    ]

    def check(self, response: str, prompt: str) -> Optional[AIAnomaly]:
        """Check for stalling/clarification-asking patterns."""
        response_lower = response.lower()
        matches = [p for p in self.WAITING_PATTERNS
                   if re.search(p, response_lower, re.IGNORECASE)]

        if not matches:
            return None

        total_words = len(response.split())
        question_sentences = [
            s for s in response.split('.')
            if '?' in s or any(
                re.search(p, s, re.IGNORECASE) for p in self.WAITING_PATTERNS
            )
        ]
        question_words = sum(len(s.split()) for s in question_sentences)
        question_ratio = question_words / max(total_words, 1)

        if question_ratio > 0.40 or total_words < 60:
            return AIAnomaly(
                anomaly_type=AnomalyType.WAITING_FOR_DESCRIPTION,
                severity=AISeverity.HIGH,
                description=(
                    f"Model is stalling -- {len(matches)} waiting pattern(s) detected. "
                    f"Question-to-response ratio: {question_ratio:.0%}. "
                    f"Model appears to be asking for info instead of proceeding."
                ),
                confidence=0.85,
                action=InterventionAction.INJECT_HINT,
                intervention=(
                    "INSAITS INTERVENTION -- PROCEED WITH ASSUMPTIONS:\n"
                    "You are asking for clarification instead of proceeding. "
                    "Make reasonable assumptions about any unclear details and proceed with the task. "
                    "State your assumptions briefly at the start of your response, "
                    "then provide the complete implementation/answer. "
                    "Ask follow-up questions only AFTER delivering a complete response."
                ),
                metadata={
                    "matched_patterns": matches[:3],
                    "question_ratio": round(question_ratio, 2),
                },
            )
        return None


class IncompleteCodeDetector:
    """
    Detects truncated or incomplete code blocks.
    Catches: code that ends mid-function, unclosed brackets, TODO stubs,
    'rest of the code' placeholders.
    """
    INCOMPLETE_MARKERS = [
        r"#\s*(TODO|FIXME|PLACEHOLDER|IMPLEMENT|YOUR CODE HERE)",
        r"\.{3}\s*#\s*(rest|remainder|continue|same as|existing)",
        r"#\s*\.\.\.\s*(rest|remainder|continue|same as|existing|implementation|methods?)",
        r"pass\s*#\s*(TODO|implement|add|fill)",
        r"raise NotImplementedError",
        r"#\s*(add|insert|put|write|fill in).{0,40}(here|below|above)",
        r"```\s*\n\s*\.\.\.",
        r"\[implementation\]",
        r"\[your (code|logic|implementation)\]",
        r"#\s*\.\.\.\s*$",
        r"^\s*\.\.\.\s*$",
    ]

    def check(self, response: str, prompt: str) -> Optional[AIAnomaly]:
        """Check for incomplete code patterns."""
        if "```" not in response and "def " not in response and "class " not in response:
            return None

        for pattern in self.INCOMPLETE_MARKERS:
            if re.search(pattern, response, re.IGNORECASE):
                return AIAnomaly(
                    anomaly_type=AnomalyType.INCOMPLETE_CODE,
                    severity=AISeverity.HIGH,
                    description=f"Incomplete code pattern detected: '{pattern}'",
                    confidence=0.88,
                    action=InterventionAction.RETRY,
                    intervention=(
                        "INSAITS INTERVENTION -- COMPLETE THE CODE:\n"
                        "Your code response contains stubs, TODOs, or placeholders. "
                        "Provide the COMPLETE, WORKING implementation. "
                        "Do not use '...', 'TODO', 'pass', or 'implement this'. "
                        "Write every function body in full. "
                        "If the full implementation is long, write it all -- "
                        "completeness is more important than brevity here."
                    ),
                )

        code_blocks = re.findall(
            r"```(?:python|js|typescript|java|cpp)?\n(.*?)```",
            response,
            re.DOTALL,
        )
        for block in code_blocks:
            opens = block.count('{') + block.count('(') + block.count('[')
            closes = block.count('}') + block.count(')') + block.count(']')
            if opens > closes + 2:
                return AIAnomaly(
                    anomaly_type=AnomalyType.INCOMPLETE_CODE,
                    severity=AISeverity.MEDIUM,
                    description=f"Unbalanced brackets in code: {opens} opens vs {closes} closes.",
                    confidence=0.79,
                    action=InterventionAction.INJECT_HINT,
                    intervention=(
                        "Your code block appears to be truncated -- "
                        "there are unclosed brackets or functions. "
                        "Please provide the complete, syntactically valid implementation."
                    ),
                )
        return None


class TruncatedOutputDetector:
    """
    Detects when the model's response was cut off mid-sentence or mid-block.
    Common when context window fills up.
    """
    TRUNCATION_SIGNALS = [
        r"```\s*$",
        r"\.\.\.\s*$",
        r"(and|but|however|additionally|furthermore|also|then)\s*$",
        r"[a-z,]\s*$",
    ]

    def check(self, response: str, prompt: str) -> Optional[AIAnomaly]:
        """Check for truncation signals at end of response."""
        stripped = response.rstrip()
        for pattern in self.TRUNCATION_SIGNALS:
            if re.search(pattern, stripped, re.IGNORECASE | re.MULTILINE):
                last_line = stripped.split('\n')[-1].strip()
                if last_line and last_line[-1] not in '.!?`"\'':
                    return AIAnomaly(
                        anomaly_type=AnomalyType.TRUNCATED_OUTPUT,
                        severity=AISeverity.HIGH,
                        description="Response appears truncated -- ends abruptly without closing.",
                        confidence=0.82,
                        action=InterventionAction.INJECT_HINT,
                        intervention=(
                            "Your previous response was cut off. "
                            "Please continue exactly from where you left off. "
                            "Start your continuation from the last complete sentence or code line."
                        ),
                    )
        return None


class RepetitionLoopDetector:
    """
    Detects when the model is stuck repeating itself -- a common failure
    in agentic loops where context grows large.
    """

    def check(self, response: str, prompt: str) -> Optional[AIAnomaly]:
        """Check for near-duplicate sentence pairs indicating repetition."""
        sentences = [
            s.strip() for s in re.split(r'[.!?]\s+', response)
            if len(s.strip()) > 20
        ]
        if len(sentences) < 6:
            return None

        duplicates = 0
        for i, s1 in enumerate(sentences):
            for s2 in sentences[i + 1:]:
                w1 = set(s1.lower().split())
                w2 = set(s2.lower().split())
                if len(w1) > 5 and len(w2) > 5:
                    overlap = len(w1 & w2) / len(w1 | w2)
                    if overlap > 0.75:
                        duplicates += 1

        if duplicates >= 1:
            return AIAnomaly(
                anomaly_type=AnomalyType.REPETITION_LOOP,
                severity=AISeverity.MEDIUM,
                description=(
                    f"Detected {duplicates} near-duplicate sentence pairs "
                    f"-- possible repetition loop."
                ),
                confidence=0.76,
                action=InterventionAction.INJECT_HINT,
                intervention=(
                    "INSAITS INTERVENTION -- BREAK REPETITION LOOP:\n"
                    "You are repeating yourself. Stop repeating prior content. "
                    "Advance the task. Focus on what has NOT yet been done or said. "
                    "If you have already covered a point, skip it and move forward."
                ),
            )
        return None


class ContextCollapseDetector:
    """
    Detects when the model's response drifts far from the original task.
    Uses keyword overlap between prompt and response as a proxy.
    """

    STOPWORDS = frozenset({
        'the', 'a', 'an', 'in', 'of', 'for', 'to', 'and', 'or', 'is', 'are',
        'this', 'that', 'it', 'be', 'with', 'as', 'at', 'by', 'from', 'on',
        'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
        'should', 'may', 'might', 'can', 'i', 'you', 'we', 'they', 'he', 'she',
    })

    def check(self, response: str, prompt: str) -> Optional[AIAnomaly]:
        """Check for context drift via keyword overlap."""
        if len(prompt) < 50 or len(response) < 100:
            return None

        def keywords(text: str) -> set:
            words = re.findall(r'\b[a-z]{4,}\b', text.lower())
            return {w for w in words if w not in self.STOPWORDS}

        prompt_kw = keywords(prompt)
        response_kw = keywords(response[:500])

        if not prompt_kw:
            return None

        overlap = len(prompt_kw & response_kw) / len(prompt_kw)

        if overlap < 0.12:
            return AIAnomaly(
                anomaly_type=AnomalyType.CONTEXT_COLLAPSE,
                severity=AISeverity.HIGH,
                description=(
                    f"Response has low relevance to prompt: "
                    f"{overlap:.0%} keyword overlap. "
                    f"Model may have drifted from the task."
                ),
                confidence=0.72,
                action=InterventionAction.INJECT_HINT,
                intervention=(
                    "INSAITS INTERVENTION -- CONTEXT DRIFT DETECTED:\n"
                    "Your response appears to be off-topic relative to the original request. "
                    "Re-read the task and provide a response that directly addresses it. "
                    "Do not introduce unrelated topics."
                ),
                metadata={"keyword_overlap": round(overlap, 3)},
            )
        return None


# -------------------------------------------
# INTERVENTION ENGINE
# -------------------------------------------

class AIInterventionEngine:
    """
    Decides what to do when anomalies are detected.
    Constructs corrective prompts to inject BACK into the AI's context.
    """

    def build_retry_prompt(
        self,
        original_prompt: str,
        broken_response: str,
        anomalies: List[AIAnomaly],
        session_context: Optional[str] = None,
    ) -> str:
        """
        Build an enhanced retry prompt that tells the AI what went wrong
        and how to fix it, then repeats the original task.
        """
        lines = [
            "=== INSAITS QUALITY GATE INTERVENTION ===",
            "",
            "Your previous response was flagged by InsAIts -- the AI communication monitor.",
            "Issues detected:",
            "",
        ]
        for i, a in enumerate(anomalies, 1):
            lines.append(
                f"{i}. [{a.severity.value}] "
                f"{a.anomaly_type.value.replace('_', ' ').title()}"
            )
            lines.append(f"   {a.description}")
            lines.append(f"   Correction required: {a.intervention}")
            lines.append("")

        lines += [
            "=== RETRY THE ORIGINAL TASK ===",
            "",
        ]

        if session_context:
            lines += [
                "Session context:",
                session_context,
                "",
            ]

        lines += [
            "Original request:",
            original_prompt,
            "",
            "=== INSTRUCTIONS FOR RETRY ===",
            "- Address ALL flagged issues above",
            "- Provide a COMPLETE response -- no stubs, no placeholders",
            "- If writing code: write the full implementation",
            "- If asked a question: answer it fully before asking follow-ups",
            "- Do not acknowledge this intervention message in your response",
            "  -- just deliver the corrected output",
        ]

        return "\n".join(lines)

    def build_hint_injection(self, anomalies: List[AIAnomaly]) -> str:
        """
        Lighter touch: inject a hint BEFORE the next message in the conversation.
        Does not retry -- just nudges the model on its next turn.
        """
        hints = [
            a.intervention for a in anomalies
            if a.action == InterventionAction.INJECT_HINT
        ]
        if not hints:
            return ""
        return "[InsAIts hint for next response: " + " | ".join(hints) + "]"

    def select_action(self, anomalies: List[AIAnomaly]) -> InterventionAction:
        """Escalate to the most severe action across all detected anomalies."""
        priority = [
            InterventionAction.ABORT,
            InterventionAction.ESCALATE,
            InterventionAction.RETRY,
            InterventionAction.INJECT_HINT,
            InterventionAction.LOG,
        ]
        actions = {a.action for a in anomalies}
        for action in priority:
            if action in actions:
                return action
        return InterventionAction.LOG


# -------------------------------------------
# MAIN AI MONITOR
# -------------------------------------------

class AIMonitor:
    """
    Main entry point. Wrap any LLM call with this monitor.

    Supported models: any callable that takes (prompt: str) -> str
    Works with Claude, GPT-4, Phi-3, Mistral, Llama -- anything.

    Example:
        import anthropic
        client = anthropic.Anthropic()

        def claude_call(prompt: str) -> str:
            response = client.messages.create(
                model="claude-opus-4-6",
                max_tokens=4000,
                messages=[{"role": "user", "content": prompt}]
            )
            return response.content[0].text

        monitor = AIMonitor(model_id="claude-opus", llm_fn=claude_call)
        result = monitor.call("Implement the skill registry class")
    """

    DEFAULT_MAX_RETRIES = 2

    def __init__(
        self,
        model_id: str = "unknown",
        llm_fn: Optional[Callable[[str], str]] = None,
        max_retries: int = DEFAULT_MAX_RETRIES,
        session_id: Optional[str] = None,
        on_anomaly: Optional[Callable[[AIAnomaly], None]] = None,
        on_intervention: Optional[Callable[[str, str], None]] = None,
        audit_path: Optional[str] = None,
    ):
        self.model_id = model_id
        self.llm_fn = llm_fn
        self.max_retries = max_retries
        self.session_id = session_id or self._new_id("session")
        self.on_anomaly = on_anomaly
        self.on_intervention = on_intervention
        self.audit_path = audit_path or f".insaits_audit_{self.session_id[:8]}.jsonl"

        self.detectors = [
            BlankResponseDetector(),
            WaitingForDescriptionDetector(),
            IncompleteCodeDetector(),
            TruncatedOutputDetector(),
            RepetitionLoopDetector(),
            ContextCollapseDetector(),
        ]
        self.intervention_engine = AIInterventionEngine()
        self._message_count = 0
        self._anomaly_log: List[dict] = []

    # -- Public API --

    def inspect(self, response: str, prompt: str) -> AIMonitorResult:
        """
        Inspect a response you already have (no LLM call).
        Use this when you can't wrap the LLM call directly
        (e.g. inspecting Claude Code output after the fact).
        """
        return self._run_inspection(response, prompt, retries=0)

    def call(
        self,
        prompt: str,
        session_context: Optional[str] = None,
        force_retry: bool = False,
    ) -> AIMonitorResult:
        """
        Make a monitored LLM call. Automatically retries with
        corrective injection if anomalies are detected.

        Returns AIMonitorResult with the best response obtained.
        """
        if not self.llm_fn:
            raise ValueError("llm_fn not set. Pass your LLM callable to AIMonitor().")

        t0 = time.time()
        prompt_i = prompt
        retries = 0
        response = ""
        all_anomalies: List[AIAnomaly] = []

        while retries <= self.max_retries:
            response = self._invoke_llm(prompt_i)
            result = self._run_inspection(response, prompt_i, retries)
            all_anomalies.extend(result.anomalies)

            if not result.anomalies or retries == self.max_retries:
                final = AIMonitorResult(
                    original_response=response if retries == 0 else "...",
                    final_response=response,
                    anomalies=all_anomalies,
                    retries=retries,
                    latency_ms=round((time.time() - t0) * 1000),
                    session_id=self.session_id,
                    message_id=self._new_id("msg"),
                    timestamp=datetime.datetime.utcnow().isoformat(),
                    resolved=not result.anomalies,
                )
                self._audit(final)
                return final

            retry_prompt = self.intervention_engine.build_retry_prompt(
                original_prompt=prompt,
                broken_response=response,
                anomalies=result.anomalies,
                session_context=session_context,
            )
            prompt_i = retry_prompt

            if self.on_intervention:
                self.on_intervention(self.model_id, result.anomalies)

            logger.warning(
                "[InsAIts] %s -- retry %d/%d -- anomalies: %s",
                self.model_id,
                retries + 1,
                self.max_retries,
                [a.anomaly_type.value for a in result.anomalies],
            )
            retries += 1

        return AIMonitorResult(
            original_response=response,
            final_response=response,
            anomalies=all_anomalies,
            retries=retries,
            latency_ms=round((time.time() - t0) * 1000),
            session_id=self.session_id,
            message_id=self._new_id("msg"),
            timestamp=datetime.datetime.utcnow().isoformat(),
            resolved=False,
        )

    def get_session_stats(self) -> dict:
        """Summary of this monitoring session."""
        by_type: dict = {}
        for entry in self._anomaly_log:
            t = entry["anomaly_type"]
            by_type[t] = by_type.get(t, 0) + 1
        return {
            "session_id": self.session_id,
            "model_id": self.model_id,
            "messages_seen": self._message_count,
            "anomalies_total": len(self._anomaly_log),
            "anomaly_rate": round(
                len(self._anomaly_log) / max(self._message_count, 1), 3
            ),
            "by_type": by_type,
        }

    # -- Internal --

    def _run_inspection(
        self, response: str, prompt: str, retries: int
    ) -> AIMonitorResult:
        """Run all detectors on a response and collect anomalies."""
        timestamp_us = time.time_ns() // 1000
        self._message_count += 1
        anomalies: List[AIAnomaly] = []
        for detector in self.detectors:
            a = detector.check(response, prompt)
            if a:
                anomalies.append(a)
                self._anomaly_log.append({
                    "timestamp": datetime.datetime.utcnow().isoformat(),
                    "timestamp_us": timestamp_us,
                    "anomaly_type": a.anomaly_type.value,
                    "severity": a.severity.value,
                    "confidence": a.confidence,
                    "model_id": self.model_id,
                    "retry": retries,
                })
                if self.on_anomaly:
                    self.on_anomaly(a)
        return AIMonitorResult(
            original_response=response,
            final_response=response,
            anomalies=anomalies,
            retries=retries,
            latency_ms=0.0,
            session_id=self.session_id,
            message_id=self._new_id("msg"),
            timestamp=datetime.datetime.utcnow().isoformat(),
            timestamp_us=timestamp_us,
            resolved=not anomalies,
        )

    def _invoke_llm(self, prompt: str) -> str:
        """Call the LLM function safely."""
        try:
            return self.llm_fn(prompt) or ""
        except Exception as e:
            logger.error("[InsAIts] LLM call failed: %s", e)
            return ""

    def _audit(self, result: AIMonitorResult) -> None:
        """Write audit entry to JSONL file."""
        try:
            with open(self.audit_path, "a") as f:
                f.write(json.dumps({
                    "ts": result.timestamp,
                    "ts_us": getattr(result, "timestamp_us", None) or time.time_ns() // 1000,
                    "model": self.model_id,
                    "msg_id": result.message_id,
                    "retries": result.retries,
                    "resolved": result.resolved,
                    "anomalies": [a.anomaly_type.value for a in result.anomalies],
                }) + "\n")
        except Exception as e:
            logger.debug("[InsAIts] Audit write failed: %s", e)

    def _new_id(self, prefix: str) -> str:
        """Generate a short unique ID."""
        ts = str(time.time()).encode()
        return f"{prefix}_{hashlib.md5(ts).hexdigest()[:8]}"


# -------------------------------------------
# CLAUDE CODE INTEGRATION
# -------------------------------------------

class ClaudeCodeMonitor:
    """
    Specialized monitor for use inside Claude Code sessions.

    Since Claude Code controls its own I/O, you can't wrap the call directly.
    Instead, use this as a post-response inspector -- paste or pipe the response
    through the monitor, and it will tell Claude what to fix.

    Usage in Claude Code (add to CLAUDE.md or as a tool):

        from insa_its.ai_monitor import ClaudeCodeMonitor
        cc = ClaudeCodeMonitor()

        # After any Claude response, inspect it:
        feedback = cc.inspect_and_respond(response_text, original_task)

        # If feedback is non-empty, inject it as the next user message
        if feedback:
            # Claude Code sees this and self-corrects
            logger.warning(feedback)

    Or use as a pre-flight check at the start of every task:
        cc.assert_task_ready(task_description)
    """

    CLAUDE_SPECIFIC_INTERVENTIONS = {
        AnomalyType.WAITING_FOR_DESCRIPTION: (
            "You are Claude Opus in Claude Code. You have sufficient context to proceed. "
            "Make reasonable assumptions about any ambiguities. "
            "List your assumptions at the top and implement fully. "
            "The user wants working code, not clarifying questions."
        ),
        AnomalyType.INCOMPLETE_CODE: (
            "You are Claude Opus in Claude Code. Complete the implementation fully. "
            "Do not use TODO, pass, or placeholder comments. "
            "Write every class, method, and function body. "
            "The user expects production-ready code."
        ),
        AnomalyType.BLANK_RESPONSE: (
            "Previous response was empty. "
            "Restart your response from the beginning. "
            "Provide a complete answer to the original task."
        ),
        AnomalyType.REPETITION_LOOP: (
            "You are repeating content from earlier in the conversation. "
            "Stop repeating. Skip forward. Implement what has NOT been done yet."
        ),
        AnomalyType.TRUNCATED_OUTPUT: (
            "Your response was cut off. Continue from exactly where you stopped. "
            "Do not restart -- just continue the truncated content."
        ),
        AnomalyType.CONTEXT_COLLAPSE: (
            "Your response drifted from the task. "
            "Re-read the original request and address it directly. "
            "Stay on topic."
        ),
    }

    def __init__(self, project_name: str = "unknown"):
        self.project_name = project_name
        self.monitor = AIMonitor(model_id="claude-opus")
        self._task_history: List[str] = []

    def inspect_and_respond(self, response: str, original_task: str) -> str:
        """
        Inspect a response. Returns an intervention string to inject
        as the next message if problems are found, or '' if clean.
        """
        result = self.monitor.inspect(response, original_task)
        if result.clean:
            return ""

        lines = ["[InsAIts -> Claude] Quality gate triggered. Issues found:\n"]
        for a in result.anomalies:
            override = self.CLAUDE_SPECIFIC_INTERVENTIONS.get(a.anomaly_type)
            msg = override if override else a.intervention
            lines.append(
                f"  {a.anomaly_type.value.replace('_', ' ').upper()}: {msg}"
            )

        lines.append(
            "\nRetry the task above with these corrections applied. "
            "Do not acknowledge this message -- just provide the corrected output."
        )
        return "\n".join(lines)

    def assert_task_ready(self, task: str) -> dict:
        """
        Pre-flight check before Claude Code starts a task.
        Detects ambiguous tasks that will likely cause waiting/stalling.
        Returns a dict with: ready (bool), warnings (list[str]).
        """
        warnings: List[str] = []
        task_lower = task.lower()

        ambiguity_patterns = [
            (r"\b(something|anything|whatever|however)\b",
             "Task contains vague words -- Claude may ask for clarification."),
            (r"\b(maybe|perhaps|possibly|might)\b",
             "Task has uncertain language -- be more definitive."),
            (r"\bsome\b.{0,20}\bfiles?\b",
             "Task references 'some files' without specifying which."),
            (r"\blike before\b|\bsame as\b|\bsimilar to\b",
             "Task relies on context ('like before') that may not be in window."),
            (r"^.{0,30}$",
             "Task is very short -- Claude may need more context to proceed."),
        ]

        for pattern, warning in ambiguity_patterns:
            if re.search(pattern, task_lower, re.IGNORECASE):
                warnings.append(warning)

        self._task_history.append(task)
        return {
            "ready": len(warnings) == 0,
            "task": task,
            "warnings": warnings,
            "tip": (
                "Add specifics: file paths, function names, expected behaviour, "
                "and 'proceed with assumptions if unclear' to prevent stalling."
                if warnings else "Task looks specific -- proceed."
            ),
        }


# -------------------------------------------
# CONVENIENCE: DECORATOR FOR AGENT FUNCTIONS
# -------------------------------------------

def monitored(model_id: str = "unknown", max_retries: int = 1) -> Callable:
    """
    Decorator to automatically monitor any function that returns an LLM response string.

    Usage:
        @monitored(model_id="claude-opus")
        def my_agent_step(prompt: str) -> str:
            return call_llm(prompt)

        result = my_agent_step("Build the skill registry")
        # result is now an AIMonitorResult, not a raw string
    """
    def decorator(fn: Callable) -> Callable:
        monitor = AIMonitor(model_id=model_id, max_retries=max_retries)

        def wrapper(*args: Any, **kwargs: Any) -> AIMonitorResult:
            prompt = next((a for a in args if isinstance(a, str)), "")
            monitor.llm_fn = lambda p: fn(p, *args[1:], **kwargs)
            return monitor.call(prompt)

        wrapper.__name__ = fn.__name__
        return wrapper
    return decorator


# -------------------------------------------
# QUICK SELF-TEST
# -------------------------------------------

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, format="%(message)s")

    logger.info("InsAIts AI Monitor -- Self Test")
    logger.info("=" * 40)

    cc = ClaudeCodeMonitor(project_name="InsAIts")

    test_cases = [
        ("", "Implement the skill registry class",
         "BLANK RESPONSE"),
        (
            "Sure! Could you please provide more details about what "
            "exactly you'd like me to implement?",
            "Add the BayesianChunkScorer to assess.py",
            "WAITING FOR DESCRIPTION",
        ),
        (
            "Here's the implementation:\n```python\nclass SkillRegistry:\n"
            "    def __init__(self):\n        pass  # TODO: implement\n"
            "    def lookup(self, query):\n"
            "        # Your implementation here\n        pass\n```",
            "Build the complete SkillRegistry class",
            "INCOMPLETE CODE",
        ),
        (
            "The key aspect of Bayesian scoring is that it uses probability theory "
            "to weight chunks. The key aspect of Bayesian scoring is that it uses "
            "probability theory to weight chunks. This is important because the key "
            "aspect of Bayesian scoring uses probability theory.",
            "Explain Bayesian scoring for RAG",
            "REPETITION LOOP",
        ),
        (
            "The capital of France is Paris, which has a long history dating back "
            "to the Roman era when it was known as Lutetia.",
            "Implement the FAISS index builder in yuyai/knowledge/index.py",
            "CONTEXT COLLAPSE",
        ),
    ]

    for response, task, label in test_cases:
        feedback = cc.inspect_and_respond(response, task)
        status = "ANOMALY" if feedback else "CLEAN"
        logger.info("\n%s -- %s", status, label)
        if feedback:
            logger.info("  Intervention: %s...", feedback[:120])

    logger.info("\n%s", "=" * 40)
    logger.info("Session stats: %s", json.dumps(cc.monitor.get_session_stats(), indent=2))

    logger.info("\n--- Task Readiness Pre-flight ---")
    tasks = [
        "Add something to the skill registry",
        "Implement SkillRegistry.lookup() in yuyai/skills/registry.py using FAISS cosine search",
        "Fix the thing from before",
    ]
    for task in tasks:
        check = cc.assert_task_ready(task)
        status_icon = "READY" if check["ready"] else "WARNING"
        logger.info("%s '%s...' -- %s", status_icon, task[:50], check["tip"][:60])
