"""Usage event sink implementations.

Provides concrete implementations of UsageEventSink for different backends.
"""

from __future__ import annotations

import asyncio
import logging
from collections import deque
from threading import Lock
from typing import TYPE_CHECKING

from noteflow.application.observability.ports import (
    NullUsageEventSink,
    UsageEvent,
    UsageEventSink,
    UsageMetrics,
)
from noteflow.config.constants import ERROR_DETAIL_PROJECT_ID
from noteflow.infrastructure.logging import get_logger
from noteflow.infrastructure.observability.otel import SpanProtocol, check_otel_available

if TYPE_CHECKING:
    from collections.abc import Callable

    from noteflow.infrastructure.persistence.repositories.usage_event_repo import (
        SqlAlchemyUsageEventRepository,
    )

logger = get_logger(__name__)


class LoggingUsageEventSink:
    """Usage event sink that logs events.

    Useful for development and debugging, or when OpenTelemetry
    is not available.
    """

    def __init__(self, logger_name: str = "noteflow.usage") -> None:
        """Initialize logging sink.

        Args:
            logger_name: Logger name for usage events.
        """
        self._logger = logging.getLogger(logger_name)

    def record(self, event: UsageEvent) -> None:
        """Log the usage event."""
        self._logger.info(
            "Usage event: %s | meeting=%s provider=%s model=%s "
            "tokens_in=%s tokens_out=%s latency_ms=%s success=%s",
            event.event_type,
            event.meeting_id,
            event.provider_name,
            event.model_name,
            event.tokens_input,
            event.tokens_output,
            event.latency_ms,
            event.success,
        )

    def record_simple(
        self,
        event_type: str,
        metrics: UsageMetrics | None = None,
        *,
        meeting_id: str | None = None,
        success: bool = True,
        error_code: str | None = None,
        **attributes: object,
    ) -> None:
        """Log a simple usage event."""
        m = metrics or UsageMetrics()
        self.record(UsageEvent(
            event_type=event_type, meeting_id=meeting_id,
            provider_name=m.provider_name, model_name=m.model_name,
            tokens_input=m.tokens_input, tokens_output=m.tokens_output,
            latency_ms=m.latency_ms, success=success,
            error_code=error_code, attributes=dict(attributes),
        ))


def _build_event_attributes(event: UsageEvent) -> dict[str, str | int | float | bool]:
    """Build OTel attributes dict from usage event, filtering None values.

    Args:
        event: Usage event with optional fields.

    Returns:
        Dictionary of primitive-typed attributes for OTel span.
    """
    # Map event fields to attribute names (None values filtered out)
    field_mappings: list[tuple[str, str | int | float | bool | None]] = [
        ("meeting_id", event.meeting_id),
        ("workspace_id", event.workspace_id),
        (ERROR_DETAIL_PROJECT_ID, event.project_id),
        ("provider_name", event.provider_name),
        ("model_name", event.model_name),
        ("tokens_input", event.tokens_input),
        ("tokens_output", event.tokens_output),
        ("latency_ms", event.latency_ms),
        ("error_code", event.error_code),
    ]

    # Build attributes from non-None field values
    attributes: dict[str, str | int | float | bool] = {
        key: value for key, value in field_mappings if value is not None
    }

    # Always include success
    attributes["success"] = event.success

    # Add custom attributes (only primitive types)
    primitive_custom = {
        key: value
        for key, value in event.attributes.items()
        if isinstance(value, (str, int, float, bool))
    }
    attributes |= primitive_custom

    return attributes


def _set_span_filter_attributes(
    span: SpanProtocol, event: UsageEvent
) -> None:
    """Set key attributes on span for filtering in observability backends.

    Args:
        span: OTel span to set attributes on.
        event: Usage event with metric values.
    """
    # Map noteflow-prefixed attributes for span-level filtering
    span_mappings: list[tuple[str, str | int | float | None]] = [
        ("noteflow.provider", event.provider_name),
        ("noteflow.model", event.model_name),
        ("noteflow.tokens_input", event.tokens_input),
        ("noteflow.tokens_output", event.tokens_output),
        ("noteflow.latency_ms", event.latency_ms),
    ]

    # Set non-None attributes on span
    for attr_name, value in span_mappings:
        if value is not None:
            span.set_attribute(attr_name, value)


class OtelUsageEventSink:
    """Usage event sink that records to OpenTelemetry spans.

    Attaches usage events as span events with attributes,
    enabling correlation with traces and export to observability backends.
    """

    def record(self, event: UsageEvent) -> None:
        """Record usage event to current OTel span."""
        if not check_otel_available():
            return

        from opentelemetry import trace

        span = trace.get_current_span()
        if span is trace.INVALID_SPAN or not span.is_recording():
            logger.debug("No active span for usage event: %s", event.event_type)
            return

        # Build and attach event attributes
        attributes = _build_event_attributes(event)
        span.add_event(event.event_type, attributes=attributes)

        # Set key attributes on span for filtering
        _set_span_filter_attributes(span, event)

    def record_simple(
        self,
        event_type: str,
        metrics: UsageMetrics | None = None,
        *,
        meeting_id: str | None = None,
        success: bool = True,
        error_code: str | None = None,
        **attributes: object,
    ) -> None:
        """Record a simple usage event to current span."""
        m = metrics or UsageMetrics()
        self.record(UsageEvent(
            event_type=event_type, meeting_id=meeting_id,
            provider_name=m.provider_name, model_name=m.model_name,
            tokens_input=m.tokens_input, tokens_output=m.tokens_output,
            latency_ms=m.latency_ms, success=success,
            error_code=error_code, attributes=dict(attributes),
        ))


