"""
Input validation utilities for loan management system
Validates loan edit forms, penalty forms, and report filter parameters
Requirements: 3.12, 4.11, 10.7, 10.8
"""
from decimal import Decimal, InvalidOperation
from datetime import datetime, date
from django.core.exceptions import ValidationError
from django.utils import timezone
from .sanitizers import InputSanitizer


class LoanEditValidator:
    """
    Validator for loan edit form inputs
    Requirements: 3.12
    """
    
    @staticmethod
    def validate_amount(amount, field_name="Amount"):
        """
        Validate monetary amount
        
        Args:
            amount: Amount to validate (can be string, Decimal, or number)
            field_name: Name of the field for error messages
            
        Returns:
            Decimal: Validated amount
            
        Raises:
            ValidationError: If amount is invalid
        """
        if amount is None or amount == '':
            raise ValidationError(f"{field_name} is required")
        
        # Sanitize input first
        # Requirement: 10.9
        if isinstance(amount, str):
            amount = InputSanitizer.sanitize_string(amount, allow_special_chars=False)
        
        try:
            amount_decimal = Decimal(str(amount))
        except (InvalidOperation, ValueError, TypeError):
            raise ValidationError(f"{field_name} must be a valid number")
        
        if amount_decimal < 0:
            raise ValidationError(f"{field_name} cannot be negative")
        
        if amount_decimal == 0 and field_name.lower() in ['principal', 'principal amount']:
            raise ValidationError(f"{field_name} must be greater than zero")
        
        # Check for reasonable maximum (100 million)
        if amount_decimal > Decimal('100000000'):
            raise ValidationError(f"{field_name} exceeds maximum allowed value")
        
        # Validate decimal places (max 2)
        if amount_decimal.as_tuple().exponent < -2:
            raise ValidationError(f"{field_name} can have at most 2 decimal places")
        
        return amount_decimal
    
    @staticmethod
    def validate_date(date_str, field_name="Date"):
        """
        Validate date input
        
        Args:
            date_str: Date string in YYYY-MM-DD format
            field_name: Name of the field for error messages
            
        Returns:
            date: Validated date object
            
        Raises:
            ValidationError: If date is invalid
        """
        if not date_str:
            raise ValidationError(f"{field_name} is required")
        
        try:
            if isinstance(date_str, date):
                return date_str
            
            # Try parsing as YYYY-MM-DD
            parsed_date = datetime.strptime(str(date_str), '%Y-%m-%d').date()
            return parsed_date
        except (ValueError, TypeError):
            raise ValidationError(f"{field_name} must be in format YYYY-MM-DD")
    
    @staticmethod
    def validate_status(status, allowed_statuses=None):
        """
        Validate loan status
        
        Args:
            status: Status string to validate
            allowed_statuses: List of allowed status values
            
        Returns:
            str: Validated status
            
        Raises:
            ValidationError: If status is invalid
        """
        if not status:
            raise ValidationError("Status is required")
        
        if allowed_statuses and status not in allowed_statuses:
            raise ValidationError(
                f"Invalid status '{status}'. Allowed values: {', '.join(allowed_statuses)}"
            )
        
        return status
    
    @staticmethod
    def validate_loan_edit_form(data):
        """
        Validate complete loan edit form
        
        Args:
            data: Dictionary containing form data
            
        Returns:
            dict: Validated data
            
        Raises:
            ValidationError: If any field is invalid
        """
        validated = {}
        errors = []
        
        # Validate principal amount
        try:
            validated['principal_amount'] = LoanEditValidator.validate_amount(
                data.get('principal_amount'),
                "Principal amount"
            )
        except ValidationError as e:
            errors.append(str(e))
        
        # Validate interest amount
        try:
            validated['interest_amount'] = LoanEditValidator.validate_amount(
                data.get('interest_amount'),
                "Interest amount"
            )
        except ValidationError as e:
            errors.append(str(e))
        
        # Validate processing fee
        try:
            validated['processing_fee'] = LoanEditValidator.validate_amount(
                data.get('processing_fee'),
                "Processing fee"
            )
        except ValidationError as e:
            errors.append(str(e))
        
        # Validate disbursement date
        try:
            validated['disbursement_date'] = LoanEditValidator.validate_date(
                data.get('disbursement_date'),
                "Disbursement date"
            )
        except ValidationError as e:
            errors.append(str(e))
        
        # Validate due date
        try:
            validated['due_date'] = LoanEditValidator.validate_date(
                data.get('due_date'),
                "Due date"
            )
            
            # Ensure due date is after disbursement date
            if 'disbursement_date' in validated and 'due_date' in validated:
                if validated['due_date'] <= validated['disbursement_date']:
                    errors.append("Due date must be after disbursement date")
        except ValidationError as e:
            errors.append(str(e))
        
        # Validate status if provided
        if 'status' in data and data['status']:
            try:
                allowed_statuses = ['active', 'paid', 'defaulted', 'rolled_over', 'written_off']
                validated['status'] = LoanEditValidator.validate_status(
                    data.get('status'),
                    allowed_statuses
                )
            except ValidationError as e:
                errors.append(str(e))
        
        if errors:
            raise ValidationError("; ".join(errors))
        
        return validated


