"""
HTTP log shipper for local runners.

Ships structlog JSON output to the Skop API's /api/logs endpoint in batches.
Cloud runners don't need this — fly-log-shipper captures their stdout.

Usage:
    from skop_runner.log_shipper import LogShipper

    shipper = LogShipper(api_url="https://skoplabs.com")
    shipper.install()       # Hooks into structlog as a processor
    await shipper.start()   # Starts background flush loop
    await shipper.stop()    # Final flush + cleanup
"""

import asyncio
import json
import time
from typing import Optional

import httpx

from .structured_log import get_logger

_log = get_logger("log_shipper")

MAX_BUFFER_SIZE = 500
FLUSH_INTERVAL_S = 30
FLUSH_TIMEOUT_S = 5


class LogShipper:
    """Batches structlog entries and ships them to /api/logs via HTTP."""

    def __init__(self, api_url: str):
        self._api_url = api_url.rstrip("/")
        self._buffer: list[dict] = []
        self._client: Optional[httpx.AsyncClient] = None
        self._task: Optional[asyncio.Task] = None

    # Fields that must NEVER be shipped (may contain code, file content, or secrets)
    _DENY_FIELDS = frozenset({
        "code", "content", "file_content", "source_code", "cell_source",
        "output", "stdout", "stderr", "result", "traceback",
        "token", "password", "secret", "api_key", "authorization",
        "cookie", "session_token", "device_token",
        # structlog internals
        "_record", "_from_stdlib",
    })

    def capture(self, logger: object, method_name: str, event_dict: dict) -> dict:
        """Structlog processor that captures a copy of each log entry.

        Uses a denylist approach — captures ALL fields except known-sensitive ones.
        New fields added to log calls are automatically included without changes here.
        """
        entry = {
            k: (v if isinstance(v, (str, int, float, bool, list, dict, type(None))) else str(v))
            for k, v in event_dict.items()
            if k not in self._DENY_FIELDS
        }

        if len(self._buffer) >= MAX_BUFFER_SIZE:
            self._buffer.pop(0)
        self._buffer.append(entry)

        return event_dict  # Pass through — don't consume

    async def start(self) -> None:
        """Start the background flush loop."""
        self._client = httpx.AsyncClient(timeout=FLUSH_TIMEOUT_S)
        self._task = asyncio.create_task(self._flush_loop())

    async def stop(self) -> None:
        """Final flush and cleanup."""
        if self._task:
            self._task.cancel()
            try:
                await self._task
            except asyncio.CancelledError:
                pass
        # Final flush
        await self._flush()
        if self._client:
            await self._client.aclose()
            self._client = None

    async def _flush_loop(self) -> None:
        """Flush buffer every FLUSH_INTERVAL_S seconds."""
        try:
            while True:
                await asyncio.sleep(FLUSH_INTERVAL_S)
                await self._flush()
        except asyncio.CancelledError:
            pass

    async def _flush(self) -> None:
        """Ship buffered entries to /api/logs."""
        if not self._buffer or not self._client:
            return

        entries, self._buffer = self._buffer, []

        try:
            await self._client.post(
                f"{self._api_url}/api/logs",
                json={"entries": entries, "source": "local-runner"},
            )
        except Exception as e:
            # Put entries back on failure (up to max buffer size)
            remaining_capacity = MAX_BUFFER_SIZE - len(self._buffer)
            self._buffer = entries[:remaining_capacity] + self._buffer
            # Don't use _log here to avoid recursion — write to stderr to avoid
            # corrupting the JSON stdout stream
            import sys
            sys.stderr.write(f"[log_shipper] Flush failed: {e}\n")
