"""APScheduler instance for the Journal — daily prep generation.

One per-user cron job: generate today's daily prep at the configured
prep_time in the user's local timezone. Replaces briefing_scheduler.

Mirrors the BackgroundScheduler + threadsafe-async-call pattern used by
event_scheduler.py.
"""
from __future__ import annotations

import asyncio
import datetime
import json
import logging
from zoneinfo import ZoneInfo

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

from fabledassistant.models import User, async_session
from fabledassistant.services.journal_prep import ensure_daily_prep_message
from fabledassistant.services.settings import get_setting

logger = logging.getLogger(__name__)

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


DEFAULT_CONFIG = {
    "prep_enabled": True,
    "prep_hour": 5,
    "prep_minute": 0,
    "day_rollover_hour": 4,
    "morning_end_hour": 12,
    "midday_end_hour": 18,
    "closeout_enabled": True,
}


async def get_journal_config(user_id: int) -> dict:
    """Load a user's journal_config (JSON in settings) merged with defaults."""
    raw = await get_setting(user_id, "journal_config", "")
    config: dict = {}
    if raw:
        try:
            parsed = json.loads(raw) if isinstance(raw, str) else raw
            if isinstance(parsed, dict):
                config = parsed
        except Exception:
            logger.warning("Invalid journal_config for user %d; using defaults", user_id)
    return {**DEFAULT_CONFIG, **config}


async def get_user_timezone(user_id: int) -> str:
    return await get_setting(user_id, "user_timezone", "UTC") or "UTC"


def _resolve_tz(tz_str: str) -> ZoneInfo:
    try:
        return ZoneInfo(tz_str)
    except Exception:
        return ZoneInfo("UTC")


async def _do_daily_prep(user_id: int) -> None:
    try:
        tz_str = await get_user_timezone(user_id)
        tz = _resolve_tz(tz_str)
        config = await get_journal_config(user_id)
        rollover = int(config.get("day_rollover_hour", 4))
        now = datetime.datetime.now(tz)
        # Today is the day after the most recent rollover.
        if now.hour < rollover:
            today = (now - datetime.timedelta(days=1)).date()
        else:
            today = now.date()
        await ensure_daily_prep_message(
            user_id=user_id, day_date=today, user_timezone=tz_str
        )
    except Exception:
        logger.exception("Daily prep failed for user %d", user_id)


def _run_daily_prep_threadsafe(user_id: int) -> None:
    if _loop is None:
        return
    asyncio.run_coroutine_threadsafe(_do_daily_prep(user_id), _loop)


async def _do_closeout(user_id: int) -> None:
    try:
        tz_str = await get_user_timezone(user_id)
        tz = _resolve_tz(tz_str)
        now = datetime.datetime.now(tz)
        # We just rolled into a new day in user-local time. The day that
        # just ended is yesterday's calendar date regardless of whether
        # rollover_hour is 0 or 4 — APScheduler fires precisely at the
        # configured hour so no clock-skew correction is needed.
        yesterday = now.date() - datetime.timedelta(days=1)
        from fabledassistant.services.journal_closeout import run_for_user
        await run_for_user(user_id=user_id, yesterday=yesterday)
    except Exception:
        logger.exception("Closeout failed for user %d", user_id)


def _run_closeout_threadsafe(user_id: int) -> None:
    if _loop is None:
        return
    asyncio.run_coroutine_threadsafe(_do_closeout(user_id), _loop)


async def _closeout_catchup(user_id: int) -> None:
    """On startup, run yesterday's closeout once if the slot already passed
    and no entry for yesterday exists in observations_raw.
    """
    try:
        tz_str = await get_user_timezone(user_id)
        tz = _resolve_tz(tz_str)
        config = await get_journal_config(user_id)
        if not config.get("closeout_enabled", True):
            return
        rollover_hour = int(config.get("day_rollover_hour", 4))
        now = datetime.datetime.now(tz)
        # Slot hasn't passed yet today → wait for the cron.
        if now.hour < rollover_hour:
            return
        yesterday = (now - datetime.timedelta(days=1)).date()

        from fabledassistant.services.user_profile import get_profile
        profile = await get_profile(user_id)
        existing_dates = {
            (e or {}).get("date") for e in (profile.observations_raw or [])
        }
        if yesterday.isoformat() in existing_dates:
            return

        from fabledassistant.services.journal_closeout import run_for_user
        await run_for_user(user_id=user_id, yesterday=yesterday)
    except Exception:
        logger.exception("Closeout catch-up failed for user %d", user_id)


async def update_user_schedule(user_id: int) -> None:
    """Add or replace this user's daily-prep + closeout jobs from current config."""
    if _scheduler is None:
        return

    config = await get_journal_config(user_id)
    tz_str = await get_user_timezone(user_id)
    tz = _resolve_tz(tz_str)

    # ── Prep job ──────────────────────────────────────────────────────────
    prep_job_id = f"journal_prep_{user_id}"
    if _scheduler.get_job(prep_job_id):
        _scheduler.remove_job(prep_job_id)
    if config.get("prep_enabled", True):
        prep_hour = int(config.get("prep_hour", 5))
        prep_minute = int(config.get("prep_minute", 0))
        _scheduler.add_job(
            _run_daily_prep_threadsafe,
            trigger=CronTrigger(hour=prep_hour, minute=prep_minute, timezone=tz),
            args=[user_id],
            id=prep_job_id,
            replace_existing=True,
        )

    # ── Closeout job ──────────────────────────────────────────────────────
    closeout_job_id = f"journal_closeout_{user_id}"
    if _scheduler.get_job(closeout_job_id):
        _scheduler.remove_job(closeout_job_id)
    if config.get("closeout_enabled", True):
        rollover_hour = int(config.get("day_rollover_hour", 4))
        _scheduler.add_job(
            _run_closeout_threadsafe,
            trigger=CronTrigger(hour=rollover_hour, minute=0, timezone=tz),
            args=[user_id],
            id=closeout_job_id,
            replace_existing=True,
        )


async def _register_all_user_jobs() -> None:
    async with async_session() as session:
        users = (await session.execute(select(User))).scalars().all()
    for user in users:
        await update_user_schedule(user.id)
        # Fire catch-up asynchronously so a slow LLM call doesn't block startup
        if _loop is not None:
            asyncio.run_coroutine_threadsafe(_closeout_catchup(user.id), _loop)


def start_journal_scheduler(loop: asyncio.AbstractEventLoop) -> None:
    global _scheduler, _loop
    if _scheduler is not None:
        return
    _loop = loop
    _scheduler = BackgroundScheduler()
    _scheduler.start()
    asyncio.run_coroutine_threadsafe(_register_all_user_jobs(), loop)
    logger.info("Journal scheduler started")


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