"""
JH Logger - Core logging functionality.

This module provides the main ConfigurableLogger class and convenience functions
for structured JSON logging with optional CloudWatch, Sentry, OpenTelemetry,
and Temporal integrations.
"""

import contextlib
import datetime
import inspect
import logging
import os
import sys
import traceback
import uuid
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Union

import structlog

# ---------------------------------------------------------------------------
# Optional integrations — graceful degradation when not installed
# ---------------------------------------------------------------------------
try:
    import sentry_sdk
except ImportError:
    sentry_sdk = None  # type: ignore[assignment]

try:
    from watchtower import CloudWatchLogHandler
except ImportError:
    CloudWatchLogHandler = None  # type: ignore[assignment, misc]

try:
    from opentelemetry import trace as _otel_trace
except ImportError:
    _otel_trace = None  # type: ignore[assignment]

try:
    from temporalio import activity as _temporal_activity
    from temporalio import workflow as _temporal_workflow
except ImportError:
    _temporal_activity = None  # type: ignore[assignment]
    _temporal_workflow = None  # type: ignore[assignment]

# Packages whose frames should be skipped when resolving the real caller.
_SKIP_MODULES = frozenset({"jhlogger", "structlog", "logging"})


# ---------------------------------------------------------------------------
# Standalone structlog processors for optional context injection
# ---------------------------------------------------------------------------
def _add_otel_context(
    logger: Any, method_name: str, event_dict: Dict[str, Any]
) -> Dict[str, Any]:
    """Inject OpenTelemetry trace_id and span_id when an active span exists."""
    if _otel_trace is None:
        return event_dict
    span = _otel_trace.get_current_span()
    ctx = span.get_span_context()
    if ctx and ctx.is_valid:
        event_dict["trace_id"] = format(ctx.trace_id, "032x")
        event_dict["span_id"] = format(ctx.span_id, "016x")
    return event_dict


def _add_temporal_context(
    logger: Any, method_name: str, event_dict: Dict[str, Any]
) -> Dict[str, Any]:
    """Inject Temporal workflow/activity context when running inside Temporal."""
    if _temporal_activity is not None:
        try:
            info = _temporal_activity.info()
            event_dict["workflow_id"] = info.workflow_id
            event_dict["activity_type"] = info.activity_type
            event_dict["task_queue"] = info.task_queue
            event_dict["attempt"] = info.attempt
            return event_dict
        except Exception:
            pass
    if _temporal_workflow is not None:
        try:
            info = _temporal_workflow.info()
            event_dict["workflow_id"] = info.workflow_id
            event_dict["workflow_type"] = info.workflow_type
            event_dict["task_queue"] = info.task_queue
            return event_dict
        except Exception:
            pass
    return event_dict


# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------
class LogLevel(Enum):
    """Enumeration for log levels."""

    DEBUG = "DEBUG"
    INFO = "INFO"
    WARNING = "WARNING"
    ERROR = "ERROR"
    CRITICAL = "CRITICAL"


