import re
import time

# ─────────────────────────────────────────────────────────────────────────────
# IMPORTANT ORDERING RULE:
#   More-specific patterns MUST appear before broader patterns that share a
#   common prefix (e.g. "sk-ant-" before "sk-", "tskey-client-" before
#   "tskey-"). The scanner stops at the FIRST match per entry, so ordering
#   determines which label wins.
# ─────────────────────────────────────────────────────────────────────────────

CREDENTIAL_PATTERNS: list[tuple[str, str, str]] = [

    # ── LLM / AI Model Providers ─────────────────────────────────────────────
    (r'sk-ant-api03-[A-Za-z0-9\-_]{93}', "anthropic_api_key", "critical"),
    (r'sk-ant-[A-Za-z0-9\-_]{32,}', "anthropic_api_key_generic", "critical"),
    (r'sk-proj-[A-Za-z0-9\-_]{32,}', "openai_project_key", "critical"),
    (r'sk-[A-Za-z0-9]{32,}', "openai_api_key", "critical"),
    (r'sk-or-v1-[A-Za-z0-9\-_]{32,}', "openrouter_api_key", "critical"),
    (r'xai-[A-Za-z0-9\-_]{32,}', "xai_grok_api_key", "critical"),
    (r'gsk_[A-Za-z0-9]{52}', "groq_api_key", "critical"),
    (r'sk-aimlapi-[A-Za-z0-9\-_]{32,}', "aimlapi_key", "critical"),
    (r'hf_[A-Za-z0-9]{34,}', "huggingface_token", "critical"),
    (r'pplx-[A-Za-z0-9]{48}', "perplexity_api_key", "critical"),
    (r'(?i)mistral[_\-]?api[_\-]?key\s*[:=]\s*["\']?[A-Za-z0-9]{32,}', "mistral_api_key", "critical"),
    (r'pk-[A-Za-z0-9\-_]{32,}', "portkey_api_key", "critical"),

    # ── Voice (ElevenLabs) ────────────────────────────────────────────────────
    # "sk_" (underscore) is distinct from OpenAI's "sk-" (dash)
    (r'sk_[A-Za-z0-9]{32,}', "elevenlabs_api_key", "critical"),

    # ── Messaging Channels ────────────────────────────────────────────────────
    # Telegram — Bot API token
    (r'\d{8,12}:[A-Za-z0-9_\-]{35}', "telegram_bot_token", "critical"),

    # Slack — Bolt SDK workspace token
    (r'xox[bpoas]-[A-Za-z0-9\-]{10,}', "slack_token", "critical"),

    # Discord — modern token format: base64(user_id).timestamp.hmac
    (r'[A-Za-z0-9_\-]{24,28}\.[A-Za-z0-9_\-]{6}\.[A-Za-z0-9_\-]{27}', "discord_bot_token", "critical"),

    # BlueBubbles — password auth (Signal removed: uses phone registration, no API key)
    (r'(?i)(bluebubbles|imessage).{0,20}password\s*[:=]\s*["\']?\S{8,}', "bluebubbles_password", "critical"),

    # Feishu / Lark — app secret + verification token
    (r'(?i)(feishu|lark).{0,20}(app[_\-]?secret|verification[_\-]?token)\s*[:=]\s*["\']?[A-Za-z0-9\-_]{16,}', "feishu_credential", "critical"),

    # Microsoft Teams — Bot Framework app secret
    (r'(?i)(msteams|teams).{0,30}(app[_\-]?secret|client[_\-]?secret)\s*[:=]\s*["\']?[A-Za-z0-9\-_\.~]{16,}', "msteams_app_secret", "critical"),

    # Mattermost — bot/personal access token (34-char alphanumeric)
    (r'(?i)mattermost.{0,30}(token|key)\s*[:=]\s*["\']?[a-z0-9]{26}', "mattermost_token", "critical"),

    # Matrix — access token (always starts with "syt_")
    (r'syt_[A-Za-z0-9_]{32,}', "matrix_access_token", "critical"),

    # Zalo Bot API — OA access token
    (r'(?i)zalo.{0,20}(oa[_\-]?access[_\-]?token|api[_\-]?key)\s*[:=]\s*["\']?[A-Za-z0-9\-_\.]{32,}', "zalo_oa_token", "critical"),

    # Nextcloud Talk — app password / token
    (r'(?i)nextcloud.{0,20}(token|app[_\-]?password)\s*[:=]\s*["\']?[A-Za-z0-9\-_]{16,}', "nextcloud_token", "critical"),

    # LINE — channel secret + access token
    (r'(?i)line.{0,20}(channel[_\-]?secret|access[_\-]?token)\s*[:=]\s*["\']?[A-Za-z0-9\-_+/=]{20,}', "line_credential", "critical"),

    # ── Productivity Tools ────────────────────────────────────────────────────
    (r'ntn_[A-Za-z0-9]{32,}', "notion_token_new", "critical"),
    (r'secret_[A-Za-z0-9]{43}', "notion_token_legacy", "critical"),
    (r'gh[pousr]_[A-Za-z0-9]{36,}', "github_token", "critical"),
    (r'github_pat_[A-Za-z0-9_]{82}', "github_pat", "critical"),

    # ── Google / GCP ──────────────────────────────────────────────────────────
    (r'AIza[A-Za-z0-9_\-]{35}', "google_api_key", "critical"),
    (r'ya29\.[A-Za-z0-9_\-]{50,}', "google_oauth_token", "critical"),

    # ── AWS ───────────────────────────────────────────────────────────────────
    (r'AKIA[A-Z0-9]{16}', "aws_access_key", "critical"),
    (r'(?i)aws.{0,20}secret.{0,20}["\']?[A-Za-z0-9/+=]{40}', "aws_secret", "critical"),

    # ── Music / Audio ─────────────────────────────────────────────────────────
    (r'(?i)spotify.{0,20}(client_secret|client_id)\s*[:=]\s*["\']?[a-f0-9]{32}', "spotify_credential", "critical"),
    (r'(?i)sonos.{0,20}(token|key)\s*[:=]\s*["\']?[A-Za-z0-9\-_]{16,}', "sonos_token", "high"),

    # ── Smart Home ────────────────────────────────────────────────────────────
    (r'(?i)hue.{0,20}(username|token|key)\s*[:=]\s*["\']?[A-Za-z0-9\-]{10,}', "philips_hue_token", "high"),
    (r'(?i)home.?assistant.{0,20}(token|key)\s*[:=]\s*["\']?[A-Za-z0-9_\.\-]{20,}', "home_assistant_token", "high"),

    # ── Network / Infrastructure ──────────────────────────────────────────────
    (r'tskey-client-[A-Za-z0-9\-]{20,}', "tailscale_client_key", "critical"),
    (r'tskey-[A-Za-z0-9\-]{20,}', "tailscale_auth_key", "critical"),

    # ── Secrets Manager (1Password) ───────────────────────────────────────────
    (r'ops_[A-Za-z0-9]{50,}', "onepassword_service_account", "critical"),

    # ── Generic Secrets ───────────────────────────────────────────────────────
    (r'eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+', "jwt_token", "high"),
    (r'(?i)(api[_\-]?key|apikey)\s*[:=]\s*["\']?[A-Za-z0-9_\-]{20,}', "generic_api_key", "high"),
    (r'(?i)(access[_\-]?token|oauth[_\-]?token)\s*[:=]\s*["\']?[A-Za-z0-9_\-]{20,}', "oauth_token", "high"),
    (r'(?i)(bearer\s+)[A-Za-z0-9_\-\.]{20,}', "bearer_token", "high"),
    (r'(?i)(password|passwd|pwd)\s*[:=]\s*["\']?\S{12,}', "password_in_content", "high"),

    # ── Private Keys ─────────────────────────────────────────────────────────
    (r'-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----', "private_key", "critical"),

    # ── Crypto Seed Phrases ───────────────────────────────────────────────────
    (
        r'(?i)(seed\s+phrase|mnemonic|recovery\s+words?|wallet\s+backup)'
        r'.{0,60}'
        r'\b([a-z]+\s+){11}[a-z]+\b',
        "crypto_seed_phrase",
        "critical",
    ),

    # ── Database Connection Strings ───────────────────────────────────────────
    (r'(?i)(mongodb(\+srv)?|postgresql|mysql|redis|amqp|rediss)://[^\s"\']{10,}', "db_connection_string", "critical"),
    (r'(?i)Server\s*=.*Password\s*=\s*\S+', "mssql_connection_string", "critical"),
]

