"""OTelLoggingHandler: Python logging.Handler producing OTel-compliant JSON logs.

Subclasses ``logging.Handler`` and wires together LogFormatter, SeverityMapper,
TraceContextExtractor, and ResourceProvider to produce structured JSON log
output conforming to the Standard Log Format schema.

Supports configuration via ResourceConfig or environment variables:
    OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION, OTEL_DEPLOYMENT_ENVIRONMENT

When an OTLPExporter is provided (or auto-configured via OTEL_EXPORTER_OTLP_ENDPOINT),
each emitted record is forwarded to the exporter.  Otherwise, JSON is written to
the configured stream (console-only mode).

Requirements: 5.1-5.5, 9.1
"""

from __future__ import annotations

import logging
import os
import sys
import traceback
from datetime import datetime, timezone
from typing import Any, Dict, IO, Optional

from .log_formatter import LogFormatter
from .log_record import LogRecord
from .otlp_exporter import ExporterConfig, OTLPExporter
from .resource_provider import ResourceConfig, ResourceProvider
from .severity_mapper import SeverityMapper
from .trace_context import TraceContextExtractor


class OTelLoggingHandler(logging.Handler):
    """Python logging.Handler that produces OTel-compliant JSON logs.

    Each emitted ``logging.LogRecord`` is mapped to an OTel ``LogRecord``,
    serialized to JSON via ``LogFormatter``, and either:
      - forwarded to the configured ``OTLPExporter`` (when set), or
      - written as JSON to the configured stream (console-only mode).

    Environment variable overrides (take precedence over ResourceConfig):
        - ``OTEL_SERVICE_NAME``          -> ``service.name``
        - ``OTEL_SERVICE_VERSION``       -> ``service.version``
        - ``OTEL_DEPLOYMENT_ENVIRONMENT``-> ``deployment.environment``
        - ``OTEL_EXPORTER_OTLP_LOGS_ENDPOINT``-> auto-configure OTLPExporter when
                                           no explicit exporter is provided
    """

    def __init__(
        self,
        resource_config: Optional[ResourceConfig] = None,
        stream: Optional[IO] = None,
        level: int = logging.NOTSET,
        exporter: Optional[OTLPExporter] = None,
    ) -> None:
        super().__init__(level)
        self._stream = stream or sys.stderr
        self._formatter_component = LogFormatter()
        self._severity_mapper = SeverityMapper()
        self._trace_extractor = TraceContextExtractor()

        # Build resource config with env var overrides
        config = resource_config or ResourceConfig()
        config = self._apply_env_overrides(config)
        self._resource_provider = ResourceProvider(config)

        # Exporter: use explicit exporter, or auto-configure from env var.
        if exporter is not None:
            self._exporter: Optional[OTLPExporter] = exporter
        else:
            endpoint = os.environ.get("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "")
            if endpoint:
                self._exporter = OTLPExporter(ExporterConfig(endpoint=endpoint, protocol="http"))
            else:
                self._exporter = None

    @staticmethod
    def _apply_env_overrides(config: ResourceConfig) -> ResourceConfig:
        """Apply environment variable overrides to a ResourceConfig."""
        service_name = os.environ.get("OTEL_SERVICE_NAME")
        service_version = os.environ.get("OTEL_SERVICE_VERSION")
        deployment_env = os.environ.get("OTEL_DEPLOYMENT_ENVIRONMENT")

        return ResourceConfig(
            service_name=service_name if service_name else config.service_name,
            service_version=service_version if service_version else config.service_version,
            deployment_environment=deployment_env if deployment_env else config.deployment_environment,
            custom_attributes=config.custom_attributes,
        )

    def emit(self, record: logging.LogRecord) -> None:
        """Emit a logging.LogRecord as OTel-compliant JSON.

        Steps:
            1. Map Python log level to SeverityNumber/SeverityText
            2. Extract trace context from active OTel span
            3. Build LogRecord with resource attributes
            4. Handle exc_info (exception type, message, stacktrace)
            5. Serialize to JSON via LogFormatter
            6. Export via OTLPExporter (if configured) or write to stream
        """
        try:
            # 1. Map severity
            severity_number = self._severity_mapper.to_severity_number(record.levelno)
            severity_text = self._severity_mapper.to_severity_text(record.levelno)

            # 2. Extract trace context
            trace_ctx = self._trace_extractor.extract()

            # 3. Build resource
            resource = self._resource_provider.get_resource()

            # 4. Build attributes from extra fields and exc_info
            attributes = self._build_attributes(record)

            # 5. Build OTel LogRecord
            timestamp = datetime.fromtimestamp(record.created, tz=timezone.utc)
            otel_record = LogRecord(
                timestamp=timestamp,
                observed_timestamp=timestamp,
                severity_text=severity_text,
                severity_number=severity_number,
                body=record.getMessage(),
                resource=resource,
                attributes=attributes if attributes else None,
                trace_id=trace_ctx.trace_id if trace_ctx else None,
                span_id=trace_ctx.span_id if trace_ctx else None,
                trace_flags=trace_ctx.trace_flags if trace_ctx else None,
            )

            # 6. Export or write to stream
            if self._exporter is not None:
                self._exporter.export([otel_record])
            else:
                json_line = self._formatter_component.serialize(otel_record)
                self._stream.write(json_line + "\n")
                self._stream.flush()
        except Exception:
            self.handleError(record)

    def _build_attributes(self, record: logging.LogRecord) -> Optional[Dict[str, Any]]:
        """Build attributes dict from logging.LogRecord extras and exc_info.

        Extracts extra attributes set on the record (beyond standard fields)
        and, when exc_info is present, adds exception.type, exception.message,
        and exception.stacktrace per OTel semantic conventions.
        """
        attrs: Dict[str, Any] = {}

        # Collect extra attributes (non-standard fields on the LogRecord)
        _STANDARD_ATTRS = {
            "name", "msg", "args", "created", "relativeCreated", "exc_info",
            "exc_text", "stack_info", "lineno", "funcName", "pathname",
            "filename", "module", "thread", "threadName", "process",
            "processName", "levelname", "levelno", "msecs", "message",
            "taskName",
        }
        for key, value in record.__dict__.items():
            if key.startswith("_") or key in _STANDARD_ATTRS:
                continue
            # Skip empty string keys per Req 11.4
            if not key:
                continue
            attrs[key] = value

        # Handle exc_info -> exception attributes (Req 5.5)
        if record.exc_info and record.exc_info[0] is not None:
            exc_type, exc_value, exc_tb = record.exc_info
            attrs["exception.type"] = (
                exc_type.__name__ if hasattr(exc_type, "__name__") else str(exc_type)
            )
            attrs["exception.message"] = str(exc_value) if exc_value else ""
            if exc_tb is not None:
                attrs["exception.stacktrace"] = "".join(
                    traceback.format_exception(exc_type, exc_value, exc_tb)
                )
            else:
                attrs["exception.stacktrace"] = ""

        return attrs if attrs else None
