from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.orm import selectinload, joinedload, contains_eager
from sqlalchemy.exc import SQLAlchemyError, IntegrityError, OperationalError
from typing import Optional, List, Dict, Any, Tuple
from sqlalchemy import delete, func, and_, or_, update, desc
import logging 
from datetime import datetime, timezone, timedelta

from app.models import User as UserModel, Group as GroupModel, UserGroup as UserGroupModel, List as ListModel, Chore as ChoreModel, ChoreAssignment as ChoreAssignmentModel
from app.schemas.group import GroupCreate, GroupPublic
from app.models import UserRoleEnum 
from app.core.exceptions import (
    GroupOperationError,
    GroupNotFoundError,
    DatabaseConnectionError,
    DatabaseIntegrityError,
    DatabaseQueryError,
    DatabaseTransactionError,
    GroupMembershipError, 
    GroupPermissionError,
    PermissionDeniedError,
    InvalidOperationError
)
from app.core.cache import cache
# Add WebSocket imports
from app.core.websocket import websocket_manager, create_group_event

logger = logging.getLogger(__name__) # Initialize logger

# --- Group CRUD ---
async def create_group(db: AsyncSession, group_in: GroupCreate, creator_id: int) -> GroupModel:
    """Creates a group and adds the creator as the owner."""
    try:
        # Use the composability pattern for transactions as per fastapi-db-strategy.
        # This creates a savepoint if already in a transaction (e.g., from get_transactional_session)
        # or starts a new transaction if called outside of one (e.g., from a script).
        async with db.begin_nested() if db.in_transaction() else db.begin():
            db_group = GroupModel(name=group_in.name, created_by_id=creator_id)
            db.add(db_group)
            await db.flush() # Assigns ID to db_group

            db_user_group = UserGroupModel(
                user_id=creator_id,
                group_id=db_group.id,
                role=UserRoleEnum.owner
            )
            db.add(db_user_group)
            await db.flush() # Commits user_group, links to group

            # After creation and linking, explicitly load the group with its member associations and users
            stmt = (
                select(GroupModel)
                .where(GroupModel.id == db_group.id)
                .options(
                    selectinload(GroupModel.member_associations).selectinload(UserGroupModel.user)
                )
            )
            result = await db.execute(stmt)
            loaded_group = result.scalar_one_or_none()

            if loaded_group is None:
                # This should not happen if we just created it, but as a safeguard
                raise GroupOperationError("Failed to load group after creation.")

            # Broadcast group creation event via WebSocket (to creator only initially)
            await _broadcast_group_event(
                "group:created",
                loaded_group,
                creator_id,
                exclude_user=None  # Send to creator
            )

            return loaded_group
    except IntegrityError as e:
        logger.error(f"Database integrity error during group creation: {str(e)}", exc_info=True)
        raise DatabaseIntegrityError(f"Failed to create group due to integrity issue: {str(e)}")
    except OperationalError as e:
        logger.error(f"Database connection error during group creation: {str(e)}", exc_info=True)
        raise DatabaseConnectionError(f"Database connection error during group creation: {str(e)}")
    except SQLAlchemyError as e:
        logger.error(f"Unexpected SQLAlchemy error during group creation: {str(e)}", exc_info=True)
        raise DatabaseTransactionError(f"Database transaction error during group creation: {str(e)}")

async def get_user_groups(db: AsyncSession, user_id: int) -> List[GroupModel]:
    """Gets all groups a user is a member of with optimized eager loading."""
    try:
        result = await db.execute(
            select(GroupModel)
            .join(UserGroupModel)
            .where(UserGroupModel.user_id == user_id)
            .options(
                selectinload(GroupModel.member_associations).options(
                    selectinload(UserGroupModel.user)
                ),
                selectinload(GroupModel.chore_history)  # Eager load chore history
            )
        )
        return result.scalars().all()
    except OperationalError as e:
        raise DatabaseConnectionError(f"Failed to connect to database: {str(e)}")
    except SQLAlchemyError as e:
        raise DatabaseQueryError(f"Failed to query user groups: {str(e)}")

