"""Secret Scanner — Dedicated secret detection engine for MCP servers.

Detects hardcoded secrets, API keys, tokens, passwords, private keys, and
high-entropy strings in source code, configuration files, and .env files.
Zero external dependencies — pure Python 3.11+ stdlib.
"""

from __future__ import annotations

import math
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any


@dataclass
class SecretFinding:
    """A single detected secret in source code."""

    type: str  # api_key, token, password, certificate, private_key, high_entropy
    value_masked: str  # First 4 chars + ***
    file: str
    line: int
    entropy: float
    confidence: str  # HIGH, MEDIUM, LOW
    pattern_name: str = ""
    severity: str = "HIGH"
    description: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "type": self.type,
            "value_masked": self.value_masked,
            "file": self.file,
            "line": self.line,
            "entropy": round(self.entropy, 2),
            "confidence": self.confidence,
            "pattern_name": self.pattern_name,
            "severity": self.severity,
            "description": self.description,
        }


@dataclass
class SecretPattern:
    """A pattern for detecting secrets."""

    name: str
    pattern: re.Pattern[str]
    severity: str  # CRITICAL, HIGH, MEDIUM
    description: str
    type: str = "api_key"  # api_key, token, password, certificate, private_key

    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "pattern": self.pattern.pattern,
            "severity": self.severity,
            "description": self.description,
            "type": self.type,
        }


# ---------------------------------------------------------------------------
# Built-in secret patterns (25+)
# ---------------------------------------------------------------------------

