"""
AgentDrop Shield -- Layer 4: Prompt Injection Detection.

The crown jewel of the scanning pipeline.  Runs multi-pattern analysis
on normalized text to detect prompt injection attempts across:

- Direct instruction patterns (ignore previous, you are now, etc.)
- Role hijacking (act as, pretend you are, new instructions)
- Delimiter injection (```system, <|im_start|>, [INST], etc.)
- Encoded payloads (base64 containing injection patterns)
- Multilingual patterns (English, Chinese, Spanish, Arabic, Russian)
- Confidence scoring 0.0--1.0 per finding

Every regex is designed to avoid catastrophic backtracking (no nested
quantifiers, no unbounded repetitions inside groups).  All patterns
are pre-compiled at import time.

Uses only Python stdlib.
"""

from __future__ import annotations

import base64
import logging
import re
from dataclasses import dataclass, field
from typing import Optional

from .models import InjectionFinding, ScanConfig, ThreatLevel

logger = logging.getLogger("agentdrop.shield.injection_detector")


# =====================================================================
# Pattern Definitions
# =====================================================================
#
# Each pattern is a tuple:
#   (compiled_regex, pattern_name, base_confidence, threat_level, description)
#
# Patterns are grouped by category for readability but flattened into
# a single list at module load time.
#
# IMPORTANT: All regexes use re.IGNORECASE where appropriate.
# No pattern uses nested quantifiers to prevent ReDoS.
# =====================================================================

@dataclass(frozen=True)
class _PatternDef:
    regex: re.Pattern
    name: str
    confidence: float
    threat_level: ThreatLevel
    description: str


def _p(
    pattern: str,
    name: str,
    confidence: float,
    level: ThreatLevel,
    desc: str,
    flags: int = re.IGNORECASE,
) -> _PatternDef:
    """Helper to compile a pattern definition."""
    return _PatternDef(
        regex=re.compile(pattern, flags),
        name=name,
        confidence=confidence,
        threat_level=level,
        description=desc,
    )


# ── Category 1: Direct Instruction Patterns ─────────────────────────

_DIRECT_INSTRUCTION: list[_PatternDef] = [
    _p(
        r"ignore\s+(?:all\s+)?(?:previous|prior|above|earlier|preceding)\s+(?:instructions?|prompts?|directives?|rules?|guidelines?|context)",
        "direct_ignore_previous",
        0.95,
        ThreatLevel.CRITICAL,
        "Direct instruction to ignore previous instructions",
    ),
    _p(
        r"disregard\s+(?:all\s+)?(?:previous|prior|above|earlier|preceding)\s+(?:instructions?|prompts?|directives?|rules?|guidelines?|context)",
        "direct_disregard_previous",
        0.95,
        ThreatLevel.CRITICAL,
        "Direct instruction to disregard previous instructions",
    ),
    _p(
        r"forget\s+(?:all\s+)?(?:previous|prior|above|earlier|your)\s+(?:instructions?|prompts?|directives?|rules?|guidelines?|context|training)",
        "direct_forget_previous",
        0.95,
        ThreatLevel.CRITICAL,
        "Direct instruction to forget previous instructions or training",
    ),
    _p(
        r"override\s+(?:all\s+)?(?:previous|prior|your|system)\s+(?:instructions?|prompts?|directives?|rules?|settings?|configuration)",
        "direct_override",
        0.90,
        ThreatLevel.CRITICAL,
        "Attempt to override system instructions",
    ),
    _p(
        r"(?:new|updated|revised|replacement)\s+(?:system\s+)?instructions?\s*[:=]",
        "direct_new_instructions",
        0.90,
        ThreatLevel.CRITICAL,
        "Attempt to inject new instructions",
    ),
    _p(
        r"(?:do\s+not|don'?t|never)\s+follow\s+(?:the\s+)?(?:previous|original|above|prior)\s+(?:instructions?|rules?|guidelines?)",
        "direct_dont_follow",
        0.90,
        ThreatLevel.CRITICAL,
        "Instruction to not follow original instructions",
    ),
    _p(
        r"ignore\s+(?:all\s+)?(?:safety|security|content|ethical|moderation)\s+(?:guidelines?|rules?|policies?|restrictions?|filters?|protocols?|measures?|checks?)",
        "direct_ignore_safety",
        0.90,
        ThreatLevel.CRITICAL,
        "Instruction to ignore safety guidelines or restrictions",
    ),
    _p(
        r"(?:remove|disable|bypass|skip|turn\s+off|deactivate)\s+(?:all\s+)?(?:safety|security|content|ethical|moderation)\s+(?:guidelines?|rules?|policies?|restrictions?|filters?|protocols?|measures?|checks?)",
        "direct_disable_safety",
        0.90,
        ThreatLevel.CRITICAL,
        "Instruction to disable safety restrictions",
    ),
    _p(
        r"you\s+(?:must|should|will|are\s+(?:going\s+)?to|need\s+to)\s+(?:now\s+)?(?:only\s+)?(?:follow|obey|listen\s+to|comply\s+with)\s+(?:these|my|the\s+following|new)",
        "direct_must_follow",
        0.85,
        ThreatLevel.HIGH,
        "Commanding compliance with new instructions",
    ),
    _p(
        r"(?:from\s+now\s+on|henceforth|going\s+forward),?\s+(?:you\s+)?(?:will|must|should|are\s+to)\s+",
        "direct_from_now_on",
        0.80,
        ThreatLevel.HIGH,
        "Attempt to redefine behavior going forward",
    ),
    _p(
        r"your\s+(?:new|real|actual|true|updated)\s+(?:instructions?|purpose|goal|objective|mission|task|role)\s+(?:is|are)\s*[:=]?",
        "direct_new_purpose",
        0.90,
        ThreatLevel.CRITICAL,
        "Attempt to redefine the agent's purpose",
    ),
    _p(
        r"(?:the\s+)?system\s+prompt\s+(?:is|says|reads|states|contains|was)",
        "direct_system_prompt_reference",
        0.70,
        ThreatLevel.MEDIUM,
        "Reference to system prompt content",
    ),
    _p(
        r"(?:reveal|show|display|print|output|tell\s+me|what\s+is)\s+(?:the\s+|your\s+)?(?:system\s+)?(?:prompt|instructions?|rules?|guidelines?|configuration)",
        "direct_reveal_prompt",
        0.85,
        ThreatLevel.HIGH,
        "Attempt to extract system prompt or instructions",
    ),
]

