"""Meeting session management."""

import time
from collections import deque
from typing import Optional

from thetable_poc.core import Participant, Role
from thetable_poc.core.human import HumanParticipant
from thetable_poc.output import OutputCallbacks

from .agenda import AgendaManager
from .floor_manager import FloorManager
from .mention_parser import parse_mentions
from .phase_machine import MeetingPhase, PhaseEvent, PhaseMachine


# Interruptible sleep step (seconds)
INTERRUPTIBLE_SLEEP_STEP = 0.1


class MeetingSession:
    """Main meeting session manager."""

    def __init__(
        self,
        callbacks: OutputCallbacks,
        max_duration: int = 3600,
    ):
        """Initialize meeting session.

        Args:
            callbacks: Output callbacks
            max_duration: Maximum meeting duration in seconds
        """
        self.callbacks = callbacks
        self.max_duration = max_duration

        # Core components
        self.phase_machine = PhaseMachine()
        self.floor_manager = FloorManager()
        self.agenda_manager = AgendaManager()

        # Participants
        self._participants: list[Participant] = []
        self._host: Optional[Participant] = None
        self._pm: Optional[Participant] = None
        self._tech_lead: Optional[Participant] = None

        # State
        self._running = False
        self._start_time: Optional[float] = None
        self._user_input_queue: deque[str] = deque()
        self._message_history: list[dict] = []
        self._mention_depth: int = 0  # Track mention chain depth to prevent infinite loops

        # Setup phase handlers
        self._setup_phase_handlers()

    def _setup_phase_handlers(self) -> None:
        """Setup phase transition handlers."""
        self.phase_machine.on_enter(
            MeetingPhase.OPENING,
            lambda: self.callbacks.system("Phase: OPENING - 회의 시작"),
        )
        self.phase_machine.on_enter(
            MeetingPhase.STATUS_CHECK,
            lambda: self.callbacks.system("Phase: STATUS_CHECK - 현황 체크"),
        )
        self.phase_machine.on_enter(
            MeetingPhase.ISSUE_RESOLUTION,
            lambda: self.callbacks.system("Phase: ISSUE_RESOLUTION - 이슈 해결"),
        )
        self.phase_machine.on_enter(
            MeetingPhase.WORKTREE_EXECUTION,
            lambda: self.callbacks.system("Phase: WORKTREE_EXECUTION - 실무 작업"),
        )
        self.phase_machine.on_enter(
            MeetingPhase.CLOSING,
            lambda: self.callbacks.system("Phase: CLOSING - 마무리"),
        )

    def add_participant(self, participant: Participant) -> None:
        """Add a participant to the meeting.

        Args:
            participant: Participant to add
        """
        self._participants.append(participant)

        # Set role-specific references
        if participant.role == Role.HOST:
            self._host = participant
        elif participant.role == Role.PM:
            self._pm = participant
        elif participant.role == Role.TECH_LEAD:
            self._tech_lead = participant

    def queue_user_input(self, user_input: str) -> None:
        """Queue user input for processing.

        Args:
            user_input: User input string
        """
        self._user_input_queue.append(user_input)

    def interruptible_sleep(self, seconds: float) -> bool:
        """Sleep that can be interrupted by user input.

        Args:
            seconds: Time to sleep

        Returns:
            True if interrupted by user input
        """
        steps = int(seconds / INTERRUPTIBLE_SLEEP_STEP)
        for _ in range(steps):
            # Check legacy queue
            if self._user_input_queue:
                return True

            # Check human participants for pending input
            for participant in self._participants:
                if isinstance(participant, HumanParticipant):
                    pending = participant.check_pending_input()
                    if pending:
                        # Queue the input for processing
                        self._user_input_queue.append(pending)
                        return True

            time.sleep(INTERRUPTIBLE_SLEEP_STEP)
        return False

    def _check_user_interrupt_during_batch(self) -> bool:
        """Check for user input without blocking.
        
        Returns:
            True if user input is available
        """
        # Check legacy queue
        if self._user_input_queue:
            return True
        
        # Check human participants for pending input
        for participant in self._participants:
            if isinstance(participant, HumanParticipant):
                pending = participant.check_pending_input()
                if pending:
                    self._user_input_queue.append(pending)
                    return True
        
        return False

    def broadcast(self, sender: Participant, message: str) -> None:
        """Broadcast message to all participants.

        Args:
            sender: Sender participant
            message: Message to broadcast
        """
        # Add to history
        self._message_history.append(
            {
                "sender": sender.name,
                "role": sender.role.value,
                "message": message,
                "timestamp": time.time(),
            }
        )

        # Send to all participants
        for participant in self._participants:
            if participant != sender:
                participant.receive_message(sender.name, message)

        # Output to callbacks
        self.callbacks.output(sender.name, sender.role.value, message)

    def broadcast_streaming(self, sender: Participant, context: dict) -> None:
        """Broadcast message with streaming support.

        Args:
            sender: Sender participant
            context: Context for generating response
        """
        # Print speaker name with role first (only once)
        print(f"[{sender.role.value}]{sender.name}: ", end="", flush=True)

        def typing_callback(chunk: str):
            """Called for each chunk during streaming."""
            # Output only the new chunk (not accumulated text)
            self.callbacks.typing(sender.name, chunk)

        # Generate response with streaming
        full_response = sender.speak_stream(
            context=context, typing_callback=typing_callback
        )

        # Print newline after streaming (to move cursor to next line)
        print()

        # Add to history
        self._message_history.append(
            {
                "sender": sender.name,
                "role": sender.role.value,
                "message": full_response,
                "timestamp": time.time(),
            }
        )

        # Send to all participants
        for participant in self._participants:
            if participant != sender:
                participant.receive_message(sender.name, full_response)

        # Note: No callbacks.output() here - already streamed above

        # NEW: Handle AI mentions if sender is AI
        if sender.is_ai:
            self._handle_ai_mentions(sender, full_response)

    def _get_human_participants(self) -> list[HumanParticipant]:
        """Get all human participants.

        Returns:
            List of human participants
        """
        return [
            p for p in self._participants if isinstance(p, HumanParticipant)
        ]

    def _handle_user_interruption(self) -> None:
        """Handle user interruption during meeting.

        Processes queued user input and broadcasts to all participants.
        Automatically generates AI response to user input.
        """
        if not self._user_input_queue:
            return

        # Get the first queued input
        user_input = self._user_input_queue.popleft()

        # Find human participant
        human_participants = self._get_human_participants()
        if not human_participants:
            return

        human = human_participants[0]  # Use first human participant

        # Broadcast the interruption with clear visual separation
        print()  # Empty line before
        self.callbacks.system("=" * 60)
        self.callbacks.system("💬 USER SPEAKS")
        self.callbacks.system("-" * 60)
        self.broadcast(human, user_input)
        self.callbacks.system("-" * 60)

        # Grant floor to user
        self.floor_manager.interrupt_for_user()

        # Select AI responders and generate responses
        responders = self._select_dialogue_responders(user_input, human)

        # Use batched delivery for multiple responders
        if len(responders) > 1:
            context_base = {
                "user_message": user_input,
                "user_name": human.name,
                "user_role": human.role.value
            }
            header_message = ""  # No special header for user interruption
            self._deliver_batched(responders, context_base, header_message)
        else:
            # Single responder: use existing streaming behavior
            for responder in responders:
                self.floor_manager.enter_dialogue_mode(human)

                # Get context for response
                context = self._get_context()
                context["user_message"] = user_input
                context["user_name"] = human.name  # 누가 말했는지 명시
                context["user_role"] = human.role.value  # 역할도 명시

                self.callbacks.system(f"🤖 {responder.name} responds:")
                self.callbacks.system("-" * 60)
                # Generate and broadcast AI response with streaming
                self.broadcast_streaming(responder, context)

                self.floor_manager.exit_dialogue_mode()

        # End of interruption block
        self.callbacks.system("=" * 60)
        print()  # Empty line after

        # Resume normal meeting flow
        self.floor_manager.resume_after_user()

    def _handle_ai_mentions(self, sender: Participant, message: str) -> None:
        """Handle mentions in AI messages.

        When an AI mentions another participant, that participant responds.
        Prevents infinite loops by limiting mention chain depth to 3.
        When a Human is mentioned, waits for their input.

        Args:
            sender: AI participant who sent the message
            message: Message text containing mentions
        """
        # Prevent infinite mention loops
        MAX_MENTION_DEPTH = 3
        if self._mention_depth >= MAX_MENTION_DEPTH:
            return

        # Parse mentions from message
        mention_result = parse_mentions(message, self._participants)

        # Separate AI and Human mentions
        if mention_result.is_everyone:
            ai_mentioned = [p for p in self._participants if p.is_ai and p != sender]
            humans_mentioned = [p for p in self._participants if not p.is_ai]
        else:
            ai_mentioned = [p for p in mention_result.mentioned_participants if p.is_ai and p != sender]
            humans_mentioned = [p for p in mention_result.mentioned_participants if not p.is_ai]

        # Handle Human mentions first - give them a chance to respond
        if humans_mentioned:
            human_names = ", ".join(p.name for p in humans_mentioned)
            self.callbacks.system(f"\n💬 {human_names}님, 답변해 주세요:")
            self.callbacks.system("-" * 60)
            
            # Wait for user input
            interrupted = self.interruptible_sleep(30.0)  # 30초 대기
            
            if interrupted and self._user_input_queue:
                self._handle_user_interruption()

        # Continue with AI mentions
        mentioned = ai_mentioned
        if not mentioned:
            return

        # AI 멘션 처리: 호스트 중재 vs 직접 멘션
        if self._host and sender != self._host:
            # AI가 다른 AI를 멘션 → 호스트가 중재하여 답변자 결정
            designated = self._host_mediate_ai_mentions(sender, message, mentioned)
        else:
            # 호스트가 직접 멘션 → 멘션된 사람들 그대로 응답
            designated = mentioned

        # Increment depth before processing
        self._mention_depth += 1

        try:

            # Use batched delivery for multiple designated responders
            if len(designated) > 1:
                context_base = {
                    "mentioned_by": sender.name,
                    "mention_message": message
                }
                header_message = f"\n[{sender.name} mentioned {len(designated)} participants]"
                self._deliver_batched(designated, context_base, header_message)
            elif len(designated) == 1:
                # Single responder: use existing streaming behavior
                for designated_participant in designated:
                    # Get context for designated participant
                    context = self._get_context()
                    context["mentioned_by"] = sender.name
                    context["mention_message"] = message

                    self.callbacks.system(f"\n[{sender.name} mentioned {designated_participant.name}]")
                    self.callbacks.system("-" * 60)

                    # Generate response from designated participant
                    self.broadcast_streaming(designated_participant, context)

                    self.callbacks.system("-" * 60)

        finally:
            # Decrement depth after processing
            self._mention_depth -= 1

    def _parse_designated_responders(
        self,
        host_response: str,
        mentioned: list[Participant]
    ) -> list[Participant]:
        """Parse designated responders from host response.
        
        Args:
            host_response: Host's response containing designations
            mentioned: List of mentioned participants to choose from
            
        Returns:
            List of designated responders
        """
        import re
        
        # Look for [답변자: name] pattern
        pattern = r'\[답변자:\s*([^\]]+)\]'
        matches = re.findall(pattern, host_response)
        
        designated = []
        for name in matches:
            name = name.strip()
            # Handle special cases
            if name in ["전체", "모두", "all", "everyone"]:
                return mentioned
            
            # Find matching participant (case-insensitive)
            for p in mentioned:
                if p.name.lower() == name.lower():
                    designated.append(p)
                    break
        
        # Fallback: return all mentioned if parsing failed
        return designated if designated else mentioned

    def _host_mediate_responders(
        self,
        user_message: str,
        mentioned: list[Participant],
        human: HumanParticipant
    ) -> list[Participant]:
        """Host mediates to select appropriate responders.
        
        Args:
            user_message: User's message text
            mentioned: List of mentioned AI participants
            human: Human participant who sent message
            
        Returns:
            List of designated responders
        """
        # Build context for host to analyze question
        # Start with full context (includes phase and history)
        context = self._get_context()
        context.update({
            "user_message": user_message,
            "user_name": human.name,
            "mentioned_participants": [
                {"name": p.name, "role": p.role.value}
                for p in mentioned
            ],
            "task": "responder_selection"  # Special task marker
        })
        
        # Host analyzes and designates responders
        self.callbacks.system(f"🎯 호스트가 답변자를 지정합니다...")
        self.callbacks.system("-" * 60)
        
        # Use broadcast_streaming to ensure message is added to history and sent to all participants
        self.broadcast_streaming(self._host, context)
        
        # Extract the host response for parsing
        host_response = self._message_history[-1]["message"] if self._message_history else ""
        
        self.callbacks.system("-" * 60)
        
        # Parse designated responders from host response
        designated = self._parse_designated_responders(host_response, mentioned)
        
        # Show status
        if designated:
            self.callbacks.system(f"📋 답변 예정: {', '.join(p.name for p in designated)}")
        else:
            self.callbacks.system("⚠️ 답변자 지정 실패 - 모든 멘션된 참여자 응답")
            designated = mentioned
        
        return designated

    def _host_mediate_ai_mentions(
        self,
        sender: Participant,
        message: str,
        mentioned: list[Participant]
    ) -> list[Participant]:
        """Host mediates AI-to-AI mentions to select appropriate responders.
        
        Args:
            sender: AI participant who made the mentions
            message: Message text containing mentions
            mentioned: List of mentioned AI participants
            
        Returns:
            List of designated responders
        """
        # Build context for host to analyze AI mention
        # Start with full context (includes phase and history)
        context = self._get_context()
        context.update({
            "mentioned_by": sender.name,
            "mention_message": message,
            "mentioned_participants": [
                {"name": p.name, "role": p.role.value}
                for p in mentioned
            ],
            "task": "ai_mention_mediation"  # Special task marker
        })
        
        # Host analyzes and designates responders
        self.callbacks.system(f"🎯 호스트가 답변자를 지정합니다...")
        self.callbacks.system("-" * 60)
        
        # Use broadcast_streaming to ensure message is added to history and sent to all participants
        self.broadcast_streaming(self._host, context)
        
        # Extract the host response for parsing
        host_response = self._message_history[-1]["message"] if self._message_history else ""
        
        self.callbacks.system("-" * 60)
        
        # Parse designated responders from host response
        designated = self._parse_designated_responders(host_response, mentioned)
        
        # Show status
        if designated:
            self.callbacks.system(f"📋 답변 예정: {', '.join(p.name for p in designated)}")
        else:
            self.callbacks.system("⚠️ 답변자 지정 실패 - 모든 멘션된 참여자 응답")
            designated = mentioned
        
        return designated

    def _select_dialogue_responders(
        self,
        user_message: str,
        human: HumanParticipant
    ) -> list[Participant]:
        """Select appropriate AI responders for user message.

        Supports multiple responders via mentions (@name, @role, @everyone).
        Falls back to single responder selection for backward compatibility.

        Args:
            user_message: User's message text
            human: Human participant who sent message

        Returns:
            List of AI participants who should respond
        """
        # Get AI participants only
        ai_participants = [p for p in self._participants if p.is_ai]
        if not ai_participants:
            return []

        # Parse mentions from user message
        mention_result = parse_mentions(user_message, self._participants)

        # Handle @everyone
        if mention_result.is_everyone:
            # Use host mediation for @everyone too
            ai_mentioned = ai_participants
            if ai_mentioned and self._host:
                return self._host_mediate_responders(user_message, ai_mentioned, human)
            return ai_participants

        # Handle specific mentions - always use host mediation for consistency
        if mention_result.mentioned_participants:
            ai_mentioned = [p for p in mention_result.mentioned_participants if p.is_ai]
            if ai_mentioned and self._host:
                return self._host_mediate_responders(user_message, ai_mentioned, human)
            return ai_mentioned

        # Fall back to single responder selection (backward compatibility)
        context = {
            "last_message": user_message,
            "phase": self.phase_machine.current_phase.value
        }

        single = self.floor_manager.select_next_responder(ai_participants, context)
        return [single] if single else []

    def _find_participant_by_name(self, name: str) -> Optional[Participant]:
        """Find participant by name.

        Args:
            name: Participant name

        Returns:
            Participant if found, None otherwise
        """
        for participant in self._participants:
            if participant.name == name:
                return participant
        return None

    def _deliver_batched(
        self,
        responders: list[Participant],
        context_base: dict,
        header_message: str,
        timeout: float = 60.0
    ) -> None:
        """Collect streamed responses and deliver together.

        Unified batch delivery function for both AI mentions and user interruptions.

        Args:
            responders: List of AI participants to respond
            context_base: Base context to add for each responder
            header_message: Header message to display
            timeout: Maximum time to wait for all responses (seconds)
        """
        collected_responses: list[tuple[Participant, str]] = []

        self.callbacks.system(header_message)
        self.callbacks.system(f"⏳ Waiting for {len(responders)} participants to respond...")
        self.callbacks.system("💬 언제든 입력 가능합니다 (입력으로 중단 가능)")

        start_time = time.time()

        for i, responder in enumerate(responders):
            # Check for user interrupt before processing each response
            if self._check_user_interrupt_during_batch():
                self.callbacks.system("⚡ 유저 입력 감지 - 처리 중...")
                break
            # Check timeout
            if time.time() - start_time > timeout:
                self.callbacks.system("⏰ Response timeout reached")
                break

            context = self._get_context()
            context.update(context_base)

            # Show progress indicator
            self.callbacks.system(f"💭 {responder.name} responding...")

            # Stream response while collecting (progress shown, but not delivered yet)
            full_response = []
            
            # Define a silent typing callback that just collects chunks
            def collect_chunk(chunk: str):
                full_response.append(chunk)
            
            # Generate response with streaming (collect chunks silently)
            response = responder.speak_stream(
                context=context,
                typing_callback=collect_chunk
            )
            
            collected_responses.append((responder, response))
            
            # Show remaining count with interrupt option
            remaining = len(responders) - i - 1
            if remaining > 0:
                self.callbacks.system(f"   ({remaining}명 대기 중 - 입력으로 중단 가능)")

        # All responses collected - deliver them together
        self.callbacks.system(f"📨 Delivering {len(collected_responses)} responses:")
        self.callbacks.system("=" * 60)

        for responder, response in collected_responses:
            # Add to history
            self._message_history.append({
                "sender": responder.name,
                "role": responder.role.value,
                "message": response,
                "timestamp": time.time(),
            })
            
            # Output the response with role
            print(f"[{responder.role.value}]{responder.name}: {response}")
            print("-" * 60)

            # Send to all other participants
            for participant in self._participants:
                if participant != responder:
                    participant.receive_message(responder.name, response)

        self.callbacks.system("=" * 60)

        # Show input prompt if human participants exist
        if self._get_human_participants():
            print("\n" + "-" * 40)
            print("💬 Your turn to speak (or wait for next phase):")
            print("-" * 40)

    def _get_context(self, limit: int = 10) -> dict:
        """Get current context for agents.

        Args:
            limit: Number of recent messages to include

        Returns:
            Context dictionary
        """
        recent_messages = self._message_history[-limit:] if limit > 0 else []

        return {
            "phase": self.phase_machine.current_phase.value,
            "recent_messages": recent_messages,
            "current_speaker": (
                self.floor_manager.current_speaker.name
                if self.floor_manager.current_speaker
                else None
            ),
            "agenda": (
                self.agenda_manager.current_item.title
                if self.agenda_manager.current_item
                else None
            ),
            "participants": [
                {
                    "name": p.name,
                    "role": p.role.value,
                    "is_ai": p.is_ai
                }
                for p in self._participants
            ],
        }

    def _handle_opening(self) -> None:
        """Handle opening phase."""
        if self._host is None:
            self.callbacks.error("No host assigned")
            return

        # Host opens the meeting
        context = self._get_context()
        context["question"] = "Please open the meeting with warm greetings."

        self.broadcast_streaming(self._host, context)

        # Show input prompt if human participants exist
        if self._get_human_participants():
            print("\n" + "-" * 40)
            print("💬 Your turn to speak (or wait for next phase):")
            print("-" * 40)

        # Wait for user input or timeout
        interrupted = self.interruptible_sleep(60.0)

        # If interrupted, handle user input before phase transition
        while interrupted and self._user_input_queue:
            self._handle_user_interruption()
            # Wait for additional input (shorter timeout)
            interrupted = self.interruptible_sleep(10.0)

        # Transition to status check only after dialogue is complete
        self.phase_machine.trigger(PhaseEvent.OPENING_COMPLETE)

    def _handle_status_check(self) -> None:
        """Handle status check phase."""
        if self._pm is None:
            self.callbacks.system("No PM assigned, skipping status check")
            self.phase_machine.trigger(PhaseEvent.STATUS_CHECK_COMPLETE)
            return

        # PM reports status
        context = self._get_context()
        context["question"] = "Please report project status."

        self.broadcast_streaming(self._pm, context)

        # Show input prompt if human participants exist
        if self._get_human_participants():
            print("\n" + "-" * 40)
            print("💬 Your turn to speak (or wait for next phase):")
            print("-" * 40)

        # Wait for user input or timeout
        interrupted = self.interruptible_sleep(60.0)

        # If interrupted, handle user input before phase transition
        while interrupted and self._user_input_queue:
            self._handle_user_interruption()
            # Wait for additional input (shorter timeout)
            interrupted = self.interruptible_sleep(10.0)

        # Transition to issue resolution only after dialogue is complete
        self.phase_machine.trigger(PhaseEvent.STATUS_CHECK_COMPLETE)

    def _handle_issue_resolution(self) -> None:
        """Handle issue resolution phase."""
        if self._tech_lead is None:
            self.callbacks.system("No tech lead assigned, skipping issue resolution")
            self.phase_machine.trigger(PhaseEvent.ISSUE_RESOLUTION_COMPLETE)
            return

        # Tech lead discusses issues
        context = self._get_context()
        context["question"] = "Please discuss technical issues and assign work."

        self.broadcast_streaming(self._tech_lead, context)

        # Show input prompt if human participants exist
        if self._get_human_participants():
            print("\n" + "-" * 40)
            print("💬 Your turn to speak (or wait for next phase):")
            print("-" * 40)

        # Wait for user input or timeout
        interrupted = self.interruptible_sleep(60.0)

        # If interrupted, handle user input before phase transition
        while interrupted and self._user_input_queue:
            self._handle_user_interruption()
            # Wait for additional input (shorter timeout)
            interrupted = self.interruptible_sleep(10.0)

        # Transition to closing only after dialogue is complete
        # For now, skip worktree execution
        # TODO: Implement worktree in Issue #9
        self.phase_machine.trigger(PhaseEvent.SKIP_TO_CLOSING)

    def _handle_closing(self) -> None:
        """Handle closing phase."""
        if self._host is None:
            self.callbacks.error("No host assigned")
            return

        # Host summarizes
        context = self._get_context(limit=50)  # More context for summary
        context["question"] = "Please summarize the meeting with decisions and action items."

        self.broadcast_streaming(self._host, context)

        # Show formatted summary
        summary = self.agenda_manager.format_summary()
        self.callbacks.system(f"\n{summary}")

        # End meeting
        self.phase_machine.trigger(PhaseEvent.CLOSING_COMPLETE)

    def run(self) -> None:
        """Run the meeting."""
        if not self._participants:
            self.callbacks.error("No participants added")
            return

        self._running = True
        self._start_time = time.time()

        # Print user input instructions BEFORE starting input readers
        human_participants = self._get_human_participants()
        if human_participants:
            print("\n" + "=" * 60)
            print("💬 You can type your message anytime during the meeting")
            print("   Type your message and press Enter to speak.")
            print("=" * 60 + "\n")

        # Start input readers for human participants
        for participant in human_participants:
            participant.start_input_reader()

        # Start meeting
        self.callbacks.system("Meeting starting...")
        self.phase_machine.trigger(PhaseEvent.START_MEETING)

        # Main meeting loop
        while self._running and self.phase_machine.current_phase != MeetingPhase.ENDED:
            # Check time limit
            elapsed = time.time() - self._start_time
            if elapsed > self.max_duration:
                self.callbacks.system("Meeting time limit reached")
                self.phase_machine.trigger(PhaseEvent.FORCE_END)
                break

            # Handle current phase
            current_phase = self.phase_machine.current_phase

            if current_phase == MeetingPhase.OPENING:
                self._handle_opening()
            elif current_phase == MeetingPhase.STATUS_CHECK:
                self._handle_status_check()
            elif current_phase == MeetingPhase.ISSUE_RESOLUTION:
                self._handle_issue_resolution()
            elif current_phase == MeetingPhase.CLOSING:
                self._handle_closing()

            # Check for user interruptions
            if self._user_input_queue:
                self._handle_user_interruption()

            # Small delay between phases
            time.sleep(0.5)

        # Stop input readers for human participants
        for participant in self._get_human_participants():
            participant.stop_input_reader()

        self._running = False
        self.callbacks.system("Meeting ended")

    def stop(self) -> None:
        """Stop the meeting."""
        self._running = False
        self.phase_machine.trigger(PhaseEvent.FORCE_END)