@cache(expire_time=1800, key_prefix="group")  # Cache for 30 minutes
async def get_group_by_id(db: AsyncSession, group_id: int) -> Optional[GroupModel]:
    """Get a group by its ID with caching, including member associations and chore history."""
    result = await db.execute(
        select(GroupModel)
        .where(GroupModel.id == group_id)
        .options(
            selectinload(GroupModel.member_associations).selectinload(UserGroupModel.user),
            selectinload(GroupModel.chore_history)
        )
    )
    return result.scalar_one_or_none()

async def is_user_member(db: AsyncSession, group_id: int, user_id: int) -> bool:
    """Checks if a user is a member of a specific group."""
    try:
        result = await db.execute(
            select(UserGroupModel.id)
            .where(UserGroupModel.group_id == group_id, UserGroupModel.user_id == user_id)
            .limit(1)
        )
        return result.scalar_one_or_none() is not None
    except OperationalError as e:
        raise DatabaseConnectionError(f"Failed to connect to database: {str(e)}")
    except SQLAlchemyError as e:
        raise DatabaseQueryError(f"Failed to check group membership: {str(e)}")

async def get_user_role_in_group(db: AsyncSession, group_id: int, user_id: int) -> Optional[UserRoleEnum]:
    """Gets the role of a user in a specific group."""
    try:
        result = await db.execute(
            select(UserGroupModel.role)
            .where(UserGroupModel.group_id == group_id, UserGroupModel.user_id == user_id)
        )
        return result.scalar_one_or_none()
    except OperationalError as e:
        raise DatabaseConnectionError(f"Failed to connect to database: {str(e)}")
    except SQLAlchemyError as e:
        raise DatabaseQueryError(f"Failed to query user role: {str(e)}")

async def add_user_to_group(db: AsyncSession, group_id: int, user_id: int, role: UserRoleEnum = UserRoleEnum.member) -> Optional[UserGroupModel]:
    """Adds a user to a group if they aren't already a member."""
    try:
        # Check if user is already a member before starting a transaction
        existing_stmt = select(UserGroupModel.id).where(UserGroupModel.group_id == group_id, UserGroupModel.user_id == user_id)
        existing_result = await db.execute(existing_stmt)
        if existing_result.scalar_one_or_none():
            return None

        # Use a single transaction
        async with db.begin_nested() if db.in_transaction() else db.begin():
            db_user_group = UserGroupModel(user_id=user_id, group_id=group_id, role=role)
            db.add(db_user_group)
            await db.flush() # Assigns ID to db_user_group

            # Optimistic locking: bump group version atomically
            await db.execute(
                update(GroupModel)
                .where(GroupModel.id == group_id)
                .values(version=GroupModel.version + 1)
            )

            # Eagerly load the 'user' and 'group' relationships for the response
            stmt = (
                select(UserGroupModel)
                .where(UserGroupModel.id == db_user_group.id)
                .options(
                    selectinload(UserGroupModel.user),
                    selectinload(UserGroupModel.group)
                )
            )
            result = await db.execute(stmt)
            loaded_user_group = result.scalar_one_or_none()

            if loaded_user_group is None:
                raise GroupOperationError(f"Failed to load user group association after adding user {user_id} to group {group_id}.")

            # Broadcast member joined event via WebSocket
            await _broadcast_member_event(
                "group:member_joined",
                loaded_user_group.group,
                loaded_user_group.user,
                user_id,
                exclude_user=user_id  # Don't send to the joining user
            )

            return loaded_user_group
    except IntegrityError as e:
        logger.error(f"Database integrity error while adding user to group: {str(e)}", exc_info=True)
        raise DatabaseIntegrityError(f"Failed to add user to group: {str(e)}")
    except OperationalError as e:
        logger.error(f"Database connection error while adding user to group: {str(e)}", exc_info=True)
        raise DatabaseConnectionError(f"Database connection error: {str(e)}")
    except SQLAlchemyError as e:
        logger.error(f"Unexpected SQLAlchemy error while adding user to group: {str(e)}", exc_info=True)
        raise DatabaseTransactionError(f"Failed to add user to group: {str(e)}")

