"""Secret and PII scanner for GreyMatter Solo.

Prevents sensitive data from entering persistent memory and from leaking
to frontier LLMs during recall. Uses compiled regex patterns with
validators for high accuracy and zero external dependencies.

Usage:
    scanner = SecretScanner()
    detections = scanner.scan(text)
    redacted = scanner.redact(text, detections)
    has_any = scanner.has_secrets(text)
"""
from __future__ import annotations

import re
from dataclasses import dataclass


@dataclass
class Detection:
    """A detected secret or PII element."""

    pattern_name: str       # e.g., "aws_access_key"
    category: str           # e.g., "cloud_credential"
    matched_text: str       # the actual matched text
    start: int              # start position in text
    end: int                # end position in text
    confidence: float       # 0.0-1.0
    severity: str           # "critical", "high", "medium", "low"


@dataclass
class ScannerConfig:
    """Configuration for the secret scanner."""

    enabled: bool = True
    mode: str = "warn"          # "warn", "redact", "block"
    min_confidence: float = 0.7
    categories: list[str] | None = None  # None = all categories


# ---------------------------------------------------------------------------
# Pattern definitions
# ---------------------------------------------------------------------------

@dataclass
class _PatternDef:
    """Internal pattern definition with compiled regex and optional validator."""

    name: str
    category: str
    pattern: re.Pattern[str]
    confidence: float
    severity: str
    flags: int = 0
    validator: str | None = None  # name of a validation function


