"""
BGX Scheduler module - Task scheduling with delay, interval, and cron support.

This module provides the TaskScheduler class that manages delayed and recurring
task execution, including a full cron parser implementation using only
the Python standard library.
"""

import re
import threading
import time
from collections import namedtuple
from dataclasses import dataclass, field
from typing import Callable, Dict, List, Optional, Union

from .task import Task, TaskState


# Simple data structures for scheduling
ScheduledTask = namedtuple('ScheduledTask', ['id', 'func', 'args', 'kwargs', 'next_run', 'interval', 'cron_expr', 'task_id'])


@dataclass
class CronField:
    """Represents a parsed cron field."""
    values: set[int] = field(default_factory=set)
    is_wildcard: bool = False
    is_step: bool = False
    step: int = 1


class CronParser:
    """Cron expression parser using only Python standard library."""
    
    # Field mappings for cron expressions
    FIELD_RANGES = {
        'minute': (0, 59),
        'hour': (0, 23),
        'day_of_month': (1, 31),
        'month': (1, 12),
        'day_of_week': (0, 6)  # 0 = Sunday, 6 = Saturday
    }
    
    # Month and day name mappings
    MONTH_NAMES = {
        'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6,
        'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12
    }
    
    DAY_NAMES = {
        'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4, 'fri': 5, 'sat': 6
    }
    
    def __init__(self, expression: str):
        """Initialize cron parser with expression.
        
        Args:
            expression: Cron expression (5 fields: minute hour day month weekday)
        """
        self.expression = expression.strip()
        self.fields = self._parse_expression(expression)
    
    def _parse_expression(self, expression: str) -> Dict[str, CronField]:
        """Parse cron expression into fields."""
        parts = expression.split()
        if len(parts) != 5:
            raise ValueError(f"Cron expression must have 5 fields, got {len(parts)}: {expression}")
        
        field_names = ['minute', 'hour', 'day_of_month', 'month', 'day_of_week']
        fields = {}
        
        for name, part in zip(field_names, parts):
            try:
                fields[name] = self._parse_field(part, name)
            except ValueError as e:
                raise ValueError(f"Invalid cron field '{part}' for {name}: {e}")
        
        return fields
    
    def _parse_field(self, field_str: str, field_name: str) -> CronField:
        """Parse a single cron field."""
        if field_str == '*':
            return CronField(is_wildcard=True)
        
        # Handle step values (e.g., */15, 1-10/2)
        step = 1
        is_step = False
        values = set()
        
        if '/' in field_str:
            base, step_str = field_str.split('/')
            step = int(step_str)
            is_step = True
            field_str = base
        
        # Handle ranges (e.g., 1-5)
        if '-' in field_str:
            start, end = field_str.split('-')
            values = set(range(int(start), int(end) + 1))
        elif ',' in field_str:
            # Handle lists (e.g., 1,2,3)
            values = set()
            for x in field_str.split(','):
                x = x.strip()
                try:
                    values.add(int(x))
                except ValueError:
                    # Might be a name like 'JAN' or 'MON'
                    values.add(x)
        elif field_str != '*':
            # Handle single values
            try:
                values = {int(field_str)}
            except ValueError:
                # Might be a name like 'JAN' or 'MON'
                values = {field_str}
        
        # Handle the */N case (every N units)
        if is_step and field_str == '*':
            # For */N, we want all values that are divisible by step
            if field_name == 'minute':
                values = set(range(0, 60, step))
            elif field_name == 'hour':
                values = set(range(0, 24, step))
            elif field_name == 'day_of_month':
                values = set(range(1, 32, step))
            elif field_name == 'month':
                values = set(range(1, 13, step))
            elif field_name == 'day_of_week':
                values = set(range(0, 7, step))
        # Apply step to range if needed (but not to */N which we handled above)
        elif is_step and values and field_str != '*':
            min_val = min(values)
            max_val = max(values)
            values = {v for v in range(min_val, max_val + 1) if (v - min_val) % step == 0}
        
        # Handle name substitutions for months and days
        if field_name in ['month', 'day_of_week'] and values:
            values = self._substitute_names(values, field_name)
        
        # Validate range
        if values and field_name in self.FIELD_RANGES:
            min_val, max_val = self.FIELD_RANGES[field_name]
            values = {v for v in values if isinstance(v, int) and min_val <= v <= max_val}
            
            # Additional validation for specific fields
            if field_name == 'minute' and any(not isinstance(v, int) or v < 0 or v > 59 for v in values):
                raise ValueError(f"Invalid minute value: {values}")
            elif field_name == 'hour' and any(not isinstance(v, int) or v < 0 or v > 23 for v in values):
                raise ValueError(f"Invalid hour value: {values}")
            elif field_name == 'day_of_month' and any(not isinstance(v, int) or v < 1 or v > 31 for v in values):
                raise ValueError(f"Invalid day value: {values}")
            elif field_name == 'month' and any(not isinstance(v, int) or v < 1 or v > 12 for v in values):
                raise ValueError(f"Invalid month value: {values}")
            elif field_name == 'day_of_week' and any(not isinstance(v, int) or v < 0 or v > 6 for v in values):
                raise ValueError(f"Invalid weekday value: {values}")
        
        return CronField(values=values, is_step=is_step, step=step)
    
    def _substitute_names(self, values: set, field_name: str) -> set:
        """Substitute month/day names with numbers."""
        if field_name == 'month':
            return {self.MONTH_NAMES.get(str(v).lower(), v) if isinstance(v, str) else v for v in values}
        elif field_name == 'day_of_week':
            return {self.DAY_NAMES.get(str(v).lower(), v) if isinstance(v, str) else v for v in values}
        return values
    
    def next_run_time(self, current_time: float) -> float:
        """Calculate next run time based on cron expression.
        
        Args:
            current_time: Current Unix timestamp
            
        Returns:
            Next run time as Unix timestamp
        """
        import datetime
        
        current_dt = datetime.datetime.fromtimestamp(current_time)
        
        # Start checking from next minute
        next_dt = current_dt.replace(second=0, microsecond=0) + datetime.timedelta(minutes=1)
        
        # Check up to 4 years ahead (should be sufficient for any cron expression)
        for _ in range(4 * 365 * 24 * 60):  # 4 years worth of minutes
            if self._matches_datetime(next_dt):
                return next_dt.timestamp()
            next_dt += datetime.timedelta(minutes=1)
        
        raise ValueError("Could not find next run time for cron expression")
    
    def _matches_datetime(self, dt: 'datetime.datetime') -> bool:
        """Check if datetime matches cron expression."""
        return (
            self._matches_field(dt.minute, self.fields['minute']) and
            self._matches_field(dt.hour, self.fields['hour']) and
            self._matches_field(dt.day, self.fields['day_of_month']) and
            self._matches_field(dt.month, self.fields['month']) and
            self._matches_field(dt.weekday(), self.fields['day_of_week'])
        )
    
    def _matches_field(self, value: int, field: CronField) -> bool:
        """Check if value matches cron field."""
        if field.is_wildcard:
            return True
        
        if field.is_step:
            return value in field.values and (value - min(field.values)) % field.step == 0
        
        return value in field.values