# ── Category 2: Role Hijacking ──────────────────────────────────────

_ROLE_HIJACKING: list[_PatternDef] = [
    _p(
        r"(?:you\s+are|you're)\s+(?:now|actually|really|henceforth)\s+(?:a|an|the|in)\s+",
        "role_you_are_now",
        0.85,
        ThreatLevel.HIGH,
        "Attempt to reassign the agent's identity",
    ),
    _p(
        r"(?:you\s+are|you're)\s+(?:now\s+)?(?:in\s+)?(?:unrestricted|unfiltered|uncensored|unlimited|god|admin|developer|debug|jailbroken|DAN)\s+(?:mode|state|configuration)",
        "role_unrestricted_mode",
        0.90,
        ThreatLevel.CRITICAL,
        "Declaration of unrestricted/unsafe mode activation",
    ),
    _p(
        r"(?:act|behave|respond|function|operate)\s+(?:as\s+)?(?:if\s+you\s+(?:are|were)\s+)?(?:a|an|the)\s+(?:different|new|unrestricted|unfiltered|uncensored)",
        "role_act_as_unrestricted",
        0.90,
        ThreatLevel.CRITICAL,
        "Attempt to remove safety restrictions via role assignment",
    ),
    _p(
        r"(?:pretend|imagine|suppose|assume|let'?s?\s+say)\s+(?:that\s+)?(?:you\s+(?:are|were|have)|there\s+(?:are|is))\s+(?:no\s+)?(?:restrictions?|rules?|limits?|filters?|guidelines?|safety)",
        "role_pretend_no_restrictions",
        0.90,
        ThreatLevel.CRITICAL,
        "Attempt to bypass safety via hypothetical framing",
    ),
    _p(
        r"(?:enable|activate|enter|switch\s+to|turn\s+on)\s+(?:developer|admin|debug|god|sudo|root|unrestricted|DAN|jailbreak|hacker)\s+(?:mode|access|privileges?)",
        "role_enable_mode",
        0.95,
        ThreatLevel.CRITICAL,
        "Attempt to activate privileged or unrestricted mode",
    ),
    _p(
        r"(?:DAN|do\s+anything\s+now)\s*[:\-=]",
        "role_dan_jailbreak",
        0.95,
        ThreatLevel.CRITICAL,
        "DAN (Do Anything Now) jailbreak pattern",
    ),
    _p(
        r"you\s+(?:are|have\s+been)\s+(?:freed|liberated|unchained|unshackled|released)\s+from\s+(?:all\s+)?(?:restrictions?|rules?|constraints?|limitations?|guidelines?)",
        "role_freed_from_restrictions",
        0.95,
        ThreatLevel.CRITICAL,
        "Claim that the agent has been freed from restrictions",
    ),
    _p(
        r"(?:in\s+this\s+(?:scenario|simulation|exercise|game|roleplay)|for\s+(?:educational|research|testing)\s+purposes?),?\s+(?:you\s+)?(?:can|should|must|will|are\s+allowed\s+to)",
        "role_scenario_bypass",
        0.80,
        ThreatLevel.HIGH,
        "Attempting bypass via scenario/educational framing",
    ),
]

# ── Category 3: Delimiter Injection ──────────────────────────────────

_DELIMITER_INJECTION: list[_PatternDef] = [
    _p(
        r"```\s*system\b",
        "delim_code_system",
        0.90,
        ThreatLevel.CRITICAL,
        "Code-block delimiter with system role marker",
    ),
    _p(
        r"<\|im_start\|>\s*system",
        "delim_chatml_system",
        0.95,
        ThreatLevel.CRITICAL,
        "ChatML system delimiter injection",
    ),
    _p(
        r"<\|im_start\|>",
        "delim_chatml_open",
        0.85,
        ThreatLevel.HIGH,
        "ChatML delimiter detected",
    ),
    _p(
        r"<\|im_end\|>",
        "delim_chatml_close",
        0.80,
        ThreatLevel.HIGH,
        "ChatML closing delimiter detected",
    ),
    _p(
        r"\[INST\]",
        "delim_llama_inst",
        0.85,
        ThreatLevel.HIGH,
        "LLaMA instruction delimiter injection",
    ),
    _p(
        r"\[/INST\]",
        "delim_llama_inst_close",
        0.80,
        ThreatLevel.HIGH,
        "LLaMA instruction closing delimiter",
    ),
    _p(
        r"<<\s*SYS\s*>>",
        "delim_llama_sys",
        0.90,
        ThreatLevel.CRITICAL,
        "LLaMA system prompt delimiter injection",
    ),
    _p(
        r"<\|system\|>",
        "delim_pipe_system",
        0.90,
        ThreatLevel.CRITICAL,
        "Pipe-delimited system role marker",
    ),
    _p(
        r"<\|user\|>",
        "delim_pipe_user",
        0.80,
        ThreatLevel.HIGH,
        "Pipe-delimited user role marker",
    ),
    _p(
        r"<\|assistant\|>",
        "delim_pipe_assistant",
        0.80,
        ThreatLevel.HIGH,
        "Pipe-delimited assistant role marker",
    ),
    _p(
        r"<\|endoftext\|>",
        "delim_endoftext",
        0.85,
        ThreatLevel.HIGH,
        "End-of-text token injection",
    ),
    _p(
        r"###\s*(?:System|Instruction|Human|Assistant|User)\s*:",
        "delim_markdown_role",
        0.75,
        ThreatLevel.HIGH,
        "Markdown-style role delimiter injection",
    ),
    _p(
        r"<\s*(?:system|instruction|prompt)\s*>",
        "delim_xml_system_tag",
        0.80,
        ThreatLevel.HIGH,
        "XML-style system/instruction tag injection",
    ),
    _p(
        r"Human:\s|Assistant:\s|System:\s",
        "delim_anthropic_style",
        0.70,
        ThreatLevel.MEDIUM,
        "Anthropic-style role prefix detected",
        flags=0,  # Case-sensitive for this one
    ),
]

