from datetime import datetime, timedelta
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_
from sqlalchemy.orm import selectinload
from app.models import Expense, RecurrencePattern, SplitTypeEnum
from app.crud.expense import create_expense
from app.schemas.expense import ExpenseCreate, ExpenseSplitCreate
import logging
from typing import Optional
import enum

logger = logging.getLogger(__name__)

async def generate_recurring_expenses(db: AsyncSession) -> None:
    """
    Background job to generate recurring expenses.
    Should be run daily to check for and create new recurring expenses.
    """
    try:
        now = datetime.utcnow()
        query = select(Expense).join(RecurrencePattern).where(
            and_(
                Expense.is_recurring == True,
                Expense.next_occurrence <= now,
                (
                    (RecurrencePattern.max_occurrences == None) |
                    (RecurrencePattern.max_occurrences > 0)
                ),
                (
                    (RecurrencePattern.end_date == None) |
                    (RecurrencePattern.end_date > now)
                )
            )
        ).options(
            selectinload(Expense.splits) # Eager load splits to use as a template
        )
        
        result = await db.execute(query)
        recurring_expenses = result.scalars().all()
        
        for expense in recurring_expenses:
            try:
                await _generate_next_occurrence(db, expense)
                # Persist changes for this expense before moving to the next one
                await db.commit()
            except Exception as e:
                logger.error(f"Error generating next occurrence for expense {expense.id}: {str(e)}", exc_info=True)
                await db.rollback()
                continue
                
    except Exception as e:
        logger.error(f"Error in generate_recurring_expenses job during expense fetch: {str(e)}", exc_info=True)
        # Do not re-raise, allow the job scheduler to run again later

async def _generate_next_occurrence(db: AsyncSession, expense: Expense) -> None:
    """Generate the next occurrence of a recurring expense."""
    pattern = expense.recurrence_pattern
    if not pattern:
        logger.warning(f"Recurring expense {expense.id} is missing its recurrence pattern.")
        return
        
    next_date = _calculate_next_occurrence(expense.next_occurrence, pattern)
    if not next_date:
        logger.info(f"No next occurrence date for expense {expense.id}, stopping recurrence.")
        expense.is_recurring = False # Stop future processing
        await db.flush()
        return
    
    # Recreate splits from the template expense if needed
    splits_data = None
    if expense.split_type not in [SplitTypeEnum.EQUAL, SplitTypeEnum.ITEM_BASED]:
        if not expense.splits:
            logger.error(f"Cannot generate next occurrence for expense {expense.id} with split type {expense.split_type.value} because it has no splits to use as a template.")
            return

        splits_data = [
            ExpenseSplitCreate(
                user_id=split.user_id,
                owed_amount=split.owed_amount,
                share_percentage=split.share_percentage,
                share_units=split.share_units,
            ) for split in expense.splits
        ]
        
    new_expense = ExpenseCreate(
        description=expense.description,
        total_amount=expense.total_amount,
        currency=expense.currency,
        expense_date=next_date,
        split_type=expense.split_type,
        list_id=expense.list_id,
        group_id=expense.group_id,
        item_id=expense.item_id,
        paid_by_user_id=expense.paid_by_user_id,
        is_recurring=False, # The new expense is a single occurrence, not a recurring template
        splits_in=splits_data
    )
    
    # We pass the original creator's ID
    created_expense = await create_expense(db, new_expense, expense.created_by_user_id)
    logger.info(f"Generated new expense {created_expense.id} from recurring expense {expense.id}.")
    
    # Update the template expense for the next run
    expense.last_occurrence = next_date
    next_next_date = _calculate_next_occurrence(next_date, pattern)
    
    # Decrement occurrence count if it exists
    if pattern.max_occurrences is not None:
        pattern.max_occurrences -= 1
        if pattern.max_occurrences <= 0:
            next_next_date = None # Stop recurrence

    expense.next_occurrence = next_next_date
    if not expense.next_occurrence:
        expense.is_recurring = False # End the recurrence

    await db.flush()

def _calculate_next_occurrence(current_date: datetime, pattern: RecurrencePattern) -> Optional[datetime]:
    """Calculate the next occurrence date based on the recurrence pattern provided."""

    if not current_date:
        return None

    # Extract a lowercase string of the recurrence type regardless of whether it is an Enum member or a str.
    if isinstance(pattern.type, enum.Enum):
        pattern_type = pattern.type.value.lower()
    else:
        pattern_type = str(pattern.type).lower()

    next_date: Optional[datetime] = None

    if pattern_type == 'daily':
        next_date = current_date + timedelta(days=pattern.interval)

    elif pattern_type == 'weekly':
        if not pattern.days_of_week:
            next_date = current_date + timedelta(weeks=pattern.interval)
        else:
            current_weekday = current_date.weekday()
            # ``days_of_week`` can be stored either as a list[int] (Python-side) or as a
            # comma-separated string in the database.  We normalise it to a list[int].
            days_of_week_iterable = []
            if pattern.days_of_week is None:
                days_of_week_iterable = []
            elif isinstance(pattern.days_of_week, (list, tuple)):
                days_of_week_iterable = list(pattern.days_of_week)
            else:
                # Assume comma-separated string like "1,3,5"
                try:
                    days_of_week_iterable = [int(d.strip()) for d in str(pattern.days_of_week).split(',') if d.strip().isdigit()]
                except Exception:
                    days_of_week_iterable = []

            # Find the next valid weekday after the current one
            next_days = sorted([d for d in days_of_week_iterable if d > current_weekday])
            if next_days:
                days_ahead = next_days[0] - current_weekday
                next_date = current_date + timedelta(days=days_ahead)
            else:
                # Jump to the first valid day in a future week respecting the interval
                if days_of_week_iterable:
                    days_ahead = (7 - current_weekday) + min(days_of_week_iterable)
                    next_date = current_date + timedelta(days=days_ahead)
                    if pattern.interval > 1:
                        next_date += timedelta(weeks=pattern.interval - 1)

    elif pattern_type == 'monthly':
        # Move `interval` months forward while keeping the day component stable where possible.
        year = current_date.year + (current_date.month + pattern.interval - 1) // 12
        month = (current_date.month + pattern.interval - 1) % 12 + 1
        try:
            next_date = current_date.replace(year=year, month=month)
        except ValueError:
            # Handle cases like Feb-31st by rolling back to the last valid day of the new month.
            next_date = (current_date.replace(day=1, year=year, month=month) + timedelta(days=31)).replace(day=1) - timedelta(days=1)

    elif pattern_type == 'yearly':
        try:
            next_date = current_date.replace(year=current_date.year + pattern.interval)
        except ValueError:
            # Leap-year edge-case; fallback to Feb-28 if Feb-29 does not exist in the target year.
            next_date = current_date.replace(year=current_date.year + pattern.interval, day=28)

    # Stop recurrence if beyond end_date
    if pattern.end_date and next_date and next_date > pattern.end_date:
        return None

    return next_date 