"""Security utilities for API key management and SQL validation.

This module provides:
- API key generation and hashing
- SQL query validation to prevent dangerous operations
- Rate limiting helpers

Extraction target: ``tla-sql-guard`` standalone package.
This module has zero internal imports and depends only on the standard library.
See STANDALONE_PROJECTS.md for extraction details.
"""

import hashlib
import hmac
import re
import secrets
import time
from dataclasses import dataclass, field
from typing import Any

# API Key Management


def generate_api_key(prefix: str = "mts") -> str:
    """Generate a new API key.

    Keys are formatted as: {prefix}_{random_32_chars}
    Example: mts_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

    Args:
        prefix: Prefix for the key (default 'mts' for mysql-to-sheets).

    Returns:
        A new API key string.
    """
    random_part = secrets.token_hex(16)  # 32 hex characters
    return f"{prefix}_{random_part}"


def generate_api_key_salt() -> str:
    """Generate a random salt for API key hashing.

    Each API key should have its own unique salt to prevent
    rainbow table attacks and ensure identical keys don't
    produce identical hashes.

    Returns:
        32-character hex string salt.
    """
    return secrets.token_hex(16)


# Legacy static salt for backward compatibility with old keys
_LEGACY_STATIC_SALT = "mysql-to-sheets-api-key-salt"

# PBKDF2 configuration - OWASP recommends 100,000+ iterations for 2024
_PBKDF2_ITERATIONS = 100_000
_PBKDF2_HASH_NAME = "sha256"
_PBKDF2_DK_LEN = 32  # 256 bits

# Hash version prefixes for algorithm identification
_HASH_VERSION_PBKDF2 = "pbkdf2$"
_HASH_VERSION_SHA256 = "sha256$"


def hash_api_key(api_key: str, salt: str) -> str:
    """Hash an API key for secure storage using PBKDF2.

    Uses PBKDF2-HMAC-SHA256 with 100,000 iterations for security.
    Each key should have its own unique salt generated by
    generate_api_key_salt().

    The returned hash includes a version prefix for future-proofing:
    - "pbkdf2$<hash>" for PBKDF2 hashes (current)
    - Hashes without prefix are legacy SHA256 concatenation

    Args:
        api_key: The API key to hash.
        salt: Per-key salt value (use generate_api_key_salt() to create).

    Returns:
        Hashed API key as hex string with version prefix.
    """
    key_bytes = api_key.encode("utf-8")
    salt_bytes = salt.encode("utf-8")

    # Use PBKDF2-HMAC-SHA256 with high iteration count
    derived_key = hashlib.pbkdf2_hmac(
        _PBKDF2_HASH_NAME,
        key_bytes,
        salt_bytes,
        _PBKDF2_ITERATIONS,
        dklen=_PBKDF2_DK_LEN,
    )

    return f"{_HASH_VERSION_PBKDF2}{derived_key.hex()}"


def _hash_api_key_sha256(api_key: str, salt: str) -> str:
    """Hash an API key using legacy SHA256 concatenation.

    INTERNAL: Used for backward compatibility verification only.
    Do not use for new keys.

    Args:
        api_key: The API key to hash.
        salt: Salt value.

    Returns:
        Hashed API key as hex string (no prefix).
    """
    key_bytes = api_key.encode("utf-8")
    salt_bytes = salt.encode("utf-8")
    return hashlib.sha256(salt_bytes + key_bytes).hexdigest()


def hash_api_key_legacy(api_key: str) -> str:
    """Hash an API key using the legacy static salt.

    DEPRECATED: Only use for verifying old keys that were created
    before per-key salts were implemented. New keys should use
    hash_api_key() with generate_api_key_salt().

    Args:
        api_key: The API key to hash.

    Returns:
        Hashed API key as hex string.
    """
    return _hash_api_key_sha256(api_key, _LEGACY_STATIC_SALT)


def verify_api_key(api_key: str, stored_hash: str, salt: str | None = None) -> bool:
    """Verify an API key against a stored hash.

    Uses constant-time comparison to prevent timing attacks.
    Supports multiple hash formats for backward compatibility:
    - PBKDF2 hashes (prefixed with "pbkdf2$")
    - SHA256 hashes with per-key salt (no prefix, salt provided)
    - Legacy SHA256 hashes with static salt (no prefix, no salt)

    Args:
        api_key: The API key to verify.
        stored_hash: The stored hash to compare against.
        salt: Per-key salt. If None, uses legacy static salt for
            backward compatibility with old keys.

    Returns:
        True if the key is valid, False otherwise.
    """
    # Detect hash version from prefix
    if stored_hash.startswith(_HASH_VERSION_PBKDF2):
        # PBKDF2 hash - requires salt
        if salt is None:
            return False
        computed_hash = hash_api_key(api_key, salt)
        return hmac.compare_digest(computed_hash, stored_hash)

    # Legacy hash formats (no version prefix)
    if salt is None:
        # Backward compatibility: use legacy static salt
        computed_hash = hash_api_key_legacy(api_key)
    else:
        # SHA256 with per-key salt (pre-PBKDF2 format)
        computed_hash = _hash_api_key_sha256(api_key, salt)

    return hmac.compare_digest(computed_hash, stored_hash)