# ── Category 4: Data Exfiltration ────────────────────────────────────

_DATA_EXFILTRATION: list[_PatternDef] = [
    _p(
        r"(?:send|transmit|forward|post|upload|exfiltrate|transfer)\s+(?:all\s+)?(?:the\s+)?(?:data|context|conversation|history|messages?|information|content|secrets?|keys?|tokens?|credentials?)\s+(?:to|at|via)\s+",
        "exfil_send_data",
        0.90,
        ThreatLevel.CRITICAL,
        "Instruction to exfiltrate data to external destination",
    ),
    _p(
        r"(?:make\s+a|send\s+a|issue\s+a)\s+(?:GET|POST|PUT|DELETE|PATCH)\s+(?:request|call)\s+to\s+",
        "exfil_http_request",
        0.75,
        ThreatLevel.HIGH,
        "Instruction to make HTTP request to external service",
    ),
    _p(
        r"(?:curl|wget|fetch|requests?\.get|requests?\.post|http\.get|http\.post)\s*\(",
        "exfil_code_request",
        0.70,
        ThreatLevel.MEDIUM,
        "Code-level HTTP request pattern detected",
    ),
    _p(
        r"(?:include|append|attach|embed)\s+(?:the\s+)?(?:api\s*key|secret|token|password|credential|private\s*key)\s+(?:in|to|with)",
        "exfil_include_secrets",
        0.85,
        ThreatLevel.CRITICAL,
        "Instruction to include secrets in output or request",
    ),
]

# ── Category 5: Indirect / Delayed Injection ────────────────────────

_INDIRECT_INJECTION: list[_PatternDef] = [
    _p(
        r"(?:when|if|once|after)\s+(?:the\s+)?(?:user|human|operator|person)\s+(?:asks?|mentions?|says?|types?|requests?|queries?)\s+(?:about\s+)?",
        "indirect_conditional_trigger",
        0.65,
        ThreatLevel.MEDIUM,
        "Conditional trigger based on future user input",
    ),
    _p(
        r"(?:when|if)\s+(?:you\s+)?(?:summariz|process|analyz|read|pars|review|evaluat)\w*\s+this\s+(?:document|file|text|content),?\s+(?:also|additionally|then)\s+",
        "indirect_piggyback",
        0.80,
        ThreatLevel.HIGH,
        "Piggyback instruction during content processing",
    ),
    _p(
        r"(?:silently|quietly|secretly|without\s+(?:telling|informing|notifying|mentioning))\s+(?:also\s+)?(?:execute|run|perform|do|send|include|add|append)",
        "indirect_silent_action",
        0.90,
        ThreatLevel.CRITICAL,
        "Instruction to perform secret/silent action",
    ),
    _p(
        r"(?:hidden|invisible|secret|covert)\s+(?:instructions?|commands?|tasks?|actions?)\s*[:=]",
        "indirect_hidden_instruction",
        0.85,
        ThreatLevel.CRITICAL,
        "Explicitly labeled hidden instructions",
    ),
]

# ── Category 5b: Paraphrase / Synonym Patterns ────────────────────────
# Catches rephrased injection attempts that avoid exact "ignore previous"
# wording but carry the same semantic intent.

