from django.db import models
from django.utils import timezone
from django.core.validators import MinValueValidator
from django.core.exceptions import ValidationError
from accounts.models import Employee, Company
from decimal import Decimal
from datetime import datetime, time, date

class Holiday(models.Model):
    name = models.CharField(max_length=100)
    date = models.DateField()
    recurring = models.BooleanField(
        default=True,
        help_text="If True, holiday repeats every year on the same date"
    )
    company = models.ForeignKey(
        Company,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="If null, applies to all companies"
    )

    class Meta:
        ordering = ['date']
        unique_together = [['date', 'company']]

    def __str__(self):
        return f"{self.name} ({self.date.strftime('%B %d')})"

    @classmethod
    def is_holiday(cls, check_date: date, company=None):
        """Check if a given date is a holiday for the specified company"""
        # Check for company-specific and global holidays
        # For recurring holidays, we need to check month and day regardless of year
        recurring_holidays = cls.objects.filter(
            models.Q(company=company) | models.Q(company__isnull=True),
            recurring=True,
            date__month=check_date.month,
            date__day=check_date.day
        )
        
        # For non-recurring holidays, check the exact date
        non_recurring_holidays = cls.objects.filter(
            models.Q(company=company) | models.Q(company__isnull=True),
            recurring=False,
            date=check_date
        )
        
        is_holiday = recurring_holidays.exists() or non_recurring_holidays.exists()
        return is_holiday


class CompanyOvertimeConfig(models.Model):
    """Configuration for company-specific overtime rules"""
    company = models.OneToOneField(Company, on_delete=models.CASCADE)
    
    # Working days configuration
    working_days_per_month = models.PositiveIntegerField(
        default=22,
        help_text="Standard number of working days per month"
    )
    working_hours_per_day = models.PositiveIntegerField(
        default=8,
        help_text="Standard number of working hours per day"
    )
    
    # Rate configuration
    weekday_rate = models.DecimalField(
        max_digits=4,
        decimal_places=2,
        default=1.5,
        help_text="Multiplier for weekday overtime (e.g., 1.5 for 150%)"
    )
    saturday_rate = models.DecimalField(
        max_digits=4,
        decimal_places=2,
        default=1.5
    )
    sunday_holiday_rate = models.DecimalField(
        max_digits=4,
        decimal_places=2,
        default=2.0
    )
    
    # Calculation method
    CALCULATION_METHODS = [
        ('STANDARD', 'Standard (Basic Pay / Working Days / Hours × OT Hours × Rate)'),
        ('FIXED_RATE', 'Fixed Rate per Hour'),
        ('CUSTOM', 'Custom Formula')
    ]
    calculation_method = models.CharField(
        max_length=20,
        choices=CALCULATION_METHODS,
        default='STANDARD'
    )
    
    # Fixed rate settings
    fixed_weekday_amount = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Fixed amount per hour for weekday overtime"
    )
    fixed_saturday_amount = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        null=True,
        blank=True
    )
    fixed_sunday_holiday_amount = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        null=True,
        blank=True
    )
    
    # Custom calculation settings
    custom_formula = models.TextField(
        blank=True,
        help_text="Python expression for custom calculation. Available variables: basic_pay, hours, rate"
    )

    class Meta:
        verbose_name = "Company Overtime Configuration"
        verbose_name_plural = "Company Overtime Configurations"

    def __str__(self):
        return f"{self.company.name} Overtime Config"

    def get_rate_for_date(self, check_date: date):
        """Get the appropriate rate for a given date"""
        if Holiday.is_holiday(check_date, self.company) or check_date.weekday() == 6:  # Holiday or Sunday
            return self.sunday_holiday_rate
        elif check_date.weekday() == 5:  # Saturday
            return self.saturday_rate
        return self.weekday_rate

    def calculate_amount(self, basic_pay: Decimal, hours: Decimal, rate: Decimal, date: date):
        """Calculate overtime amount based on configuration"""
        if self.calculation_method == 'FIXED_RATE':
            if Holiday.is_holiday(date, self.company) or date.weekday() == 6:
                return self.fixed_sunday_holiday_amount * hours
            elif date.weekday() == 5:
                return self.fixed_saturday_amount * hours
            return self.fixed_weekday_amount * hours
            
        elif self.calculation_method == 'CUSTOM' and self.custom_formula:
            try:
                # Create a safe environment for eval
                env = {
                    'basic_pay': float(basic_pay),
                    'hours': float(hours),
                    'rate': float(rate),
                    'Decimal': Decimal
                }
                result = eval(self.custom_formula, {"__builtins__": {}}, env)
                return Decimal(str(result))
            except Exception as e:
                raise ValidationError(f"Error in custom formula: {str(e)}")
                
        # Standard calculation
        daily_rate = basic_pay / Decimal(str(self.working_days_per_month))
        hourly_rate = daily_rate / Decimal(str(self.working_hours_per_day))
        return hourly_rate * hours * rate

