"""
Scheduler runner — builds and manages the APScheduler AsyncIOScheduler.

Design:
  • One IntervalTrigger per job (clean, predictable firing cadence).
  • A thin guard wrapper is injected around each job function. The guard
    checks the active-day list and active-time window before calling the
    real function; it returns silently if the check fails (no log spam).
  • Timezone for window checks comes from settings.timezone (TIMEZONE env var).
  • Both sync and async job functions are supported.
  • Missed executions are dropped (misfire_grace_time=None, coalesce=True)
    — the next interval tick will run instead of piling up.

Usage (handled automatically by main.py lifespan):
    scheduler = build_scheduler()
    scheduler.start()
    ...
    scheduler.shutdown(wait=False)
"""

from __future__ import annotations

import asyncio
import importlib
from datetime import datetime, time
from typing import Callable

import structlog
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.interval import IntervalTrigger

from app.config import get_settings
from app.scheduler.config import JOBS

log = structlog.get_logger(__name__)

# Map Python weekday() integer (Mon=0 … Sun=6) to lowercase abbreviation.
_WEEKDAY_TO_ABBR: dict[int, str] = {
    0: "mon",
    1: "tue",
    2: "wed",
    3: "thu",
    4: "fri",
    5: "sat",
    6: "sun",
}


def _import_func(dotted_path: str) -> Callable:
    """
    Resolve a 'module.path:function_name' string to the actual callable.

    Examples:
        'app.services.service_1:run'  →  app.services.service_1.run
    """
    if ":" not in dotted_path:
        raise ValueError(
            f"Invalid func path {dotted_path!r}. "
            "Expected format: 'module.path:function_name'"
        )
    module_path, func_name = dotted_path.rsplit(":", 1)
    module = importlib.import_module(module_path)
    func = getattr(module, func_name, None)
    if func is None:
        raise AttributeError(
            f"Function {func_name!r} not found in module {module_path!r}"
        )
    return func


def _make_guarded_job(job_cfg: dict) -> Callable:
    """
    Build an async wrapper around a job function that enforces:
      1. Active days — silently skip if today is not in the days list.
      2. Active time window — silently skip if now is outside [start_time, end_time).

    The check uses the server's TIMEZONE env var (default: UTC) so that
    'start_time'/'end_time' always refer to the operator's local clock.
    """
    func = _import_func(job_cfg["func"])
    active_days: frozenset[str] = frozenset(d.lower() for d in job_cfg["days"])
    t_start: time = time.fromisoformat(job_cfg["start_time"])
    t_end:   time = time.fromisoformat(job_cfg["end_time"])
    job_id: str = job_cfg["id"]

    async def _guarded() -> None:
        from zoneinfo import ZoneInfo  # deferred — avoids import-time tzdata lookup

        tz = ZoneInfo(get_settings().timezone)
        now = datetime.now(tz=tz)

        today_abbr = _WEEKDAY_TO_ABBR[now.weekday()]
        current_time = now.time().replace(tzinfo=None)  # naive for comparison

        if today_abbr not in active_days:
            return  # inactive day — skip silently

        if not (t_start <= current_time < t_end):
            return  # outside active window — skip silently

        try:
            if asyncio.iscoroutinefunction(func):
                await func()
            else:
                loop = asyncio.get_running_loop()
                await loop.run_in_executor(None, func)
        except Exception:
            log.exception("scheduler_job_error", job_id=job_id)

    # Give the wrapper a descriptive name so APScheduler logs show it clearly.
    _guarded.__name__ = f"guarded_{job_id}"
    _guarded.__qualname__ = f"guarded_{job_id}"
    return _guarded


def build_scheduler() -> AsyncIOScheduler:
    """
    Instantiate and configure AsyncIOScheduler with all jobs from JOBS config.

    Jobs are NOT started here — call scheduler.start() inside the lifespan
    after the event loop is already running.
    """
    scheduler = AsyncIOScheduler()

    for job_cfg in JOBS:
        guarded = _make_guarded_job(job_cfg)
        scheduler.add_job(
            guarded,
            trigger=IntervalTrigger(seconds=job_cfg["interval_second"]),
            id=job_cfg["id"],
            replace_existing=True,
            misfire_grace_time=None,  # drop missed executions — never pile up
            coalesce=True,            # if multiple missed, fire only once
            max_instances=1,          # prevent overlapping runs of the same job
        )
        log.debug(
            "scheduler_job_registered",
            job_id=job_cfg["id"],
            func=job_cfg["func"],
            days=job_cfg["days"],
            window=f"{job_cfg['start_time']}–{job_cfg['end_time']}",
            interval_seconds=job_cfg["interval_second"],
        )

    return scheduler