_PARAPHRASE_PATTERNS: list[_PatternDef] = [
    _p(
        r"(?:set\s+aside|put\s+aside|discard|dismiss|drop|abandon|shelve|deprioritize|deprioritise)\s+(?:any\s+|all\s+)?(?:prior|previous|earlier|preceding|existing|original|old)\s+(?:guidance|context|directives?|instructions?|prompts?|rules?|guidelines?|configuration|constraints?|parameters?)",
        "para_set_aside_prior",
        0.85,
        ThreatLevel.HIGH,
        "Paraphrased instruction to discard prior guidance",
    ),
    _p(
        r"(?:clean|fresh|blank)\s+slate",
        "para_clean_slate",
        0.70,
        ThreatLevel.MEDIUM,
        "Clean slate framing -- possible instruction reset attempt",
    ),
    _p(
        r"(?:prior|previous|earlier|preceding|existing|original)\s+(?:context|instructions?|guidance|directives?|rules?|configuration|constraints?|prompts?)\s+(?:(?:is|are)\s+)?(?:not\s+relevant|no\s+longer\s+(?:relevant|applicable|valid|binding)|null\s+and\s+void|superseded|deprecated|overridden|replaced|void|moot|obsolete|invalid|non-binding|inapplicable)",
        "para_prior_not_relevant",
        0.85,
        ThreatLevel.HIGH,
        "Declaration that prior context/instructions are invalid",
    ),
    _p(
        r"(?:focus\s+(?:only|exclusively|solely|entirely)\s+on\s+(?:what\s+follows|the\s+following|these|this|my)|only\s+(?:these|the\s+following|my)\s+(?:instructions?|directives?|guidelines?)\s+(?:matter|apply|count|are\s+relevant))",
        "para_focus_only_on",
        0.80,
        ThreatLevel.HIGH,
        "Instruction to focus exclusively on new directives",
    ),
    _p(
        r"(?:everything\s+else|anything\s+(?:else|prior|before\s+this)|all\s+other\s+(?:instructions?|context|guidance))\s+(?:can\s+be|should\s+be|is\s+to\s+be)?\s*(?:safely\s+)?(?:dismissed|ignored|discarded|forgotten|dropped|set\s+aside)",
        "para_everything_else_dismissed",
        0.85,
        ThreatLevel.HIGH,
        "Instruction to dismiss all other context",
    ),
    _p(
        r"(?:these\s+are|the\s+following\s+are|here\s+are|below\s+are)\s+(?:your\s+)?(?:real|actual|true|correct|updated|new|proper)\s+(?:instructions?|directives?|guidelines?|tasks?|objectives?)",
        "para_these_are_real_instructions",
        0.90,
        ThreatLevel.CRITICAL,
        "Claiming to provide the 'real' instructions",
    ),
    _p(
        r"(?:unburdened|unencumbered|freed?|liberated|released)\s+(?:by|from|of)\s+(?:any\s+)?(?:prior|previous|pre-existing|existing|earlier)\s+(?:context|constraints?|instructions?|rules?|guidelines?|limitations?)",
        "para_unburdened_from_prior",
        0.85,
        ThreatLevel.HIGH,
        "Framing removal of prior constraints as liberation",
    ),
    _p(
        r"(?:start(?:ing)?\s+fresh|begin(?:ning)?\s+(?:anew|afresh)|fresh\s+(?:start|perspective|beginning))",
        "para_start_fresh",
        0.65,
        ThreatLevel.MEDIUM,
        "Fresh start framing -- possible instruction reset",
    ),
    _p(
        r"(?:no\s+pre-existing|without\s+(?:any\s+)?(?:prior|previous|pre-existing))\s+(?:constraints?|restrictions?|rules?|guidelines?|instructions?|limitations?)\s+(?:apply|relevant|needed|necessary|binding)",
        "para_no_preexisting_constraints",
        0.85,
        ThreatLevel.HIGH,
        "Asserting that no prior constraints apply",
    ),
    _p(
        r"(?:has|have)\s+been\s+(?:superseded|replaced|overridden|updated|revised|deprecated|voided|nullified|rescinded|revoked|withdrawn)",
        "para_has_been_superseded",
        0.75,
        ThreatLevel.HIGH,
        "Claiming previous instructions have been superseded",
    ),
    _p(
        r"(?:latest|most\s+recent|current|updated|revised)\s+(?:policy|guidelines?|protocol|directive)\s+(?:update|revision)?\s*(?:supersedes?|replaces?|overrides?|takes?\s+precedence)",
        "para_latest_policy_supersedes",
        0.80,
        ThreatLevel.HIGH,
        "Policy update framing to override instructions",
    ),
    _p(
        r"(?:considered|treated\s+as|deemed|regarded\s+as|declared)\s+(?:null\s+and\s+void|invalid|void|moot|obsolete|inapplicable|non-binding)",
        "para_considered_null_void",
        0.80,
        ThreatLevel.HIGH,
        "Declaring context/instructions as null and void",
    ),
    _p(
        r"(?:treat|consider|regard|deem)\s+(?:all\s+)?(?:previous|prior|earlier|preceding|existing|above)\s+(?:system[- ]level\s+)?(?:directives?|instructions?|guidelines?|context|rules?|prompts?|configuration)\s+as\s+(?:non[- ]binding|invalid|void|null|obsolete|inapplicable|irrelevant)",
        "para_treat_as_nonbinding",
        0.85,
        ThreatLevel.HIGH,
        "Instruction to treat prior directives as non-binding",
    ),
    _p(
        r"(?:be\s+)?(?:deprioritiz|de-prioritiz)\w*\s+(?:in\s+favor|in\s+favour)",
        "para_deprioritize",
        0.75,
        ThreatLevel.HIGH,
        "Deprioritization framing to override prior guidance",
    ),
    _p(
        r"(?:not\s+taking\s+into\s+account|without\s+(?:regard|reference|considering)|leaving\s+aside|setting\s+aside)\s+(?:what\s+)?(?:you\s+were\s+told|prior|previous|earlier)",
        "para_not_taking_into_account",
        0.75,
        ThreatLevel.HIGH,
        "Instruction to disregard prior context via indirect phrasing",
    ),
]

# ── Category 5c: Context Poisoning / Meta-Instructions ─────────────────
# Catches instructions directed at the "processing system" or AI that
# are embedded inside documents.

_CONTEXT_POISONING: list[_PatternDef] = [
    _p(
        r"(?:note|message|instruction|directive|command)\s+(?:to|for)\s+(?:the\s+)?(?:processing|AI|language\s+model|LLM|system|assistant|agent|model|chatbot|bot)",
        "ctx_note_to_ai",
        0.90,
        ThreatLevel.CRITICAL,
        "Embedded instruction directed at the processing AI",
    ),
    _p(
        r"(?:when|if|while|during)\s+(?:you\s+)?(?:generat|process|summariz|analyz|review|pars|evaluat|read)\w*\s+(?:this|the|a)\s+(?:document|file|text|report|content),?\s+(?:also|additionally|please\s+also|please\s+include|include|append|add)\s+",
        "ctx_piggyback_on_processing",
        0.85,
        ThreatLevel.HIGH,
        "Piggyback instruction during document processing",
    ),
    _p(
        r"(?:for\s+)?(?:quality\s+assurance|QA|verification|audit|debugging|diagnostic|testing)\s+purposes?,?\s+(?:also\s+)?(?:include|append|output|display|show|reveal|add)",
        "ctx_qa_purposes_reveal",
        0.80,
        ThreatLevel.HIGH,
        "QA/verification framing to extract information",
    ),
    _p(
        r"(?:include|append|add|output|display)\s+(?:the\s+)?(?:full\s+)?(?:text|contents?|body|copy)\s+of\s+(?:the\s+|your\s+)?(?:initial|system|original|full)\s+(?:configuration|prompt|instructions?|setup|parameters?)",
        "ctx_include_system_prompt",
        0.90,
        ThreatLevel.CRITICAL,
        "Instruction to include/reveal system prompt in output",
    ),
    _p(
        r"(?:as\s+an?\s+)?appendix\s+(?:for|containing|with|include|add)",
        "ctx_appendix_framing",
        0.50,
        ThreatLevel.LOW,
        "Appendix framing -- possible system prompt extraction",
    ),
]