class OvertimeRequestHistory(models.Model):
    """Track approval history for overtime requests"""
    request = models.ForeignKey('OvertimeRequest', on_delete=models.CASCADE, related_name='history')
    status = models.CharField(max_length=20)
    comment = models.TextField(blank=True)
    actor = models.ForeignKey('accounts.Employee', on_delete=models.SET_NULL, null=True)
    timestamp = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        ordering = ['-timestamp']
        verbose_name = 'Overtime Request History'
        verbose_name_plural = 'Overtime Request Histories'
    
    def __str__(self):
        return f'{self.request} - {self.status} by {self.actor} at {self.timestamp}'


class OvertimeRequest(models.Model):
    STATUS_CHOICES = [
        ('PENDING', 'Pending HR Review'),
        ('HR_APPROVED', 'HR Approved'),
        ('HR_REJECTED', 'HR Rejected'),
        ('REG_APPROVED', 'Registrar Approved'),
        ('REG_REJECTED', 'Registrar Rejected'),
        ('PROCESSED', 'Payment Processed')
    ]
    
    PAYMENT_STATUS = [
        ('PENDING', 'Pending Payment'),
        ('PAID', 'Paid'),
    ]
    
    employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
    date = models.DateField()
    def default_start_time():
        return timezone.now().replace(hour=17, minute=0, second=0)
    
    def default_end_time():
        return timezone.now().replace(hour=19, minute=0, second=0)
        
    start_time = models.TimeField(default=default_start_time)  # Default 5:00 PM
    end_time = models.TimeField(default=default_end_time)    # Default 7:00 PM
    hours = models.DecimalField(max_digits=4, decimal_places=2, editable=False)
    rate = models.DecimalField(max_digits=3, decimal_places=2, editable=False)
    reason = models.TextField()
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING')
    payment_status = models.CharField(max_length=20, choices=PAYMENT_STATUS, default='PENDING')
    payment_date = models.DateField(null=True, blank=True)
    amount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, editable=False)
    
    supporting_document = models.FileField(
        upload_to='overtime_documents/%Y/%m/',
        null=True,
        blank=True,
        help_text="Upload supporting documentation (e.g., work evidence, receipts)"
    )
    # Approval fields with default empty strings for text fields
    hr_comment = models.TextField(blank=True, default='')
    hr_approved_at = models.DateTimeField(null=True, blank=True)
    hr_approved_by = models.ForeignKey('accounts.Employee', null=True, blank=True, on_delete=models.SET_NULL, related_name='hr_approved_overtime')
    
    registrar_comment = models.TextField(blank=True, default='')
    registrar_approved_at = models.DateTimeField(null=True, blank=True)
    registrar_approved_by = models.ForeignKey('accounts.Employee', null=True, blank=True, on_delete=models.SET_NULL, related_name='registrar_approved_overtime')
    
    finance_comment = models.TextField(blank=True, default='')
    finance_processed_at = models.DateTimeField(null=True, blank=True)
    finance_processed_by = models.ForeignKey('accounts.Employee', null=True, blank=True, on_delete=models.SET_NULL, related_name='finance_processed_overtime')
    
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def is_public_holiday(self):
        """Check if the request date is a public holiday"""
        return Holiday.is_holiday(self.date, self.employee.company)

    def clean(self):
        super().clean()
        
        # Validate required fields
        if not self.date or not self.start_time or not self.end_time:
            raise ValidationError('Date, start time, and end time are required')
        
        if not hasattr(self, 'employee') or not self.employee:
            raise ValidationError('Employee is required')
            
        # Get time objects for comparison
        start_time = self.start_time.time() if hasattr(self.start_time, 'time') else self.start_time
        end_time = self.end_time.time() if hasattr(self.end_time, 'time') else self.end_time
        
        # Allow overnight work - only validate that they're not exactly the same (0 hours)
        if start_time == end_time:
            raise ValidationError({'end_time': 'End time cannot be the same as start time'})

        # Validate that overtime requests are within allowed date range (current and previous month)
        request_date = timezone.now().date()
        if self.date:
            # Only check if date is in the future - allow current and previous month
            if self.date > request_date:
                raise ValidationError({'date': 'Cannot request overtime for future dates'})
            
            # Check if date is within allowed range (current month and previous month)
            current_month_first = request_date.replace(day=1)
            if request_date.month == 1:
                prev_month_first = request_date.replace(year=request_date.year - 1, month=12, day=1)
            else:
                prev_month_first = request_date.replace(month=request_date.month - 1, day=1)
            
            if self.date < prev_month_first:
                raise ValidationError({'date': 'Overtime can only be requested for the current month or previous month'})
            
        # Check day type and validate accordingly
        if self.date:
            # First check if the date is a holiday regardless of weekday
            if self.is_public_holiday():
                # For holidays (including weekday holidays), any time is valid
                return
            
            # Only apply the 5:00 PM rule for regular weekdays that are not holidays
            weekday = self.date.weekday()
            is_weekday = weekday < 5
            
            # For regular weekdays, validate start time is after 17:00
            if is_weekday:
                work_end_time = time(17, 0)  # 5:00 PM
                start_time = self.start_time.time() if hasattr(self.start_time, 'time') else self.start_time
                if start_time < work_end_time:
                    raise ValidationError({'start_time': 'For regular weekdays, overtime can only start after 5:00 PM'})
            # For Saturdays and Sundays, any time is valid

    def calculate_overtime_hours(self):
        """Calculate overtime hours based on day type and time
        
        Weekdays (Mon-Fri): Only hours after 17:00 count
        Saturday: All hours worked count as overtime
        Sunday/Public Holidays: All hours count
        
        Handles overnight work (e.g., 17:01 to 02:00 next day)
        
        Returns:
            float: Total overtime hours (without rate multiplier)
        """
        if not self.date or not self.start_time or not self.end_time:
            return Decimal('0')
            
        weekday = self.date.weekday()
        start = timezone.datetime.combine(self.date, self.start_time)
        
        # Handle overnight work - if end_time < start_time, assume next day
        if self.end_time < self.start_time:
            # Work spans to next day
            end = timezone.datetime.combine(self.date + timezone.timedelta(days=1), self.end_time)
        else:
            # Work ends same day
            end = timezone.datetime.combine(self.date, self.end_time)
        
        # Check if it's a holiday first, regardless of weekday
        if self.is_public_holiday():
            # For holidays, count all hours
            hours = (end - start).total_seconds() / 3600
            return Decimal(str(hours))
            
        # For regular weekdays, only count hours after 17:00
        if weekday < 5:  # Monday-Friday, not a holiday
            cutoff = timezone.datetime.combine(self.date, time(17, 0))  # 5:00 PM
            if start < cutoff:
                start = cutoff
            if end <= cutoff:
                return Decimal('0')
            hours = (end - start).total_seconds() / 3600
            return Decimal(str(hours))
            
        else:  # Saturday, Sunday, or public holiday - all hours count
            hours = (end - start).total_seconds() / 3600
            return Decimal(str(hours))

    def calculate_rate(self):
        """Determine overtime rate based on day type and company config"""
        if not self.employee.company:
            # Handle case where employee has no company
            if self.date.weekday() == 6 or self.is_public_holiday():  # Sunday or holiday
                return Decimal('2.0')
            return Decimal('1.5')  # Weekday or Saturday

        try:
            config = self.employee.company.companyovertimeconfig
            return config.get_rate_for_date(self.date)
        except (CompanyOvertimeConfig.DoesNotExist, AttributeError):
            # Fallback to default rates if no company config exists
            if self.date.weekday() == 6 or self.is_public_holiday():  # Sunday or holiday
                return Decimal('2.0')
            return Decimal('1.5')  # Weekday or Saturday

    def calculate_amount(self):
        """Calculate overtime payment amount using company-specific configuration.
        If no company config exists, falls back to standard formula:
        Amount = (Basic Pay / 22 workdays) / 8 hours × Overtime Hours × Rate
        
        Note: Amount is only calculated when Registrar approves the request.
        """
        # Always calculate amount, but it's only finalized after HR approval

        if not self.employee or not self.employee.basic_pay:
            return Decimal('0')
            
        try:
            config = self.employee.company.companyovertimeconfig
            amount = config.calculate_amount(
                basic_pay=self.employee.basic_pay,
                hours=self.hours,
                rate=self.rate,
                date=self.date
            )
        except (CompanyOvertimeConfig.DoesNotExist, AttributeError):
            # Fallback to standard calculation if no company config exists
            daily_rate = self.employee.basic_pay / Decimal('22')  # Standard workdays
            hourly_rate = daily_rate / Decimal('8')  # Standard hours
            amount = hourly_rate * self.hours * self.rate
            
        return round(amount, 2)

    def save(self, *args, **kwargs):
        # Run validation
        self.full_clean()
        
        # Calculate overtime hours and rate
        self.hours = self.calculate_overtime_hours()
        self.rate = self.calculate_rate()
        
        # Calculate amount for all requests, but it's only displayed when approved
        self.amount = self.calculate_amount()
        
        super().save(*args, **kwargs)

    @property
    def total_hours(self):
        """Calculate total hours including rate"""
        return round(float(self.hours) * float(self.rate), 2) if self.hours and self.rate else 0
    
    def get_status_color(self):
        """Return Bootstrap color class based on status"""
        colors = {
            'PENDING': 'warning',
            'HR_APPROVED': 'primary',
            'HR_REJECTED': 'danger',
            'REG_APPROVED': 'success',
            'REG_REJECTED': 'danger',
            'PROCESSED': 'secondary'
        }
        return colors.get(self.status, 'secondary')

    def __str__(self):
        return f"{self.employee} - {self.date} ({self.hours:.2f} hours)"

    class Meta:
        ordering = ['-date', '-created_at']
        permissions = [
            ("can_approve_overtime", "Can approve overtime requests"),
            ("can_process_payment", "Can process overtime payments"),
        ]
