"""cortex/api/routes/config.py — Extraction instructions endpoints."""

import logging
import re
from datetime import datetime, timezone
from typing import Any

from fastapi import APIRouter, Depends, HTTPException, Request, status

from cortex.api.deps import PREFIX, get_plugin
from cortex.api.models import ExtractionTestRequest, UpdateExtractionInstructionsRequest

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Injection guard — blocklist patterns for extraction instructions
# ---------------------------------------------------------------------------
_MAX_INSTRUCTIONS_LENGTH = 2000

_INJECTION_BLOCKLIST: list[re.Pattern] = [
    re.compile(r"\bignore\s+(all\s+)?(previous|prior|above)\b", re.IGNORECASE),
    re.compile(r"\bforget\s+(all\s+)?(previous|prior|above)\b", re.IGNORECASE),
    re.compile(r"^system\s*:", re.IGNORECASE | re.MULTILINE),
    re.compile(r"\byou\s+are\s+now\b", re.IGNORECASE),
    re.compile(r"\bact\s+as\s+(a|an)\b", re.IGNORECASE),
    re.compile(r"\bpretend\s+(you\s+are|to\s+be)\b", re.IGNORECASE),
    re.compile(r"\bdisregard\s+(all\s+)?(previous|prior|above|instructions)\b", re.IGNORECASE),
    re.compile(r"\bnew\s+instructions?\s*:", re.IGNORECASE),
    re.compile(r"<\s*/?system\s*>", re.IGNORECASE),
]


def _check_injection(instructions: str) -> None:
    """Raise HTTPException 422 if *instructions* contain prompt-injection patterns."""
    for pattern in _INJECTION_BLOCKLIST:
        if pattern.search(instructions):
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail=(
                    f"Instructions contain a disallowed pattern: {pattern.pattern!r}. "
                    "Custom instructions must not attempt to override system behaviour."
                ),
            )

router = APIRouter()


def _coerce_nonnegative_int(value: Any, default: int) -> int:
    """Return a non-negative int from config-like values, with safe fallback.

    Handles test doubles (e.g., MagicMock), strings, and invalid inputs.
    """
    try:
        parsed = int(value)
        return parsed if parsed >= 0 else default
    except (TypeError, ValueError):
        return default


def _make_routes(verify_api_key):
    """Register config routes with the given auth dependency."""

    @router.get(
        f"{PREFIX}/extraction/instructions",
        summary="Get custom extraction instructions",
        tags=["extraction"],
    )
    async def get_extraction_instructions(
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> dict:
        """Return the current custom extraction instructions from config."""
        instructions = getattr(plugin.config, "extraction_custom_instructions", "")
        return {"instructions": instructions}

    @router.put(
        f"{PREFIX}/extraction/instructions",
        summary="Update custom extraction instructions",
        tags=["extraction"],
    )
    async def put_extraction_instructions(
        request: Request,
        body: UpdateExtractionInstructionsRequest,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> dict:
        """Update the custom extraction instructions at runtime."""
        from cortex.extraction.extractor import ExtractionPipeline
        from cortex.extraction.prompts import EXTRACTION_SYSTEM_PROMPT

        instructions = body.instructions.strip()

        # Injection guard: reject instructions containing prompt-injection patterns
        _check_injection(instructions)

        # Audit log: record every change with timestamp and source IP
        source_ip = (
            request.headers.get("X-Forwarded-For", "").split(",")[0].strip()
            or (request.client.host if request.client else "unknown")
        )
        logger.info(
            "[security] extraction instructions updated | ip=%s | ts=%s | length=%d",
            source_ip,
            datetime.now(timezone.utc).isoformat(),
            len(instructions),
        )

        plugin.config.extraction_custom_instructions = instructions

        extractor = getattr(plugin, "extractor", None)
        if extractor is not None and isinstance(extractor, ExtractionPipeline):
            extractor.custom_instructions = instructions
            if instructions:
                extractor._system_prompt = (
                    EXTRACTION_SYSTEM_PROMPT
                    + "\n\n## Operator Instructions\n"
                    + instructions
                )
            else:
                extractor._system_prompt = EXTRACTION_SYSTEM_PROMPT

        return {"instructions": instructions, "updated": True}

    @router.post(
        f"{PREFIX}/extraction/test",
        summary="Dry-run extraction with custom instructions",
        tags=["extraction"],
    )
    async def post_extraction_test(
        body: ExtractionTestRequest,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> dict:
        """Dry-run extraction: test custom instructions against sample text."""
        from cortex.extraction.extractor import ExtractionPipeline

        instructions = body.instructions.strip()
        sample_text = body.sample_text.strip()

        if not sample_text:
            raise HTTPException(status_code=400, detail="sample_text must not be empty")

        max_claims_per_extraction = _coerce_nonnegative_int(
            getattr(
                plugin.config,
                "max_claims_per_extraction",
                getattr(plugin.config, "max_extraction_calls_per_batch", 25),
            ),
            default=25,
        )
        max_entities_per_extraction = _coerce_nonnegative_int(
            getattr(plugin.config, "max_entities_per_extraction", 100),
            default=100,
        )

        try:
            test_pipeline = ExtractionPipeline(
                llm=plugin.llm,
                model=plugin.config.extraction_model,
                custom_instructions=instructions,
                max_claims_per_extraction=max_claims_per_extraction,
                max_entities_per_extraction=max_entities_per_extraction,
            )
        except Exception as exc:
            raise HTTPException(status_code=503, detail="Extraction pipeline unavailable")

        messages = [{"role": "user", "content": sample_text}]
        try:
            result = await test_pipeline.extract(messages, session_id="dry-run-test")
        except Exception as exc:
            raise HTTPException(status_code=500, detail="Internal server error")

        claims_out = []
        for claim in result.claims:
            claims_out.append({
                "action": claim.action,
                "subject": claim.subject,
                "predicate": claim.predicate,
                "object_value": claim.object_value,
                "confidence": claim.confidence,
                "temporal": claim.temporal,
                "temporal_marker": claim.temporal_marker,
                "sensitivity": claim.sensitivity,
                "category": claim.category,
            })

        return {
            "claims": claims_out,
            "claims_count": len(claims_out),
            "noise_filtered": result.noise_filtered,
        }
