import logging
import queue
import time
import threading
import requests
from .config import Config

logger = logging.getLogger(__name__)

# ── Tunables ──────────────────────────────────────────────────────────────────
_CONNECT_TIMEOUT  = 2.0   # seconds to establish TCP connection
_READ_TIMEOUT     = 5.0   # seconds waiting for server to respond
_MAX_ATTEMPTS     = 3     # initial attempt + 2 retries
_RETRY_BASE_DELAY = 1.0   # exponential backoff base (1s, 2s)

# ── Background send queue ─────────────────────────────────────────────────────
# A single daemon thread drains this queue, so SDK I/O never touches the
# request thread. Bounded at 500 items — if the platform is unreachable for
# a sustained period, excess payloads are dropped rather than leaking memory
# or blocking the host app. Monitoring data is preferable to app downtime.
_QUEUE_MAXSIZE = 500
_send_queue    = queue.Queue(maxsize=_QUEUE_MAXSIZE)

_worker_lock    = threading.Lock()
_worker_started = False


# ── Low-level POST ────────────────────────────────────────────────────────────

def _post(path: str, payload: dict) -> None:
    """
    Raw HTTP POST to the platform. Raises on any failure.
    Uses separate connect + read timeouts so a slow server
    does not hold a retry slot open indefinitely.
    """
    platform_url = Config.platform_url
    api_key      = Config.api_key
    if not platform_url or not api_key:
        # SDK was never initialised — fail immediately instead of
        # trying to connect to "None/api/v1/..." and wasting a timeout.
        raise RuntimeError("AIOps SDK not initialised — call aiops_sdk.init() first")

    requests.post(
        f"{platform_url}{path}",
        json=payload,
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
        timeout=(_CONNECT_TIMEOUT, _READ_TIMEOUT),
    )


# ── Background worker ─────────────────────────────────────────────────────────

def _background_worker() -> None:
    """
    Single long-lived daemon thread that drains the send queue.

    Retries with exponential backoff (1s, 2s). On final failure, logs at
    DEBUG level — WARNING would recurse back into the logging hook.
    The loop is unconditionally protected: any unexpected crash is swallowed
    so the thread never dies silently and leaves the queue permanently stuck.
    """
    while True:
        try:
            path, payload = _send_queue.get(timeout=1.0)
        except queue.Empty:
            continue
        except Exception:
            continue  # should never happen, but guard regardless

        for attempt in range(1, _MAX_ATTEMPTS + 1):
            try:
                _post(path, payload)
                break
            except Exception as exc:
                if attempt < _MAX_ATTEMPTS:
                    time.sleep(_RETRY_BASE_DELAY * attempt)  # 1s, then 2s
                else:
                    # Log at DEBUG — WARNING/ERROR would trigger the logging
                    # hook which calls send_async again, creating a feedback loop.
                    logger.debug(
                        "AIOps SDK: Dropped payload for %s after %d attempts: %s",
                        path, _MAX_ATTEMPTS, exc,
                    )


def _ensure_worker_running() -> None:
    """Lazily start the background worker on the first send_async call."""
    global _worker_started
    if _worker_started:   # fast path, no lock
        return
    with _worker_lock:
        if _worker_started:   # double-checked locking
            return
        _worker_started = True
        t = threading.Thread(
            target=_background_worker,
            daemon=True,
            name="aiops-sender",
        )
        t.start()


# ── Public API ────────────────────────────────────────────────────────────────

def send_async(path: str, payload: dict) -> None:
    """
    Enqueue a payload for background delivery. Returns immediately.

    This is the correct function to call from any request-handling context
    (Flask after_request, logging handlers, signal handlers) because it
    never blocks the caller regardless of platform availability.

    If the queue is full (platform unreachable for an extended period),
    the payload is silently dropped — the host app is always the priority.
    """
    _ensure_worker_running()
    try:
        _send_queue.put_nowait((path, payload))
    except queue.Full:
        logger.debug("AIOps SDK: Queue full, dropping payload for %s", path)


def send_sync(path: str, payload: dict) -> None:
    """
    Synchronous blocking send with retry.

    ONLY use this from sys.excepthook (_excepthook in exceptions.py) where
    the process is about to exit. In that scenario daemon threads will not
    run, so the background worker cannot deliver the payload — a direct
    blocking call is the only way to guarantee delivery.

    Do NOT call from request handlers, signal handlers, or logging emit().
    """
    for attempt in range(1, _MAX_ATTEMPTS + 1):
        try:
            _post(path, payload)
            return
        except Exception as exc:
            if attempt < _MAX_ATTEMPTS:
                time.sleep(_RETRY_BASE_DELAY)
            else:
                logger.warning(
                    "AIOps SDK: Failed to send to %s after %d attempts: %s",
                    path, _MAX_ATTEMPTS, exc,
                )
