from datetime import datetime, timedelta from sqlalchemy import select from sqlalchemy.orm import selectinload from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.ext.asyncio import AsyncSession from app.models import Event, EventType, Marathon, Challenge, Difficulty from app.schemas.event import EventEffects, EVENT_INFO, COMMON_ENEMY_BONUSES class EventService: """Service for managing marathon events""" async def get_active_event(self, db: AsyncSession, marathon_id: int) -> Event | None: """Get currently active event for marathon""" now = datetime.utcnow() result = await db.execute( select(Event) .options(selectinload(Event.created_by)) .where( Event.marathon_id == marathon_id, Event.is_active == True, Event.start_time <= now, ) .order_by(Event.start_time.desc()) ) event = result.scalar_one_or_none() # Check if event has expired if event and event.end_time and event.end_time < now: await self.end_event(db, event.id) return None return event async def can_start_event(self, db: AsyncSession, marathon_id: int) -> bool: """Check if we can start a new event (no active event exists)""" active = await self.get_active_event(db, marathon_id) return active is None async def start_event( self, db: AsyncSession, marathon_id: int, event_type: str, created_by_id: int | None = None, duration_minutes: int | None = None, challenge_id: int | None = None, ) -> Event: """Start a new event""" # Check no active event if not await self.can_start_event(db, marathon_id): raise ValueError("An event is already active") # Get default duration if not provided event_info = EVENT_INFO.get(EventType(event_type), {}) if duration_minutes is None: duration_minutes = event_info.get("default_duration") now = datetime.utcnow() end_time = now + timedelta(minutes=duration_minutes) if duration_minutes else None # Build event data data = {} if event_type == EventType.COMMON_ENEMY.value and challenge_id: data["challenge_id"] = challenge_id data["completions"] = [] # Track who completed and when event = Event( marathon_id=marathon_id, type=event_type, start_time=now, end_time=end_time, is_active=True, created_by_id=created_by_id, data=data if data else None, ) db.add(event) await db.commit() await db.refresh(event) # Load created_by relationship if created_by_id: await db.refresh(event, ["created_by"]) return event async def end_event(self, db: AsyncSession, event_id: int) -> None: """End an event""" result = await db.execute(select(Event).where(Event.id == event_id)) event = result.scalar_one_or_none() if event: event.is_active = False if not event.end_time: event.end_time = datetime.utcnow() await db.commit() async def consume_jackpot(self, db: AsyncSession, event_id: int) -> None: """Consume jackpot event after one spin""" await self.end_event(db, event_id) def get_event_effects(self, event: Event | None) -> EventEffects: """Get effects of an event""" if not event: return EventEffects(description="Нет активного события") event_info = EVENT_INFO.get(EventType(event.type), {}) return EventEffects( points_multiplier=event_info.get("points_multiplier", 1.0), drop_free=event_info.get("drop_free", False), special_action=event_info.get("special_action"), description=event_info.get("description", ""), ) async def get_random_hard_challenge( self, db: AsyncSession, marathon_id: int ) -> Challenge | None: """Get a random hard challenge for jackpot event""" result = await db.execute( select(Challenge) .join(Challenge.game) .where( Challenge.game.has(marathon_id=marathon_id), Challenge.difficulty == Difficulty.HARD.value, ) ) challenges = result.scalars().all() if not challenges: # Fallback to any challenge result = await db.execute( select(Challenge) .join(Challenge.game) .where(Challenge.game.has(marathon_id=marathon_id)) ) challenges = result.scalars().all() if challenges: import random return random.choice(challenges) return None async def record_common_enemy_completion( self, db: AsyncSession, event: Event, participant_id: int, user_id: int, ) -> tuple[int, bool, list[dict] | None]: """ Record completion for common enemy event. Returns: (bonus_points, event_closed, winners_list) - bonus_points: bonus for this completion (top 3 get bonuses) - event_closed: True if event was auto-closed (3 completions reached) - winners_list: list of winners if event closed, None otherwise """ if event.type != EventType.COMMON_ENEMY.value: return 0, False, None data = event.data or {} completions = data.get("completions", []) # Check if already completed if any(c["participant_id"] == participant_id for c in completions): return 0, False, None # Add completion rank = len(completions) + 1 completions.append({ "participant_id": participant_id, "user_id": user_id, "completed_at": datetime.utcnow().isoformat(), "rank": rank, }) # Update event data - need to flag_modified for SQLAlchemy to detect JSON changes event.data = {**data, "completions": completions} flag_modified(event, "data") bonus = COMMON_ENEMY_BONUSES.get(rank, 0) # Auto-close event when 3 players completed event_closed = False winners_list = None if rank >= 3: event.is_active = False event.end_time = datetime.utcnow() event_closed = True winners_list = completions[:3] # Top 3 await db.commit() return bonus, event_closed, winners_list async def get_common_enemy_challenge( self, db: AsyncSession, event: Event ) -> Challenge | None: """Get the challenge for common enemy event""" if event.type != EventType.COMMON_ENEMY.value: return None data = event.data or {} challenge_id = data.get("challenge_id") if not challenge_id: return None result = await db.execute( select(Challenge) .options(selectinload(Challenge.game)) .where(Challenge.id == challenge_id) ) return result.scalar_one_or_none() def get_time_remaining(self, event: Event | None) -> int | None: """Get remaining time in seconds for an event""" if not event or not event.end_time: return None remaining = (event.end_time - datetime.utcnow()).total_seconds() return max(0, int(remaining)) event_service = EventService()