# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Secrets Detection and Masking for Familiar

Detects potential secrets in text and masks them to prevent
accidental exposure in logs and outputs.

Usage:
    from familiar.core.secrets_detection import mask_secrets, detect_secrets

    # Mask secrets before logging
    logger.info(mask_secrets(api_response))

    # Check if text contains secrets
    findings = detect_secrets(user_input)
    if findings:
        logger.warning(f"Input contains {len(findings)} potential secrets")
"""

import hashlib
import logging
import re
from dataclasses import dataclass
from enum import Enum
from typing import List, NamedTuple, Optional, Tuple

logger = logging.getLogger(__name__)


class SecretType(str, Enum):
    """Types of secrets that can be detected."""

    # API Keys
    ANTHROPIC_KEY = "anthropic_api_key"
    OPENAI_KEY = "openai_api_key"
    GITHUB_TOKEN = "github_token"
    SLACK_TOKEN = "slack_token"
    DISCORD_TOKEN = "discord_token"
    TELEGRAM_TOKEN = "telegram_token"

    # Cloud Providers
    AWS_ACCESS_KEY = "aws_access_key"
    AWS_SECRET_KEY = "aws_secret_key"
    GCP_API_KEY = "gcp_api_key"
    AZURE_KEY = "azure_key"

    # Generic
    API_KEY = "api_key"
    PASSWORD = "password"
    SECRET = "secret"
    TOKEN = "token"
    PRIVATE_KEY = "private_key"

    # Credentials in URLs
    URL_CREDENTIALS = "url_credentials"
    JWT_TOKEN = "jwt_token"

    # Personal Data
    SSN = "ssn"
    CREDIT_CARD = "credit_card"


class SecretFinding(NamedTuple):
    """A detected secret in text."""

    secret_type: SecretType
    matched_text: str
    start: int
    end: int
    confidence: float  # 0.0 to 1.0


# Pattern definitions: (regex, secret_type, confidence)
SECRET_PATTERNS: List[Tuple[str, SecretType, float]] = [
    # Anthropic API Keys
    (r"sk-ant-[a-zA-Z0-9_-]{20,}", SecretType.ANTHROPIC_KEY, 0.95),
    # OpenAI API Keys
    (r"sk-[a-zA-Z0-9]{48,}", SecretType.OPENAI_KEY, 0.95),
    (r"sk-proj-[a-zA-Z0-9_-]{48,}", SecretType.OPENAI_KEY, 0.95),
    # GitHub Tokens
    (r"ghp_[a-zA-Z0-9]{36}", SecretType.GITHUB_TOKEN, 0.95),
    (r"gho_[a-zA-Z0-9]{36}", SecretType.GITHUB_TOKEN, 0.95),
    (r"ghu_[a-zA-Z0-9]{36}", SecretType.GITHUB_TOKEN, 0.95),
    (r"ghs_[a-zA-Z0-9]{36}", SecretType.GITHUB_TOKEN, 0.95),
    (r"ghr_[a-zA-Z0-9]{36}", SecretType.GITHUB_TOKEN, 0.95),
    # Slack Tokens
    (r"xox[baprs]-[a-zA-Z0-9-]+", SecretType.SLACK_TOKEN, 0.90),
    # Discord Tokens
    (r"[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27}", SecretType.DISCORD_TOKEN, 0.85),
    # Telegram Bot Tokens
    (r"\d{9,10}:[a-zA-Z0-9_-]{35}", SecretType.TELEGRAM_TOKEN, 0.90),
    # AWS
    (r"AKIA[0-9A-Z]{16}", SecretType.AWS_ACCESS_KEY, 0.95),
    (
        r'(?i)aws[_-]?secret[_-]?access[_-]?key\s*[=:]\s*[\'"]?([A-Za-z0-9/+=]{40})[\'"]?',
        SecretType.AWS_SECRET_KEY,
        0.90,
    ),
    # GCP
    (r"AIza[0-9A-Za-z_-]{35}", SecretType.GCP_API_KEY, 0.90),
    # Generic patterns (lower confidence)
    (r'(?i)api[_-]?key\s*[=:]\s*[\'"]?([a-zA-Z0-9_-]{20,})[\'"]?', SecretType.API_KEY, 0.70),
    (r'(?i)password\s*[=:]\s*[\'"]?([^\s\'"]{8,})[\'"]?', SecretType.PASSWORD, 0.65),
    (r'(?i)secret\s*[=:]\s*[\'"]?([a-zA-Z0-9_-]{16,})[\'"]?', SecretType.SECRET, 0.70),
    (r'(?i)token\s*[=:]\s*[\'"]?([a-zA-Z0-9_-]{20,})[\'"]?', SecretType.TOKEN, 0.65),
    # Private Keys
    (r"-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----", SecretType.PRIVATE_KEY, 0.95),
    # URL with credentials
    (r"://[^:]+:([^@]+)@", SecretType.URL_CREDENTIALS, 0.85),
    # Database connection strings
    (
        r"(?i)(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis)://[^\s'\"]+",
        SecretType.URL_CREDENTIALS,
        0.85,
    ),
    # JWT tokens
    (
        r"eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}",
        SecretType.JWT_TOKEN,
        0.80,
    ),
    # Personal Data
    (r"\b\d{3}[-]?\d{2}[-]?\d{4}\b", SecretType.SSN, 0.75),  # US SSN format
    (
        r"\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13})\b",
        SecretType.CREDIT_CARD,
        0.80,
    ),  # Major credit cards
]

# Compile patterns for efficiency
_COMPILED_PATTERNS = [
    (re.compile(pattern), secret_type, confidence)
    for pattern, secret_type, confidence in SECRET_PATTERNS
]


def detect_secrets(text: str, min_confidence: float = 0.5) -> List[SecretFinding]:
    """
    Detect potential secrets in text.

    Args:
        text: Text to scan for secrets
        min_confidence: Minimum confidence threshold (0.0-1.0)

    Returns:
        List of SecretFinding objects for each detected secret
    """
    if not text:
        return []

    findings: List[SecretFinding] = []

    for compiled_pattern, secret_type, confidence in _COMPILED_PATTERNS:
        if confidence < min_confidence:
            continue

        for match in compiled_pattern.finditer(text):
            findings.append(
                SecretFinding(
                    secret_type=secret_type,
                    matched_text=match.group(0),
                    start=match.start(),
                    end=match.end(),
                    confidence=confidence,
                )
            )

    # Sort by position
    findings.sort(key=lambda f: f.start)

    # Remove overlapping findings (keep higher confidence)
    if len(findings) > 1:
        filtered: List[SecretFinding] = []
        for finding in findings:
            # Check if this overlaps with any already-added finding
            overlaps = False
            for existing in filtered:
                if finding.start < existing.end and finding.end > existing.start:
                    # Overlapping - keep the one with higher confidence
                    if finding.confidence > existing.confidence:
                        filtered.remove(existing)
                    else:
                        overlaps = True
                    break
            if not overlaps:
                filtered.append(finding)
        findings = filtered

    return findings


def mask_secrets(
    text: str, mask: str = "***REDACTED***", min_confidence: float = 0.5, show_type: bool = True
) -> str:
    """
    Replace detected secrets with masked values.

    Args:
        text: Text containing potential secrets
        mask: Replacement string for secrets
        min_confidence: Minimum confidence to mask
        show_type: Include secret type in mask (e.g., "[API_KEY:***]")

    Returns:
        Text with secrets replaced by mask
    """
    if not text:
        return text

    findings = detect_secrets(text, min_confidence)

    if not findings:
        return text

    # Sort by position descending to replace from end
    findings.sort(key=lambda f: f.start, reverse=True)

    result = text
    for finding in findings:
        if show_type:
            replacement = f"[{finding.secret_type.value}:{mask}]"
        else:
            replacement = mask

        result = result[: finding.start] + replacement + result[finding.end :]

    return result


def hash_secret(secret: str, salt: Optional[str] = None) -> str:
    """
    Create a deterministic hash of a secret for logging.

    This allows tracking the same secret across logs without
    exposing the actual value.

    Args:
        secret: The secret value to hash
        salt: Optional salt for the hash

    Returns:
        Truncated SHA256 hash (first 8 characters)
    """
    to_hash = secret
    if salt:
        to_hash = f"{salt}:{secret}"

    full_hash = hashlib.sha256(to_hash.encode()).hexdigest()
    return full_hash[:8]


def mask_secrets_with_hash(
    text: str, salt: Optional[str] = None, min_confidence: float = 0.5
) -> str:
    """
    Replace secrets with hashed values (for tracking).

    Args:
        text: Text containing potential secrets
        salt: Salt for hashing
        min_confidence: Minimum confidence to mask

    Returns:
        Text with secrets replaced by hashes
    """
    if not text:
        return text

    findings = detect_secrets(text, min_confidence)

    if not findings:
        return text

    # Sort by position descending
    findings.sort(key=lambda f: f.start, reverse=True)

    result = text
    for finding in findings:
        secret_hash = hash_secret(finding.matched_text, salt)
        replacement = f"[{finding.secret_type.value}:hash={secret_hash}]"
        result = result[: finding.start] + replacement + result[finding.end :]

    return result


def safe_log(
    message: str, logger_instance: Optional[logging.Logger] = None, level: int = logging.INFO
) -> None:
    """
    Log a message with secrets automatically masked.

    Args:
        message: Message to log
        logger_instance: Logger to use (default: module logger)
        level: Log level
    """
    log = logger_instance or logger
    masked = mask_secrets(message)
    log.log(level, masked)


@dataclass
class SecretsAuditResult:
    """Result of auditing text for secrets."""

    contains_secrets: bool
    secret_count: int
    secret_types: List[SecretType]
    high_confidence_count: int
    masked_text: str


def audit_for_secrets(text: str, high_confidence_threshold: float = 0.85) -> SecretsAuditResult:
    """
    Comprehensive audit of text for secrets.

    Args:
        text: Text to audit
        high_confidence_threshold: Threshold for "high confidence" classification

    Returns:
        SecretsAuditResult with detailed findings
    """
    findings = detect_secrets(text, min_confidence=0.5)

    secret_types = list(set(f.secret_type for f in findings))
    high_confidence = sum(1 for f in findings if f.confidence >= high_confidence_threshold)

    return SecretsAuditResult(
        contains_secrets=len(findings) > 0,
        secret_count=len(findings),
        secret_types=secret_types,
        high_confidence_count=high_confidence,
        masked_text=mask_secrets(text),
    )


# ============================================================
# EXPORTS
# ============================================================

__all__ = [
    # Enums
    "SecretType",
    # Classes
    "SecretFinding",
    "SecretsAuditResult",
    # Functions
    "detect_secrets",
    "mask_secrets",
    "mask_secrets_with_hash",
    "hash_secret",
    "safe_log",
    "audit_for_secrets",
]
