"""Task scheduling system using APScheduler with SQLite job store."""

from __future__ import annotations

import contextlib
import time
import uuid
from dataclasses import dataclass, field
from enum import StrEnum
from typing import Any

from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_EXECUTED, EVENT_JOB_MISSED
from apscheduler.jobstores.memory import MemoryJobStore
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger

from agent_scout.events import Event, EventBus, EventType


class ScheduleType(StrEnum):
    INTERVAL = "interval"
    CRON = "cron"
    ONCE = "once"


@dataclass
class ScheduledTask:
    """A task definition scheduled for recurring execution."""

    id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
    description: str = ""
    schedule_type: ScheduleType = ScheduleType.INTERVAL
    interval_seconds: int = 3600
    cron_expression: str = ""
    enabled: bool = True
    last_run: float = 0.0
    next_run: float = 0.0
    run_count: int = 0
    max_runs: int = 0  # 0 = unlimited
    metadata: dict[str, Any] = field(default_factory=dict)
    created_at: float = field(default_factory=time.time)


class TaskScheduler:
    """Schedule recurring tasks using APScheduler.

    Supports interval-based and cron-based scheduling.
    Persists jobs in memory (can be upgraded to SQLite store).
    Handles missed jobs by running them on next startup.
    """

    def __init__(
        self,
        event_bus: EventBus | None = None,
        task_callback: Any | None = None,
    ) -> None:
        self._bus = event_bus
        self._task_callback = task_callback
        self._scheduled_tasks: dict[str, ScheduledTask] = {}

        self._scheduler = AsyncIOScheduler(
            jobstores={"default": MemoryJobStore()},
            job_defaults={
                "coalesce": True,  # combine missed runs
                "max_instances": 1,
                "misfire_grace_time": 3600,  # 1 hour grace for missed jobs
            },
        )

        # Listen for job events
        self._scheduler.add_listener(
            self._on_job_event,
            EVENT_JOB_EXECUTED | EVENT_JOB_ERROR | EVENT_JOB_MISSED,
        )

    @property
    def is_running(self) -> bool:
        return self._scheduler.running

    def start(self) -> None:
        """Start the scheduler."""
        if not self._scheduler.running:
            self._scheduler.start()

    def shutdown(self, wait: bool = True) -> None:
        """Shut down the scheduler."""
        if self._scheduler.running:
            self._scheduler.shutdown(wait=wait)

    def add_task(self, task: ScheduledTask) -> ScheduledTask:
        """Add a scheduled task."""
        self._scheduled_tasks[task.id] = task

        if task.enabled:
            self._add_job(task)

        return task

    def remove_task(self, task_id: str) -> bool:
        """Remove a scheduled task."""
        task = self._scheduled_tasks.pop(task_id, None)
        if not task:
            return False

        with contextlib.suppress(Exception):
            self._scheduler.remove_job(task_id)

        return True

    def enable_task(self, task_id: str) -> bool:
        """Enable a paused task."""
        task = self._scheduled_tasks.get(task_id)
        if not task:
            return False

        task.enabled = True
        self._add_job(task)
        return True

    def disable_task(self, task_id: str) -> bool:
        """Disable a task (pause scheduling)."""
        task = self._scheduled_tasks.get(task_id)
        if not task:
            return False

        task.enabled = False
        with contextlib.suppress(Exception):
            self._scheduler.remove_job(task_id)

        return True

    def get_task(self, task_id: str) -> ScheduledTask | None:
        """Get a scheduled task by ID."""
        return self._scheduled_tasks.get(task_id)

    def list_tasks(self) -> list[ScheduledTask]:
        """List all scheduled tasks."""
        return list(self._scheduled_tasks.values())

    def get_pending_jobs(self) -> list[dict[str, Any]]:
        """Get info about pending scheduler jobs."""
        jobs = self._scheduler.get_jobs()
        return [
            {
                "id": job.id,
                "name": job.name,
                "next_run_time": (job.next_run_time.timestamp() if job.next_run_time else None),
                "trigger": str(job.trigger),
            }
            for job in jobs
        ]

    def to_cron_expression(self, task: ScheduledTask) -> str:
        """Convert a scheduled task to a cron expression (for system cron fallback)."""
        if task.schedule_type == ScheduleType.CRON:
            return task.cron_expression

        if task.schedule_type == ScheduleType.INTERVAL:
            seconds = task.interval_seconds
            if seconds < 60:
                return f"*/{seconds} * * * * *"  # every N seconds (non-standard)
            minutes = seconds // 60
            if minutes < 60:
                return f"*/{minutes} * * * *"
            hours = minutes // 60
            if hours < 24:
                return f"0 */{hours} * * *"
            days = hours // 24
            return f"0 0 */{days} * *"

        return "# one-time task, no cron equivalent"

    def _add_job(self, task: ScheduledTask) -> None:
        """Add or replace an APScheduler job for a task."""
        # Remove existing job if any
        with contextlib.suppress(Exception):
            self._scheduler.remove_job(task.id)

        if task.schedule_type == ScheduleType.INTERVAL:
            trigger = IntervalTrigger(seconds=task.interval_seconds)
        elif task.schedule_type == ScheduleType.CRON:
            parts = task.cron_expression.split()
            trigger = CronTrigger(
                minute=parts[0] if len(parts) > 0 else "*",
                hour=parts[1] if len(parts) > 1 else "*",
                day=parts[2] if len(parts) > 2 else "*",
                month=parts[3] if len(parts) > 3 else "*",
                day_of_week=parts[4] if len(parts) > 4 else "*",
            )
        else:
            # One-time: don't schedule, just store
            return

        self._scheduler.add_job(
            self._execute_task,
            trigger=trigger,
            id=task.id,
            name=task.description,
            args=[task.id],
            replace_existing=True,
        )

    async def _execute_task(self, task_id: str) -> None:
        """Execute a scheduled task."""
        task = self._scheduled_tasks.get(task_id)
        if not task or not task.enabled:
            return

        task.last_run = time.time()
        task.run_count += 1

        if self._bus:
            await self._bus.emit(
                Event(
                    event_type=EventType.TASK_CREATED,
                    data={
                        "scheduled_task_id": task_id,
                        "description": task.description,
                        "run_count": task.run_count,
                    },
                )
            )

        if self._task_callback:
            await self._task_callback(task.description, task.metadata)

        # Disable if max runs reached
        if task.max_runs > 0 and task.run_count >= task.max_runs:
            self.disable_task(task_id)

    def _on_job_event(self, event: Any) -> None:
        """Handle APScheduler job events (sync callback)."""
        # APScheduler listeners are synchronous, so we just update state
        task = self._scheduled_tasks.get(event.job_id)
        if task:
            job = None
            with contextlib.suppress(Exception):
                job = self._scheduler.get_job(event.job_id)
            if job and job.next_run_time:
                task.next_run = job.next_run_time.timestamp()
