"""Background scheduler for automatic container backups."""

from __future__ import annotations

import logging
import threading
import time
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Callable

from gaol.backup.models import (
    BackupPolicy,
    BackupResult,
    BackupSchedule,
    BackupScheduleType,
    SchedulerState,
)
from gaol.backup.store import BackupStore

if TYPE_CHECKING:
    pass


logger = logging.getLogger(__name__)


class BackupScheduler:
    """Background scheduler for automatic container backups.

    Monitors backup policies and executes backups at scheduled times.
    Uses a daemon thread that checks every minute for due backups.

    Example:
        >>> scheduler = BackupScheduler(store, on_backup=run_backup)
        >>> scheduler.start()
        >>> # Later...
        >>> scheduler.stop()
    """

    def __init__(
        self,
        store: BackupStore,
        on_backup: Callable[[str], BackupResult] | None = None,
        check_interval: float = 60.0,
    ) -> None:
        """Initialize the scheduler.

        Args:
            store: Backup store for policies and history
            on_backup: Callback to execute backups (container_name) -> BackupResult
            check_interval: Interval in seconds between schedule checks
        """
        self._store = store
        self._on_backup = on_backup
        self._check_interval = check_interval

        self._running = False
        self._thread: threading.Thread | None = None
        self._state = SchedulerState()
        self._lock = threading.Lock()

    @property
    def is_running(self) -> bool:
        """Check if scheduler is running."""
        return self._running

    @property
    def state(self) -> SchedulerState:
        """Get current scheduler state."""
        with self._lock:
            return self._state.model_copy()

    def start(self) -> None:
        """Start the scheduler background thread."""
        if self._running:
            logger.warning("Backup scheduler already running")
            return

        self._running = True
        with self._lock:
            self._state.running = True
            self._state.started_at = datetime.now()

        self._thread = threading.Thread(
            target=self._scheduler_loop,
            daemon=True,
            name="backup-scheduler",
        )
        self._thread.start()
        logger.info("Backup scheduler started")

    def stop(self) -> None:
        """Stop the scheduler background thread."""
        if not self._running:
            return

        self._running = False
        with self._lock:
            self._state.running = False

        if self._thread:
            self._thread.join(timeout=5.0)
            self._thread = None

        logger.info("Backup scheduler stopped")

    def _scheduler_loop(self) -> None:
        """Main scheduler loop running in background thread."""
        while self._running:
            try:
                self._check_and_run_backups()
            except Exception as e:
                logger.error(f"Error in backup scheduler loop: {e}")

            # Sleep in small intervals to allow quick shutdown
            for _ in range(int(self._check_interval)):
                if not self._running:
                    break
                time.sleep(1.0)

    def _check_and_run_backups(self) -> None:
        """Check all policies and run due backups."""
        with self._lock:
            self._state.last_check = datetime.now()

        policies = self._store.list_policies()
        now = datetime.now()

        for policy in policies:
            if not policy.schedule.enabled:
                continue

            if policy.schedule.schedule_type == BackupScheduleType.MANUAL:
                continue

            next_run = self._calculate_next_run(policy)
            if next_run is None:
                continue

            # Update state with next scheduled time
            with self._lock:
                self._state.next_scheduled[policy.container_name] = next_run

            # Check if backup is due
            if now >= next_run:
                self._run_scheduled_backup(policy)

    def _run_scheduled_backup(self, policy: BackupPolicy) -> None:
        """Run a scheduled backup for a container."""
        container_name = policy.container_name
        logger.info(f"Running scheduled backup for {container_name}")

        if self._on_backup:
            try:
                result = self._on_backup(container_name)
                logger.info(
                    f"Scheduled backup for {container_name} completed: {result.status}"
                )
            except Exception as e:
                logger.error(f"Scheduled backup for {container_name} failed: {e}")

        # Calculate and store next run time
        next_run = self._calculate_next_run_from_now(policy)
        if next_run:
            with self._lock:
                self._state.next_scheduled[policy.container_name] = next_run

    def _calculate_next_run(self, policy: BackupPolicy) -> datetime | None:
        """Calculate when the next backup should run.

        Uses history to determine if a backup is due, then calculates
        the next scheduled time based on the policy schedule.

        Args:
            policy: Backup policy with schedule configuration

        Returns:
            Next scheduled run time, or None if not applicable
        """
        schedule = policy.schedule
        history = self._store.get_history(policy.container_name)

        # Get last backup time
        last_backup = history.last_backup

        if last_backup is None:
            # Never backed up - run immediately at next scheduled time
            return self._calculate_next_scheduled_time(schedule)

        # Calculate next run based on schedule type
        if schedule.schedule_type == BackupScheduleType.DAILY:
            next_run = last_backup + timedelta(days=1)
        elif schedule.schedule_type == BackupScheduleType.WEEKLY:
            next_run = last_backup + timedelta(weeks=1)
        elif schedule.schedule_type == BackupScheduleType.MONTHLY:
            # Add approximately one month
            next_run = last_backup + timedelta(days=28)
        else:
            return None

        # Adjust to scheduled time of day
        next_run = next_run.replace(
            hour=int(schedule.time_of_day.split(":")[0]),
            minute=int(schedule.time_of_day.split(":")[1]),
            second=0,
            microsecond=0,
        )

        return next_run

    def _calculate_next_run_from_now(self, policy: BackupPolicy) -> datetime | None:
        """Calculate next run time starting from now."""
        return self._calculate_next_scheduled_time(policy.schedule)

    def _calculate_next_scheduled_time(
        self, schedule: BackupScheduleType | BackupSchedule
    ) -> datetime | None:
        """Calculate the next scheduled time based on schedule config.

        Args:
            schedule: Schedule configuration

        Returns:
            Next scheduled datetime
        """
        if isinstance(schedule, BackupScheduleType):
            return None

        if not isinstance(schedule, BackupSchedule):
            return None

        now = datetime.now()
        hour = int(schedule.time_of_day.split(":")[0])
        minute = int(schedule.time_of_day.split(":")[1])

        if schedule.schedule_type == BackupScheduleType.DAILY:
            # Next occurrence of the scheduled time
            next_time = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
            if next_time <= now:
                next_time += timedelta(days=1)
            return next_time

        elif schedule.schedule_type == BackupScheduleType.WEEKLY:
            # Next occurrence on the scheduled day of week
            days_ahead = schedule.day_of_week - now.weekday()
            if days_ahead < 0:  # Target day already happened this week
                days_ahead += 7
            elif days_ahead == 0:
                # Same day - check if time has passed
                next_time = now.replace(
                    hour=hour, minute=minute, second=0, microsecond=0
                )
                if next_time <= now:
                    days_ahead = 7
                else:
                    return next_time

            next_time = now + timedelta(days=days_ahead)
            return next_time.replace(hour=hour, minute=minute, second=0, microsecond=0)

        elif schedule.schedule_type == BackupScheduleType.MONTHLY:
            # Next occurrence on the scheduled day of month
            try:
                next_time = now.replace(
                    day=schedule.day_of_month,
                    hour=hour,
                    minute=minute,
                    second=0,
                    microsecond=0,
                )
                if next_time <= now:
                    # Move to next month
                    if now.month == 12:
                        next_time = next_time.replace(year=now.year + 1, month=1)
                    else:
                        next_time = next_time.replace(month=now.month + 1)
                return next_time
            except ValueError:
                # Invalid day for month (e.g., Feb 30)
                # Move to last day of month or next month
                return None

        return None

    def get_next_backup(self, container_name: str) -> datetime | None:
        """Get the next scheduled backup time for a container.

        Args:
            container_name: Container name

        Returns:
            Next scheduled backup time, or None if not scheduled
        """
        with self._lock:
            return self._state.next_scheduled.get(container_name)

    def get_scheduled_backups(self) -> dict[str, datetime]:
        """Get all scheduled backups.

        Returns:
            Dictionary mapping container names to next backup times
        """
        with self._lock:
            return dict(self._state.next_scheduled)

    def trigger_backup(self, container_name: str) -> BackupResult | None:
        """Manually trigger a backup for a container.

        Args:
            container_name: Container name

        Returns:
            BackupResult or None if no callback configured
        """
        if self._on_backup:
            return self._on_backup(container_name)
        return None

    def remove_container(self, container_name: str) -> None:
        """Remove a container from the scheduler state.

        Args:
            container_name: Container name to remove
        """
        with self._lock:
            self._state.next_scheduled.pop(container_name, None)


# Global scheduler instance
_scheduler: BackupScheduler | None = None


def get_backup_scheduler(
    store: BackupStore | None = None,
    on_backup: Callable[[str], BackupResult] | None = None,
) -> BackupScheduler:
    """Get the global backup scheduler instance.

    Args:
        store: Backup store (required on first call)
        on_backup: Backup callback (optional)

    Returns:
        Global BackupScheduler instance
    """
    global _scheduler
    if _scheduler is None:
        if store is None:
            from gaol.backup.store import get_backup_store

            store = get_backup_store()
        _scheduler = BackupScheduler(store, on_backup)
    return _scheduler