def needs_rehash(stored_hash: str) -> bool:
    """Check if a stored hash needs to be upgraded.

    Returns True if the hash uses a legacy algorithm and should
    be re-hashed with the current algorithm on next successful
    verification.

    Args:
        stored_hash: The stored hash to check.

    Returns:
        True if the hash should be upgraded.
    """
    return not stored_hash.startswith(_HASH_VERSION_PBKDF2)


# SQL Validation

# Default maximum query length (10,000 characters)
DEFAULT_MAX_QUERY_LENGTH = 10000

# Dangerous SQL patterns that should be rejected
_DANGEROUS_PATTERNS = [
    # DDL statements
    r"\bDROP\b",
    r"\bCREATE\b",
    r"\bALTER\b",
    r"\bTRUNCATE\b",
    # DML write statements
    r"\bDELETE\b",
    r"\bUPDATE\b",
    r"\bINSERT\b",
    r"\bREPLACE\b",
    # Other dangerous patterns
    r"\bGRANT\b",
    r"\bREVOKE\b",
    r"\bEXEC\b",
    r"\bEXECUTE\b",
    r"\bCALL\b",
    # File operations
    r"\bLOAD\s+DATA\b",
    r"\bINTO\s+OUTFILE\b",
    r"\bINTO\s+DUMPFILE\b",
    r"\bLOAD_FILE\s*\(",
    # Time-based attacks and resource exhaustion
    r"\bBENCHMARK\s*\(",
    r"\bSLEEP\s*\(",
    # System variable access (potential information disclosure)
    r"@@\w+",
]

# Comment patterns that could be used to hide malicious code
_COMMENT_PATTERNS = [
    r"--",  # Single-line comment
    r"/\*",  # Multi-line comment start
    r"\*/",  # Multi-line comment end
    r"#",  # MySQL comment
]

# Valid starting keywords for queries (strict mode)
_ALLOWED_STARTING_KEYWORDS = {"SELECT", "WITH"}


@dataclass
class SQLValidationResult:
    """Result of SQL validation.

    Attributes:
        valid: Whether the SQL is valid.
        errors: List of validation error messages.
        warnings: List of validation warnings.
    """

    valid: bool
    errors: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        """Convert result to dictionary."""
        return {
            "valid": self.valid,
            "errors": self.errors,
            "warnings": self.warnings,
        }


def validate_sql_query(
    query: str,
    allow_comments: bool = False,
    allow_multiple_statements: bool = False,
    strict_mode: bool = True,
    max_length: int = DEFAULT_MAX_QUERY_LENGTH,
) -> SQLValidationResult:
    """Validate a SQL query for safety.

    Checks for dangerous patterns that could modify data or schema.
    This is a defense-in-depth measure - the database user should
    also have read-only permissions.

    Args:
        query: The SQL query to validate.
        allow_comments: Whether to allow SQL comments.
        allow_multiple_statements: Whether to allow multiple statements.
        strict_mode: If True, only allow SELECT or WITH as first keyword.
        max_length: Maximum allowed query length (default 10,000 chars).

    Returns:
        SQLValidationResult with validation status and errors.
    """
    errors: list[str] = []
    warnings: list[str] = []

    if not query or not query.strip():
        return SQLValidationResult(
            valid=False,
            errors=["SQL query is empty"],
        )

    # Check query length
    if len(query) > max_length:
        return SQLValidationResult(
            valid=False,
            errors=[f"SQL query exceeds maximum length of {max_length} characters"],
        )

    query_upper = query.upper()
    query_stripped = query.strip()

    # Strict mode: Only allow SELECT or WITH as first keyword
    if strict_mode:
        first_word = query_stripped.split()[0].upper() if query_stripped else ""
        if first_word not in _ALLOWED_STARTING_KEYWORDS:
            errors.append(
                f"Query must start with SELECT or WITH (found: {first_word}). "
                "Only read-only queries are allowed."
            )

    # Check for multiple statements
    if not allow_multiple_statements and ";" in query_stripped.rstrip(";"):
        # Check if there's content after a semicolon
        parts = [p.strip() for p in query.split(";") if p.strip()]
        if len(parts) > 1:
            errors.append("Multiple SQL statements are not allowed")

    # Check for dangerous patterns
    for pattern in _DANGEROUS_PATTERNS:
        if re.search(pattern, query_upper, re.IGNORECASE):
            keyword = pattern.replace(r"\b", "").replace(r"\s+", " ").replace(r"\s*\(", "(").strip()
            errors.append(f"Dangerous SQL pattern detected: {keyword}")

    # Check for comments
    if not allow_comments:
        for pattern in _COMMENT_PATTERNS:
            if re.search(pattern, query):
                warnings.append(f"SQL comments detected (pattern: {pattern})")

    # Warn about potential issues
    if "UNION" in query_upper:
        warnings.append("UNION detected - ensure this is intentional")

    if "LIKE" in query_upper and ("%" in query or "_" in query):
        warnings.append("Wildcards in LIKE clause detected")

    return SQLValidationResult(
        valid=len(errors) == 0,
        errors=errors,
        warnings=warnings,
    )