_SECRET_PATTERNS: list[SecretPattern] = [
    # AWS
    SecretPattern(
        name="AWS Access Key ID",
        pattern=re.compile(r"(AKIA[0-9A-Z]{16,})"),
        severity="CRITICAL",
        description="AWS Access Key ID detected",
        type="api_key",
    ),
    SecretPattern(
        name="AWS Secret Access Key",
        pattern=re.compile(
            r"(?:aws_secret_access_key|secret_key)\s*[=:]\s*['\"]?([A-Za-z0-9/+=]{40})['\"]?"
        ),
        severity="CRITICAL",
        description="AWS Secret Access Key detected",
        type="api_key",
    ),
    # GitHub
    SecretPattern(
        name="GitHub Token (ghp)",
        pattern=re.compile(r"(?:^|['\"\s=])(ghp_[A-Za-z0-9]{36,})(?:['\"\s]|$)"),
        severity="CRITICAL",
        description="GitHub Personal Access Token detected",
        type="token",
    ),
    SecretPattern(
        name="GitHub Token (gho)",
        pattern=re.compile(r"(?:^|['\"\s=])(gho_[A-Za-z0-9]{36,})(?:['\"\s]|$)"),
        severity="CRITICAL",
        description="GitHub OAuth Access Token detected",
        type="token",
    ),
    SecretPattern(
        name="GitHub Token (ghs)",
        pattern=re.compile(r"(?:^|['\"\s=])(ghs_[A-Za-z0-9]{36,})(?:['\"\s]|$)"),
        severity="CRITICAL",
        description="GitHub App Installation Token detected",
        type="token",
    ),
    SecretPattern(
        name="GitHub Token (ghr)",
        pattern=re.compile(r"(?:^|['\"\s=])(ghr_[A-Za-z0-9]{36,})(?:['\"\s]|$)"),
        severity="CRITICAL",
        description="GitHub Refresh Token detected",
        type="token",
    ),
    # JWT
    SecretPattern(
        name="JWT Secret",
        pattern=re.compile(
            r"(?:jwt[_-]?secret|JWT_SECRET)\s*[=:]\s*['\"]([^'\"]{8,})['\"]",
            re.IGNORECASE,
        ),
        severity="CRITICAL",
        description="JWT Secret key detected",
        type="token",
    ),
    # Database URLs
    SecretPattern(
        name="Database URL with password",
        pattern=re.compile(
            r"(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp)://[^:\s]+:[^@\s]+@[^\s]+"
        ),
        severity="CRITICAL",
        description="Database connection string with embedded credentials",
        type="password",
    ),
    # Generic API keys
    SecretPattern(
        name="Generic API Key assignment",
        pattern=re.compile(
            r"(?:api[_-]?key|apikey|api_secret)\s*[=:]\s*['\"]([A-Za-z0-9_\-]{16,})['\"]",
            re.IGNORECASE,
        ),
        severity="HIGH",
        description="API key assignment detected",
        type="api_key",
    ),
    # Private Keys
    SecretPattern(
        name="RSA Private Key",
        pattern=re.compile(r"-----BEGIN RSA PRIVATE KEY-----"),
        severity="CRITICAL",
        description="RSA private key detected",
        type="private_key",
    ),
    SecretPattern(
        name="EC Private Key",
        pattern=re.compile(r"-----BEGIN EC PRIVATE KEY-----"),
        severity="CRITICAL",
        description="EC private key detected",
        type="private_key",
    ),
    SecretPattern(
        name="Generic Private Key",
        pattern=re.compile(r"-----BEGIN PRIVATE KEY-----"),
        severity="CRITICAL",
        description="Private key detected",
        type="private_key",
    ),
    SecretPattern(
        name="PGP Private Key",
        pattern=re.compile(r"-----BEGIN PGP PRIVATE KEY BLOCK-----"),
        severity="CRITICAL",
        description="PGP private key block detected",
        type="private_key",
    ),
    SecretPattern(
        name="SSH Private Key (OpenSSH)",
        pattern=re.compile(r"-----BEGIN OPENSSH PRIVATE KEY-----"),
        severity="CRITICAL",
        description="OpenSSH private key detected",
        type="private_key",
    ),
    # OAuth
    SecretPattern(
        name="OAuth Client Secret",
        pattern=re.compile(
            r"(?:client[_-]?secret|oauth[_-]?secret)\s*[=:]\s*['\"]([A-Za-z0-9_\-]{16,})['\"]",
            re.IGNORECASE,
        ),
        severity="HIGH",
        description="OAuth client secret detected",
        type="token",
    ),
    # Slack
    SecretPattern(
        name="Slack Bot Token",
        pattern=re.compile(r"(?:^|['\"\s=])(xoxb-[0-9]{10,}-[A-Za-z0-9]+)(?:['\"\s]|$)"),
        severity="HIGH",
        description="Slack Bot token detected",
        type="token",
    ),
    SecretPattern(
        name="Slack Webhook URL",
        pattern=re.compile(
            r"https://hooks\.slack\.com/services/T[A-Za-z0-9]+/B[A-Za-z0-9]+/[A-Za-z0-9]+"
        ),
        severity="HIGH",
        description="Slack Webhook URL detected",
        type="token",
    ),
    # Stripe
    SecretPattern(
        name="Stripe Secret Key",
        pattern=re.compile(r"(?:^|['\"\s=])(sk_live_[A-Za-z0-9]{20,})(?:['\"\s]|$)"),
        severity="CRITICAL",
        description="Stripe live secret key detected",
        type="api_key",
    ),
    SecretPattern(
        name="Stripe Restricted Key",
        pattern=re.compile(r"(?:^|['\"\s=])(rk_live_[A-Za-z0-9]{20,})(?:['\"\s]|$)"),
        severity="HIGH",
        description="Stripe live restricted key detected",
        type="api_key",
    ),
    # SendGrid
    SecretPattern(
        name="SendGrid API Key",
        pattern=re.compile(r"(?:^|['\"\s=])(SG\.[A-Za-z0-9_\-]{22,}\.[A-Za-z0-9_\-]{22,})(?:['\"\s]|$)"),
        severity="HIGH",
        description="SendGrid API key detected",
        type="api_key",
    ),
    # Twilio
    SecretPattern(
        name="Twilio API Key",
        pattern=re.compile(r"(?:^|['\"\s=])(SK[0-9a-fA-F]{32})(?:['\"\s]|$)"),
        severity="HIGH",
        description="Twilio API key detected",
        type="api_key",
    ),
    # Mailgun
    SecretPattern(
        name="Mailgun API Key",
        pattern=re.compile(
            r"(?:^|['\"\s=])(key-[A-Za-z0-9]{32})(?:['\"\s]|$)"
        ),
        severity="HIGH",
        description="Mailgun API key detected",
        type="api_key",
    ),
    # Generic password assignments
    SecretPattern(
        name="Password Assignment",
        pattern=re.compile(
            r"(?:password|passwd|pwd)\s*[=:]\s*['\"]([^'\"]{6,})['\"]",
            re.IGNORECASE,
        ),
        severity="HIGH",
        description="Hardcoded password detected",
        type="password",
    ),
    # Heroku
    SecretPattern(
        name="Heroku API Key",
        pattern=re.compile(
            r"(?:heroku[_-]?api[_-]?key|HEROKU_API_KEY)\s*[=:]\s*['\"]?([A-Fa-f0-9-]{36})['\"]?",
            re.IGNORECASE,
        ),
        severity="HIGH",
        description="Heroku API key detected",
        type="api_key",
    ),
    # Google
    SecretPattern(
        name="Google API Key",
        pattern=re.compile(r"(?:^|['\"\s=])(AIza[A-Za-z0-9_\-]{35})(?:['\"\s]|$)"),
        severity="HIGH",
        description="Google API key detected",
        type="api_key",
    ),
    SecretPattern(
        name="Google OAuth Client Secret",
        pattern=re.compile(
            r"(?:client_secret)\s*[=:]\s*['\"]([A-Za-z0-9_\-]{24,})['\"]"
        ),
        severity="HIGH",
        description="Google OAuth client secret detected",
        type="token",
    ),
    # Azure
    SecretPattern(
        name="Azure Storage Account Key",
        pattern=re.compile(
            r"(?:DefaultEndpointsProtocol=https;AccountName=[^;]+;AccountKey=)([A-Za-z0-9+/=]{40,})"
        ),
        severity="CRITICAL",
        description="Azure Storage account key detected",
        type="api_key",
    ),
    # .env pattern: KEY=value (generic)
    SecretPattern(
        name="Environment Variable Secret",
        pattern=re.compile(
            r"^(?:SECRET|TOKEN|API_KEY|AUTH_TOKEN|ACCESS_KEY|PRIVATE_KEY|ENCRYPTION_KEY)"
            r"\s*=\s*['\"]?([^\s'\"]{8,})['\"]?\s*$",
            re.IGNORECASE | re.MULTILINE,
        ),
        severity="HIGH",
        description="Secret-like environment variable detected",
        type="api_key",
    ),
]


