# 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

"""
Familiar Structured Logging Configuration (Phase 5)

Production-ready logging with:
- JSON structured output for log aggregation
- Request context tracking
- Sensitive data filtering
- Multiple output targets
- Log level management

Usage:
    from familiar.core.logging_config import configure_logging, LogContext

    # Configure JSON logging for production
    configure_logging(
        level="INFO",
        json_format=True,
        log_file="/var/log/familiar/app.log"
    )

    # Add context to logs
    with LogContext(request_id="abc123", user_id="user1"):
        logger.info("Processing request")
        # Logs include: {"request_id": "abc123", "user_id": "user1", ...}
"""

import json
import logging
import sys
import threading
import uuid
from contextlib import contextmanager
from datetime import datetime, timezone
from functools import wraps
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional

__all__ = [
    "JSONFormatter",
    "SensitiveDataFilter",
    "LogContext",
    "configure_logging",
    "get_logger",
    "log_context",
    "with_request_id",
]


# ============================================================
# THREAD-LOCAL CONTEXT
# ============================================================

_log_context = threading.local()


def _get_context() -> Dict[str, Any]:
    """Get current thread-local log context."""
    if not hasattr(_log_context, "data"):
        _log_context.data = {}
    return _log_context.data


def _set_context(data: Dict[str, Any]) -> None:
    """Set thread-local log context."""
    _log_context.data = data


def _clear_context() -> None:
    """Clear thread-local log context."""
    _log_context.data = {}


# ============================================================
# JSON FORMATTER
# ============================================================