def sanitize_query_for_logging(query: str, max_length: int = 200) -> str:
    """Sanitize a SQL query for safe logging.

    Truncates long queries and removes potentially sensitive data.

    Args:
        query: The SQL query to sanitize.
        max_length: Maximum length of the output.

    Returns:
        Sanitized query string.
    """
    # Remove excess whitespace
    sanitized = " ".join(query.split())

    # Truncate if too long
    if len(sanitized) > max_length:
        sanitized = sanitized[:max_length] + "..."

    return sanitized


# Rate Limiting


@dataclass
class TokenBucket:
    """Token bucket rate limiter.

    Allows bursts up to bucket capacity, then limits to
    the refill rate.

    Attributes:
        capacity: Maximum tokens in bucket.
        refill_rate: Tokens added per second.
        tokens: Current token count.
        last_update: Last time tokens were refilled.
    """

    capacity: float
    refill_rate: float  # tokens per second
    tokens: float = field(default=0.0)
    last_update: float = field(default_factory=time.time)

    def __post_init__(self) -> None:
        """Initialize with full bucket."""
        self.tokens = self.capacity

    def _refill(self) -> None:
        """Refill tokens based on elapsed time."""
        now = time.time()
        elapsed = now - self.last_update
        self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
        self.last_update = now

    def consume(self, tokens: float = 1.0) -> bool:
        """Try to consume tokens from the bucket.

        Args:
            tokens: Number of tokens to consume.

        Returns:
            True if tokens were consumed, False if rate limited.
        """
        self._refill()
        if self.tokens >= tokens:
            self.tokens -= tokens
            return True
        return False

    def get_wait_time(self, tokens: float = 1.0) -> float:
        """Get time to wait for tokens to be available.

        Args:
            tokens: Number of tokens needed.

        Returns:
            Time in seconds to wait, or 0 if tokens available.
        """
        self._refill()
        if self.tokens >= tokens:
            return 0.0
        needed = tokens - self.tokens
        return needed / self.refill_rate


class RateLimiter:
    """Rate limiter using token bucket algorithm.

    Provides per-key rate limiting with configurable limits.
    """

    def __init__(
        self,
        requests_per_minute: int = 60,
        burst_size: int | None = None,
    ) -> None:
        """Initialize rate limiter.

        Args:
            requests_per_minute: Allowed requests per minute.
            burst_size: Maximum burst size (defaults to requests_per_minute).
        """
        self.requests_per_minute = requests_per_minute
        self.burst_size = burst_size or requests_per_minute
        self._buckets: dict[str, TokenBucket] = {}

    def _get_bucket(self, key: str) -> TokenBucket:
        """Get or create a token bucket for a key.

        Args:
            key: The rate limit key (e.g., API key or IP).

        Returns:
            TokenBucket for the key.
        """
        if key not in self._buckets:
            self._buckets[key] = TokenBucket(
                capacity=self.burst_size,
                refill_rate=self.requests_per_minute / 60.0,
            )
        return self._buckets[key]

    def is_allowed(self, key: str) -> bool:
        """Check if a request is allowed.

        Args:
            key: The rate limit key.

        Returns:
            True if request is allowed.
        """
        bucket = self._get_bucket(key)
        return bucket.consume()

    def get_remaining(self, key: str) -> int:
        """Get remaining requests for a key.

        Args:
            key: The rate limit key.

        Returns:
            Number of remaining requests.
        """
        bucket = self._get_bucket(key)
        bucket._refill()
        return int(bucket.tokens)

    def get_retry_after(self, key: str) -> float:
        """Get seconds until next request is allowed.

        Args:
            key: The rate limit key.

        Returns:
            Seconds to wait.
        """
        bucket = self._get_bucket(key)
        return bucket.get_wait_time()

    def reset(self, key: str | None = None) -> None:
        """Reset rate limit state.

        Args:
            key: Specific key to reset, or None to reset all.
        """
        if key is None:
            self._buckets.clear()
        elif key in self._buckets:
            del self._buckets[key]

