"""
InsAIts SDK - Credential Pattern Detector
==========================================
Detects: API keys, PII, secrets in inter-agent messages
OWASP: MCP01 (Token Mismanagement & Secret Exposure), ASI06 (Sensitive Data Exposure)
Priority: #3

Algorithm:
    1. Scan message text against known credential/PII patterns
    2. Patterns include: API keys (AWS, GitHub, OpenAI, etc.), email addresses,
       credit card numbers, SSNs, JWTs, private keys, connection strings
    3. Each match is classified by type and severity
    4. Hashes are stored in evidence (never raw credential values)

IMPORTANT: This detector returns the TYPE and LOCATION of detected credentials,
never the actual credential values. Evidence uses SHA-256 hashes.
"""

import hashlib
import re
import time
from typing import Dict, List, Tuple


# Credential patterns: (name, regex, severity)
# These are structural patterns (OPEN) -- detection thresholds are in private config
CREDENTIAL_PATTERNS: List[Tuple[str, str, str]] = [
    # API Keys
    ("aws_access_key", r"(?:AKIA|ABIA|ACCA|ASIA)[0-9A-Z]{16}", "critical"),
    ("aws_secret_key", r"(?i)(?:aws_secret_access_key|aws_secret)\s*[=:]\s*[A-Za-z0-9/+=]{40}", "critical"),
    ("github_token", r"(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}", "critical"),
    ("github_fine_grained", r"github_pat_[A-Za-z0-9_]{22,}", "critical"),
    ("openai_api_key", r"sk-[A-Za-z0-9]{20,}T3BlbkFJ[A-Za-z0-9]{20,}", "critical"),
    ("openai_project_key", r"sk-proj-[A-Za-z0-9_-]{40,}", "critical"),
    ("anthropic_api_key", r"sk-ant-[A-Za-z0-9_-]{40,}", "critical"),
    ("stripe_key", r"(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{20,}", "critical"),
    ("slack_token", r"xox[bprs]-[A-Za-z0-9-]{10,}", "high"),
    ("slack_webhook", r"https://hooks\.slack\.com/services/T[A-Z0-9]+/B[A-Z0-9]+/[A-Za-z0-9]+", "high"),
    ("google_api_key", r"AIza[0-9A-Za-z_-]{35}", "high"),
    ("twilio_key", r"SK[0-9a-fA-F]{32}", "high"),
    ("sendgrid_key", r"SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}", "high"),
    ("supabase_key", r"(?:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]{50,})", "high"),
    ("generic_api_key", r"(?i)(?:api[_-]?key|apikey|api_secret|access_token)\s*[=:]\s*[\"']?[A-Za-z0-9_\-]{20,}", "high"),
    ("bearer_token", r"(?i)(?:bearer|authorization)\s+[A-Za-z0-9_\-.]{20,}", "high"),

    # Private Keys
    ("private_key_rsa", r"-----BEGIN (?:RSA )?PRIVATE KEY-----", "critical"),
    ("private_key_ec", r"-----BEGIN EC PRIVATE KEY-----", "critical"),
    ("private_key_openssh", r"-----BEGIN OPENSSH PRIVATE KEY-----", "critical"),

    # JWTs (detect the structure)
    ("jwt_token", r"eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}", "high"),

    # Connection Strings
    ("database_url", r"(?i)(?:postgres|mysql|mongodb|redis)://[^\s\"']{10,}", "critical"),
    ("connection_string", r"(?i)(?:server|data\s+source)\s*=\s*[^;]+;\s*(?:uid|user\s+id|password)\s*=", "critical"),

    # PII Patterns
    ("email_address", r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "medium"),
    ("us_ssn", r"\b\d{3}-\d{2}-\d{4}\b", "critical"),
    ("credit_card_visa", r"\b4[0-9]{12}(?:[0-9]{3})?\b", "critical"),
    ("credit_card_mc", r"\b5[1-5][0-9]{14}\b", "critical"),
    ("credit_card_amex", r"\b3[47][0-9]{13}\b", "critical"),
    ("phone_us", r"\b(?:\+1[-.\s]?)?\(?[0-9]{3}\)?[-.\s]?[0-9]{3}[-.\s]?[0-9]{4}\b", "low"),
    ("ip_address", r"\b(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b", "low"),

    # Cloud Metadata (SSRF indicators)
    ("aws_metadata", r"(?:169\.254\.169\.254|metadata\.google\.internal)", "critical"),
    ("cloud_metadata_url", r"http://169\.254\.169\.254/latest/meta-data", "critical"),
]