# ---------------------------------------------------------------------------
# BoundConfigurableLogger — preserves jhlogger API after .bind()
# ---------------------------------------------------------------------------
class BoundConfigurableLogger:
    """
    A bound logger that preserves ConfigurableLogger's API.

    Returned by ``ConfigurableLogger.bind()`` so that bound loggers retain
    the ``error(exception=)`` signature, Sentry integration, and other
    jhlogger features that a raw ``structlog.BoundLogger`` would lose.
    """

    def __init__(
        self, parent: "ConfigurableLogger", bound_logger: structlog.BoundLogger
    ) -> None:
        self._parent = parent
        self._logger = bound_logger

    def debug(self, message: str, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None:
        """Log a DEBUG level message."""
        log_data = self._parent._format_log_entry("DEBUG", message, data)
        self._logger.debug(message, **log_data, **kwargs)

    def info(self, message: str, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None:
        """Log an INFO level message."""
        log_data = self._parent._format_log_entry("INFO", message, data)
        self._logger.info(message, **log_data, **kwargs)

    def warning(self, message: str, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None:
        """Log a WARNING level message."""
        log_data = self._parent._format_log_entry("WARNING", message, data)
        self._logger.warning(message, **log_data, **kwargs)

    def warn(self, message: str, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None:
        """Alias for warning method."""
        self.warning(message, data, **kwargs)

    def error(
        self,
        message: str,
        data: Optional[Dict[str, Any]] = None,
        exception: Optional[Exception] = None,
        **kwargs: Any,
    ) -> None:
        """Log an ERROR level message."""
        log_data = self._parent._format_log_entry("ERROR", message, data, exception)
        self._logger.error(message, **log_data, **kwargs)

        if self._parent.enable_sentry and exception and sentry_sdk is not None:
            with contextlib.suppress(Exception):
                sentry_sdk.capture_exception(exception)

    def critical(
        self,
        message: str,
        data: Optional[Dict[str, Any]] = None,
        exception: Optional[Exception] = None,
        **kwargs: Any,
    ) -> None:
        """Log a CRITICAL level message."""
        log_data = self._parent._format_log_entry("CRITICAL", message, data, exception)
        self._logger.critical(message, **log_data, **kwargs)

        if self._parent.enable_sentry and exception and sentry_sdk is not None:
            with contextlib.suppress(Exception):
                sentry_sdk.capture_exception(exception)

    def log(
        self,
        level: Union[LogLevel, str],
        message: str,
        data: Optional[Dict[str, Any]] = None,
        exception: Optional[Exception] = None,
        **kwargs: Any,
    ) -> None:
        """Log a message at the specified level."""
        normalized_level = self._parent._normalize_log_level(level)

        if normalized_level not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
            raise ValueError(f"Invalid log level: {level}")

        log_data = self._parent._format_log_entry(normalized_level, message, data, exception)

        if normalized_level == "DEBUG":
            self._logger.debug(message, **log_data, **kwargs)
        elif normalized_level == "INFO":
            self._logger.info(message, **log_data, **kwargs)
        elif normalized_level == "WARNING":
            self._logger.warning(message, **log_data, **kwargs)
        elif normalized_level == "ERROR":
            self._logger.error(message, **log_data, **kwargs)
            if self._parent.enable_sentry and exception and sentry_sdk is not None:
                with contextlib.suppress(Exception):
                    sentry_sdk.capture_exception(exception)
        elif normalized_level == "CRITICAL":
            self._logger.critical(message, **log_data, **kwargs)
            if self._parent.enable_sentry and exception and sentry_sdk is not None:
                with contextlib.suppress(Exception):
                    sentry_sdk.capture_exception(exception)

    def bind(self, **kwargs: Any) -> "BoundConfigurableLogger":
        """Create a new bound logger with additional context."""
        return BoundConfigurableLogger(self._parent, self._logger.bind(**kwargs))

    def unbind(self, *keys: str) -> "BoundConfigurableLogger":
        """Create a new bound logger without the specified keys."""
        return BoundConfigurableLogger(self._parent, self._logger.unbind(*keys))

    def get_logger(self) -> structlog.BoundLogger:
        """Get the underlying structlog logger."""
        return self._logger


# ---------------------------------------------------------------------------
# ConfigurableLogger — the main public class
# ---------------------------------------------------------------------------
class ConfigurableLogger:
    """
    A feature-rich, configurable logging module with structured JSON output.

    This logger provides comprehensive logging capabilities including:
    - Structured JSON logging with configurable formatting
    - CloudWatch integration for AWS environments
    - Sentry error tracking
    - OpenTelemetry trace context injection
    - Temporal workflow/activity context injection
    - Enhanced debugging with caller information
    - Configurable log levels and custom processors
    """

    def __init__(
        self,
        name: str = "configurable-logger",
        log_level: Union[LogLevel, str] = LogLevel.INFO,
        enable_cloudwatch: bool = True,
        enable_sentry: bool = True,
        enable_bugsnag: bool = True,
        cloudwatch_log_group: Optional[str] = None,
        include_system_info: bool = True,
        custom_processors: Optional[List[Callable]] = None,
        json_indent: Optional[int] = 4,
        enable_otel_context: bool = True,
        enable_temporal_context: bool = True,
    ):
        """
        Initialize the ConfigurableLogger.

        Args:
            name: Logger name/identifier
            log_level: Minimum log level (LogLevel enum or string)
            enable_cloudwatch: Enable CloudWatch integration
            enable_sentry: Enable Sentry error tracking
            enable_bugsnag: Enable Bugsnag integration (legacy support)
            cloudwatch_log_group: CloudWatch log group name
            include_system_info: Include system metadata in logs
            custom_processors: List of custom structlog processors
            json_indent: JSON indentation level (default 4; use None for
                compact output in production)
            enable_otel_context: Inject OpenTelemetry trace_id / span_id
                into every log entry (requires ``opentelemetry-api``)
            enable_temporal_context: Inject Temporal workflow / activity
                metadata into every log entry (requires ``temporalio``)
        """
        self.name = name
        # Create a unique stdlib logger name to prevent interference between instances
        self._stdlib_logger_name = f"{name}.{uuid.uuid4().hex[:8]}"
        self.log_level = self._normalize_log_level(log_level)
        self.enable_cloudwatch = enable_cloudwatch
        self.enable_sentry = enable_sentry
        self.enable_bugsnag = enable_bugsnag
        self.cloudwatch_log_group = cloudwatch_log_group
        self.include_system_info = include_system_info
        self.custom_processors = custom_processors or []
        self.json_indent = json_indent
        self.enable_otel_context = enable_otel_context
        self.enable_temporal_context = enable_temporal_context

        # Environment variables
        self.app_env = os.getenv("APP_ENV", "development")
        self.country = os.getenv("COUNTRY", "Unknown")

        # Configure the logger
        self.logger = self._configure_logger()

    def _normalize_log_level(self, level: Union[LogLevel, str]) -> str:
        """Normalize log level to string format."""
        if isinstance(level, LogLevel):
            return level.value
        if isinstance(level, str):
            return level.upper()
        raise ValueError(f"Invalid log level: {level}")

    def _get_caller_info(self) -> Dict[str, Any]:
        """
        Get information about the calling function/method.

        Walks the stack dynamically, skipping frames that belong to jhlogger,
        structlog, or stdlib logging so that the reported caller is the
        application code that initiated the log call.
        """
        try:
            frame = inspect.currentframe()
            if not frame:
                return {}

            # Walk past internal frames to find the real caller
            frame = frame.f_back
            while frame:
                try:
                    module = frame.f_globals.get("__name__", "") or ""
                except (AttributeError, TypeError):
                    module = ""
                top_package = module.split(".")[0] if module else ""
                if top_package not in _SKIP_MODULES:
                    break
                frame = frame.f_back

            if not frame:
                return {}

            filename = frame.f_code.co_filename
            function_name = frame.f_code.co_name
            line_number = frame.f_lineno

            # Extract module name
            module_name = inspect.getmodulename(filename) or "unknown"

            # Try to determine class name if we're in a method
            class_name = None
            if "self" in frame.f_locals:
                class_name = frame.f_locals["self"].__class__.__name__
            elif "cls" in frame.f_locals:
                class_name = frame.f_locals["cls"].__name__

            return {
                "filename": os.path.basename(filename),
                "file_path": filename,
                "function": function_name,
                "line_number": line_number,
                "module": module_name,
                "class": class_name,
            }

        except Exception:
            return {}

    def _add_system_info(
        self, logger: Any, method_name: str, event_dict: Dict[str, Any]
    ) -> Dict[str, Any]:
        """Add system information to log entries."""
        if not self.include_system_info:
            return event_dict

        system_info = {
            "country": self.country,
            "environment": self.app_env,
            "service": self.name,
            "timestamp_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(),
            "process_id": os.getpid(),
        }

        return {**event_dict, **system_info}

    def _add_logger_name_processor(
        self, logger: Any, method_name: str, event_dict: Dict[str, Any]
    ) -> Dict[str, Any]:
        """Add the user-provided logger name to log entries."""
        event_dict["logger"] = self.name
        return event_dict

    def _add_caller_info_processor(
        self, logger: Any, method_name: str, event_dict: Dict[str, Any]
    ) -> Dict[str, Any]:
        """Add caller information to log entries."""
        caller_info = self._get_caller_info()
        if caller_info:
            event_dict["caller"] = caller_info
        return event_dict

    def _configure_logger(self) -> structlog.BoundLogger:
        """Configure and return the structured logger.

        Uses ``structlog.wrap_logger`` for per-instance configuration so that
        creating multiple ``ConfigurableLogger`` instances does not overwrite
        each other's processor chains (unlike the global
        ``structlog.configure``).
        """
        # Build processor chain
        processors: List[Any] = [
            structlog.stdlib.filter_by_level,
            self._add_logger_name_processor,
            structlog.stdlib.add_log_level,
            structlog.stdlib.PositionalArgumentsFormatter(),
            structlog.processors.TimeStamper(fmt="iso"),
            structlog.processors.StackInfoRenderer(),
            structlog.processors.format_exc_info,
            self._add_system_info,
            self._add_caller_info_processor,
        ]

        # Optional context processors (no-ops when packages not installed)
        if self.enable_otel_context:
            processors.append(_add_otel_context)

        if self.enable_temporal_context:
            processors.append(_add_temporal_context)

        # Add custom processors
        processors.extend(self.custom_processors)

        # Add JSON processor for formatting
        processors.append(
            structlog.processors.JSONRenderer(
                indent=self.json_indent, sort_keys=True, ensure_ascii=False
            )
        )

        # Get standard library logger with unique name to prevent interference
        stdlib_logger = logging.getLogger(self._stdlib_logger_name)
        stdlib_logger.setLevel(getattr(logging, self.log_level))

        # Clear existing handlers to avoid duplication
        stdlib_logger.handlers.clear()

        # Add console handler
        console_handler = logging.StreamHandler(sys.stdout)
        console_handler.setLevel(getattr(logging, self.log_level))
        stdlib_logger.addHandler(console_handler)

        # Add CloudWatch handler if enabled and available
        if (
            self.enable_cloudwatch
            and self.cloudwatch_log_group
            and CloudWatchLogHandler is not None
        ):
            try:
                cloudwatch_handler = CloudWatchLogHandler(
                    log_group=self.cloudwatch_log_group,
                    stream_name=f"{self.name}-{os.getpid()}",
                )
                cloudwatch_handler.setLevel(getattr(logging, self.log_level))
                stdlib_logger.addHandler(cloudwatch_handler)
            except Exception:
                pass

        return structlog.wrap_logger(  # type: ignore[return-value]
            stdlib_logger,
            processors=processors,
            wrapper_class=structlog.stdlib.BoundLogger,
        )

    def _format_log_entry(
        self,
        level: str,
        message: str,
        data: Optional[Dict[str, Any]] = None,
        exception: Optional[Exception] = None,
    ) -> Dict[str, Any]:
        """Format a log entry with all metadata."""
        entry: Dict[str, Any] = {
            "message": message,
            "level": level,
        }

        if data:
            entry["data"] = data

        if exception:
            entry["exception"] = {
                "type": type(exception).__name__,
                "message": str(exception),
                "traceback": (
                    traceback.format_exception(type(exception), exception, exception.__traceback__)
                    if exception.__traceback__
                    else []
                ),
            }

        return entry

    def debug(self, message: str, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None:
        """Log a DEBUG level message."""
        log_data = self._format_log_entry("DEBUG", message, data)
        self.logger.debug(message, **log_data, **kwargs)

    def info(self, message: str, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None:
        """Log an INFO level message."""
        log_data = self._format_log_entry("INFO", message, data)
        self.logger.info(message, **log_data, **kwargs)

    def warning(self, message: str, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None:
        """Log a WARNING level message."""
        log_data = self._format_log_entry("WARNING", message, data)
        self.logger.warning(message, **log_data, **kwargs)

    def warn(self, message: str, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None:
        """Alias for warning method."""
        self.warning(message, data, **kwargs)

    def error(
        self,
        message: str,
        data: Optional[Dict[str, Any]] = None,
        exception: Optional[Exception] = None,
        **kwargs: Any,
    ) -> None:
        """Log an ERROR level message."""
        log_data = self._format_log_entry("ERROR", message, data, exception)
        self.logger.error(message, **log_data, **kwargs)

        # Send to Sentry if enabled and exception provided
        if self.enable_sentry and exception and sentry_sdk is not None:
            with contextlib.suppress(Exception):
                sentry_sdk.capture_exception(exception)

    def critical(
        self,
        message: str,
        data: Optional[Dict[str, Any]] = None,
        exception: Optional[Exception] = None,
        **kwargs: Any,
    ) -> None:
        """Log a CRITICAL level message."""
        log_data = self._format_log_entry("CRITICAL", message, data, exception)
        self.logger.critical(message, **log_data, **kwargs)

        # Send to Sentry if enabled and exception provided
        if self.enable_sentry and exception and sentry_sdk is not None:
            with contextlib.suppress(Exception):
                sentry_sdk.capture_exception(exception)

    def log(
        self,
        level: Union[LogLevel, str],
        message: str,
        data: Optional[Dict[str, Any]] = None,
        exception: Optional[Exception] = None,
        **kwargs: Any,
    ) -> None:
        """Log a message at the specified level."""
        normalized_level = self._normalize_log_level(level)

        if normalized_level not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
            raise ValueError(f"Invalid log level: {level}")

        log_data = self._format_log_entry(normalized_level, message, data, exception)

        if normalized_level == "DEBUG":
            self.logger.debug(message, **log_data, **kwargs)
        elif normalized_level == "INFO":
            self.logger.info(message, **log_data, **kwargs)
        elif normalized_level == "WARNING":
            self.logger.warning(message, **log_data, **kwargs)
        elif normalized_level == "ERROR":
            self.logger.error(message, **log_data, **kwargs)
            if self.enable_sentry and exception and sentry_sdk is not None:
                with contextlib.suppress(Exception):
                    sentry_sdk.capture_exception(exception)
        elif normalized_level == "CRITICAL":
            self.logger.critical(message, **log_data, **kwargs)
            if self.enable_sentry and exception and sentry_sdk is not None:
                with contextlib.suppress(Exception):
                    sentry_sdk.capture_exception(exception)

    def bind(self, **kwargs: Any) -> BoundConfigurableLogger:
        """Create a bound logger with additional context.

        Unlike the previous behaviour (which returned a raw
        ``structlog.BoundLogger``), the returned
        :class:`BoundConfigurableLogger` retains the full jhlogger API
        including the ``error(exception=...)`` signature and Sentry
        integration.
        """
        return BoundConfigurableLogger(self, self.logger.bind(**kwargs))

    def set_level(self, level: Union[LogLevel, str]) -> None:
        """Dynamically change the log level."""
        self.log_level = self._normalize_log_level(level)

        # Update the standard library logger level using unique name
        stdlib_logger = logging.getLogger(self._stdlib_logger_name)
        stdlib_logger.setLevel(getattr(logging, self.log_level))

        # Update all handlers
        for handler in stdlib_logger.handlers:
            handler.setLevel(getattr(logging, self.log_level))

    def get_logger(self) -> structlog.BoundLogger:
        """Get the underlying structlog logger."""
        return self.logger


# ---------------------------------------------------------------------------
# Global default logger & convenience functions
# ---------------------------------------------------------------------------
_default_logger = None


def _get_default_logger() -> ConfigurableLogger:
    """Get or create the default logger instance."""
    global _default_logger
    if _default_logger is None:
        _default_logger = ConfigurableLogger()
    return _default_logger


def create_logger(
    name: str = "configurable-logger",
    log_level: Union[LogLevel, str] = LogLevel.INFO,
    **kwargs: Any,
) -> ConfigurableLogger:
    """
    Create a new ConfigurableLogger instance.

    Args:
        name: Logger name
        log_level: Minimum log level
        **kwargs: Additional logger configuration options

    Returns:
        ConfigurableLogger instance
    """
    return ConfigurableLogger(name=name, log_level=log_level, **kwargs)


# Convenience functions for module-level logging
def debug(message: str, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None:
    """Log a DEBUG level message using the default logger."""
    _get_default_logger().debug(message, data, **kwargs)


def info(message: str, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None:
    """Log an INFO level message using the default logger."""
    _get_default_logger().info(message, data, **kwargs)


def warning(message: str, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None:
    """Log a WARNING level message using the default logger."""
    _get_default_logger().warning(message, data, **kwargs)


def warn(message: str, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None:
    """Alias for warning function."""
    warning(message, data, **kwargs)


def error(
    message: str,
    data: Optional[Dict[str, Any]] = None,
    exception: Optional[Exception] = None,
    **kwargs: Any,
) -> None:
    """Log an ERROR level message using the default logger."""
    _get_default_logger().error(message, data, exception, **kwargs)


def critical(
    message: str,
    data: Optional[Dict[str, Any]] = None,
    exception: Optional[Exception] = None,
    **kwargs: Any,
) -> None:
    """Log a CRITICAL level message using the default logger."""
    _get_default_logger().critical(message, data, exception, **kwargs)


def log(
    level: Union[LogLevel, str],
    message: str,
    data: Optional[Dict[str, Any]] = None,
    exception: Optional[Exception] = None,
    **kwargs: Any,
) -> None:
    """Log a message at the specified level using the default logger."""
    _get_default_logger().log(level, message, data, exception, **kwargs)