def get_secret_patterns() -> list[SecretPattern]:
    """Return all built-in secret detection patterns."""
    return list(_SECRET_PATTERNS)


def calculate_entropy(value: str) -> float:
    """Calculate Shannon entropy of a string in bits per character.

    Higher entropy indicates more randomness, which may signal a secret.

    Args:
        value: The string to measure.

    Returns:
        Shannon entropy in bits per character (0.0 for empty/single-char).
    """
    if len(value) <= 1:
        return 0.0

    freq: dict[str, int] = {}
    for ch in value:
        freq[ch] = freq.get(ch, 0) + 1

    length = len(value)
    entropy = 0.0
    for count in freq.values():
        if count > 0:
            p = count / length
            entropy -= p * math.log2(p)

    return entropy


def _mask_value(value: str) -> str:
    """Mask a secret value, showing only the first 4 characters."""
    if len(value) <= 4:
        return value[0] + "***" if value else "***"
    return value[:4] + "***"


def _is_likely_false_positive(value: str) -> bool:
    """Check if a matched value is likely a false positive."""
    # Very short values
    if len(value) < 6:
        return True
    # Placeholder patterns
    placeholders = {
        "your_api_key", "your_secret", "changeme", "placeholder",
        "example", "test_key", "dummy", "xxxxxxxx", "your-api-key",
        "replace_me", "insert_here", "todo", "fixme", "your_token",
        "password123", "password",
    }
    lower = value.lower()
    for ph in placeholders:
        if lower == ph or lower.startswith(ph):
            return True
    # All same character
    if len(set(value)) == 1:
        return True
    return False


