"""
Celery application instance.

This is the entry point for `celery -A app.workers.celery_app worker`.
Each worker process, when it starts, loads all 6 ML models into its own
memory ONCE (via the worker_process_init signal below) — not per task.
This is what makes scaling workers horizontally cheap: spin up more
processes, each pays the model-loading cost once at boot, then serves
many tasks from warm memory.

Separating these workers from the Gunicorn/Uvicorn web workers means ML
load (CPU-bound, multi-second) never competes with or blocks web request
handling (I/O-bound, sub-100ms for everything except /analyze).
"""

from celery import Celery
from celery.signals import worker_process_init

from app.core.config import get_settings
from app.core.logging_config import get_logger, setup_logging
from app.models_loader.registry import model_registry
from app.services.feature_builder import feature_builder
from app.services.llm_service import llm_service
from app.services.reference_data import reference_data

settings = get_settings()

celery_app = Celery(
    "crop_ai_worker",
    broker=settings.CELERY_BROKER_URL,
    backend=settings.CELERY_RESULT_BACKEND,
)

celery_app.conf.update(
    task_serializer="json",
    accept_content=["json"],
    result_serializer="json",
    timezone="UTC",
    enable_utc=True,
    task_time_limit=settings.CELERY_TASK_TIME_LIMIT,
    task_soft_time_limit=settings.CELERY_TASK_SOFT_TIME_LIMIT,
    worker_prefetch_multiplier=1,  # one task at a time per worker process —
                                    # prevents one slow analysis from starving
                                    # others queued behind it on the same worker
    worker_max_tasks_per_child=200,  # restart worker periodically to avoid
                                       # slow memory creep from numpy/pandas
                                       # over thousands of tasks
    task_acks_late=True,  # task is only ack'd after it completes — if a
                            # worker crashes mid-task, the job is requeued
                            # instead of silently lost
    task_reject_on_worker_lost=True,
)

celery_app.autodiscover_tasks(["app.workers"])


@worker_process_init.connect
def init_worker(**kwargs) -> None:
    """
    Runs once per worker process at startup, before it accepts any tasks.
    If this raises, the worker process fails to start — which is correct:
    a worker with no models loaded must never silently pick up analysis jobs.
    """
    setup_logging()
    logger = get_logger("celery.worker_init")
    logger.info("Initializing Celery worker process: loading models and reference data")

    reference_data.load()
    feature_builder.load()
    model_registry.load_all()
    llm_service.init()

    logger.info("Celery worker process ready")
