"""
모니터링 데이터를 Producer 서버로 전송하는 배치 발송자
배치 전송 전략을 구현함: 버퍼가 가득 차거나 일정 시간이 경과하면 전송함

역할: 로그와 Span 데이터를 별도의 메모리 버퍼에 모아서 각각의 엔드포인트로
      일괄 전송하여 네트워크 오버헤드를 최소화함
"""

import asyncio
import threading
from typing import List
import httpx
from ..types.interfaces import DataEntry, LogEntry, SpanEntry, MonitoringConfig


class BatchSender:
    """
    모니터링 데이터를 배치로 전송하는 클래스
    """

    def __init__(self, config: MonitoringConfig):
        """
        BatchSender 초기화

        Args:
            config: 모니터링 설정
        """
        self.config = config
        self.log_buffer: List[LogEntry] = []
        self.span_buffer: List[SpanEntry] = []
        self._lock = threading.Lock()
        self._is_flushing = False
        self._flush_task: Optional[asyncio.Task] = None
        self._should_stop = False

        # HTTP 클라이언트 설정
        self.log_client = httpx.AsyncClient(
            base_url=config.get('log_endpoint', ''),
            timeout=10.0,
            headers={
                'Content-Type': 'application/json',
                'X-API-Key': config['api_key']
            }
        )

        self.trace_client = httpx.AsyncClient(
            base_url=config.get('trace_endpoint', ''),
            timeout=10.0,
            headers={
                'Content-Type': 'application/json',
                'X-API-Key': config['api_key']
            }
        )

        # 주기적 플러시 타이머 시작
        self._start_flush_timer()

    def add(self, entry: DataEntry) -> None:
        """
        버퍼에 데이터 엔트리를 추가함

        역할: 새 데이터를 타입에 따라 적절한 버퍼에 추가하고, 버퍼가 가득 차면 자동으로 전송함
        사용: LogCollectorHandler, HttpTracerMiddleware, HttpClientCollector,
              BedrockCollector에서 데이터를 수집할 때마다 호출됨

        Args:
            entry: Log 또는 Span 엔트리
        """
        with self._lock:
            # 타입에 따라 적절한 버퍼에 추가
            if entry['type'] == 'log':
                self.log_buffer.append(entry)  # type: ignore
            elif entry['type'] == 'span':
                self.span_buffer.append(entry)  # type: ignore

            batch_size = self.config.get('batch_size', 100)

            # 각 버퍼가 배치 크기에 도달하면 플러시
            if len(self.log_buffer) >= batch_size or len(self.span_buffer) >= batch_size:
                # 백그라운드에서 플러시 실행
                asyncio.create_task(self._flush_async())

    async def flush(self) -> None:
        """
        버퍼의 데이터를 Producer 서버로 전송함

        역할: 로그와 트레이스 버퍼에 쌓인 모든 데이터를 각각의 엔드포인트로 전송함
        사용: 버퍼가 가득 차거나(batch_size), 일정 시간마다(flush_interval), 또는 앱 종료 시 호출됨
        """
        await self._flush_async()

    async def _flush_async(self) -> None:
        """
        비동기로 버퍼를 플러시함 (내부 메서드)
        """
        if self._is_flushing:
            return

        with self._lock:
            # 두 버퍼 모두 비어있으면 리턴
            if len(self.log_buffer) == 0 and len(self.span_buffer) == 0:
                return

            self._is_flushing = True

            # 버퍼 복사 및 초기화
            logs_to_send = self.log_buffer.copy()
            spans_to_send = self.span_buffer.copy()
            self.log_buffer.clear()
            self.span_buffer.clear()

        try:
            # 로그와 트레이스를 병렬로 전송
            tasks = []

            if logs_to_send:
                tasks.append(self._send_logs(logs_to_send))

            if spans_to_send:
                tasks.append(self._send_traces(spans_to_send))

            await asyncio.gather(*tasks, return_exceptions=True)

        except Exception as e:
            print(f'[MonitoringSDK] Failed to send data to Producer: {e}')
        finally:
            self._is_flushing = False

    async def _send_logs(self, logs: List[LogEntry]) -> None:
        """
        로그 데이터를 전송함

        Args:
            logs: 전송할 로그 목록
        """
        try:
            response = await self.log_client.post('', json=logs)
            response.raise_for_status()
        except Exception as e:
            print(f'[MonitoringSDK] Failed to send logs: {e}')

    async def _send_traces(self, spans: List[SpanEntry]) -> None:
        """
        트레이스 데이터를 전송함

        Args:
            spans: 전송할 span 목록
        """
        try:
            response = await self.trace_client.post('', json=spans)
            response.raise_for_status()
        except Exception as e:
            print(f'[MonitoringSDK] Failed to send traces: {e}')

    def _start_flush_timer(self) -> None:
        """
        주기적인 플러시 타이머를 시작함

        역할: 일정 시간(기본 5초)마다 자동으로 버퍼를 비우도록 타이머를 설정함
        사용: BatchSender 생성 시 자동으로 호출되어 주기적 전송을 활성화함
        """
        async def periodic_flush():
            interval = self.config.get('flush_interval', 5)
            while not self._should_stop:
                await asyncio.sleep(interval)
                if not self._should_stop:
                    await self._flush_async()

        # 백그라운드 태스크로 실행
        loop = asyncio.get_event_loop()
        self._flush_task = loop.create_task(periodic_flush())

    async def shutdown(self) -> None:
        """
        플러시 타이머를 중지하고 남은 데이터를 전송함

        역할: 앱 종료 시 타이머를 정리하고 버퍼에 남은 모든 데이터를 전송함
        사용: MonitoringSDK의 shutdown hook에서 호출되어 데이터 손실을 방지함
        """
        self._should_stop = True

        if self._flush_task:
            self._flush_task.cancel()
            try:
                await self._flush_task
            except asyncio.CancelledError:
                pass

        # 남은 데이터 플러시
        await self._flush_async()

        # HTTP 클라이언트 종료
        await self.log_client.aclose()
        await self.trace_client.aclose()

    def get_buffer_size(self) -> int:
        """
        현재 버퍼 크기를 조회함

        역할: 현재 버퍼에 쌓인 데이터 개수를 반환함 (로그 + 트레이스)
        사용: 모니터링이나 디버깅 목적으로 사용됨

        Returns:
            버퍼에 쌓인 데이터 개수
        """
        with self._lock:
            return len(self.log_buffer) + len(self.span_buffer)

    def get_log_buffer_size(self) -> int:
        """로그 버퍼 크기를 조회함"""
        with self._lock:
            return len(self.log_buffer)

    def get_trace_buffer_size(self) -> int:
        """트레이스 버퍼 크기를 조회함"""
        with self._lock:
            return len(self.span_buffer)