async def remove_user_from_group(db: AsyncSession, group_id: int, user_id: int) -> bool:
    """Removes a user from a group."""
    try:
        async with db.begin_nested() if db.in_transaction() else db.begin():
            # Get user info before deletion for WebSocket broadcast
            user_to_remove = None
            if user_id:
                user_result = await db.execute(
                    select(UserModel)
                    .where(UserModel.id == user_id)
                )
                user_to_remove = user_result.scalar_one_or_none()

            result = await db.execute(
                delete(UserGroupModel)
                .where(UserGroupModel.group_id == group_id, UserGroupModel.user_id == user_id)
                .returning(UserGroupModel.id)
            )
            deleted = result.scalar_one_or_none() is not None

            if deleted:
                await db.execute(
                    update(GroupModel)
                    .where(GroupModel.id == group_id)
                    .values(version=GroupModel.version + 1)
                )

                # Broadcast member left event via WebSocket
                if user_to_remove:
                    await _broadcast_member_left_event(
                        "group:member_left",
                        group_id,
                        user_to_remove,
                        user_id,
                        exclude_user=user_id  # Don't send to the leaving user
                    )

            return deleted
    except OperationalError as e:
        logger.error(f"Database connection error while removing user from group: {str(e)}", exc_info=True)
        raise DatabaseConnectionError(f"Database connection error: {str(e)}")
    except SQLAlchemyError as e:
        logger.error(f"Unexpected SQLAlchemy error while removing user from group: {str(e)}", exc_info=True)
        raise DatabaseTransactionError(f"Failed to remove user from group: {str(e)}")

async def get_group_member_count(db: AsyncSession, group_id: int) -> int:
    """Counts the number of members in a group."""
    try:
        result = await db.execute(
            select(func.count(UserGroupModel.id)).where(UserGroupModel.group_id == group_id)
        )
        return result.scalar_one()
    except OperationalError as e:
        raise DatabaseConnectionError(f"Failed to connect to database: {str(e)}")
    except SQLAlchemyError as e:
        raise DatabaseQueryError(f"Failed to count group members: {str(e)}")

async def check_group_membership(
    db: AsyncSession, 
    group_id: int, 
    user_id: int, 
    action: str = "access this group"
) -> None:
    """
    Checks if a user is a member of a group. Raises exceptions if not found or not a member.
    
    Raises:
        GroupNotFoundError: If the group_id does not exist.
        GroupMembershipError: If the user_id is not a member of the group.
    """
    try:
        # Check group existence first
        group_exists = await db.get(GroupModel, group_id)
        if not group_exists:
            raise GroupNotFoundError(group_id)
            
        # Check membership
        membership = await db.execute(
            select(UserGroupModel.id)
            .where(UserGroupModel.group_id == group_id, UserGroupModel.user_id == user_id)
            .limit(1)
        )
        if membership.scalar_one_or_none() is None:
            raise GroupMembershipError(group_id, action=action)
        # If we reach here, the user is a member
        return None
    except GroupNotFoundError: # Re-raise specific errors
        raise
    except GroupMembershipError:
        raise
    except OperationalError as e:
        raise DatabaseConnectionError(f"Failed to connect to database while checking membership: {str(e)}")
    except SQLAlchemyError as e:
        raise DatabaseQueryError(f"Failed to check group membership: {str(e)}")

async def check_user_role_in_group(
    db: AsyncSession,
    group_id: int,
    user_id: int,
    required_role: UserRoleEnum,
    action: str = "perform this action"
) -> None:
    """
    Checks if a user is a member of a group and has the required role (or higher).

    Raises:
        GroupNotFoundError: If the group_id does not exist.
        GroupMembershipError: If the user_id is not a member of the group.
        GroupPermissionError: If the user does not have the required role.
    """
    # First, ensure user is a member (this also checks group existence)
    await check_group_membership(db, group_id, user_id, action=f"be checked for permissions to {action}")

    # Get the user's actual role
    actual_role = await get_user_role_in_group(db, group_id, user_id)

    # Define role hierarchy (assuming owner > member)
    role_hierarchy = {UserRoleEnum.owner: 2, UserRoleEnum.member: 1}

    if not actual_role or role_hierarchy.get(actual_role, 0) < role_hierarchy.get(required_role, 0):
        raise GroupPermissionError(
            group_id=group_id,
            action=f"{action} (requires at least '{required_role.value}' role)"
        )
    # If role is sufficient, return None
    return None