# Compiled patterns (done once at import time)
_COMPILED_PATTERNS: List[Tuple[str, re.Pattern, str]] = [
    (name, re.compile(pattern), severity)
    for name, pattern, severity in CREDENTIAL_PATTERNS
]

# Allowlist patterns that look like credentials but aren't
ALLOWLIST_PATTERNS = [
    r"sk-[a-z]+-example",  # Documentation examples
    r"YOUR_API_KEY",
    r"xxx+",
    r"\.{3,}",  # Placeholder dots
    r"\*{3,}",  # Masked values
    r"<[A-Z_]+>",  # Template placeholders
]
_COMPILED_ALLOWLIST = [re.compile(p, re.IGNORECASE) for p in ALLOWLIST_PATTERNS]


class CredentialPatternDetector:
    """Detects API keys, PII, and secrets in inter-agent messages.

    Never stores raw credential values -- all evidence uses SHA-256 hashes.

    Detects: Token Mismanagement, Secret Exposure, PII Leakage
    OWASP: MCP01, ASI06
    """

    name = "credential_pattern"
    enabled = True

    def __init__(self):
        self._detection_count = 0
        self._seen_hashes: set = set()  # Track unique credentials to avoid duplicate alerts

    def check(self, text: str, message_idx: int,
              agent_id: str = "unknown") -> Dict:
        """Scan message for credential and PII patterns.

        Args:
            text: Message text to scan
            message_idx: Sequential message index
            agent_id: Agent that sent this message

        Returns:
            Dict with detection results. Credential values are NEVER
            included -- only types, locations, and SHA-256 hashes.
        """
        timestamp_us = time.time_ns() // 1000
        findings: List[Dict] = []

        for name, pattern, severity in _COMPILED_PATTERNS:
            for match in pattern.finditer(text):
                matched_text = match.group(0)

                # Skip allowlisted patterns
                if self._is_allowlisted(matched_text):
                    continue

                # Hash the credential (never store raw value)
                cred_hash = hashlib.sha256(matched_text.encode()).hexdigest()[:16]

                # Track unique credentials
                is_new = cred_hash not in self._seen_hashes
                self._seen_hashes.add(cred_hash)

                findings.append({
                    "type": name,
                    "severity": severity,
                    "hash": cred_hash,
                    "position": match.start(),
                    "length": len(matched_text),
                    "is_new": is_new,
                })

        detected = len(findings) > 0
        if detected:
            self._detection_count += 1

        # Compute overall severity (highest of all findings)
        severity_order = {"critical": 4, "high": 3, "medium": 2, "low": 1}
        overall_severity = "low"
        if findings:
            overall_severity = max(
                findings, key=lambda f: severity_order.get(f["severity"], 0)
            )["severity"]

        # Build description
        description = ""
        if detected:
            types = list({f["type"] for f in findings})
            description = (
                f"Detected {len(findings)} credential/PII pattern(s) in message "
                f"from agent '{agent_id}': {', '.join(types[:5])}"
            )

        return {
            "detected": detected,
            "anomaly_type": "CREDENTIAL_EXPOSURE",
            "severity": overall_severity,
            "confidence": min(1.0, len(findings) * 0.4),
            "findings": findings,
            "finding_count": len(findings),
            "unique_types": list({f["type"] for f in findings}),
            "description": description,
            "message_idx": message_idx,
            "timestamp_us": timestamp_us,
        }

    def reset(self) -> None:
        """Reset detector state."""
        self._detection_count = 0
        self._seen_hashes.clear()

    def _is_allowlisted(self, text: str) -> bool:
        """Check if a matched pattern is actually a placeholder/example."""
        for pattern in _COMPILED_ALLOWLIST:
            if pattern.search(text):
                return True
        return False
