"""
Session Service - CRUD operations for game sessions.

This service handles creating, reading, updating, and managing game sessions,
with support for both solo and multiplayer sessions.
"""

import json
from typing import List, Optional
from datetime import datetime, timezone

from appwrite.query import Query
from appwrite.id import ID

from app.models.session import GameSession, GameState, ConversationEntry, SessionConfig
from app.models.enums import SessionStatus, SessionType
from app.models.action_prompt import LocationType
from app.services.database_service import get_database_service
from app.services.appwrite_service import AppwriteService
from app.services.character_service import get_character_service, CharacterNotFound
from app.services.location_loader import get_location_loader
from app.utils.logging import get_logger

logger = get_logger(__file__)


# Session limits per user
MAX_ACTIVE_SESSIONS = 5


class SessionNotFound(Exception):
    """Raised when session ID doesn't exist or user doesn't own it."""
    pass


class SessionLimitExceeded(Exception):
    """Raised when user tries to create more sessions than allowed."""
    pass


class SessionValidationError(Exception):
    """Raised when session validation fails."""
    pass


class SessionService:
    """
    Service for managing game sessions.

    This service provides:
    - Session creation (solo and multiplayer)
    - Session retrieval and listing
    - Session state updates
    - Conversation history management
    - Game state tracking
    """

    def __init__(self):
        """Initialize the session service with dependencies."""
        self.db = get_database_service()
        self.appwrite = AppwriteService()
        self.character_service = get_character_service()
        self.collection_id = "game_sessions"

        logger.info("SessionService initialized")

    def create_solo_session(
        self,
        user_id: str,
        character_id: str,
        starting_location: Optional[str] = None,
        starting_location_type: Optional[LocationType] = None
    ) -> GameSession:
        """
        Create a new solo game session.

        This method:
        1. Validates user owns the character
        2. Validates user hasn't exceeded session limit
        3. Determines starting location from location data
        4. Creates session with initial game state
        5. Stores in Appwrite database

        Args:
            user_id: Owner's user ID
            character_id: Character ID for this session
            starting_location: Initial location ID (optional, uses default starting location)
            starting_location_type: Initial location type (optional, derived from location data)

        Returns:
            Created GameSession instance

        Raises:
            CharacterNotFound: If character doesn't exist or user doesn't own it
            SessionLimitExceeded: If user has reached active session limit
        """
        try:
            logger.info("Creating solo session",
                       user_id=user_id,
                       character_id=character_id)

            # Validate user owns the character
            character = self.character_service.get_character(character_id, user_id)
            if not character:
                raise CharacterNotFound(f"Character not found: {character_id}")

            # Determine starting location from location data if not provided
            if not starting_location:
                location_loader = get_location_loader()
                starting_locations = location_loader.get_starting_locations()

                if starting_locations:
                    # Use first starting location (usually crossville_village)
                    start_loc = starting_locations[0]
                    starting_location = start_loc.location_id
                    # Convert from enums.LocationType to action_prompt.LocationType via string value
                    starting_location_type = LocationType(start_loc.location_type.value)
                    logger.info("Using starting location from data",
                               location_id=starting_location,
                               location_type=starting_location_type.value)
                else:
                    # Fallback to crossville_village
                    starting_location = "crossville_village"
                    starting_location_type = LocationType.TOWN
                    logger.warning("No starting locations found, using fallback",
                                  location_id=starting_location)

            # Ensure location type is set
            if not starting_location_type:
                starting_location_type = LocationType.TOWN

            # Check session limit
            active_count = self.count_user_sessions(user_id, active_only=True)
            if active_count >= MAX_ACTIVE_SESSIONS:
                logger.warning("Session limit exceeded",
                             user_id=user_id,
                             current=active_count,
                             limit=MAX_ACTIVE_SESSIONS)
                raise SessionLimitExceeded(
                    f"Maximum active sessions reached ({active_count}/{MAX_ACTIVE_SESSIONS}). "
                    f"Please end an existing session to start a new one."
                )

            # Generate unique session ID
            session_id = ID.unique()

            # Create game state with starting location
            game_state = GameState(
                current_location=starting_location,
                location_type=starting_location_type,
                discovered_locations=[starting_location],
                active_quests=[],
                world_events=[]
            )

            # Create session instance
            session = GameSession(
                session_id=session_id,
                session_type=SessionType.SOLO,
                solo_character_id=character_id,
                user_id=user_id,
                party_member_ids=[],
                config=SessionConfig(),
                game_state=game_state,
                turn_order=[character_id],
                current_turn=0,
                turn_number=0,
                status=SessionStatus.ACTIVE
            )

            # Serialize and store
            session_dict = session.to_dict()
            session_json = json.dumps(session_dict)

            document_data = {
                'userId': user_id,
                'characterId': character_id,
                'sessionData': session_json,
                'status': SessionStatus.ACTIVE.value,
                'sessionType': SessionType.SOLO.value
            }

            self.db.create_document(
                collection_id=self.collection_id,
                data=document_data,
                document_id=session_id
            )

            logger.info("Solo session created successfully",
                       session_id=session_id,
                       user_id=user_id,
                       character_id=character_id)

            return session

        except (CharacterNotFound, SessionLimitExceeded):
            raise
        except Exception as e:
            logger.error("Failed to create solo session",
                        user_id=user_id,
                        character_id=character_id,
                        error=str(e))
            raise

    def get_session(self, session_id: str, user_id: Optional[str] = None) -> GameSession:
        """
        Get a session by ID.

        Args:
            session_id: Session ID
            user_id: Optional user ID for ownership validation

        Returns:
            GameSession instance

        Raises:
            SessionNotFound: If session doesn't exist or user doesn't own it
        """
        try:
            logger.debug("Fetching session", session_id=session_id)

            # Get document from database
            document = self.db.get_row(self.collection_id, session_id)

            if not document:
                logger.warning("Session not found", session_id=session_id)
                raise SessionNotFound(f"Session not found: {session_id}")

            # Verify ownership if user_id provided
            if user_id and document.data.get('userId') != user_id:
                logger.warning("Session ownership mismatch",
                             session_id=session_id,
                             expected_user=user_id,
                             actual_user=document.data.get('userId'))
                raise SessionNotFound(f"Session not found: {session_id}")

            # Parse session data
            session_json = document.data.get('sessionData')
            session_dict = json.loads(session_json)
            session = GameSession.from_dict(session_dict)

            logger.debug("Session fetched successfully", session_id=session_id)
            return session

        except SessionNotFound:
            raise
        except Exception as e:
            logger.error("Failed to fetch session",
                        session_id=session_id,
                        error=str(e))
            raise

    def update_session(self, session: GameSession) -> GameSession:
        """
        Update a session in the database.

        Args:
            session: GameSession instance with updated data

        Returns:
            Updated GameSession instance
        """
        try:
            logger.debug("Updating session", session_id=session.session_id)

            # Serialize session
            session_dict = session.to_dict()
            session_json = json.dumps(session_dict)

            # Update in database
            self.db.update_document(
                collection_id=self.collection_id,
                document_id=session.session_id,
                data={
                    'sessionData': session_json,
                    'status': session.status.value
                }
            )

            logger.debug("Session updated successfully", session_id=session.session_id)
            return session

        except Exception as e:
            logger.error("Failed to update session",
                        session_id=session.session_id,
                        error=str(e))
            raise

    def get_user_sessions(
        self,
        user_id: str,
        active_only: bool = True,
        limit: int = 25
    ) -> List[GameSession]:
        """
        Get all sessions for a user.

        Args:
            user_id: User ID
            active_only: If True, only return active sessions
            limit: Maximum number of sessions to return

        Returns:
            List of GameSession instances
        """
        try:
            logger.debug("Fetching user sessions",
                        user_id=user_id,
                        active_only=active_only)

            # Build query
            queries = [Query.equal('userId', user_id)]
            if active_only:
                queries.append(Query.equal('status', SessionStatus.ACTIVE.value))

            documents = self.db.list_rows(
                table_id=self.collection_id,
                queries=queries,
                limit=limit
            )

            # Parse all sessions
            sessions = []
            for document in documents:
                try:
                    session_json = document.data.get('sessionData')
                    session_dict = json.loads(session_json)
                    session = GameSession.from_dict(session_dict)
                    sessions.append(session)
                except Exception as e:
                    logger.error("Failed to parse session",
                               document_id=document.id,
                               error=str(e))
                    continue

            logger.debug("User sessions fetched",
                        user_id=user_id,
                        count=len(sessions))

            return sessions

        except Exception as e:
            logger.error("Failed to fetch user sessions",
                        user_id=user_id,
                        error=str(e))
            raise

    def count_user_sessions(self, user_id: str, active_only: bool = True) -> int:
        """
        Count sessions for a user.

        Args:
            user_id: User ID
            active_only: If True, only count active sessions

        Returns:
            Number of sessions
        """
        try:
            queries = [Query.equal('userId', user_id)]
            if active_only:
                queries.append(Query.equal('status', SessionStatus.ACTIVE.value))

            count = self.db.count_documents(
                collection_id=self.collection_id,
                queries=queries
            )

            logger.debug("Session count",
                        user_id=user_id,
                        active_only=active_only,
                        count=count)
            return count

        except Exception as e:
            logger.error("Failed to count sessions",
                        user_id=user_id,
                        error=str(e))
            return 0

    def end_session(self, session_id: str, user_id: str) -> GameSession:
        """
        End a session by marking it as completed.

        Args:
            session_id: Session ID
            user_id: User ID for ownership validation

        Returns:
            Updated GameSession instance

        Raises:
            SessionNotFound: If session doesn't exist or user doesn't own it
        """
        try:
            logger.info("Ending session", session_id=session_id, user_id=user_id)

            session = self.get_session(session_id, user_id)
            session.status = SessionStatus.COMPLETED
            session.update_activity()

            return self.update_session(session)

        except SessionNotFound:
            raise
        except Exception as e:
            logger.error("Failed to end session",
                        session_id=session_id,
                        error=str(e))
            raise

    def add_conversation_entry(
        self,
        session_id: str,
        character_id: str,
        character_name: str,
        action: str,
        dm_response: str,
        combat_log: Optional[List] = None,
        quest_offered: Optional[dict] = None
    ) -> GameSession:
        """
        Add an entry to the conversation history.

        This method automatically:
        - Increments turn number
        - Adds timestamp
        - Updates last activity

        Args:
            session_id: Session ID
            character_id: Acting character's ID
            character_name: Acting character's name
            action: Player's action text
            dm_response: AI DM's response
            combat_log: Optional combat actions
            quest_offered: Optional quest offering info

        Returns:
            Updated GameSession instance
        """
        try:
            logger.debug("Adding conversation entry",
                        session_id=session_id,
                        character_id=character_id)

            session = self.get_session(session_id)

            # Create conversation entry
            entry = ConversationEntry(
                turn=session.turn_number + 1,
                character_id=character_id,
                character_name=character_name,
                action=action,
                dm_response=dm_response,
                combat_log=combat_log or [],
                quest_offered=quest_offered
            )

            # Add entry and increment turn
            session.conversation_history.append(entry)
            session.turn_number += 1
            session.update_activity()

            # Save to database
            return self.update_session(session)

        except Exception as e:
            logger.error("Failed to add conversation entry",
                        session_id=session_id,
                        error=str(e))
            raise

    def get_conversation_history(
        self,
        session_id: str,
        limit: Optional[int] = None,
        offset: int = 0
    ) -> List[ConversationEntry]:
        """
        Get conversation history for a session.

        Args:
            session_id: Session ID
            limit: Maximum entries to return (None for all)
            offset: Number of entries to skip from end

        Returns:
            List of ConversationEntry instances
        """
        try:
            session = self.get_session(session_id)
            history = session.conversation_history

            # Apply offset (from end)
            if offset > 0:
                history = history[:-offset] if offset < len(history) else []

            # Apply limit (from end)
            if limit and len(history) > limit:
                history = history[-limit:]

            return history

        except Exception as e:
            logger.error("Failed to get conversation history",
                        session_id=session_id,
                        error=str(e))
            raise

    def get_recent_history(self, session_id: str, num_turns: int = 3) -> List[ConversationEntry]:
        """
        Get the most recent conversation entries for AI context.

        Args:
            session_id: Session ID
            num_turns: Number of recent turns to return

        Returns:
            List of most recent ConversationEntry instances
        """
        return self.get_conversation_history(session_id, limit=num_turns)

    def update_location(
        self,
        session_id: str,
        new_location: str,
        location_type: LocationType
    ) -> GameSession:
        """
        Update the current location in the session.

        Also adds location to discovered_locations if not already there.

        Args:
            session_id: Session ID
            new_location: New location name
            location_type: New location type

        Returns:
            Updated GameSession instance
        """
        try:
            logger.debug("Updating location",
                        session_id=session_id,
                        new_location=new_location)

            session = self.get_session(session_id)
            session.game_state.current_location = new_location
            session.game_state.location_type = location_type

            # Track discovered locations
            if new_location not in session.game_state.discovered_locations:
                session.game_state.discovered_locations.append(new_location)

            session.update_activity()
            return self.update_session(session)

        except Exception as e:
            logger.error("Failed to update location",
                        session_id=session_id,
                        error=str(e))
            raise

    def add_discovered_location(self, session_id: str, location: str) -> GameSession:
        """
        Add a location to the discovered locations list.

        Args:
            session_id: Session ID
            location: Location name to add

        Returns:
            Updated GameSession instance
        """
        try:
            session = self.get_session(session_id)

            if location not in session.game_state.discovered_locations:
                session.game_state.discovered_locations.append(location)
                session.update_activity()
                return self.update_session(session)

            return session

        except Exception as e:
            logger.error("Failed to add discovered location",
                        session_id=session_id,
                        error=str(e))
            raise

    def add_active_quest(self, session_id: str, quest_id: str) -> GameSession:
        """
        Add a quest to the active quests list.

        Validates max 2 active quests limit.

        Args:
            session_id: Session ID
            quest_id: Quest ID to add

        Returns:
            Updated GameSession instance

        Raises:
            SessionValidationError: If max quests limit exceeded
        """
        try:
            session = self.get_session(session_id)

            # Check max active quests (2)
            if len(session.game_state.active_quests) >= 2:
                raise SessionValidationError(
                    "Maximum active quests reached (2/2). "
                    "Complete or abandon a quest to accept a new one."
                )

            if quest_id not in session.game_state.active_quests:
                session.game_state.active_quests.append(quest_id)
                session.update_activity()
                return self.update_session(session)

            return session

        except SessionValidationError:
            raise
        except Exception as e:
            logger.error("Failed to add active quest",
                        session_id=session_id,
                        quest_id=quest_id,
                        error=str(e))
            raise

    def remove_active_quest(self, session_id: str, quest_id: str) -> GameSession:
        """
        Remove a quest from the active quests list.

        Args:
            session_id: Session ID
            quest_id: Quest ID to remove

        Returns:
            Updated GameSession instance
        """
        try:
            session = self.get_session(session_id)

            if quest_id in session.game_state.active_quests:
                session.game_state.active_quests.remove(quest_id)
                session.update_activity()
                return self.update_session(session)

            return session

        except Exception as e:
            logger.error("Failed to remove active quest",
                        session_id=session_id,
                        quest_id=quest_id,
                        error=str(e))
            raise

    def add_world_event(self, session_id: str, event: dict) -> GameSession:
        """
        Add a world event to the session.

        Args:
            session_id: Session ID
            event: Event dictionary with type, description, etc.

        Returns:
            Updated GameSession instance
        """
        try:
            session = self.get_session(session_id)

            # Add timestamp if not present
            if 'timestamp' not in event:
                event['timestamp'] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")

            session.game_state.world_events.append(event)
            session.update_activity()
            return self.update_session(session)

        except Exception as e:
            logger.error("Failed to add world event",
                        session_id=session_id,
                        error=str(e))
            raise


# Global instance for convenience
_service_instance: Optional[SessionService] = None


def get_session_service() -> SessionService:
    """
    Get the global SessionService instance.

    Returns:
        Singleton SessionService instance
    """
    global _service_instance
    if _service_instance is None:
        _service_instance = SessionService()
    return _service_instance