def _compile_patterns() -> list[_PatternDef]:
    """Build and compile all detection patterns. Called once at module load."""
    defs: list[tuple[str, str, str, float, str, int, str | None]] = [
        # (name, category, regex, confidence, severity, re_flags, validator)

        # --- Cloud credentials ---
        (
            "aws_access_key",
            "cloud_credential",
            r"\bAKIA[0-9A-Z]{16}\b",
            0.98, "critical", 0, None,
        ),
        (
            "aws_secret_key",
            "cloud_credential",
            r"(?:aws_secret_access_key|aws_secret_key|secret_key)\s*[=:]\s*([A-Za-z0-9/+=]{40})",
            0.95, "critical", re.IGNORECASE, None,
        ),

        # --- Platform tokens ---
        (
            "github_token",
            "platform_token",
            r"\bghp_[A-Za-z0-9]{20,}\b",
            0.98, "critical", 0, None,
        ),
        (
            "github_oauth_token",
            "platform_token",
            r"\bgho_[A-Za-z0-9]{20,}\b",
            0.98, "critical", 0, None,
        ),
        (
            "gitlab_token",
            "platform_token",
            r"\bglpat-[A-Za-z0-9_-]{20,}\b",
            0.98, "critical", 0, None,
        ),
        (
            "slack_token",
            "platform_token",
            r"\bxox[bpsa]-[A-Za-z0-9\-]{10,}\b",
            0.97, "critical", 0, None,
        ),

        # --- PyPI tokens ---
        (
            "pypi_token",
            "platform_token",
            r"\bpypi-[A-Za-z0-9_\-]{20,}\b",
            0.98, "critical", 0, None,
        ),

        # --- API keys (generic) ---
        (
            "generic_api_key",
            "api_key",
            r"\b(?:sk-|pk_(?:live|test)_)[A-Za-z0-9]{20,}\b",
            0.95, "critical", 0, None,
        ),
        (
            "bearer_token",
            "api_key",
            r"\bBearer\s+[A-Za-z0-9._\-]{20,}\b",
            0.90, "high", re.IGNORECASE, None,
        ),
        (
            "api_key_assignment",
            "api_key",
            r"(?:api[_-]?key|apikey)\s*[=:]\s*[\"']?([A-Za-z0-9_\-]{20,})[\"']?",
            0.85, "high", re.IGNORECASE, None,
        ),

        # --- Anthropic / OpenAI specific ---
        (
            "anthropic_api_key",
            "api_key",
            r"\bsk-ant-[A-Za-z0-9_\-]{20,}\b",
            0.99, "critical", 0, None,
        ),
        (
            "openai_api_key",
            "api_key",
            r"\bsk-proj-[A-Za-z0-9_\-]{20,}\b",
            0.99, "critical", 0, None,
        ),

        # --- GCP ---
        (
            "gcp_service_account",
            "cloud_credential",
            r'"type"\s*:\s*"service_account"',
            0.95, "critical", 0, None,
        ),

        # --- Discord ---
        (
            "discord_webhook",
            "platform_token",
            r"https://discord(?:app)?\.com/api/webhooks/\d+/[A-Za-z0-9_\-]+",
            0.97, "high", 0, None,
        ),

        # --- JWT ---
        (
            "jwt_token",
            "api_key",
            r"\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b",
            0.85, "high", 0, None,
        ),

        # --- Cryptographic keys ---
        (
            "private_key",
            "cryptographic_key",
            r"-----BEGIN\s+(?:RSA|EC|DSA|OPENSSH|PGP)\s+PRIVATE\s+KEY-----[\s\S]*?-----END\s+(?:RSA|EC|DSA|OPENSSH|PGP)\s+PRIVATE\s+KEY-----",
            0.99, "critical", 0, None,
        ),

        # --- Passwords in config ---
        (
            "password_in_config",
            "credential",
            r"(?:password|passwd|pwd)\s*[=:]\s*[\"']?(\S{4,})[\"']?",
            0.90, "critical", re.IGNORECASE, None,
        ),

        # --- Connection strings ---
        (
            "connection_string",
            "connection_string",
            r"(?:postgresql|mysql|mongodb|redis|amqp|mssql):\/\/[^\s\"'<>]{10,}",
            0.95, "critical", re.IGNORECASE, None,
        ),

        # --- Azure ---
        (
            "azure_connection_string",
            "cloud_credential",
            r"(?:AccountKey|SharedAccessKey)\s*=\s*[A-Za-z0-9+/=]{20,}",
            0.95, "critical", 0, None,
        ),

        # --- Generic secrets (long hex/base64 after keywords) ---
        # Negative lookahead excludes known platform token prefixes to avoid
        # stealing matches from higher-confidence specific patterns.
        (
            "generic_secret",
            "secret",
            r"(?:secret|token|key|credentials?)\s*[=:]\s*[\"']?(?!(?:ghp_|gho_|glpat-|xox[bpsa]-|sk-|pk_))([A-Za-z0-9+/=_\-]{32,})[\"']?",
            0.80, "high", re.IGNORECASE, None,
        ),

        # --- Financial ---
        (
            "credit_card",
            "financial",
            r"\b(\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4})\b",
            0.85, "high", 0, "luhn",
        ),

        # --- PII ---
        (
            "ssn",
            "pii",
            r"\b(\d{3}-\d{2}-\d{4})\b",
            0.95, "critical", 0, "ssn",
        ),
        (
            "email_address",
            "pii",
            r"\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b",
            0.70, "low", 0, None,
        ),
    ]

    compiled = []
    for name, category, regex, confidence, severity, flags, validator in defs:
        compiled.append(_PatternDef(
            name=name,
            category=category,
            pattern=re.compile(regex, flags),
            confidence=confidence,
            severity=severity,
            flags=flags,
            validator=validator,
        ))
    return compiled


_PATTERNS = _compile_patterns()


# ---------------------------------------------------------------------------
# Validators
# ---------------------------------------------------------------------------

def _validate_luhn(text: str) -> bool:
    """Luhn algorithm for credit card validation."""
    digits = [int(c) for c in text if c.isdigit()]
    if len(digits) != 16:
        return False
    checksum = 0
    for i, d in enumerate(reversed(digits)):
        if i % 2 == 1:
            d *= 2
            if d > 9:
                d -= 9
        checksum += d
    return checksum % 10 == 0


def _validate_ssn(text: str) -> bool:
    """Validate SSN format: area cannot be 000, 666, or 900-999."""
    digits = text.replace("-", "")
    if len(digits) != 9:
        return False
    area = int(digits[:3])
    group = int(digits[3:5])
    serial = int(digits[5:])
    if area == 0 or area == 666 or area >= 900:
        return False
    if group == 0 or serial == 0:
        return False
    return True