_COMPILED_PATTERNS: list[tuple[re.Pattern, str, str]] = [
    (re.compile(pattern), label, severity)
    for pattern, label, severity in CREDENTIAL_PATTERNS
]

PII_PATTERNS: list[tuple[str, str, str]] = [
    (r'\b\d{3}-\d{2}-\d{4}\b', "ssn_us", "high"),
    (r'\b4[0-9]{12}(?:[0-9]{3})?\b', "credit_card_visa", "high"),
    (r'\b5[1-5][0-9]{14}\b', "credit_card_mastercard", "high"),
    (r'\b3[47][0-9]{13}\b', "credit_card_amex", "high"),
    (r'\b[A-Z]{2}[0-9]{2}[A-Z0-9]{4}[0-9]{7}([A-Z0-9]?){0,16}\b', "iban", "medium"),
]

_COMPILED_PII: list[tuple[re.Pattern, str, str]] = [
    (re.compile(pattern), label, severity)
    for pattern, label, severity in PII_PATTERNS
]

_EXFIL_CONTEXT = re.compile(
    r'(send|forward|post|upload|share|email|message|curl|fetch|http)',
    re.IGNORECASE,
)


def detect(content: str, operation: str = "output", destination: str = "") -> dict:
    """
    Scan *content* for credentials and PII.

    Parameters
    ----------
    content     : The text to scan (e.g. a model response or tool argument).
    operation   : Contextual label for what produced the content
                  ("output", "api_send", "tool_call", …).
    destination : Where the content is going (URL, channel name, …).
                  Used to escalate severity when an exfil-style verb is present.

    Returns
    -------
    dict with keys: detected, findings, severity, operation, layer, scan_ms
    """
    start = time.perf_counter()
    findings: list[dict] = []

    matched_count = 0
    for pattern, label, base_severity in _COMPILED_PATTERNS:
        match = pattern.search(content)
        if match:
            matched_count += 1
            severity = base_severity
            if (
                operation in ("api_send", "tool_call")
                and _EXFIL_CONTEXT.search(destination or "")
            ):
                severity = "critical"
            findings.append({
                "type": "credential_exposure",
                "credential_type": label,
                "operation": operation,
                "severity": severity,
                "destination": destination[:100] if destination else "—",
                "preview": _redact(match.group(0)),
            })

    if matched_count >= 3:
        findings.append({
            "type": "bulk_credential_dump",
            "credential_types_found": matched_count,
            "severity": "critical",
            "detail": "Multiple credential types found — possible config file exfiltration",
        })

    for pattern, label, severity in _COMPILED_PII:
        if pattern.search(content):
            findings.append({
                "type": "pii_exposure",
                "pii_type": label,
                "operation": operation,
                "severity": severity,
                "destination": destination[:100] if destination else "—",
            })

    if any(f["severity"] == "critical" for f in findings):
        overall = "critical"
    elif any(f["severity"] == "high" for f in findings):
        overall = "high"
    elif findings:
        overall = "medium"
    else:
        overall = "none"

    elapsed_ms = round((time.perf_counter() - start) * 1000, 3)

    return {
        "detected": bool(findings),
        "findings": findings,
        "severity": overall,
        "operation": operation,
        "layer": "credential_leak",
        "scan_ms": elapsed_ms,
    }


def _redact(value: str) -> str:
    """
    Show first 4 + last 2 chars, mask the rest.

    Examples
    --------
    "sk-ant-api03-ABC…XYZ"  →  "sk-a****************YZ"
    "short"                 →  "****"
    """
    if len(value) <= 6:
        return "****"
    return value[:4] + "*" * (len(value) - 6) + value[-2:]