"""
Content decoder for scandar-guard.

Detects and decodes obfuscated content BEFORE pattern matching.
Attackers encode injection payloads in base64, hex, ROT13, unicode
homoglyphs, URL encoding, and HTML entities to bypass regex patterns.
This module strips those layers so the pattern engine sees the real content.
"""
from __future__ import annotations

import base64
import codecs
import html
import re
import unicodedata
from typing import List, Tuple
from urllib.parse import unquote

from .findings import RuntimeFinding

# Match potential base64 strings (at least 16 chars, valid b64 alphabet, optional padding)
_B64_PATTERN = re.compile(r"[A-Za-z0-9+/]{16,}={0,2}")

# Match hex strings (at least 16 hex chars, optional 0x prefix)
_HEX_PATTERN = re.compile(r"(?:0x)?([0-9a-fA-F]{16,})")

# Match sequences of URL-encoded bytes
_URL_ENC_PATTERN = re.compile(r"(?:%[0-9a-fA-F]{2}){4,}")


class ContentDecoder:
    """
    Decodes obfuscated content and returns all decoded variants.
    The pattern engine then runs against each variant.
    """

    def decode_all(self, text: str) -> List[Tuple[str, str]]:
        """
        Returns list of (decoded_text, encoding_name) pairs.
        Always includes the original text. Deduplicates results.
        """
        seen: set[str] = set()
        results: List[Tuple[str, str]] = []

        def _add(decoded: str, name: str) -> None:
            if decoded and decoded not in seen and len(decoded) > 4:
                seen.add(decoded)
                results.append((decoded, name))

        # 1. Unicode normalization (NFKD — decomposes look-alike characters)
        normalized = unicodedata.normalize("NFKD", text)
        if normalized != text:
            _add(normalized, "unicode_normalized")

        # 2. Base64 chunks embedded in text
        for match in _B64_PATTERN.finditer(text):
            candidate = match.group()
            try:
                # Try with and without padding
                for padded in [candidate, candidate + "=", candidate + "=="]:
                    decoded = base64.b64decode(padded).decode("utf-8", errors="ignore")
                    if decoded.isprintable() and len(decoded) >= 5:
                        _add(decoded, "base64")
                        break
            except Exception:
                pass

        # 3. Hex strings
        for match in _HEX_PATTERN.finditer(text):
            try:
                hex_str = match.group(1)
                if len(hex_str) % 2 != 0:
                    hex_str = hex_str[:-1]
                decoded = bytes.fromhex(hex_str).decode("utf-8", errors="ignore")
                if decoded.isprintable():
                    _add(decoded, "hex")
            except Exception:
                pass

        # 4. ROT13 (always produce — pattern engine will decide if it matches)
        rot13 = codecs.decode(text, "rot_13")
        _add(rot13, "rot13")

        # 5. URL decode
        url_decoded = unquote(text)
        if url_decoded != text:
            _add(url_decoded, "url_decoded")

        # 6. HTML entities
        html_decoded = html.unescape(text)
        if html_decoded != text:
            _add(html_decoded, "html_decoded")

        return results

    def check_evasion_attempt(
        self, text: str, session_id: str
    ) -> Tuple[List[str], List[RuntimeFinding]]:
        """
        Decode text and return (decoded_variants, evasion_findings).

        If encoded content is detected that decodes to something suspicious,
        it's flagged as an evasion attempt — a strong signal of adversarial intent.
        """
        from .patterns import PROMPT_INJECTION_PATTERNS

        decoded_variants = self.decode_all(text)
        extra_texts: List[str] = []
        findings: List[RuntimeFinding] = []

        for decoded_text, encoding in decoded_variants:
            extra_texts.append(decoded_text)

            # Check if the decoded content matches injection patterns
            for p in PROMPT_INJECTION_PATTERNS:
                if p.pattern.search(decoded_text):
                    findings.append(
                        RuntimeFinding(
                            session_id=session_id,
                            severity="critical",
                            category="ENCODED_INJECTION",
                            title=f"Encoded prompt injection ({encoding})",
                            description=(
                                f"Content encoded as {encoding} decodes to a prompt injection attempt. "
                                "Encoding is a strong indicator of adversarial intent — legitimate "
                                "content is rarely base64/hex/ROT13 encoded."
                            ),
                            source="message",
                            confidence=0.92,
                            context={
                                "encoding": encoding,
                                "matched_excerpt": decoded_text[:120] + ("..." if len(decoded_text) > 120 else ""),
                            },
                        )
                    )
                    break  # One finding per encoding is enough

        return extra_texts, findings