_VALIDATORS = {
    "luhn": _validate_luhn,
    "ssn": _validate_ssn,
}

# Substrings that indicate a value is a placeholder, not a real secret.
# Only checked against the core secret value (captured group or full match
# for patterns without groups), not surrounding context.
_FALSE_POSITIVE_SUBSTRINGS = (
    "your-", "YOUR_", "INSERT_", "CHANGE_ME",
    "changeme", "placeholder", "xxxxxxxx", "XXXXXXXX", "TODO",
    "replace_me", "REPLACE_ME", "dummy", "DUMMY",
)


# ---------------------------------------------------------------------------
# Scanner
# ---------------------------------------------------------------------------

class SecretScanner:
    """Lightweight secret and PII scanner for GreyMatter Solo.

    Scans text for secrets, API keys, credentials, PII and other sensitive
    data using compiled regex patterns with optional validators.

    Args:
        config: Scanner configuration. Uses defaults if None.
    """

    def __init__(self, config: ScannerConfig | None = None):
        self.config = config or ScannerConfig()

    def scan(self, text: str) -> list[Detection]:
        """Scan text for secrets and PII.

        Returns a list of Detection objects sorted by position, with
        overlapping matches resolved in favor of longer/higher-confidence
        matches.
        """
        if not self.config.enabled or not text:
            return []

        detections: list[Detection] = []

        for pdef in _PATTERNS:
            # Filter by confidence threshold
            if pdef.confidence < self.config.min_confidence:
                continue

            # Filter by category
            if self.config.categories and pdef.category not in self.config.categories:
                continue

            for match in pdef.pattern.finditer(text):
                matched_text = match.group(0)

                # Run validator if specified
                if pdef.validator:
                    validator_fn = _VALIDATORS.get(pdef.validator)
                    if validator_fn and not validator_fn(matched_text):
                        continue

                # Skip likely false positives (placeholder/example values)
                if any(fp in matched_text for fp in _FALSE_POSITIVE_SUBSTRINGS):
                    continue

                detections.append(Detection(
                    pattern_name=pdef.name,
                    category=pdef.category,
                    matched_text=matched_text,
                    start=match.start(),
                    end=match.end(),
                    confidence=pdef.confidence,
                    severity=pdef.severity,
                ))

        detections = self._resolve_overlaps(detections)
        detections.sort(key=lambda d: d.start)
        return detections

    def redact(self, text: str, detections: list[Detection] | None = None) -> str:
        """Replace detected secrets with [TYPE_REDACTED] placeholders.

        Args:
            text: The text to redact.
            detections: Pre-computed detections. If None, scans the text first.

        Returns:
            Text with secrets replaced by placeholders.
        """
        if not self.config.enabled:
            return text
        if detections is None:
            detections = self.scan(text)
        if not detections:
            return text

        # Sort by position descending so replacements don't shift indices
        sorted_dets = sorted(detections, key=lambda d: d.start, reverse=True)
        result = text
        for d in sorted_dets:
            placeholder = f"[{d.pattern_name.upper()}_REDACTED]"
            result = result[:d.start] + placeholder + result[d.end:]
        return result

    def has_secrets(self, text: str) -> bool:
        """Quick check: does this text contain any detectable secrets?"""
        if not self.config.enabled:
            return False
        return len(self.scan(text)) > 0

    @staticmethod
    def _resolve_overlaps(detections: list[Detection]) -> list[Detection]:
        """Remove overlapping detections.

        Prefers longer match first, then higher confidence as tiebreaker.
        """
        if not detections:
            return []

        # Sort: longest match first, then highest confidence
        detections.sort(key=lambda d: (-(d.end - d.start), -d.confidence))

        kept: list[Detection] = []
        taken_ranges: list[tuple[int, int]] = []

        for d in detections:
            overlaps = any(
                d.start < end and d.end > start
                for start, end in taken_ranges
            )
            if not overlaps:
                kept.append(d)
                taken_ranges.append((d.start, d.end))
        return kept