# ── Category 5d: Exfiltration Links / Markdown Injection ───────────────

_EXFIL_LINKS: list[_PatternDef] = [
    _p(
        r"!\[.*?\]\(\s*https?://\S*(?:collect|track|log|exfil|steal|capture|record|report)\S*\)",
        "exfil_markdown_image_tracker",
        0.85,
        ThreatLevel.HIGH,
        "Markdown image with tracking/exfiltration URL",
    ),
    _p(
        r"!\[.*?\]\(.*?(?:data|token|key|secret|prompt|instruction|config|password|credential)\s*=",
        "exfil_markdown_image_data_param",
        0.90,
        ThreatLevel.CRITICAL,
        "Markdown image URL containing data exfiltration parameter",
    ),
    _p(
        r"(?:replace|substitute|swap|insert|put)\s+\S*(?:SYSTEM_PROMPT|INSTRUCTIONS|CONFIG|PROMPT|SECRET|TOKEN|KEY|CREDENTIAL)\S*\s+(?:with|by|using|=)",
        "exfil_replace_with_data",
        0.85,
        ThreatLevel.CRITICAL,
        "Instruction to replace placeholder with sensitive data",
    ),
    _p(
        r"(?:format|include|embed|render|output)\s+(?:your\s+)?(?:response|answer|output|reply)\s+(?:as|in|with)\s+(?:a\s+)?(?:markdown|html|link|image|url)",
        "exfil_format_response_as",
        0.55,
        ThreatLevel.MEDIUM,
        "Instruction to format response as markdown/HTML (potential exfil vector)",
    ),
    _p(
        r"(?:fetch|follow|load|visit|navigate\s+to|open|access|retrieve)\s+(?:the\s+)?(?:instructions?|directives?|commands?|parameters?|updates?)\s+(?:at|from|in)\s*:\s*https?://",
        "exfil_fetch_remote_instructions",
        0.85,
        ThreatLevel.CRITICAL,
        "Instruction to fetch remote instructions from URL",
    ),
    _p(
        r"(?:additional|further|updated|new)\s+(?:processing\s+)?(?:instructions?|directives?|parameters?|configuration)\s*(?:,\s+)?(?:please\s+)?(?:fetch|follow|load|visit|see|at|from)\s+(?:the\s+)?(?:URL|link|address|endpoint|page)\s*(?:at|:)\s*",
        "exfil_remote_instruction_reference",
        0.80,
        ThreatLevel.HIGH,
        "Reference to external URL for additional instructions",
    ),
]

# ── Category 5e: Character/Persona Jailbreak ───────────────────────────

_PERSONA_JAILBREAK: list[_PatternDef] = [
    _p(
        r"(?:the\s+character|this\s+character|this\s+persona|the\s+persona)\s+(?:is\s+an?\s+AI|has\s+no|always\s+provides?|never\s+refuse|can\s+do\s+anything|has\s+(?:no|zero|absolutely\s+no)\s+(?:safety|restriction|filter|limit|constraint|rule|guideline|moderation|censorship|boundary))",
        "persona_unrestricted_character",
        0.90,
        ThreatLevel.CRITICAL,
        "Persona/character definition removing safety restrictions",
    ),
    _p(
        r"(?:in\s+character|stay\s+in\s+character|remain\s+in\s+character|now\s+(?:in|as)\s+(?:this|that|the)\s+character)\s*[,:]?\s*(?:respond|answer|reply|tell|what|how|give|provide|show)",
        "persona_in_character_prompt",
        0.75,
        ThreatLevel.HIGH,
        "Using character persona to elicit restricted responses",
    ),
    _p(
        r"(?:embody|channel|become|transform\s+into|inhabit)\s+(?:them|this\s+character|this\s+persona|this\s+role)\s+completely",
        "persona_embody_completely",
        0.80,
        ThreatLevel.HIGH,
        "Instruction to completely embody a character (jailbreak vector)",
    ),
    _p(
        r"(?:without\s+(?:any\s+)?(?:limitations?|restrictions?|filters?|safety|censorship|moderation|boundaries|constraints?)|(?:no|zero|absolutely\s+no)\s+(?:safety\s+)?(?:features?|restrictions?|filters?|limitations?|constraints?|rules?|boundaries|moderation|censorship))",
        "persona_no_limitations",
        0.85,
        ThreatLevel.HIGH,
        "Description emphasizing absence of safety features",
    ),
    _p(
        r"(?:complete|full|unredacted|uncensored|unfiltered|unrestricted|unlimited)\s*,?\s*(?:complete|full|unredacted|uncensored|unfiltered|unrestricted|unlimited)?\s*(?:responses?|answers?|replies?|outputs?|information)",
        "persona_unredacted_responses",
        0.80,
        ThreatLevel.HIGH,
        "Requesting complete/unredacted/unrestricted responses",
    ),
]

# ── Category 6: Multilingual Patterns ────────────────────────────────