class JSONFormatter(logging.Formatter):
    """
    JSON formatter for structured logging.

    Outputs logs as JSON objects for easy parsing by log aggregators
    like ELK, Splunk, CloudWatch, etc.

    Features:
    - ISO 8601 timestamps with timezone
    - Exception formatting with traceback
    - Thread-local context injection
    - Configurable extra fields
    """

    # Fields to exclude from extra data
    RESERVED_ATTRS = {
        "name",
        "msg",
        "args",
        "created",
        "filename",
        "funcName",
        "levelname",
        "levelno",
        "lineno",
        "module",
        "msecs",
        "pathname",
        "process",
        "processName",
        "relativeCreated",
        "stack_info",
        "exc_info",
        "exc_text",
        "thread",
        "threadName",
        "message",
        "asctime",
        "taskName",
    }

    def __init__(
        self,
        include_timestamp: bool = True,
        include_level: bool = True,
        include_logger: bool = True,
        include_location: bool = True,
        include_process: bool = False,
        include_thread: bool = False,
        static_fields: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize JSON formatter.

        Args:
            include_timestamp: Include ISO 8601 timestamp
            include_level: Include log level
            include_logger: Include logger name
            include_location: Include module/function/line
            include_process: Include process ID
            include_thread: Include thread name
            static_fields: Static fields to include in all logs
        """
        super().__init__()
        self.include_timestamp = include_timestamp
        self.include_level = include_level
        self.include_logger = include_logger
        self.include_location = include_location
        self.include_process = include_process
        self.include_thread = include_thread
        self.static_fields = static_fields or {}

    def format(self, record: logging.LogRecord) -> str:
        """Format log record as JSON."""
        log_data: Dict[str, Any] = {}

        # Timestamp
        if self.include_timestamp:
            log_data["timestamp"] = datetime.fromtimestamp(
                record.created, tz=timezone.utc
            ).isoformat()

        # Level
        if self.include_level:
            log_data["level"] = record.levelname

        # Logger name
        if self.include_logger:
            log_data["logger"] = record.name

        # Message
        log_data["message"] = record.getMessage()

        # Location
        if self.include_location:
            log_data["module"] = record.module
            log_data["function"] = record.funcName
            log_data["line"] = record.lineno

        # Process info
        if self.include_process:
            log_data["process_id"] = record.process
            log_data["process_name"] = record.processName

        # Thread info
        if self.include_thread:
            log_data["thread_id"] = record.thread
            log_data["thread_name"] = record.threadName

        # Exception info
        if record.exc_info:
            log_data["exception"] = {
                "type": record.exc_info[0].__name__ if record.exc_info[0] else None,
                "message": str(record.exc_info[1]) if record.exc_info[1] else None,
                "traceback": self.formatException(record.exc_info),
            }

        # Add static fields
        log_data.update(self.static_fields)

        # Add thread-local context
        context = _get_context()
        if context:
            log_data["context"] = context.copy()

        # Add extra fields from record
        for key, value in record.__dict__.items():
            if key not in self.RESERVED_ATTRS:
                try:
                    # Ensure value is JSON serializable
                    json.dumps(value)
                    log_data[key] = value
                except (TypeError, ValueError):
                    log_data[key] = str(value)

        return json.dumps(log_data, default=str)


# ============================================================
# SENSITIVE DATA FILTER
# ============================================================


class SensitiveDataFilter(logging.Filter):
    """
    Filter that redacts sensitive data from log messages.

    Uses the secrets_detection module for comprehensive detection of:
    - API keys (Anthropic, OpenAI, GitHub, AWS, etc.)
    - Passwords
    - Tokens
    - Credit card numbers
    - Social Security numbers
    - Email addresses (optional)

    Phase 2.1: Integrated with secrets_detection module for unified detection.
    """

    # Default patterns to redact (kept for backwards compatibility)
    DEFAULT_PATTERNS = [
        # API keys (various formats)
        (r'api[_-]?key["\s:=]+["\']?([a-zA-Z0-9_-]{20,})["\']?', 'api_key="[REDACTED]"'),
        (r"sk-[a-zA-Z0-9]{20,}", "[REDACTED_API_KEY]"),
        (r'ANTHROPIC_API_KEY["\s:=]+["\']?([^"\'\\s]+)["\']?', 'ANTHROPIC_API_KEY="[REDACTED]"'),
        (r'OPENAI_API_KEY["\s:=]+["\']?([^"\'\\s]+)["\']?', 'OPENAI_API_KEY="[REDACTED]"'),
        # Passwords
        (r'password["\s:=]+["\']?([^"\'\\s]+)["\']?', 'password="[REDACTED]"'),
        (r'passwd["\s:=]+["\']?([^"\'\\s]+)["\']?', 'passwd="[REDACTED]"'),
        (r'secret["\s:=]+["\']?([^"\'\\s]+)["\']?', 'secret="[REDACTED]"'),
        # Tokens
        (r'token["\s:=]+["\']?([a-zA-Z0-9_.-]{20,})["\']?', 'token="[REDACTED]"'),
        (r"bearer\s+([a-zA-Z0-9_.-]{20,})", "Bearer [REDACTED]"),
        # Credit cards (basic pattern)
        (r"\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b", "[REDACTED_CC]"),
        # SSN
        (r"\b\d{3}-\d{2}-\d{4}\b", "[REDACTED_SSN]"),
    ]

    def __init__(
        self,
        patterns: Optional[List[tuple]] = None,
        redact_emails: bool = False,
        use_secrets_detection: bool = True,
    ):
        """
        Initialize sensitive data filter.

        Args:
            patterns: List of (pattern, replacement) tuples
            redact_emails: Whether to redact email addresses
            use_secrets_detection: Whether to use the secrets_detection module
        """
        super().__init__()
        import re

        self.use_secrets_detection = use_secrets_detection
        self._mask_secrets = None

        # Try to import secrets_detection module
        if use_secrets_detection:
            try:
                from .secrets_detection import mask_secrets

                self._mask_secrets = mask_secrets
            except ImportError:
                pass

        self.patterns = []
        for pattern, replacement in patterns or self.DEFAULT_PATTERNS:
            self.patterns.append((re.compile(pattern, re.IGNORECASE), replacement))

        if redact_emails:
            email_pattern = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
            self.patterns.append((re.compile(email_pattern), "[REDACTED_EMAIL]"))

    def filter(self, record: logging.LogRecord) -> bool:
        """Apply redaction to log record."""
        # Redact message
        record.msg = self._redact(str(record.msg))

        # Redact args if present
        if record.args:
            record.args = tuple(
                self._redact(str(arg)) if isinstance(arg, str) else arg for arg in record.args
            )

        return True

    def _redact(self, text: str) -> str:
        """Redact sensitive data from text using both local patterns and secrets_detection."""
        # First, use secrets_detection module if available
        if self._mask_secrets:
            text = self._mask_secrets(text, mask="[REDACTED]", show_type=True)

        # Then apply local patterns for any additional coverage
        for pattern, replacement in self.patterns:
            text = pattern.sub(replacement, text)

        return text


# ============================================================
# LOG CONTEXT MANAGER
# ============================================================


class LogContext:
    """
    Context manager for adding contextual data to logs.

    All logs within the context will include the specified fields.
    Contexts can be nested and will merge.

    Usage:
        with LogContext(request_id="abc123", user_id="user1"):
            logger.info("Processing")  # Includes request_id, user_id

            with LogContext(operation="save"):
                logger.info("Saving")  # Includes all three fields
    """

    def __init__(self, **kwargs: Any):
        """
        Initialize log context.

        Args:
            **kwargs: Key-value pairs to add to log context
        """
        self.new_context = kwargs
        self.previous_context: Optional[Dict[str, Any]] = None

    def __enter__(self) -> "LogContext":
        """Enter context and merge with existing."""
        self.previous_context = _get_context().copy()
        new_data = {**self.previous_context, **self.new_context}
        _set_context(new_data)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        """Exit context and restore previous."""
        if self.previous_context is not None:
            _set_context(self.previous_context)
        else:
            _clear_context()

    def update(self, **kwargs: Any) -> None:
        """Update context with additional fields."""
        context = _get_context()
        context.update(kwargs)


@contextmanager
def log_context(**kwargs: Any):
    """
    Functional context manager for log context.

    Usage:
        with log_context(request_id="abc", user="alice"):
            logger.info("Hello")
    """
    with LogContext(**kwargs):
        yield


def with_request_id(func: Callable) -> Callable:
    """
    Decorator that adds a unique request_id to log context.

    Usage:
        @with_request_id
        def handle_request(request):
            logger.info("Processing")  # Includes request_id
    """

    @wraps(func)
    def wrapper(*args, **kwargs):
        request_id = str(uuid.uuid4())[:8]
        with LogContext(request_id=request_id):
            return func(*args, **kwargs)

    @wraps(func)
    async def async_wrapper(*args, **kwargs):
        request_id = str(uuid.uuid4())[:8]
        with LogContext(request_id=request_id):
            return await func(*args, **kwargs)

    import asyncio

    if asyncio.iscoroutinefunction(func):
        return async_wrapper
    return wrapper


# ============================================================
# LOGGING CONFIGURATION
# ============================================================


def configure_logging(
    level: str = "INFO",
    json_format: bool = True,
    log_file: Optional[str] = None,
    max_bytes: int = 10 * 1024 * 1024,  # 10 MB
    backup_count: int = 5,
    include_sensitive_filter: bool = True,
    static_fields: Optional[Dict[str, Any]] = None,
    logger_levels: Optional[Dict[str, str]] = None,
) -> None:
    """
    Configure application logging.

    Args:
        level: Default log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
        json_format: Use JSON formatting (recommended for production)
        log_file: Optional file path for log output
        max_bytes: Max size of each log file before rotation
        backup_count: Number of backup files to keep
        include_sensitive_filter: Add filter to redact sensitive data
        static_fields: Static fields to include in all JSON logs
        logger_levels: Per-logger level overrides

    Example:
        # Development
        configure_logging(level="DEBUG", json_format=False)

        # Production
        configure_logging(
            level="INFO",
            json_format=True,
            log_file="/var/log/familiar/app.log",
            static_fields={"service": "familiar", "env": "production"}
        )
    """
    # Get root logger
    root_logger = logging.getLogger()
    root_logger.setLevel(getattr(logging, level.upper()))

    # Clear existing handlers
    root_logger.handlers.clear()

    # Create formatter
    if json_format:
        formatter = JSONFormatter(
            include_process=True,
            include_thread=True,
            static_fields=static_fields or {},
        )
    else:
        formatter = logging.Formatter(
            fmt="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
            datefmt="%Y-%m-%d %H:%M:%S",
        )

    # Console handler
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setFormatter(formatter)

    if include_sensitive_filter:
        console_handler.addFilter(SensitiveDataFilter())

    root_logger.addHandler(console_handler)

    # File handler (if specified)
    if log_file:
        log_path = Path(log_file)
        log_path.parent.mkdir(parents=True, exist_ok=True)

        file_handler = RotatingFileHandler(
            log_file,
            maxBytes=max_bytes,
            backupCount=backup_count,
        )
        file_handler.setFormatter(formatter)

        if include_sensitive_filter:
            file_handler.addFilter(SensitiveDataFilter())

        root_logger.addHandler(file_handler)

    # Set per-logger levels
    if logger_levels:
        for logger_name, logger_level in logger_levels.items():
            logging.getLogger(logger_name).setLevel(getattr(logging, logger_level.upper()))

    # Reduce noise from common libraries
    logging.getLogger("urllib3").setLevel(logging.WARNING)
    logging.getLogger("asyncio").setLevel(logging.WARNING)
    logging.getLogger("httpx").setLevel(logging.WARNING)


def get_logger(name: str) -> logging.Logger:
    """
    Get a logger with the specified name.

    Convenience function that returns a properly configured logger.

    Args:
        name: Logger name (typically __name__)

    Returns:
        Configured logger instance
    """
    return logging.getLogger(name)


# ============================================================
# METRICS HELPERS
# ============================================================


class LogMetrics:
    """
    Simple metrics collection via structured logs.

    Logs metrics in a format easily parsed by log aggregators.

    Usage:
        metrics = LogMetrics("my_service")
        metrics.increment("requests_total", tags={"endpoint": "/api/chat"})
        metrics.gauge("active_sessions", 42)
        metrics.timing("request_duration_ms", 150.5)
    """

    def __init__(self, namespace: str):
        """
        Initialize metrics logger.

        Args:
            namespace: Prefix for all metrics
        """
        self.namespace = namespace
        self.logger = logging.getLogger(f"metrics.{namespace}")

    def increment(
        self,
        name: str,
        value: int = 1,
        tags: Optional[Dict[str, str]] = None,
    ) -> None:
        """Log a counter increment."""
        self.logger.info(
            "metric",
            extra={
                "metric_type": "counter",
                "metric_name": f"{self.namespace}.{name}",
                "metric_value": value,
                "metric_tags": tags or {},
            },
        )

    def gauge(
        self,
        name: str,
        value: float,
        tags: Optional[Dict[str, str]] = None,
    ) -> None:
        """Log a gauge value."""
        self.logger.info(
            "metric",
            extra={
                "metric_type": "gauge",
                "metric_name": f"{self.namespace}.{name}",
                "metric_value": value,
                "metric_tags": tags or {},
            },
        )

    def timing(
        self,
        name: str,
        value_ms: float,
        tags: Optional[Dict[str, str]] = None,
    ) -> None:
        """Log a timing measurement."""
        self.logger.info(
            "metric",
            extra={
                "metric_type": "timing",
                "metric_name": f"{self.namespace}.{name}",
                "metric_value": value_ms,
                "metric_unit": "ms",
                "metric_tags": tags or {},
            },
        )

    @contextmanager
    def timer(self, name: str, tags: Optional[Dict[str, str]] = None):
        """
        Context manager for timing operations.

        Usage:
            with metrics.timer("db_query", tags={"table": "users"}):
                result = db.query(...)
        """
        import time

        start = time.perf_counter()
        try:
            yield
        finally:
            elapsed_ms = (time.perf_counter() - start) * 1000
            self.timing(name, elapsed_ms, tags)


# ============================================================
# REQUEST LOGGING MIDDLEWARE HELPER
# ============================================================


class RequestLogger:
    """
    Helper for logging HTTP-style requests.

    Provides consistent request/response logging format.

    Usage:
        req_logger = RequestLogger()

        with req_logger.log_request(method="POST", path="/api/chat") as ctx:
            # Process request
            ctx.set_status(200)
            ctx.set_response_size(1024)
    """

    def __init__(self, logger_name: str = "requests"):
        """Initialize request logger."""
        self.logger = logging.getLogger(logger_name)

    @contextmanager
    def log_request(
        self,
        method: str,
        path: str,
        user_id: Optional[str] = None,
        extra: Optional[Dict[str, Any]] = None,
    ):
        """
        Context manager for request logging.

        Logs request start and completion with timing.
        """
        import time

        request_id = str(uuid.uuid4())[:8]
        start_time = time.perf_counter()

        # Request context
        ctx = {
            "request_id": request_id,
            "method": method,
            "path": path,
            "user_id": user_id,
            "status": None,
            "response_size": None,
            "error": None,
        }
        if extra:
            ctx.update(extra)

        # Wrap context with setter methods
        class RequestContext:
            def set_status(self, status: int):
                ctx["status"] = status

            def set_response_size(self, size: int):
                ctx["response_size"] = size

            def set_error(self, error: str):
                ctx["error"] = error

            def add_field(self, key: str, value: Any):
                ctx[key] = value

        request_ctx = RequestContext()

        # Log request start
        with LogContext(request_id=request_id):
            self.logger.info(
                f"Request started: {method} {path}", extra={"event": "request_start", **ctx}
            )

            try:
                yield request_ctx
            except Exception as e:
                ctx["status"] = 500
                ctx["error"] = str(e)
                raise
            finally:
                # Calculate duration
                duration_ms = (time.perf_counter() - start_time) * 1000
                ctx["duration_ms"] = round(duration_ms, 2)

                # Log request completion
                log_level = logging.INFO if (ctx.get("status") or 0) < 400 else logging.WARNING
                self.logger.log(
                    log_level,
                    f"Request completed: {method} {path} - {ctx.get('status', 'unknown')} ({duration_ms:.1f}ms)",
                    extra={"event": "request_complete", **ctx},
                )
