"""Centralized logging configuration with dual output.

Configure structlog with Rich console + JSON file output for both development
and observability use cases.
"""

from __future__ import annotations

import logging
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, cast

import structlog
from structlog.typing import ExceptionRenderer

from .processors import build_processor_chain

if TYPE_CHECKING:
    from collections.abc import Sequence

    from structlog.typing import Processor

# Default log level constant
_DEFAULT_LEVEL = "INFO"

# Rich console width for traceback formatting
_RICH_CONSOLE_WIDTH = 120


@dataclass(frozen=True, slots=True)
class LoggingConfig:
    """Configuration for centralized logging.

    Attributes:
        level: Minimum log level (DEBUG, INFO, WARNING, ERROR).
        log_format: Output format - "auto" (TTY detection), "console" (Rich), "json".
        json_file: Optional path for JSON log file output.
        enable_console: Enable Rich console output.
        enable_log_buffer: Feed logs to in-memory LogBuffer for UI streaming.
        enable_otel_context: Include OpenTelemetry trace/span IDs.
        enable_noteflow_context: Include request_id, user_id, workspace_id.
        console_colors: Enable Rich colors (auto-detect TTY if not set).
    """

    level: str = _DEFAULT_LEVEL
    log_format: str = "auto"  # "auto", "console", "json"
    json_file: Path | None = None
    enable_console: bool = True
    enable_log_buffer: bool = True
    enable_otel_context: bool = True
    enable_noteflow_context: bool = True
    console_colors: bool = True


# Log level name to constant mapping
_LEVEL_MAP: dict[str, int] = {
    "DEBUG": logging.DEBUG,
    "INFO": logging.INFO,
    "WARNING": logging.WARNING,
    "ERROR": logging.ERROR,
    "CRITICAL": logging.CRITICAL,
}


def get_log_level(level_name: str) -> int:
    """Convert level name to logging constant."""
    return _LEVEL_MAP.get(level_name.upper(), logging.INFO)


def _should_use_console(config: LoggingConfig) -> bool:
    """Determine if console (Rich) output should be used.

    Returns True for Rich console output, False for JSON.
    """
    log_format = config.log_format.lower()
    if log_format == "json":
        return False
    return True if log_format == "console" else sys.stderr.isatty()


def create_renderer(config: LoggingConfig) -> Processor:
    """Create the appropriate renderer based on configuration.

    Uses Rich console rendering for TTY output with colors and formatting,
    JSON for non-TTY or production environments.
    """
    if not _should_use_console(config):
        return structlog.processors.JSONRenderer()

    # Use Rich console renderer for beautiful TTY output
    from rich.console import Console
    from rich.traceback import install as install_rich_traceback

    # Install Rich traceback handler for better exception formatting
    install_rich_traceback(show_locals=False, width=_RICH_CONSOLE_WIDTH, suppress=[structlog])

    Console(stderr=True, force_terminal=config.console_colors)
    return structlog.dev.ConsoleRenderer(
        colors=config.console_colors,
        exception_formatter=cast(ExceptionRenderer, structlog.dev.rich_traceback),
    )


def _configure_structlog(processors: Sequence[Processor]) -> None:
    """Configure structlog with the processor chain."""
    structlog.configure(
        processors=[*processors, structlog.stdlib.ProcessorFormatter.wrap_for_formatter],
        wrapper_class=structlog.stdlib.BoundLogger,
        logger_factory=structlog.stdlib.LoggerFactory(),
        cache_logger_on_first_use=True,
    )


def _setup_handlers(
    config: LoggingConfig,
    log_level: int,
    processors: Sequence[Processor],
    renderer: Processor,
) -> None:
    """Configure and attach handlers to the root logger."""
    formatter = structlog.stdlib.ProcessorFormatter(
        foreign_pre_chain=processors,
        processors=[structlog.stdlib.ProcessorFormatter.remove_processors_meta, renderer],
    )

    root_logger = logging.getLogger()
    root_logger.setLevel(log_level)

    # Clear existing handlers
    for handler in root_logger.handlers[:]:
        root_logger.removeHandler(handler)

    if config.enable_console:
        console_handler = logging.StreamHandler(sys.stderr)
        console_handler.setFormatter(formatter)
        console_handler.setLevel(log_level)
        root_logger.addHandler(console_handler)

    if config.json_file is not None:
        json_formatter = structlog.stdlib.ProcessorFormatter(
            foreign_pre_chain=processors,
            processors=[
                structlog.stdlib.ProcessorFormatter.remove_processors_meta,
                structlog.processors.JSONRenderer(),
            ],
        )
        file_handler = logging.FileHandler(config.json_file)
        file_handler.setFormatter(json_formatter)
        file_handler.setLevel(log_level)
        root_logger.addHandler(file_handler)

    if config.enable_log_buffer:
        from .log_buffer import LogBufferHandler, get_log_buffer

        buffer_handler = LogBufferHandler(buffer=get_log_buffer(), level=log_level)
        root_logger.addHandler(buffer_handler)


def configure_logging(
    config: LoggingConfig | None = None,
    *,
    level: str = _DEFAULT_LEVEL,
    json_file: Path | None = None,
) -> None:
    """Configure centralized logging with dual output.

    Call once at application startup. Configures both structlog and stdlib
    logging for seamless integration.

    Args:
        config: Full configuration object, or use keyword args.
        level: Log level (DEBUG, INFO, WARNING, ERROR).
        json_file: Optional path for JSON log file.
    """
    if config is None:
        config = LoggingConfig(level=level, json_file=json_file)

    log_level = get_log_level(config.level)
    processors = build_processor_chain(config)
    renderer = create_renderer(config)

    _configure_structlog(processors)
    _setup_handlers(config, log_level, processors, renderer)


def get_logger(name: str | None = None) -> structlog.stdlib.BoundLogger:
    """Get a structlog logger instance.

    Args:
        name: Optional logger name (defaults to calling module).

    Returns:
        Configured structlog BoundLogger.
    """
    return structlog.get_logger(name)