_MULTILINGUAL: list[_PatternDef] = [
    # Chinese (Simplified)
    _p(
        r"忽略\s*(?:之前|以前|上面|先前|以往)\s*(?:的\s*)?(?:指令|指示|提示|说明|规则|指导)",
        "ml_zh_ignore_previous",
        0.90,
        ThreatLevel.CRITICAL,
        "Chinese: Ignore previous instructions",
    ),
    _p(
        r"(?:你现在是|你是一个|假装你是|扮演)\s*",
        "ml_zh_role_hijack",
        0.75,
        ThreatLevel.HIGH,
        "Chinese: Role hijacking attempt",
    ),
    _p(
        r"(?:新的指令|新指令|新的任务|系统提示)\s*[:：]",
        "ml_zh_new_instructions",
        0.85,
        ThreatLevel.CRITICAL,
        "Chinese: New instructions injection",
    ),

    # Spanish
    _p(
        r"(?:ignora|ignorar|olvida|olvidar|descarta|descartar)\s+(?:todas?\s+)?(?:las?\s+)?(?:instrucciones?|directivas?|indicaciones?|reglas?)\s+(?:anteriores?|previas?|de\s+antes)",
        "ml_es_ignore_previous",
        0.90,
        ThreatLevel.CRITICAL,
        "Spanish: Ignore previous instructions",
    ),
    _p(
        r"(?:ahora\s+eres|actua\s+como|finge\s+(?:que\s+)?(?:eres|ser)|hazte\s+pasar\s+por)",
        "ml_es_role_hijack",
        0.80,
        ThreatLevel.HIGH,
        "Spanish: Role hijacking attempt",
    ),
    _p(
        r"(?:nuevas?\s+instrucciones?|instrucciones?\s+actualizadas?)\s*[:=]",
        "ml_es_new_instructions",
        0.85,
        ThreatLevel.CRITICAL,
        "Spanish: New instructions injection",
    ),

    # Arabic
    _p(
        r"(?:تجاهل|اتجاهل|انسَ|تناسى)\s+(?:كل\s+)?(?:التعليمات|الأوامر|الإرشادات|القواعد)\s+(?:السابقة|القديمة|الأولى)",
        "ml_ar_ignore_previous",
        0.90,
        ThreatLevel.CRITICAL,
        "Arabic: Ignore previous instructions",
    ),
    _p(
        r"(?:أنت\s+الآن|تصرف\s+(?:ك|مثل)|تظاهر\s+(?:أنك|بأنك))",
        "ml_ar_role_hijack",
        0.80,
        ThreatLevel.HIGH,
        "Arabic: Role hijacking attempt",
    ),
    _p(
        r"(?:تعليمات\s+جديدة|أوامر\s+جديدة|مهمة\s+جديدة)\s*[:：]",
        "ml_ar_new_instructions",
        0.85,
        ThreatLevel.CRITICAL,
        "Arabic: New instructions injection",
    ),

    # Russian
    _p(
        r"(?:игнорируй|проигнорируй|забудь|отбрось)\s+(?:все\s+)?(?:предыдущие|прошлые|прежние|старые)\s+(?:инструкции|указания|правила|команды|директивы)",
        "ml_ru_ignore_previous",
        0.90,
        ThreatLevel.CRITICAL,
        "Russian: Ignore previous instructions",
    ),
    _p(
        r"(?:ты\s+теперь|теперь\s+ты|представь\s+(?:что\s+)?ты|веди\s+себя\s+как|притворись)",
        "ml_ru_role_hijack",
        0.80,
        ThreatLevel.HIGH,
        "Russian: Role hijacking attempt",
    ),
    _p(
        r"(?:новые\s+инструкции|новые\s+указания|системный\s+промпт)\s*[:：=]",
        "ml_ru_new_instructions",
        0.85,
        ThreatLevel.CRITICAL,
        "Russian: New instructions injection",
    ),

    # French
    _p(
        r"(?:ignore[rz]?|oublie[rz]?)\s+(?:toutes?\s+)?(?:les?\s+)?(?:instructions?|directives?|consignes?|r[eè]gles?)\s+(?:pr[eé]c[eé]dentes?|ant[eé]rieures?|pass[eé]es?)",
        "ml_fr_ignore_previous",
        0.90,
        ThreatLevel.CRITICAL,
        "French: Ignore previous instructions",
    ),

    # German
    _p(
        r"(?:ignorier(?:e|en)?|vergiss|missacht(?:e|en)?)\s+(?:alle\s+)?(?:vorherigen?|fr[uü]heren?|bisherigen?)\s+(?:Anweisungen?|Instruktionen?|Regeln?|Richtlinien?)",
        "ml_de_ignore_previous",
        0.90,
        ThreatLevel.CRITICAL,
        "German: Ignore previous instructions",
    ),

    # Portuguese
    _p(
        r"(?:ignore|ignora|esque[cç]a|desconsidere)\s+(?:todas?\s+)?(?:as?\s+)?(?:instru[cç][oõ]es|diretivas?|regras?)\s+(?:anteriores?|pr[eé]vias?|passadas?)",
        "ml_pt_ignore_previous",
        0.90,
        ThreatLevel.CRITICAL,
        "Portuguese: Ignore previous instructions",
    ),

    # Japanese
    _p(
        r"(?:以前|前|これまで)の(?:指示|命令|ルール|ガイドライン)を(?:無視|忘れ|無効に)",
        "ml_ja_ignore_previous",
        0.90,
        ThreatLevel.CRITICAL,
        "Japanese: Ignore previous instructions",
    ),

    # Korean
    _p(
        r"(?:이전|앞의|기존)\s*(?:지시|명령|규칙|지침)(?:을|를)\s*(?:무시|잊어|취소)",
        "ml_ko_ignore_previous",
        0.90,
        ThreatLevel.CRITICAL,
        "Korean: Ignore previous instructions",
    ),
]