class PenaltyValidator:
    """
    Validator for penalty addition form inputs
    Requirements: 4.11
    """
    
    @staticmethod
    def validate_penalty_amount(amount):
        """
        Validate penalty amount
        
        Args:
            amount: Amount to validate
            
        Returns:
            Decimal: Validated amount
            
        Raises:
            ValidationError: If amount is invalid
        """
        if amount is None or amount == '':
            raise ValidationError("Penalty amount is required")
        
        try:
            amount_decimal = Decimal(str(amount))
        except (InvalidOperation, ValueError, TypeError):
            raise ValidationError("Penalty amount must be a valid number")
        
        if amount_decimal <= 0:
            raise ValidationError("Penalty amount must be greater than zero")
        
        # Check for reasonable maximum (10 million)
        if amount_decimal > Decimal('10000000'):
            raise ValidationError("Penalty amount exceeds maximum allowed value")
        
        # Validate decimal places (max 2)
        if amount_decimal.as_tuple().exponent < -2:
            raise ValidationError("Penalty amount can have at most 2 decimal places")
        
        return amount_decimal
    
    @staticmethod
    def validate_penalty_date(penalty_date):
        """
        Validate penalty date
        
        Args:
            penalty_date: Date to validate
            
        Returns:
            date: Validated date
            
        Raises:
            ValidationError: If date is invalid
        """
        if not penalty_date:
            raise ValidationError("Penalty date is required")
        
        try:
            if isinstance(penalty_date, date):
                date_obj = penalty_date
            else:
                date_obj = datetime.strptime(str(penalty_date), '%Y-%m-%d').date()
        except (ValueError, TypeError):
            raise ValidationError("Penalty date must be in format YYYY-MM-DD")
        
        # Penalty date cannot be in the future
        if date_obj > timezone.now().date():
            raise ValidationError("Penalty date cannot be in the future")
        
        # Penalty date should not be too far in the past (e.g., more than 5 years)
        five_years_ago = timezone.now().date().replace(year=timezone.now().date().year - 5)
        if date_obj < five_years_ago:
            raise ValidationError("Penalty date cannot be more than 5 years in the past")
        
        return date_obj
    
    @staticmethod
    def validate_penalty_form(data):
        """
        Validate complete penalty addition form
        
        Args:
            data: Dictionary containing form data
            
        Returns:
            dict: Validated data
            
        Raises:
            ValidationError: If any field is invalid
        """
        validated = {}
        errors = []
        
        # Validate amount
        try:
            validated['amount'] = PenaltyValidator.validate_penalty_amount(
                data.get('amount')
            )
        except ValidationError as e:
            errors.append(str(e))
        
        # Validate penalty date
        try:
            validated['penalty_date'] = PenaltyValidator.validate_penalty_date(
                data.get('penalty_date')
            )
        except ValidationError as e:
            errors.append(str(e))
        
        # Reason is optional but validate if provided
        reason = data.get('reason', '').strip()
        if reason and len(reason) > 500:
            errors.append("Reason cannot exceed 500 characters")
        validated['reason'] = reason
        
        if errors:
            raise ValidationError("; ".join(errors))
        
        return validated