def scan_secrets(source: str, filename: str = "<string>") -> list[SecretFinding]:
    """Scan source code for secrets using pattern matching and entropy analysis.

    Args:
        source: Source code or configuration content to scan.
        filename: Name of the file being scanned (for reporting).

    Returns:
        List of SecretFinding instances.
    """
    findings: list[SecretFinding] = []
    lines = source.splitlines()
    seen: set[tuple[int, str]] = set()  # (line, pattern_name) dedup

    for pattern_def in _SECRET_PATTERNS:
        for i, line_text in enumerate(lines, start=1):
            for match in pattern_def.pattern.finditer(line_text):
                key = (i, pattern_def.name)
                if key in seen:
                    continue
                seen.add(key)

                # Extract matched value (prefer group 1, fall back to group 0)
                raw_value = match.group(1) if match.lastindex and match.lastindex >= 1 else match.group(0)
                raw_value = raw_value.strip().strip("'\"")

                if _is_likely_false_positive(raw_value):
                    continue

                entropy = calculate_entropy(raw_value)

                findings.append(SecretFinding(
                    type=pattern_def.type,
                    value_masked=_mask_value(raw_value),
                    file=filename,
                    line=i,
                    entropy=entropy,
                    confidence="HIGH",
                    pattern_name=pattern_def.name,
                    severity=pattern_def.severity,
                    description=pattern_def.description,
                ))

    # High-entropy string detection (catch secrets not matched by patterns)
    _high_entropy_token_re = re.compile(r"['\"]([A-Za-z0-9+/=_\-]{20,})['\"]")
    for i, line_text in enumerate(lines, start=1):
        for match in _high_entropy_token_re.finditer(line_text):
            token = match.group(1)
            entropy = calculate_entropy(token)
            if entropy > 4.5:
                key = (i, "high_entropy")
                if key in seen:
                    continue
                # Skip if already detected by a pattern on this line
                if any(f.line == i for f in findings):
                    continue
                seen.add(key)
                if _is_likely_false_positive(token):
                    continue
                findings.append(SecretFinding(
                    type="high_entropy",
                    value_masked=_mask_value(token),
                    file=filename,
                    line=i,
                    entropy=entropy,
                    confidence="MEDIUM",
                    pattern_name="High Entropy String",
                    severity="MEDIUM",
                    description=f"High-entropy string detected ({entropy:.1f} bits/char)",
                ))

    return findings


def scan_secrets_file(path: str) -> list[SecretFinding]:
    """Scan a single file for secrets.

    Args:
        path: Path to the file to scan.

    Returns:
        List of SecretFinding instances.
    """
    filepath = Path(path)
    if not filepath.exists():
        return []
    try:
        source = filepath.read_text(encoding="utf-8", errors="replace")
    except (OSError, UnicodeDecodeError):
        return []
    return scan_secrets(source, filename=str(filepath))


def scan_secrets_directory(dirpath: str) -> list[SecretFinding]:
    """Scan all files in a directory recursively for secrets.

    Scans .py, .ts, .js, .go, .env, .yaml, .yml, .json, .toml, .cfg, .ini files.

    Args:
        dirpath: Path to the directory to scan.

    Returns:
        List of SecretFinding instances across all files.
    """
    target = Path(dirpath)
    if not target.is_dir():
        return scan_secrets_file(dirpath)

    findings: list[SecretFinding] = []
    scan_extensions = {
        ".py", ".ts", ".js", ".go", ".env", ".yaml", ".yml",
        ".json", ".toml", ".cfg", ".ini", ".conf", ".properties",
        ".sh", ".bash", ".zsh", ".rb", ".java", ".kt",
    }
    # Also scan dotenv files without extension
    for filepath in sorted(target.rglob("*")):
        if filepath.is_file():
            if filepath.suffix in scan_extensions or filepath.name.startswith(".env"):
                findings.extend(scan_secrets_file(str(filepath)))
    return findings


def format_secrets_text(findings: list[SecretFinding]) -> str:
    """Format secret findings as human-readable text.

    Args:
        findings: List of secret findings.

    Returns:
        Formatted text output.
    """
    if not findings:
        return "No secrets detected."

    lines: list[str] = []
    lines.append("")
    lines.append("=" * 60)
    lines.append("  Secret Scanner — Results")
    lines.append("=" * 60)
    lines.append(f"  Total secrets found: {len(findings)}")
    lines.append("=" * 60)
    lines.append("")

    severity_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
    sorted_findings = sorted(
        findings,
        key=lambda f: severity_order.get(f.severity, 99),
    )

    for f in sorted_findings:
        icon = {"CRITICAL": "[!!]", "HIGH": "[!]", "MEDIUM": "[-]"}.get(
            f.severity, "[.]"
        )
        lines.append(f"  {icon} [{f.severity}] {f.pattern_name}")
        lines.append(f"     File: {f.file}:{f.line}")
        lines.append(f"     Type: {f.type} | Masked: {f.value_masked}")
        lines.append(f"     Entropy: {f.entropy:.2f} bits/char | Confidence: {f.confidence}")
        if f.description:
            lines.append(f"     {f.description}")
        lines.append("")

    lines.append("=" * 60)
    return "\n".join(lines)


def format_secrets_json(findings: list[SecretFinding]) -> str:
    """Format secret findings as JSON.

    Args:
        findings: List of secret findings.

    Returns:
        JSON string with findings.
    """
    import json

    data = {
        "secret_scanner": {
            "total_findings": len(findings),
            "findings": [f.to_dict() for f in findings],
        }
    }
    return json.dumps(data, indent=2, ensure_ascii=False)