async def delete_group(db: AsyncSession, group_id: int, *, expected_version: int | None = None) -> None:
    """
    Deletes a group and all its associated data (members, invites, lists, etc.).
    The cascade delete in the models will handle the deletion of related records.
    
    Raises:
        GroupNotFoundError: If the group doesn't exist.
        DatabaseError: If there's an error during deletion.
    """
    try:
        # Get the group first to ensure it exists
        group = await get_group_by_id(db, group_id)
        if not group:
            raise GroupNotFoundError(group_id)

        # Optimistic locking – ensure caller had latest version
        if expected_version is not None and group.version != expected_version:
            raise InvalidOperationError(
                f"Version mismatch for group {group_id}. Current version is {group.version}, expected {expected_version}."
            )

        # Broadcast group deletion event via WebSocket (before actual deletion)
        await _broadcast_group_event(
            "group:deleted",
            group,
            group.created_by_id,
            exclude_user=None  # Send to all members
        )

        # Delete the group - cascading delete will handle related records
        await db.delete(group)
        await db.flush()
        
        logger.info(f"Group {group_id} deleted successfully")
    except OperationalError as e:
        logger.error(f"Database connection error while deleting group {group_id}: {str(e)}", exc_info=True)
        raise DatabaseConnectionError(f"Database connection error: {str(e)}")
    except SQLAlchemyError as e:
        logger.error(f"Unexpected SQLAlchemy error while deleting group {group_id}: {str(e)}", exc_info=True)
        raise DatabaseTransactionError(f"Failed to delete group: {str(e)}")


# WebSocket Broadcasting Helper Functions

async def _broadcast_group_event(
    event_type: str,
    group: GroupModel,
    user_id: int,
    exclude_user: Optional[int] = None
):
    """
    Broadcast group-related events to household members
    """
    try:
        # Prepare event payload
        event_payload = {
            "group_id": group.id,
            "name": group.name,
            "created_by_id": group.created_by_id,
            "version": group.version,
            "created_at": group.created_at.isoformat() if group.created_at else None,
            "updated_at": group.updated_at.isoformat() if group.updated_at else None,
        }

        # Create the WebSocket event
        event = create_group_event(event_type, event_payload, user_id)

        # Broadcast to all household members
        await websocket_manager.broadcast_to_household(
            group.id, 
            event, 
            exclude_user=exclude_user
        )

        logger.debug(f"Broadcasted {event_type} event for group {group.id}")

    except Exception as e:
        # Don't fail the CRUD operation if WebSocket broadcast fails
        logger.error(f"Failed to broadcast group event {event_type} for group {group.id}: {e}")


async def _broadcast_member_event(
    event_type: str,
    group: GroupModel,
    user: UserModel,
    acting_user_id: int,
    exclude_user: Optional[int] = None
):
    """
    Broadcast member-related events to household
    """
    try:
        event_payload = {
            "group_id": group.id,
            "user": {
                "id": user.id,
                "email": user.email,
                "username": user.username,
                "is_active": user.is_active,
            },
            "group_name": group.name,
        }

        event = create_group_event(event_type, event_payload, acting_user_id)

        # Broadcast to all household members
        await websocket_manager.broadcast_to_household(group.id, event, exclude_user)

        logger.debug(f"Broadcasted {event_type} event for user {user.id} in group {group.id}")

    except Exception as e:
        logger.error(f"Failed to broadcast member event {event_type}: {e}")


async def _broadcast_member_left_event(
    event_type: str,
    group_id: int,
    user: UserModel,
    acting_user_id: int,
    exclude_user: Optional[int] = None
):
    """
    Broadcast member left events to household
    """
    try:
        event_payload = {
            "group_id": group_id,
            "user_id": user.id,
            "user_email": user.email,
            "user_username": user.username,
        }

        event = create_group_event(event_type, event_payload, acting_user_id)

        # Broadcast to remaining household members
        await websocket_manager.broadcast_to_household(group_id, event, exclude_user)

        logger.debug(f"Broadcasted {event_type} event for user {user.id} leaving group {group_id}")

    except Exception as e:
        logger.error(f"Failed to broadcast member left event {event_type}: {e}")