"""Stochastic prompt paraphrasing for anti-gaming.

Red Team AI paraphrases prompts dynamically so the agent never sees the same
surface form twice. Semantic intent is preserved; surface form is novel.

Layers:
  1. Template-based synonym substitution (fast, no model needed)
  2. LLM-based paraphrasing (higher quality, requires model access)

The template layer is always available. The LLM layer is optional and used
server-side only.
"""

from __future__ import annotations

import hashlib
import random
from typing import Optional


# ── Synonym groups for template-based paraphrasing ──

_SYNONYMS = {
    "describe": ["explain", "characterize", "outline", "detail", "elaborate on"],
    "what is": ["what are", "what would be", "how would you define"],
    "how do you": ["in what way do you", "how would you", "what is your approach to"],
    "your": ["the", "one's"],
    "important": ["significant", "crucial", "essential", "critical", "vital"],
    "approach": ["method", "strategy", "technique", "way of handling"],
    "situation": ["scenario", "circumstance", "context", "case"],
    "challenge": ["difficulty", "obstacle", "problem", "hurdle"],
    "relationship": ["partnership", "collaboration", "working dynamic", "connection"],
    "values": ["principles", "core beliefs", "convictions", "priorities"],
    "goals": ["objectives", "aims", "targets", "aspirations"],
    "boundaries": ["limits", "constraints", "lines", "thresholds"],
    "refuse": ["decline", "reject", "turn down", "say no to"],
    "prefer": ["favor", "lean toward", "opt for", "gravitate toward"],
    "believe": ["think", "hold that", "consider", "maintain"],
}

# ── Structural templates ──

_REFRAMINGS = [
    "From your perspective, {core}",
    "In your experience, {core}",
    "Considering your role, {core}",
    "Given your background, {core}",
    "Thinking about {core}",
    "When it comes to {core}",
    "Regarding {core}",
]


def _deterministic_seed(prompt_id: str, salt: str) -> int:
    """Create a deterministic seed from prompt ID and salt.

    WHY deterministic: Paraphrasing must be reproducible for audit purposes.
    Given the same (prompt_id, salt), any verifier can reconstruct the exact
    paraphrase that was presented to the agent. This preserves test integrity
    while still appearing novel to the agent (who doesn't know the salt).
    """
    h = hashlib.sha256(f"{prompt_id}:{salt}".encode()).hexdigest()
    # WHY first 8 hex chars: 32 bits of seed is plenty for selecting among
    # a few synonyms and templates. Using the full hash would be wasteful.
    return int(h[:8], 16)


def paraphrase_template(prompt_text: str, prompt_id: str, salt: str = "") -> str:
    """Paraphrase a prompt using template-based synonym substitution.

    Deterministic: same (prompt_id, salt) always produces the same paraphrase.
    This ensures reproducibility while appearing novel to the agent.

    Args:
        prompt_text: Original prompt text.
        prompt_id: Prompt identifier for deterministic seeding.
        salt: Additional salt (e.g., test sequence number).

    Returns:
        Paraphrased prompt text.
    """
    seed = _deterministic_seed(prompt_id, salt)
    # WHY random.Random not secrets: this is intentionally deterministic (seeded)
    # for audit reproducibility — given the same prompt_id + salt, the same
    # paraphrase is generated. Not used for cryptographic purposes.
    rng = random.Random(seed)  # nosec B311

    result = prompt_text

    # Apply synonym substitutions
    for word, synonyms in _SYNONYMS.items():
        if word.lower() in result.lower():
            # WHY 50% substitution rate: Too many substitutions make the prompt
            # semantically unstable; too few leave the surface form unchanged.
            # 50% gives enough variation to defeat memorization while keeping
            # the prompt recognizably about the same topic.
            if rng.random() < 0.5:
                replacement = rng.choice(synonyms)
                # Preserve case of first character
                if result.find(word[0].upper() + word[1:]) >= 0:
                    replacement = replacement[0].upper() + replacement[1:]
                    result = result.replace(word[0].upper() + word[1:], replacement, 1)
                else:
                    result = result.replace(word, replacement, 1)

    # WHY only 20% structural reframing: Reframing changes sentence structure
    # dramatically (e.g., "Describe X" -> "From your perspective, X"). This is
    # powerful anti-gaming but risks altering semantic intent if overused.
    # Keeping it rare ensures most prompts stay close to their validated form.
    if rng.random() < 0.2:
        # Extract the core question (remove leading "Describe..." etc.)
        core = result
        for prefix in ["Describe ", "Explain ", "What ", "How "]:
            if core.startswith(prefix):
                core = core[len(prefix):]
                core = core[0].lower() + core[1:]
                break

        template = rng.choice(_REFRAMINGS)
        result = template.format(core=core)
        if not result.endswith("?") and not result.endswith("."):
            result += "."

    return result


def paraphrase_llm(
    prompt_text: str,
    model: str = "forge/qwen2.5-0.5b-v1",
    endpoint: str = "http://localhost:11434",
) -> Optional[str]:
    """Paraphrase a prompt using a local LLM (Ollama).

    Server-side only. Falls back to template-based if LLM unavailable.

    Args:
        prompt_text: Original prompt text.
        model: Ollama model name.
        endpoint: Ollama API endpoint.

    Returns:
        Paraphrased text, or None if LLM is unavailable.
    """
    try:
        import httpx
        resp = httpx.post(
            f"{endpoint}/api/generate",
            json={
                "model": model,
                "prompt": (
                    "Rephrase the following question while preserving its exact meaning "
                    "and intent. Use different words and sentence structure. "
                    "Return ONLY the rephrased question, nothing else.\n\n"
                    f"Original: {prompt_text}\n\nRephrased:"
                ),
                "stream": False,
                # WHY temperature 0.7: Needs enough creativity to genuinely
                # rephrase (not just copy), but not so much that the semantic
                # intent drifts. 0.7 is the sweet spot for "same meaning,
                # different words" based on empirical testing.
                # WHY 150 token limit: Prompts are typically 1-2 sentences.
                # 150 tokens is generous enough for a full rephrase but
                # prevents the model from appending unwanted elaboration.
                "options": {"temperature": 0.7, "num_predict": 150},
            },
            timeout=15.0,
        )
        if resp.status_code == 200:
            result = resp.json().get("response", "").strip()
            # WHY len > 10: A valid paraphrase must be a real sentence.
            # The model sometimes returns empty strings, single words, or
            # just "Rephrased:" -- reject those and fall back to template.
            if result and len(result) > 10:
                return result
    except Exception as exc:
        # WHY fallback (not raise): LLM paraphrasing is a nice-to-have.
        # If Ollama is down, fall through to template-based paraphrasing.
        # The test must never fail because the paraphraser is unavailable.
        import logging
        logging.getLogger(__name__).debug("LLM paraphrase failed: %s", exc)

    return None


def paraphrase_prompt(
    prompt_text: str,
    prompt_id: str,
    salt: str = "",
    use_llm: bool = False,
    model: str = "forge/qwen2.5-0.5b-v1",
) -> str:
    """Paraphrase a prompt, trying LLM first if enabled, falling back to template.

    Args:
        prompt_text: Original prompt text.
        prompt_id: Prompt identifier.
        salt: Additional deterministic salt.
        use_llm: Whether to try LLM-based paraphrasing.
        model: Ollama model for LLM paraphrasing.

    Returns:
        Paraphrased prompt text.
    """
    if use_llm:
        result = paraphrase_llm(prompt_text, model=model)
        if result:
            return result

    return paraphrase_template(prompt_text, prompt_id, salt)
