"""In-memory ring buffer for recent application logs."""

from __future__ import annotations

import logging
from collections import deque
from dataclasses import dataclass, field
from datetime import UTC, datetime
from threading import Lock

from noteflow.domain.utils.time import utc_now


@dataclass(frozen=True, slots=True)
class LogEntry:
    """Single log entry.

    Attributes:
        timestamp: When the log was created.
        level: Log level (debug, info, warning, error).
        source: Source component (app, api, sync, auth, system).
        message: Log message content.
        details: Optional key-value details.
        trace_id: OpenTelemetry trace ID (if available).
        span_id: OpenTelemetry span ID (if available).
    """

    timestamp: datetime
    level: str
    source: str
    message: str
    details: dict[str, str] = field(default_factory=dict)
    trace_id: str | None = None
    span_id: str | None = None


class LogBuffer:
    """Thread-safe ring buffer for recent log entries.

    Provides a bounded in-memory store for recent application logs
    that can be queried via the observability gRPC endpoints.
    """

    DEFAULT_CAPACITY = 1000

    def __init__(self, capacity: int = DEFAULT_CAPACITY) -> None:
        """Initialize the log buffer.

        Args:
            capacity: Maximum number of log entries to retain.
        """
        self._buffer: deque[LogEntry] = deque(maxlen=capacity)
        self._lock = Lock()

    def append(self, entry: LogEntry) -> None:
        """Add a log entry to the buffer.

        Thread-safe. Oldest entries are evicted when capacity is reached.

        Args:
            entry: Log entry to store.
        """
        with self._lock:
            self._buffer.append(entry)

    def log(
        self,
        level: str,
        source: str,
        message: str,
        details: dict[str, str] | None = None,
    ) -> None:
        """Create and append a log entry.

        Convenience method that creates a LogEntry with current timestamp.

        Args:
            level: Log level (debug, info, warning, error).
            source: Source component (app, api, sync, auth, system).
            message: Log message.
            details: Optional additional details.
        """
        self.append(
            LogEntry(
                timestamp=utc_now(),
                level=level,
                source=source,
                message=message,
                details=details or {},
            )
        )

    def get_recent(
        self,
        limit: int = 100,
        level: str | None = None,
        source: str | None = None,
    ) -> list[LogEntry]:
        """Get recent log entries, optionally filtered.

        Args:
            limit: Maximum entries to return.
            level: Optional level filter.
            source: Optional source filter.

        Returns:
            Log entries (most recent first).
        """
        with self._lock:
            entries = list(self._buffer)

        # Apply filters
        if level:
            entries = [e for e in entries if e.level == level]
        if source:
            entries = [e for e in entries if e.source == source]

        # Return most recent first, limited
        return list(reversed(entries))[:limit]

    def clear(self) -> None:
        """Clear all log entries."""
        with self._lock:
            self._buffer.clear()

    @property
    def count(self) -> int:
        """Number of entries currently in buffer."""
        with self._lock:
            return len(self._buffer)


def _get_current_trace_context() -> tuple[str | None, str | None]:
    """Extract trace and span IDs from current OpenTelemetry context.

    Returns:
        Tuple of (trace_id, span_id), both None if OTel not available.
    """
    try:
        from opentelemetry import trace

        span = trace.get_current_span()
        if span is trace.INVALID_SPAN:
            return None, None

        ctx = span.get_span_context()
        if not ctx.is_valid:
            return None, None

        # Format as hex strings (standard OTel format)
        trace_id = format(ctx.trace_id, "032x")
        span_id = format(ctx.span_id, "016x")
        return trace_id, span_id
    except Exception:
        return None, None


class LogBufferHandler(logging.Handler):
    """Logging handler that writes log records to the LogBuffer.

    Automatically captures OpenTelemetry trace/span IDs when available.
    """

    def __init__(
        self,
        buffer: LogBuffer | None = None,
        level: int = logging.NOTSET,
    ) -> None:
        """Initialize handler.

        Args:
            buffer: Optional LogBuffer instance to write into.
            level: Logging level threshold for this handler.
        """
        super().__init__(level=level)
        self._buffer = buffer or get_log_buffer()

    def emit(self, record: logging.LogRecord) -> None:
        """Write a log record to the buffer."""
        try:
            record_data = record.__dict__
            get = record_data.get
            timestamp = datetime.fromtimestamp(get("created", 0.0), tz=UTC)
            source = get("source", get("name", ""))
            msg = get("msg", "")
            if args := get("args", None):
                try:
                    message = msg % args
                except (TypeError, ValueError):
                    message = str(msg)
            else:
                message = str(msg)
            details = {
                "logger": get("name", ""),
                "module": get("module", ""),
                "func": get("funcName", ""),
            }

            # Extract trace context if available
            trace_id, span_id = _get_current_trace_context()

            self._buffer.append(
                LogEntry(
                    timestamp=timestamp,
                    level=str(get("levelname", "")).lower(),
                    source=str(source),
                    message=message,
                    details={k: str(v) for k, v in details.items()},
                    trace_id=trace_id,
                    span_id=span_id,
                )
            )
        except (KeyError, TypeError, ValueError):
            self.handleError(record)


# Global singleton
_log_buffer: LogBuffer | None = None


def get_log_buffer() -> LogBuffer:
    """Get the global log buffer singleton.

    Returns:
        The shared LogBuffer instance.
    """
    global _log_buffer
    if _log_buffer is None:
        _log_buffer = LogBuffer()
    return _log_buffer