class BufferedDatabaseUsageEventSink:
    """Usage event sink that persists events to database with buffering.

    Buffers events in memory and flushes to database periodically or when
    buffer is full. This prevents blocking on database writes during
    request processing.

    Thread-safe: uses a lock for buffer access and schedules flushes
    on an async event loop.
    """

    DEFAULT_BUFFER_SIZE = 100
    DEFAULT_FLUSH_INTERVAL_SECONDS = 5.0

    def __init__(
        self,
        repository_factory: Callable[[], SqlAlchemyUsageEventRepository],
        *,
        buffer_size: int = DEFAULT_BUFFER_SIZE,
        flush_interval: float = DEFAULT_FLUSH_INTERVAL_SECONDS,
    ) -> None:
        """Initialize buffered database sink.

        Args:
            repository_factory: Factory function that creates a repository instance.
            buffer_size: Maximum events to buffer before forcing flush.
            flush_interval: Seconds between automatic flushes.
        """
        self._repository_factory = repository_factory
        self._buffer_size = buffer_size
        self._flush_interval = flush_interval
        self._buffer: deque[UsageEvent] = deque(maxlen=buffer_size * 2)
        self._lock = Lock()
        self._flush_task: asyncio.Task[None] | None = None
        self._loop: asyncio.AbstractEventLoop | None = None

    def record(self, event: UsageEvent) -> None:
        """Buffer a usage event for later persistence.

        Non-blocking: adds to buffer and schedules flush if needed.
        """
        with self._lock:
            self._buffer.append(event)
            buffer_len = len(self._buffer)

        # Schedule flush if buffer is getting full
        if buffer_len >= self._buffer_size:
            self._schedule_flush()

    def record_simple(
        self,
        event_type: str,
        metrics: UsageMetrics | None = None,
        *,
        meeting_id: str | None = None,
        success: bool = True,
        error_code: str | None = None,
        **attributes: object,
    ) -> None:
        """Buffer a simple usage event."""
        m = metrics or UsageMetrics()
        self.record(UsageEvent(
            event_type=event_type, meeting_id=meeting_id,
            provider_name=m.provider_name, model_name=m.model_name,
            tokens_input=m.tokens_input, tokens_output=m.tokens_output,
            latency_ms=m.latency_ms, success=success,
            error_code=error_code, attributes=dict(attributes),
        ))

    def _schedule_flush(self) -> None:
        """Schedule an async flush on the event loop."""
        try:
            loop = asyncio.get_running_loop()
            self._loop = loop
            # Store task reference to prevent garbage collection
            self._flush_task = loop.create_task(self._flush_async())
        except RuntimeError:
            # No running loop, will flush on next opportunity
            logger.debug("No event loop available for flush scheduling")

    async def _flush_async(self) -> None:
        """Flush buffered events to database."""
        with self._lock:
            if not self._buffer:
                return
            events = list(self._buffer)
            self._buffer.clear()

        if not events:
            return

        try:
            repo = self._repository_factory()
            count = await repo.add_batch(events)
            logger.debug("Flushed %d usage events to database", count)
        except Exception:
            logger.exception("Failed to flush usage events to database")
            # Re-add events to buffer for retry (at front)
            with self._lock:
                for event in reversed(events):
                    self._buffer.appendleft(event)

    async def flush(self) -> int:
        """Manually flush all buffered events.

        Returns:
            Number of events flushed.
        """
        with self._lock:
            if not self._buffer:
                return 0
            events = list(self._buffer)
            self._buffer.clear()

        if not events:
            return 0

        try:
            repo = self._repository_factory()
            return await repo.add_batch(events)
        except Exception:
            logger.exception("Failed to flush usage events")
            # Re-add events to buffer
            with self._lock:
                for event in reversed(events):
                    self._buffer.appendleft(event)
            return 0

    @property
    def pending_count(self) -> int:
        """Number of events waiting to be flushed."""
        with self._lock:
            return len(self._buffer)


def create_usage_event_sink(
    *,
    use_otel: bool = True,
    fallback_to_logging: bool = True,
) -> UsageEventSink:
    """Create an appropriate usage event sink.

    Factory function that returns the best available sink based on
    configuration and available dependencies.

    Args:
        use_otel: Whether to use OpenTelemetry if available.
        fallback_to_logging: Whether to fall back to logging if OTel unavailable.

    Returns:
        A UsageEventSink implementation.
    """
    if use_otel and check_otel_available():
        logger.debug("Using OtelUsageEventSink")
        return OtelUsageEventSink()

    if fallback_to_logging:
        logger.debug("Using LoggingUsageEventSink")
        return LoggingUsageEventSink()

    logger.debug("Using NullUsageEventSink")
    return NullUsageEventSink()