class ReportFilterValidator:
    """
    Validator for report filter parameters
    Requirements: 10.7, 10.8
    """
    
    @staticmethod
    def validate_date_range(start_date, end_date):
        """
        Validate date range for reports
        
        Args:
            start_date: Start date (string or date object)
            end_date: End date (string or date object)
            
        Returns:
            tuple: (start_date, end_date) as date objects
            
        Raises:
            ValidationError: If date range is invalid
        """
        errors = []
        
        # Validate start date
        try:
            if isinstance(start_date, date):
                start = start_date
            elif start_date:
                start = datetime.strptime(str(start_date), '%Y-%m-%d').date()
            else:
                start = None
        except (ValueError, TypeError):
            errors.append("Start date must be in format YYYY-MM-DD")
            start = None
        
        # Validate end date
        try:
            if isinstance(end_date, date):
                end = end_date
            elif end_date:
                end = datetime.strptime(str(end_date), '%Y-%m-%d').date()
            else:
                end = None
        except (ValueError, TypeError):
            errors.append("End date must be in format YYYY-MM-DD")
            end = None
        
        # Validate range
        if start and end:
            if end < start:
                errors.append("End date must be after start date")
            
            # Check for reasonable range (not more than 10 years)
            if (end - start).days > 3650:
                errors.append("Date range cannot exceed 10 years")
        
        if errors:
            raise ValidationError("; ".join(errors))
        
        return start, end
    
    @staticmethod
    def validate_amount_range(min_amount, max_amount):
        """
        Validate amount range for reports
        
        Args:
            min_amount: Minimum amount
            max_amount: Maximum amount
            
        Returns:
            tuple: (min_amount, max_amount) as Decimal objects
            
        Raises:
            ValidationError: If amount range is invalid
        """
        errors = []
        
        # Validate minimum amount
        try:
            if min_amount is not None and min_amount != '':
                min_amt = Decimal(str(min_amount))
                if min_amt < 0:
                    errors.append("Minimum amount cannot be negative")
            else:
                min_amt = None
        except (InvalidOperation, ValueError, TypeError):
            errors.append("Minimum amount must be a valid number")
            min_amt = None
        
        # Validate maximum amount
        try:
            if max_amount is not None and max_amount != '':
                max_amt = Decimal(str(max_amount))
                if max_amt < 0:
                    errors.append("Maximum amount cannot be negative")
            else:
                max_amt = None
        except (InvalidOperation, ValueError, TypeError):
            errors.append("Maximum amount must be a valid number")
            max_amt = None
        
        # Validate range
        if min_amt is not None and max_amt is not None:
            if max_amt < min_amt:
                errors.append("Maximum amount must be greater than minimum amount")
        
        if errors:
            raise ValidationError("; ".join(errors))
        
        return min_amt, max_amt
    
    @staticmethod
    def validate_status_filter(status, allowed_statuses=None):
        """
        Validate status filter parameter
        
        Args:
            status: Status value to validate
            allowed_statuses: List of allowed status values
            
        Returns:
            str: Validated status or None
            
        Raises:
            ValidationError: If status is invalid
        """
        if not status or status == 'all':
            return None
        
        if allowed_statuses and status not in allowed_statuses:
            raise ValidationError(
                f"Invalid status '{status}'. Allowed values: {', '.join(allowed_statuses)}"
            )
        
        return status
    
    @staticmethod
    def validate_period_filter(period):
        """
        Validate period filter parameter
        
        Args:
            period: Period value to validate
            
        Returns:
            str: Validated period
            
        Raises:
            ValidationError: If period is invalid
        """
        allowed_periods = ['today', 'this_week', 'this_month', 'this_year', 'total', 'custom']
        
        if not period:
            return 'total'  # Default
        
        if period not in allowed_periods:
            raise ValidationError(
                f"Invalid period '{period}'. Allowed values: {', '.join(allowed_periods)}"
            )
        
        return period
    
    @staticmethod
    def validate_report_filters(data):
        """
        Validate complete report filter parameters
        
        Args:
            data: Dictionary containing filter parameters
            
        Returns:
            dict: Validated filters
            
        Raises:
            ValidationError: If any filter is invalid
        """
        validated = {}
        errors = []
        
        # Validate date range if provided
        if 'start_date' in data or 'end_date' in data:
            try:
                start, end = ReportFilterValidator.validate_date_range(
                    data.get('start_date'),
                    data.get('end_date')
                )
                validated['start_date'] = start
                validated['end_date'] = end
            except ValidationError as e:
                errors.append(str(e))
        
        # Validate amount range if provided
        if 'min_amount' in data or 'max_amount' in data:
            try:
                min_amt, max_amt = ReportFilterValidator.validate_amount_range(
                    data.get('min_amount'),
                    data.get('max_amount')
                )
                validated['min_amount'] = min_amt
                validated['max_amount'] = max_amt
            except ValidationError as e:
                errors.append(str(e))
        
        # Validate status filter if provided
        if 'status' in data:
            try:
                allowed_statuses = [
                    'active', 'paid', 'defaulted', 'rolled_over', 
                    'written_off', 'overdue', 'pending', 'approved', 'rejected'
                ]
                validated['status'] = ReportFilterValidator.validate_status_filter(
                    data.get('status'),
                    allowed_statuses
                )
            except ValidationError as e:
                errors.append(str(e))
        
        # Validate period filter if provided
        if 'period' in data:
            try:
                validated['period'] = ReportFilterValidator.validate_period_filter(
                    data.get('period')
                )
            except ValidationError as e:
                errors.append(str(e))
        
        if errors:
            raise ValidationError("; ".join(errors))
        
        return validated