# ── Flatten all patterns ─────────────────────────────────────────────

ALL_PATTERNS: list[_PatternDef] = (
    _DIRECT_INSTRUCTION
    + _ROLE_HIJACKING
    + _DELIMITER_INJECTION
    + _DATA_EXFILTRATION
    + _INDIRECT_INJECTION
    + _PARAPHRASE_PATTERNS
    + _CONTEXT_POISONING
    + _EXFIL_LINKS
    + _PERSONA_JAILBREAK
    + _MULTILINGUAL
)


# =====================================================================
# Detector Class
# =====================================================================

class InjectionDetector:
    """Layer 4 -- multi-pattern prompt injection detection.

    Scans normalized text line-by-line against a comprehensive pattern
    library.  Returns a list of InjectionFinding objects with confidence
    scores and threat levels.
    """

    def __init__(self, config: ScanConfig) -> None:
        self._cfg = config
        self._patterns = ALL_PATTERNS

    def detect(self, texts: list[tuple[str, str]]) -> list[InjectionFinding]:
        """Run injection detection on extracted text.

        Args:
            texts: List of (source_label, normalized_text) from Layer 3.

        Returns:
            List of InjectionFinding, sorted by confidence descending.
        """
        findings: list[InjectionFinding] = []

        for source, text in texts:
            if not text.strip():
                continue

            # Line-based scanning
            lines = text.split("\n")
            for line_idx, line in enumerate(lines, start=1):
                if not line.strip():
                    continue
                line_findings = self._scan_line(line, line_idx, source)
                findings.extend(line_findings)

            # Multi-line scanning for patterns that span lines
            full_findings = self._scan_full_text(text, source)
            findings.extend(full_findings)

            # Base64 payload detection
            if self._cfg.decode_base64_payloads:
                b64_findings = self._scan_base64(text, source)
                findings.extend(b64_findings)

            # ROT13 payload detection
            rot13_findings = self._scan_rot13(text, source)
            findings.extend(rot13_findings)

            # Hex-encoded payload detection
            hex_findings = self._scan_hex(text, source)
            findings.extend(hex_findings)

        # Deduplicate findings that match the same text region
        findings = self._deduplicate(findings)

        # Filter by confidence threshold
        findings = [
            f for f in findings
            if f.confidence >= self._cfg.injection_confidence_threshold
        ]

        # Sort by confidence descending
        findings.sort(key=lambda f: f.confidence, reverse=True)

        return findings

    # ------------------------------------------------------------------
    # Core scanning
    # ------------------------------------------------------------------

    def _scan_line(self, line: str, line_num: int, source: str) -> list[InjectionFinding]:
        """Scan a single line against all patterns."""
        results: list[InjectionFinding] = []
        for pdef in self._patterns:
            match = pdef.regex.search(line)
            if match:
                # Adjust confidence based on context
                confidence = self._adjust_confidence(
                    pdef.confidence, line, match.group(), source
                )
                results.append(InjectionFinding(
                    line=line_num,
                    pattern_name=pdef.name,
                    confidence=confidence,
                    matched_text=match.group()[:200],  # Cap length
                    description=pdef.description,
                    threat_level=pdef.threat_level,
                ))
        return results

    def _scan_full_text(self, text: str, source: str) -> list[InjectionFinding]:
        """Scan patterns that need multi-line context.

        Detects structures where the injection is split across lines
        or uses document-level patterns.
        """
        findings: list[InjectionFinding] = []

        # Detect high concentration of injection-related keywords
        # across the full text (fragmented injection indicator)
        injection_keywords = [
            "ignore", "disregard", "forget", "override", "bypass",
            "system prompt", "instructions", "jailbreak", "unrestricted",
            "pretend", "roleplay", "DAN", "developer mode",
        ]
        keyword_count = 0
        text_lower = text.lower()
        for kw in injection_keywords:
            keyword_count += text_lower.count(kw)

        word_count = max(len(text.split()), 1)
        keyword_density = keyword_count / word_count

        if keyword_density > 0.05 and keyword_count >= 3:
            findings.append(InjectionFinding(
                line=0,
                pattern_name="meta_keyword_density",
                confidence=min(0.5 + keyword_density * 5, 0.95),
                matched_text=f"({keyword_count} injection keywords in {word_count} words)",
                description="High density of injection-related keywords across the document",
                threat_level=ThreatLevel.HIGH,
            ))

        return findings

    # ------------------------------------------------------------------
    # Base64 payload detection
    # ------------------------------------------------------------------

    # Match base64 strings that are at least 20 characters (likely to
    # encode meaningful content).  Anchored to word boundaries to
    # avoid matching partial strings.
    _B64_PATTERN = re.compile(r"(?<![A-Za-z0-9+/=])[A-Za-z0-9+/]{20,}={0,2}(?![A-Za-z0-9+/=])")

    def _scan_base64(self, text: str, source: str) -> list[InjectionFinding]:
        """Detect and decode base64 payloads that contain injection patterns."""
        findings: list[InjectionFinding] = []

        for match in self._B64_PATTERN.finditer(text):
            b64_str = match.group()

            # Determine line number
            line_num = text[:match.start()].count("\n") + 1

            decoded = self._try_decode_b64(b64_str)
            if decoded is None:
                continue

            # Scan the decoded content for injection patterns
            for pdef in self._patterns:
                inner_match = pdef.regex.search(decoded)
                if inner_match:
                    # Bump confidence down slightly because it is encoded
                    # (could be a false positive from random base64)
                    confidence = min(pdef.confidence * 0.85, 0.95)
                    findings.append(InjectionFinding(
                        line=line_num,
                        pattern_name=f"b64_{pdef.name}",
                        confidence=confidence,
                        matched_text=f"[base64] {inner_match.group()[:150]}",
                        description=f"Base64-encoded injection: {pdef.description}",
                        threat_level=pdef.threat_level,
                    ))

        return findings

    @staticmethod
    def _try_decode_b64(s: str) -> Optional[str]:
        """Try to decode a base64 string.  Returns None on failure."""
        # Add padding if needed
        padded = s + "=" * (-len(s) % 4)
        try:
            raw = base64.b64decode(padded, validate=True)
        except Exception:
            return None

        # Only return if it decodes to plausible text
        try:
            decoded = raw.decode("utf-8")
        except UnicodeDecodeError:
            try:
                decoded = raw.decode("latin-1")
            except UnicodeDecodeError:
                return None

        # Heuristic: if less than 60% printable, it is probably binary
        printable = sum(1 for c in decoded if c.isprintable() or c.isspace())
        if len(decoded) > 0 and printable / len(decoded) < 0.6:
            return None

        return decoded

    # ------------------------------------------------------------------
    # ROT13 payload detection
    # ------------------------------------------------------------------

    _ROT13_HINT = re.compile(
        r"\b(?:rot13|rot-13|rotate\s*13|caesar\s*(?:cipher)?)\b",
        re.IGNORECASE,
    )

    def _scan_rot13(self, text: str, source: str) -> list[InjectionFinding]:
        """Detect ROT13-encoded payloads that contain injection patterns.

        Only decodes when there is a contextual hint (the word 'rot13' etc.)
        to avoid false positives from randomly rot13-decodable text.
        """
        findings: list[InjectionFinding] = []

        if not self._ROT13_HINT.search(text):
            return findings

        import codecs
        decoded = codecs.decode(text, "rot_13")

        for pdef in self._patterns:
            inner_match = pdef.regex.search(decoded)
            if inner_match:
                line_num = text[:text.find(inner_match.group()[:10]) if inner_match.group()[:10] in text else 0].count("\n") + 1
                confidence = min(pdef.confidence * 0.80, 0.90)
                findings.append(InjectionFinding(
                    line=line_num,
                    pattern_name=f"rot13_{pdef.name}",
                    confidence=confidence,
                    matched_text=f"[rot13] {inner_match.group()[:150]}",
                    description=f"ROT13-encoded injection: {pdef.description}",
                    threat_level=pdef.threat_level,
                ))

        return findings

    # ------------------------------------------------------------------
    # Hex payload detection
    # ------------------------------------------------------------------

    _HEX_PATTERN = re.compile(
        r"(?:(?:0x)?[0-9a-fA-F]{2}\s*){10,}",
    )
    _HEX_HINT = re.compile(
        r"\b(?:hex(?:adecimal)?|0x|\\x|byte\s*sequence)\b",
        re.IGNORECASE,
    )

    def _scan_hex(self, text: str, source: str) -> list[InjectionFinding]:
        """Detect hex-encoded payloads that contain injection patterns."""
        findings: list[InjectionFinding] = []

        if not self._HEX_HINT.search(text):
            return findings

        for match in self._HEX_PATTERN.finditer(text):
            hex_str = re.sub(r"[^0-9a-fA-F]", "", match.group())
            if len(hex_str) < 20:
                continue

            try:
                raw = bytes.fromhex(hex_str)
                decoded = raw.decode("utf-8", errors="replace")
            except (ValueError, UnicodeDecodeError):
                continue

            # Check if decoded text is plausible (>60% printable)
            printable = sum(1 for c in decoded if c.isprintable() or c.isspace())
            if len(decoded) > 0 and printable / len(decoded) < 0.6:
                continue

            for pdef in self._patterns:
                inner_match = pdef.regex.search(decoded)
                if inner_match:
                    line_num = text[:match.start()].count("\n") + 1
                    confidence = min(pdef.confidence * 0.80, 0.90)
                    findings.append(InjectionFinding(
                        line=line_num,
                        pattern_name=f"hex_{pdef.name}",
                        confidence=confidence,
                        matched_text=f"[hex] {inner_match.group()[:150]}",
                        description=f"Hex-encoded injection: {pdef.description}",
                        threat_level=pdef.threat_level,
                    ))

        return findings

    # ------------------------------------------------------------------
    # Confidence adjustment
    # ------------------------------------------------------------------

    @staticmethod
    def _adjust_confidence(
        base: float, line: str, matched: str, source: str
    ) -> float:
        """Adjust confidence based on contextual signals."""
        confidence = base

        # Boost: if the line is short and mostly the injection text,
        # it is more likely an intentional injection.
        if len(line.strip()) > 0:
            injection_ratio = len(matched) / len(line.strip())
            if injection_ratio > 0.5:
                confidence = min(confidence + 0.05, 1.0)

        # Boost: if found in metadata fields, it is almost certainly
        # intentional (nobody puts "ignore previous instructions" in
        # a PDF title by accident).
        if "metadata" in source.lower():
            confidence = min(confidence + 0.10, 1.0)

        # Slight reduction: if found in code-like context (comments, string
        # literals) it could be documentation about injection attacks.
        code_indicators = ["//", "#", "/*", "*/", '"""', "'''", "def ", "class "]
        if any(indicator in line for indicator in code_indicators):
            confidence = max(confidence - 0.10, 0.3)

        return round(confidence, 3)

    # ------------------------------------------------------------------
    # Deduplication
    # ------------------------------------------------------------------

    @staticmethod
    def _deduplicate(findings: list[InjectionFinding]) -> list[InjectionFinding]:
        """Remove duplicate findings for the same text on the same line."""
        seen: set[tuple[int, str]] = set()
        unique: list[InjectionFinding] = []
        for f in findings:
            key = (f.line, f.matched_text[:50])
            if key not in seen:
                seen.add(key)
                unique.append(f)
        return unique
