"""Daily APScheduler cron for basic DB maintenance (targeted VACUUM ANALYZE).

Mirrors trash_scheduler.py: a single global BackgroundScheduler job bridges
into the asyncio loop to run the async maintenance. Scheduled for 04:00 UTC by
default — after the 03:30 trash purge — so it collects the dead tuples that
night's delete sweeps leave behind.

Two things are operator-tunable from the admin Settings card:
  - db_maintenance_enabled ("true"/"false") — checked at fire time, so toggling
    it needs no reschedule.
  - db_maintenance_hour ("0".."23", UTC) — the cron hour. Changing it calls
    reschedule_db_maintenance() so the live job moves without a restart.
"""
from __future__ import annotations

import asyncio
import logging

from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger

from scribe.services.settings import get_admin_setting

logger = logging.getLogger(__name__)

_JOB_ID = "db_maintenance_vacuum"
_DEFAULT_HOUR = 4

_scheduler: BackgroundScheduler | None = None
_loop: asyncio.AbstractEventLoop | None = None


async def get_maintenance_hour() -> int:
    """The configured run-hour (UTC, 0–23), clamped; default 04:00."""
    raw = await get_admin_setting("db_maintenance_hour", str(_DEFAULT_HOUR))
    try:
        hour = int(raw)
    except (TypeError, ValueError):
        return _DEFAULT_HOUR
    return hour if 0 <= hour <= 23 else _DEFAULT_HOUR


async def is_maintenance_enabled() -> bool:
    """Whether the scheduled run is enabled (default on)."""
    return (await get_admin_setting("db_maintenance_enabled", "true")) != "false"


def _run_maintenance_threadsafe() -> None:
    """APScheduler invokes this from a worker thread; bridge into the loop."""
    if _loop is None:
        logger.warning("db maintenance scheduler: no loop registered")
        return

    async def _runner():
        try:
            if not await is_maintenance_enabled():
                logger.debug("db maintenance: disabled, skipping scheduled run")
                return
            from scribe.services.db_maintenance import run_maintenance
            await run_maintenance()
        except Exception:
            logger.exception("db maintenance run failed")

    asyncio.run_coroutine_threadsafe(_runner(), _loop)


def start_db_maintenance_scheduler(
    loop: asyncio.AbstractEventLoop, hour: int = _DEFAULT_HOUR
) -> None:
    """Start the daily job. `hour` is the configured UTC run-hour, resolved by
    the caller (which has an async context) via get_maintenance_hour() — passed
    in rather than read here so we never block the event loop at startup. The
    job's enabled-gate is re-checked at every fire, so only the hour is needed
    up front."""
    global _scheduler, _loop
    if _scheduler is not None:
        return
    _loop = loop
    hour = hour if 0 <= hour <= 23 else _DEFAULT_HOUR
    _scheduler = BackgroundScheduler()
    _scheduler.add_job(
        _run_maintenance_threadsafe,
        trigger=CronTrigger(hour=hour, minute=0, timezone="UTC"),
        id=_JOB_ID,
        replace_existing=True,
    )
    _scheduler.start()
    logger.info("DB maintenance scheduler started (daily %02d:00 UTC)", hour)


def reschedule_db_maintenance(hour: int) -> None:
    """Move the live job to a new UTC hour (called when the admin changes it)."""
    if _scheduler is None:
        return
    hour = hour if 0 <= hour <= 23 else _DEFAULT_HOUR
    _scheduler.reschedule_job(
        _JOB_ID, trigger=CronTrigger(hour=hour, minute=0, timezone="UTC")
    )
    logger.info("DB maintenance scheduler rescheduled to %02d:00 UTC", hour)


def stop_db_maintenance_scheduler() -> None:
    global _scheduler
    if _scheduler is not None:
        _scheduler.shutdown(wait=False)
        _scheduler = None
        logger.info("DB maintenance scheduler stopped")
