"""Persistent schedule loading for recurring JWT generation jobs."""

from __future__ import annotations

import re
from collections.abc import Awaitable, Callable
from typing import Any
from zoneinfo import ZoneInfo

from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger

from storage import Storage


ScheduleCallback = Callable[[dict[str, Any]], Awaitable[None]]
BD_TZ = ZoneInfo("Asia/Dhaka")


EVERY_RE = re.compile(r"^(\d+)(m|h)$", re.IGNORECASE)
DAILY_RE = re.compile(r"^([01]?\d|2[0-3]):([0-5]\d)$")


def parse_schedule(mode: str, value: str):
    normalized_mode = mode.lower()
    normalized_value = value.lower()
    if normalized_mode == "every":
        match = EVERY_RE.match(normalized_value)
        if not match:
            raise ValueError("every value must look like 30m or 2h")
        amount = int(match.group(1))
        unit = match.group(2).lower()
        if amount <= 0:
            raise ValueError("interval must be positive")
        if unit == "m":
            return IntervalTrigger(minutes=amount)
        return IntervalTrigger(hours=amount)
    if normalized_mode == "daily":
        match = DAILY_RE.match(normalized_value)
        if not match:
            raise ValueError("daily value must look like 04:00")
        return CronTrigger(hour=int(match.group(1)), minute=int(match.group(2)))
    raise ValueError("schedule mode must be every or daily")


class BotScheduler:
    def __init__(self, storage: Storage, callback: ScheduleCallback):
        self.storage = storage
        self.callback = callback
        self.scheduler = AsyncIOScheduler(timezone="Asia/Dhaka")

    def start(self) -> None:
        for schedule in self.storage.list_schedules():
            self.add_runtime_job(schedule)
        self.scheduler.start()

    def shutdown(self) -> None:
        self.scheduler.shutdown(wait=False)

    def add_runtime_job(self, schedule: dict[str, Any]) -> None:
        trigger = parse_schedule(str(schedule["mode"]), str(schedule["value"]))
        self.scheduler.add_job(
            self.callback,
            trigger=trigger,
            args=[schedule],
            id=f"schedule:{schedule['id']}",
            replace_existing=True,
            coalesce=True,
            max_instances=1,
        )

    def remove_runtime_job(self, schedule_id: int) -> None:
        job_id = f"schedule:{schedule_id}"
        if self.scheduler.get_job(job_id):
            self.scheduler.remove_job(job_id)

    def next_run_text(self, schedule_id: int) -> str:
        job = self.scheduler.get_job(f"schedule:{schedule_id}")
        if not job or not job.next_run_time:
            return "not scheduled"
        return job.next_run_time.astimezone(BD_TZ).strftime("%B %d, %Y %I:%M %p BD")
