from __future__ import annotations

import atexit
import signal
import sys
import time
from typing import Any, Dict, List, Optional

from pulselog.config import Config
from pulselog.checkpoint import CheckpointStore
from pulselog.exceptions import InvalidStatusError
from pulselog.levels import Level
from pulselog.queue import LogQueue, QUEUE_MAX_SIZE
from pulselog.record import LogRecord
from pulselog.worker import BackgroundWorker


class Logger:
    """Non-blocking real-time logger with an embedded browser dashboard.

    Every logging call is O(1) and never blocks the calling thread.  A daemon
    background worker drains the queue and pushes batches to the WebSocket
    server every 50 ms (configurable).

    Usage::

        logger = Logger("my-app")
        logger.info("hello", user_id=42)
        logger.save("step1", {"acc": 0.9}, status="DONE", progress=100)
        logger.shutdown()

    Multiple Logger instances with different names may coexist in the same
    process — they do *not* share a queue, worker, or checkpoint store.
    """

    def __init__(
        self,
        name: str = "pulselog",
        host: Optional[str] = None,
        port: Optional[int] = None,
        auto_open: Optional[bool] = None,
        dashboard: Optional[bool] = None,
        checkpoint_path: Optional[str] = None,
        level: Optional[str] = None,
        worker_interval: Optional[float] = None,
    ) -> None:
        self._start_time = time.time()
        self._records_logged = 0   # guarded by queue lock (incremented in _log)
        self._checkpoints_saved = 0  # guarded by checkpoint store

        # --- Config ---
        self._config = Config.load(
            name=name,
            host=host,
            port=port,
            auto_open=auto_open,
            dashboard=dashboard,
            checkpoint_path=checkpoint_path,
            level=level,
            worker_interval=worker_interval,
        )
        self._current_tag: Optional[str] = None

        # --- Core pipeline ---
        self._queue = LogQueue(maxsize=QUEUE_MAX_SIZE)

        # --- Checkpoint store (always available, regardless of dashboard) ---
        self._checkpoint_store = CheckpointStore(self._config.checkpoint_path)

        # --- Dashboard server ---
        self._server = None
        handlers: List[Any] = []
        if self._config.dashboard:
            from pulselog.server.websocket import DashboardServer
            self._server = DashboardServer(self._config)
            self._server.start()
            handlers.append(self._server.broadcast)

        # --- Background worker ---
        self._worker = BackgroundWorker(
            queue=self._queue,
            handlers=handlers,
            interval=self._config.worker_interval,
        )
        self._worker.start()

        # --- Graceful shutdown hooks ---
        atexit.register(self._atexit_handler)
        try:
            signal.signal(signal.SIGTERM, lambda *_: self._atexit_handler())
        except (OSError, ValueError):
            # SIGTERM registration can fail in non-main threads or on Windows
            pass

    # ------------------------------------------------------------------
    # Core logging methods
    # ------------------------------------------------------------------

    def _log(self, level: Level, msg: str, **extra: Any) -> None:
        """Internal log dispatch — O(1), never blocks.

        Level filtering happens BEFORE LogRecord construction to avoid
        unnecessary allocations.

        Args:
            level: Severity Level enum value.
            msg:   Log message string.
            **extra: Arbitrary key/value metadata.
        """
        if level.value < self._config.level_value:
            return
        record = LogRecord(
            level=level,
            msg=str(msg),
            timestamp=time.time(),
            extra=extra,
            tag=self._current_tag,
        )
        self._queue.put(record)
        self._records_logged += 1  # approximate — not under lock, that's fine

    def debug(self, msg: str, **extra: Any) -> None:
        """Log a DEBUG-level message."""
        self._log(Level.DEBUG, msg, **extra)

    def info(self, msg: str, **extra: Any) -> None:
        """Log an INFO-level message."""
        self._log(Level.INFO, msg, **extra)

    def warning(self, msg: str, **extra: Any) -> None:
        """Log a WARNING-level message."""
        self._log(Level.WARNING, msg, **extra)

    def error(self, msg: str, **extra: Any) -> None:
        """Log an ERROR-level message."""
        self._log(Level.ERROR, msg, **extra)

    def critical(self, msg: str, **extra: Any) -> None:
        """Log a CRITICAL-level message."""
        self._log(Level.CRITICAL, msg, **extra)

    # ------------------------------------------------------------------
    # Checkpoints
    # ------------------------------------------------------------------

    def save(
        self,
        name: str,
        data: Dict[str, Any],
        status: str = "DONE",
        note: str = "",
        progress: Optional[float] = None,
    ) -> Dict[str, Any]:
        """Persist a checkpoint and emit it as a log event.

        Args:
            name:     Unique identifier for this checkpoint.
            data:     JSON-serialisable dict to store.
            status:   One of DONE | IN_PROGRESS | FAILED | SKIPPED.
            note:     Human-readable description.
            progress: Optional 0–100 progress value.

        Returns:
            Full record dict including ``_meta``.

        Raises:
            InvalidStatusError: If *status* is not one of the allowed values.
        """
        result = self._checkpoint_store.save(
            name=name, data=data, status=status, note=note, progress=progress
        )
        self._checkpoints_saved += 1

        # Also emit as a log record so it appears in the dashboard stream.
        self._log(
            Level.INFO,
            f"[checkpoint] {name} → {status.upper()}"
            + (f" ({progress:.0f}%)" if progress is not None else ""),
            _checkpoint={"name": name, "status": status.upper(), "note": note,
                         "progress": progress, **data},
        )
        return result

    def load(self, name: str) -> Optional[Dict[str, Any]]:
        """Load a checkpoint by name.

        Returns:
            Dict with data and ``_meta``, or None if not found (never raises).
        """
        return self._checkpoint_store.load(name)

    def checkpoints(self) -> List[str]:
        """Return all checkpoint names ordered by most-recently saved first."""
        return self._checkpoint_store.checkpoints()

    def delete_checkpoint(self, name: str) -> None:
        """Delete a checkpoint.  Silent if it doesn't exist."""
        self._checkpoint_store.delete_checkpoint(name)

    # ------------------------------------------------------------------
    # Utilities
    # ------------------------------------------------------------------

    def tag(self, label: str) -> None:
        """Group subsequent log entries under *label*.

        Pushes a visual divider to the queue and sets the active tag.

        Args:
            label: Short descriptive label shown in the dashboard.
        """
        self._current_tag = label
        divider_record = LogRecord(
            level=Level.INFO,
            msg=label,
            tag=label,
            extra={"_divider": True},
        )
        divider_record._divider = True  # type: ignore[attr-defined]
        self._queue.put(divider_record)

    def divider(self, label: str = "") -> None:
        """Insert a visual divider line in the dashboard log stream.

        Args:
            label: Optional text label on the divider.
        """
        record = LogRecord(
            level=Level.INFO,
            msg=label or "─" * 40,
            tag=self._current_tag,
            extra={"_divider": True},
        )
        record._divider = True  # type: ignore[attr-defined]
        self._queue.put(record)

    def flush(self) -> None:
        """Force-drain the queue synchronously.

        Blocks until the worker has processed all currently queued records.
        Call before process exit to ensure no logs are lost.
        """
        # Wake worker immediately and wait for one full drain cycle.
        self._queue._event.set()
        # Give the worker time to drain (it loops at worker_interval).
        deadline = time.monotonic() + 0.5
        while not self._queue.is_empty() and time.monotonic() < deadline:
            time.sleep(0.01)

    def shutdown(self) -> None:
        """Gracefully shut down the worker and dashboard server.

        After this call the Logger instance should not be used.
        """
        self.flush()
        self._worker.shutdown()
        if self._server:
            self._server.shutdown()

    def stats(self) -> Dict[str, Any]:
        """Return operational metrics for monitoring.

        Returns:
            Dict with counters and current queue state.
        """
        return {
            "records_logged": self._records_logged,
            "records_dropped": self._queue.dropped_count(),
            "queue_size": len(self._queue),
            "checkpoints_saved": self._checkpoints_saved,
            "dashboard_clients": self._server.client_count if self._server else 0,
            "uptime_seconds": time.time() - self._start_time,
        }

    # ------------------------------------------------------------------
    # Private
    # ------------------------------------------------------------------

    def _atexit_handler(self) -> None:
        """Called on process exit or SIGTERM — flush and shut down silently."""
        try:
            self.flush()
            self.shutdown()
        except Exception:
            pass  # must never raise in atexit
