"""
Staff Shift Automation Service
Enterprise-grade automated shift status management with business logic
"""

from datetime import datetime, time, timedelta
from typing import List, Dict, Any, Optional, Tuple
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_, func

from ...shared.config.logging_config import get_logger
from ..models.staff_shift import StaffShift, ShiftStatus, ShiftType
from ...analytics.models.audit_log import AuditLog
from ...notifications.services.notification_service import NotificationService
from ...notifications.models.notification import NotificationType, NotificationChannel

logger = get_logger(__name__)


class ShiftAutomationService:
    """Service for automated shift status management"""
    
    # Configuration constants
    NO_SHOW_THRESHOLD_MINUTES = 15  # Mark as no_show if 15 minutes past start time
    AUTO_COMPLETE_BUFFER_MINUTES = 0  # Auto-complete at exact end time
    AUTO_START_BUFFER_MINUTES = 0  # Auto-start at exact start time
    
    @staticmethod
    def get_shift_datetime(shift: StaffShift) -> Tuple[datetime, datetime]:
        """
        Convert shift date and time to full datetime objects
        Returns (start_datetime, end_datetime)
        """
        shift_date = shift.shift_date.date() if isinstance(shift.shift_date, datetime) else shift.shift_date
        
        # Combine date with time
        start_datetime = datetime.combine(shift_date, shift.start_time)
        end_datetime = datetime.combine(shift_date, shift.end_time)
        
        # Handle overnight shifts (end time is next day)
        if shift.end_time < shift.start_time:
            end_datetime += timedelta(days=1)
        
        return start_datetime, end_datetime
    
    @staticmethod
    def should_auto_start(shift: StaffShift, current_time: datetime) -> bool:
        """Check if shift should be automatically started - at exact start time"""
        if shift.status != ShiftStatus.scheduled:
            return False
        
        start_datetime, _ = ShiftAutomationService.get_shift_datetime(shift)
        
        # Auto-start at exact start time (or after)
        return current_time >= start_datetime
    
    @staticmethod
    def should_auto_complete(shift: StaffShift, current_time: datetime) -> bool:
        """Check if shift should be automatically completed - at exact end time"""
        if shift.status != ShiftStatus.in_progress:
            return False
        
        _, end_datetime = ShiftAutomationService.get_shift_datetime(shift)
        
        # Auto-complete at exact end time (or after)
        return current_time >= end_datetime
    
    @staticmethod
    def should_mark_no_show(shift: StaffShift, current_time: datetime) -> bool:
        """Check if shift should be marked as no_show"""
        if shift.status != ShiftStatus.scheduled:
            return False
        
        start_datetime, _ = ShiftAutomationService.get_shift_datetime(shift)
        
        # Mark as no_show if past start time + threshold and no actual start recorded
        no_show_time = start_datetime + timedelta(minutes=ShiftAutomationService.NO_SHOW_THRESHOLD_MINUTES)
        
        return current_time >= no_show_time and shift.actual_start_time is None
    
    @staticmethod
    def auto_start_shift(
        db: Session,
        shift: StaffShift,
        current_time: Optional[datetime] = None,
        triggered_by: Optional[int] = None
    ) -> bool:
        """
        Automatically start a shift
        Returns True if status was changed, False otherwise
        """
        if shift.status != ShiftStatus.scheduled:
            return False
        
        current_time = current_time or datetime.utcnow()
        
        try:
            old_status = shift.status
            shift.status = ShiftStatus.in_progress
            shift.actual_start_time = current_time
            shift.updated_at = current_time
            
            db.commit()
            
            # Log audit trail
            ShiftAutomationService._log_status_change(
                db=db,
                shift_id=shift.id,
                old_status=old_status,
                new_status=ShiftStatus.in_progress,
                user_id=triggered_by,
                reason="AUTO_START",
                details={"triggered_at": current_time.isoformat()}
            )
            
            # Send notification to staff (with fallback)
            try:
                try:
                    NotificationService.send_notification(
                        db=db,
                        user_id=shift.staff_id,
                        notification_type=NotificationType.shift_update,
                        channel=NotificationChannel.in_app,
                        content=f"Your shift has automatically started at {current_time.strftime('%H:%M')}",
                        subject="Shift Started",
                        meta_data={
                            "shift_id": shift.id,
                            "status": "in_progress",
                            "auto_started": True
                        }
                    )
                except Exception as enum_error:
                    # Fallback to system_alert if shift_update enum not in database yet
                    NotificationService.send_notification(
                        db=db,
                        user_id=shift.staff_id,
                        notification_type=NotificationType.system_alert,
                        channel=NotificationChannel.in_app,
                        content=f"Your shift has automatically started at {current_time.strftime('%H:%M')}",
                        subject="Shift Started",
                        meta_data={
                            "shift_id": shift.id,
                            "status": "in_progress",
                            "auto_started": True
                        }
                    )
            except Exception as e:
                logger.warning(f"Failed to send notification for shift {shift.id}: {str(e)}")
                try:
                    db.rollback()
                except:
                    pass
            
            logger.info(f"Shift {shift.id} automatically started at {current_time.isoformat()}")
            return True
            
        except Exception as e:
            logger.error(f"Error auto-starting shift {shift.id}: {str(e)}", exc_info=True)
            db.rollback()
            return False
    
    @staticmethod
    def auto_complete_shift(
        db: Session,
        shift: StaffShift,
        current_time: Optional[datetime] = None,
        triggered_by: Optional[int] = None
    ) -> bool:
        """
        Automatically complete a shift
        Returns True if status was changed, False otherwise
        """
        if shift.status != ShiftStatus.in_progress:
            return False
        
        current_time = current_time or datetime.utcnow()
        
        try:
            old_status = shift.status
            shift.status = ShiftStatus.completed
            shift.actual_end_time = current_time
            shift.updated_at = current_time
            
            # If no actual start time was recorded, set it to scheduled start
            if not shift.actual_start_time:
                start_datetime, _ = ShiftAutomationService.get_shift_datetime(shift)
                shift.actual_start_time = start_datetime
            
            db.commit()
            
            # Log audit trail
            ShiftAutomationService._log_status_change(
                db=db,
                shift_id=shift.id,
                old_status=old_status,
                new_status=ShiftStatus.completed,
                user_id=triggered_by,
                reason="AUTO_COMPLETE",
                details={
                    "triggered_at": current_time.isoformat(),
                    "auto_completed": True
                }
            )
            
            # Send notification to staff (with fallback)
            try:
                try:
                    NotificationService.send_notification(
                        db=db,
                        user_id=shift.staff_id,
                        notification_type=NotificationType.shift_update,
                        channel=NotificationChannel.in_app,
                        content=f"Your shift has been automatically completed at {current_time.strftime('%H:%M')}",
                        subject="Shift Completed",
                        meta_data={
                            "shift_id": shift.id,
                            "status": "completed",
                            "auto_completed": True
                        }
                    )
                except Exception as enum_error:
                    # Fallback to system_alert if shift_update enum not in database yet
                    NotificationService.send_notification(
                        db=db,
                        user_id=shift.staff_id,
                        notification_type=NotificationType.system_alert,
                        channel=NotificationChannel.in_app,
                        content=f"Your shift has been automatically completed at {current_time.strftime('%H:%M')}",
                        subject="Shift Completed",
                        meta_data={
                            "shift_id": shift.id,
                            "status": "completed",
                            "auto_completed": True
                        }
                    )
            except Exception as e:
                logger.warning(f"Failed to send notification for shift {shift.id}: {str(e)}")
                try:
                    db.rollback()
                except:
                    pass
            
            logger.info(f"Shift {shift.id} automatically completed at {current_time.isoformat()}")
            return True
            
        except Exception as e:
            logger.error(f"Error auto-completing shift {shift.id}: {str(e)}", exc_info=True)
            db.rollback()
            return False
    
    @staticmethod
    def mark_no_show(
        db: Session,
        shift: StaffShift,
        current_time: Optional[datetime] = None,
        triggered_by: Optional[int] = None
    ) -> bool:
        """
        Mark a shift as no_show
        Returns True if status was changed, False otherwise
        """
        if shift.status != ShiftStatus.scheduled:
            return False
        
        current_time = current_time or datetime.utcnow()
        
        try:
            old_status = shift.status
            shift.status = ShiftStatus.no_show
            shift.updated_at = current_time
            
            db.commit()
            
            # Log audit trail
            ShiftAutomationService._log_status_change(
                db=db,
                shift_id=shift.id,
                old_status=old_status,
                new_status=ShiftStatus.no_show,
                user_id=triggered_by,
                reason="AUTO_NO_SHOW",
                details={
                    "triggered_at": current_time.isoformat(),
                    "auto_marked": True
                }
            )
            
            # Send notification to staff and admin (with fallback)
            try:
                try:
                    NotificationService.send_notification(
                        db=db,
                        user_id=shift.staff_id,
                        notification_type=NotificationType.shift_update,
                        channel=NotificationChannel.in_app,
                        content=f"Your shift was marked as no-show. Please contact your supervisor.",
                        subject="Shift No-Show",
                        meta_data={
                            "shift_id": shift.id,
                            "status": "no_show",
                            "auto_marked": True
                        }
                    )
                except Exception as enum_error:
                    # Fallback to system_alert if shift_update enum not in database yet
                    NotificationService.send_notification(
                        db=db,
                        user_id=shift.staff_id,
                        notification_type=NotificationType.system_alert,
                        channel=NotificationChannel.in_app,
                        content=f"Your shift was marked as no-show. Please contact your supervisor.",
                        subject="Shift No-Show",
                        meta_data={
                            "shift_id": shift.id,
                            "status": "no_show",
                            "auto_marked": True
                        }
                    )
            except Exception as e:
                logger.warning(f"Failed to send notification for shift {shift.id}: {str(e)}")
                try:
                    db.rollback()
                except:
                    pass
            
            logger.warning(f"Shift {shift.id} marked as no-show at {current_time.isoformat()}")
            return True
            
        except Exception as e:
            logger.error(f"Error marking shift {shift.id} as no-show: {str(e)}", exc_info=True)
            db.rollback()
            return False
    
    @staticmethod
    def manual_status_change(
        db: Session,
        shift: StaffShift,
        new_status: ShiftStatus,
        user_id: int,
        reason: Optional[str] = None,
        notes: Optional[str] = None
    ) -> bool:
        """
        Manually change shift status - ONLY for cancellation
        All other status changes (start, complete) are handled automatically by scheduler
        Returns True if status was changed, False otherwise
        """
        old_status = shift.status
        current_time = datetime.utcnow()
        
        # Only allow manual cancellation - all other status changes are automatic
        if new_status != ShiftStatus.cancelled:
            logger.warning(
                f"Manual status change to {new_status.value} not allowed. "
                f"Only cancellation is allowed manually. Shift {shift.id} by user {user_id}"
            )
            return False
        
        # Validate status transition (only cancellation allowed)
        if not ShiftAutomationService._is_valid_transition(old_status, new_status):
            logger.warning(
                f"Invalid status transition from {old_status.value} to {new_status.value} "
                f"for shift {shift.id} by user {user_id}"
            )
            return False
        
        try:
            shift.status = new_status
            shift.updated_at = current_time
            
            # Update timestamps based on status
            if new_status == ShiftStatus.in_progress and not shift.actual_start_time:
                shift.actual_start_time = current_time
            elif new_status == ShiftStatus.completed and not shift.actual_end_time:
                shift.actual_end_time = current_time
                # If no actual start time, set it to scheduled start
                if not shift.actual_start_time:
                    start_datetime, _ = ShiftAutomationService.get_shift_datetime(shift)
                    shift.actual_start_time = start_datetime
            
            # Update notes if provided
            if notes:
                if shift.notes:
                    shift.notes += f"\n[{current_time.strftime('%Y-%m-%d %H:%M')}] {notes}"
                else:
                    shift.notes = f"[{current_time.strftime('%Y-%m-%d %H:%M')}] {notes}"
            
            db.commit()
            
            # Log audit trail
            ShiftAutomationService._log_status_change(
                db=db,
                shift_id=shift.id,
                old_status=old_status,
                new_status=new_status,
                user_id=user_id,
                reason=reason or "MANUAL",
                details={
                    "notes": notes,
                    "changed_at": current_time.isoformat()
                }
            )
            
            # Send notification for cancellation (use system_alert as fallback if shift_update not in DB)
            try:
                cancellation_message = f"Your shift has been cancelled"
                if notes:
                    cancellation_message += f". Reason: {notes}"
                
                # Try shift_update first, fallback to system_alert if enum not updated in DB
                try:
                    NotificationService.send_notification(
                        db=db,
                        user_id=shift.staff_id,
                        notification_type=NotificationType.shift_update,
                        channel=NotificationChannel.in_app,
                        content=cancellation_message,
                        subject="Shift Cancelled",
                        meta_data={
                            "shift_id": shift.id,
                            "status": "cancelled",
                            "changed_by": user_id,
                            "reason": reason,
                            "notes": notes
                        }
                    )
                except Exception as enum_error:
                    # Fallback to system_alert if shift_update enum not in database yet
                    logger.warning(f"shift_update enum not available, using system_alert: {str(enum_error)}")
                    NotificationService.send_notification(
                        db=db,
                        user_id=shift.staff_id,
                        notification_type=NotificationType.system_alert,
                        channel=NotificationChannel.in_app,
                        content=cancellation_message,
                        subject="Shift Cancelled",
                        meta_data={
                            "shift_id": shift.id,
                            "status": "cancelled",
                            "changed_by": user_id,
                            "reason": reason,
                            "notes": notes
                        }
                    )
            except Exception as e:
                # Log but don't fail the operation if notification fails
                logger.warning(f"Failed to send notification for shift cancellation: {str(e)}")
                # Rollback notification transaction if it failed
                try:
                    db.rollback()
                except:
                    pass
            
            logger.info(
                f"Shift {shift.id} status changed from {old_status.value} to {new_status.value} "
                f"by user {user_id}"
            )
            return True
            
        except Exception as e:
            shift_id_str = str(shift.id) if shift and hasattr(shift, 'id') else 'unknown'
            logger.error(f"Error changing shift {shift_id_str} status: {str(e)}", exc_info=True)
            try:
                db.rollback()
            except:
                pass
            return False
    
    @staticmethod
    def _is_valid_transition(old_status: ShiftStatus, new_status: ShiftStatus) -> bool:
        """
        Validate if a status transition is allowed
        Enterprise business rules for status transitions
        """
        # Same status is always valid (no-op)
        if old_status == new_status:
            return True
        
        # Define valid transitions
        valid_transitions = {
            ShiftStatus.scheduled: [
                ShiftStatus.in_progress,
                ShiftStatus.cancelled,
                ShiftStatus.no_show
            ],
            ShiftStatus.in_progress: [
                ShiftStatus.completed,
                ShiftStatus.cancelled
            ],
            ShiftStatus.completed: [],  # Terminal state - cannot transition from
            ShiftStatus.cancelled: [],  # Terminal state - cannot transition from
            ShiftStatus.no_show: [
                ShiftStatus.cancelled  # Can cancel a no-show
            ]
        }
        
        allowed = valid_transitions.get(old_status, [])
        return new_status in allowed
    
    @staticmethod
    def _log_status_change(
        db: Session,
        shift_id: int,
        old_status: ShiftStatus,
        new_status: ShiftStatus,
        user_id: Optional[int],
        reason: str,
        details: Optional[Dict[str, Any]] = None
    ):
        """Log shift status change to audit log"""
        try:
            audit_log = AuditLog(
                user_id=user_id,
                action="shift_status_change",
                resource_type="staff_shift",
                resource_id=shift_id,
                details={
                    "old_status": old_status.value,
                    "new_status": new_status.value,
                    "reason": reason,
                    **(details or {})
                },
                status="success"
            )
            db.add(audit_log)
            db.commit()
        except Exception as e:
            logger.error(f"Failed to log status change for shift {shift_id}: {str(e)}")
            # Don't fail the operation if audit logging fails
    
    @staticmethod
    def process_shift_automation(db: Session, current_time: Optional[datetime] = None) -> Dict[str, Any]:
        """
        Process all shifts that need automatic status updates
        This is the main method called by the scheduler
        Returns statistics about processed shifts
        """
        current_time = current_time or datetime.utcnow()
        stats = {
            "processed": 0,
            "auto_started": 0,
            "auto_completed": 0,
            "marked_no_show": 0,
            "errors": 0
        }
        
        try:
            # Get all shifts that need processing
            # Only process shifts from today and future (not too far in the past)
            cutoff_date = current_time.date() - timedelta(days=1)
            
            shifts_to_process = db.query(StaffShift).filter(
                and_(
                    func.date(StaffShift.shift_date) >= cutoff_date,
                    StaffShift.status.in_([ShiftStatus.scheduled, ShiftStatus.in_progress])
                )
            ).all()
            
            logger.info(f"Processing {len(shifts_to_process)} shifts for automation")
            
            for shift in shifts_to_process:
                try:
                    stats["processed"] += 1
                    
                    # Check for no-show first (highest priority)
                    if ShiftAutomationService.should_mark_no_show(shift, current_time):
                        if ShiftAutomationService.mark_no_show(db, shift, current_time):
                            stats["marked_no_show"] += 1
                        continue
                    
                    # Check for auto-complete
                    if ShiftAutomationService.should_auto_complete(shift, current_time):
                        if ShiftAutomationService.auto_complete_shift(db, shift, current_time):
                            stats["auto_completed"] += 1
                        continue
                    
                    # Check for auto-start
                    if ShiftAutomationService.should_auto_start(shift, current_time):
                        if ShiftAutomationService.auto_start_shift(db, shift, current_time):
                            stats["auto_started"] += 1
                        continue
                    
                except Exception as e:
                    logger.error(f"Error processing shift {shift.id}: {str(e)}", exc_info=True)
                    stats["errors"] += 1
            
            logger.info(
                f"Shift automation completed: {stats['auto_started']} started, "
                f"{stats['auto_completed']} completed, {stats['marked_no_show']} no-shows, "
                f"{stats['errors']} errors"
            )
            
            return stats
            
        except Exception as e:
            logger.error(f"Error in shift automation process: {str(e)}", exc_info=True)
            stats["errors"] += 1
            return stats


# Singleton instance
shift_automation_service = ShiftAutomationService()

