"""Fire-and-forget background execution for slow network I/O (FCM, SMTP).

Production runs WSGI-only on cPanel (no Celery/Redis), so a small daemon
thread pool is the pragmatic way to keep external round-trips off the
request thread. Tasks are best-effort: failures are logged, never raised,
and pending tasks die with the process (acceptable for notifications).

Set BACKGROUND_TASKS_EAGER=True (tests) to run tasks inline.

PERF-005: the submission queue is bounded (BACKGROUND_MAX_QUEUE, default 200
pending tasks beyond the worker pool itself). Overload policy is deliberate:
drop and count via `background_tasks_dropped_total`, never block the request
thread — these are explicitly best-effort notifications/pushes, not durable
work, so a dropped job is an acceptable degradation and a bounded queue is
strictly better than the previous unbounded one.
"""

import logging
import threading
from concurrent.futures import ThreadPoolExecutor

from django.conf import settings
from django.db import connections

logger = logging.getLogger(__name__)

_executor = None
_queue_lock = threading.Lock()
_pending_count = 0
background_tasks_dropped_total = 0


def _get_executor():
    global _executor
    if _executor is None:
        workers = getattr(settings, "BACKGROUND_MAX_WORKERS", 4)
        _executor = ThreadPoolExecutor(max_workers=workers, thread_name_prefix="khub-bg")
    return _executor


def run_in_background(fn, *args, **kwargs):
    """Schedule fn(*args, **kwargs) on the background pool. Never raises.

    Drops the task (logged, counted) instead of submitting it if the bounded
    queue is already full, rather than blocking the calling request thread.
    """
    if getattr(settings, "BACKGROUND_TASKS_EAGER", False):
        try:
            fn(*args, **kwargs)
        except Exception:
            logger.exception("background task failed (eager): %s", getattr(fn, "__name__", fn))
        return

    global _pending_count, background_tasks_dropped_total
    max_queue = getattr(settings, "BACKGROUND_MAX_QUEUE", 200)
    with _queue_lock:
        if _pending_count >= max_queue:
            background_tasks_dropped_total += 1
            logger.warning(
                "background task queue full (%d pending) — dropping: %s",
                max_queue, getattr(fn, "__name__", fn),
            )
            return
        _pending_count += 1

    def _wrapped():
        global _pending_count
        try:
            fn(*args, **kwargs)
        except Exception:
            logger.exception("background task failed: %s", getattr(fn, "__name__", fn))
        finally:
            # Worker threads get their own DB connections; close them outright
            # (close_old_connections would keep them open for CONN_MAX_AGE).
            for conn in connections.all(initialized_only=True):
                conn.close()
            with _queue_lock:
                _pending_count -= 1

    try:
        _get_executor().submit(_wrapped)
    except Exception:
        with _queue_lock:
            _pending_count -= 1
        logger.exception("could not submit background task: %s", getattr(fn, "__name__", fn))