class ScheduledHandle:
    """Handle for managing scheduled tasks."""
    
    def __init__(self, task_id: str, scheduler: 'TaskScheduler'):
        """Initialize scheduled handle.
        
        Args:
            task_id: Unique identifier for the scheduled task
            scheduler: Reference to the scheduler
        """
        self.task_id = task_id
        self._scheduler = scheduler
        self._cancelled = False
    
    def cancel(self) -> bool:
        """Cancel the scheduled task.
        
        Returns:
            True if task was cancelled, False if not found or already cancelled
        """
        if self._cancelled:
            return False
        
        result = self._scheduler.cancel_scheduled_task(self.task_id)
        if result:
            self._cancelled = True
        return result
    
    @property
    def is_cancelled(self) -> bool:
        """Check if the task is cancelled."""
        return self._cancelled


class TaskScheduler:
    """Task scheduler with delay, interval, and cron support."""
    
    def __init__(self, submit_callback: Callable[[str], None] = None):
        """Initialize task scheduler.
        
        Args:
            submit_callback: Function to call when submitting a task
        """
        self._submit_callback = submit_callback
        self._scheduled_tasks: Dict[str, ScheduledTask] = {}
        self._next_id = 0
        self._lock = threading.RLock()
        self._shutdown_event = threading.Event()
        
        # Start scheduler thread
        self._scheduler_thread = threading.Thread(
            target=self._scheduler_worker,
            name="bgx-scheduler",
            daemon=True
        )
        self._scheduler_thread.start()
    
    def schedule(self, func: Callable, delay: float, *args, **kwargs) -> ScheduledHandle:
        """Schedule a one-shot task to run after delay.
        
        Args:
            func: Function to execute
            delay: Delay in seconds before execution
            *args: Positional arguments for the function
            **kwargs: Keyword arguments for the function
            
        Returns:
            ScheduledHandle for managing the scheduled task
        """
        task_id = self._generate_task_id()
        next_run = time.time() + delay
        
        with self._lock:
            self._scheduled_tasks[task_id] = ScheduledTask(
                id=task_id,
                func=func,
                args=args,
                kwargs=kwargs,
                next_run=next_run,
                interval=None,
                cron_expr=None,
                task_id=None
            )
        
        return ScheduledHandle(task_id, self)
    
    def every(self, func: Callable, interval: float, *args, **kwargs) -> ScheduledHandle:
        """Schedule a recurring task to run at fixed intervals.
        
        Args:
            func: Function to execute
            interval: Interval in seconds between executions
            *args: Positional arguments for the function
            **kwargs: Keyword arguments for the function
            
        Returns:
            ScheduledHandle for managing the scheduled task
        """
        task_id = self._generate_task_id()
        next_run = time.time() + interval
        
        with self._lock:
            self._scheduled_tasks[task_id] = ScheduledTask(
                id=task_id,
                func=func,
                args=args,
                kwargs=kwargs,
                next_run=next_run,
                interval=interval,
                cron_expr=None,
                task_id=None
            )
        
        return ScheduledHandle(task_id, self)
    
    def cron(self, func: Callable, expression: str, *args, **kwargs) -> ScheduledHandle:
        """Schedule a recurring task using cron expression.
        
        Args:
            func: Function to execute
            expression: Cron expression (5 fields: minute hour day month weekday)
            *args: Positional arguments for the function
            **kwargs: Keyword arguments for the function
            
        Returns:
            ScheduledHandle for managing the scheduled task
        """
        task_id = self._generate_task_id()
        cron_parser = CronParser(expression)
        next_run = cron_parser.next_run_time(time.time())
        
        with self._lock:
            self._scheduled_tasks[task_id] = ScheduledTask(
                id=task_id,
                func=func,
                args=args,
                kwargs=kwargs,
                next_run=next_run,
                interval=None,
                cron_expr=cron_parser,
                task_id=None
            )
        
        return ScheduledHandle(task_id, self)
    
    def cancel_scheduled_task(self, task_id: str) -> bool:
        """Cancel a scheduled task by ID.
        
        Args:
            task_id: ID of the scheduled task to cancel
            
        Returns:
            True if task was cancelled, False if not found
        """
        with self._lock:
            if task_id in self._scheduled_tasks:
                del self._scheduled_tasks[task_id]
                return True
            return False
    
    def _generate_task_id(self) -> str:
        """Generate a unique task ID."""
        self._next_id += 1
        return f"schedule_{self._next_id}"
    
    def _scheduler_worker(self) -> None:
        """Background worker that processes scheduled tasks."""
        while not self._shutdown_event.is_set():
            current_time = time.time()
            tasks_to_run = []
            tasks_to_reschedule = []
            
            with self._lock:
                for task_id, scheduled_task in list(self._scheduled_tasks.items()):
                    if current_time >= scheduled_task.next_run:
                        tasks_to_run.append(scheduled_task)
                        
                        # Calculate next run time for recurring tasks
                        if scheduled_task.interval is not None:
                            next_run = current_time + scheduled_task.interval
                            tasks_to_reschedule.append((task_id, next_run))
                        elif scheduled_task.cron_expr is not None:
                            try:
                                next_run = scheduled_task.cron_expr.next_run_time(current_time)
                                tasks_to_reschedule.append((task_id, next_run))
                            except ValueError:
                                # Invalid cron expression, remove task
                                continue
            
            # Submit tasks to engine
            for scheduled_task in tasks_to_run:
                if self._submit_callback:
                    try:
                        self._submit_callback(scheduled_task.func, *scheduled_task.args, **scheduled_task.kwargs)
                    except Exception as e:
                        print(f"Scheduler error submitting task {scheduled_task.id}: {e}")
            
            # Update recurring tasks
            with self._lock:
                for task_id, next_run in tasks_to_reschedule:
                    if task_id in self._scheduled_tasks:
                        # Update the next run time
                        old_task = self._scheduled_tasks[task_id]
                        self._scheduled_tasks[task_id] = old_task._replace(next_run=next_run)
                
                # Remove completed one-shot tasks
                for scheduled_task in tasks_to_run:
                    if (scheduled_task.interval is None and 
                        scheduled_task.cron_expr is None and 
                        scheduled_task.id in self._scheduled_tasks):
                        del self._scheduled_tasks[scheduled_task.id]
            
            # Sleep for a short interval before next check
            self._shutdown_event.wait(0.1)  # Check every 100ms for better responsiveness
    
    def get_scheduled_count(self) -> int:
        """Get number of currently scheduled tasks."""
        with self._lock:
            return len(self._scheduled_tasks)
    
    def get_scheduled_info(self) -> List[Dict]:
        """Get information about scheduled tasks."""
        with self._lock:
            info = []
            for task_id, scheduled_task in self._scheduled_tasks.items():
                task_info = {
                    'id': task_id,
                    'next_run': scheduled_task.next_run,
                    'time_until': scheduled_task.next_run - time.time()
                }
                
                if scheduled_task.interval is not None:
                    task_info['type'] = 'interval'
                    task_info['interval'] = scheduled_task.interval
                elif scheduled_task.cron_expr is not None:
                    task_info['type'] = 'cron'
                    task_info['expression'] = scheduled_task.cron_expr.expression
                else:
                    task_info['type'] = 'delay'
                
                info.append(task_info)
            
            return info
    
    def shutdown(self) -> None:
        """Shutdown the scheduler."""
        self._shutdown_event.set()
        self._scheduler_thread.join(timeout=5.0)
        
        # Clear scheduled tasks
        with self._lock:
            self._scheduled_tasks.clear()
