from typing import List, Optional, Tuple

from mirix.orm.errors import NoResultFound
from mirix.orm.organization import Organization as OrganizationModel
from mirix.orm.user import User as UserModel
from mirix.schemas.user import User as PydanticUser
from mirix.schemas.user import UserUpdate
from mirix.services.organization_manager import OrganizationManager
from mirix.utils import enforce_types


class UserManager:
    """Manager class to handle business logic related to Users."""

    DEFAULT_USER_NAME = "default_user"
    DEFAULT_USER_ID = "user-00000000-0000-4000-8000-000000000000"
    DEFAULT_TIME_ZONE = "UTC (UTC+00:00)"

    def __init__(self):
        # Fetching the db_context similarly as in OrganizationManager
        from mirix.server.server import db_context

        self.session_maker = db_context

    @enforce_types
    def create_default_user(
        self, org_id: str = OrganizationManager.DEFAULT_ORG_ID
    ) -> PydanticUser:
        """Create the default user."""
        with self.session_maker() as session:
            # Make sure the org id exists
            try:
                OrganizationModel.read(db_session=session, identifier=org_id)
            except NoResultFound:
                raise ValueError(
                    f"No organization with {org_id} exists in the organization table."
                )

            # Try to retrieve the user
            try:
                user = UserModel.read(
                    db_session=session, identifier=self.DEFAULT_USER_ID
                )
            except NoResultFound:
                # If it doesn't exist, make it
                user = UserModel(
                    id=self.DEFAULT_USER_ID,
                    name=self.DEFAULT_USER_NAME,
                    status="active",
                    timezone=self.DEFAULT_TIME_ZONE,
                    organization_id=org_id,
                )
                user.create(session)

            return user.to_pydantic()

    @enforce_types
    def create_user(self, pydantic_user: PydanticUser) -> PydanticUser:
        """Create a new user if it doesn't already exist (with Redis caching)."""
        with self.session_maker() as session:
            new_user = UserModel(**pydantic_user.model_dump())
            new_user.create_with_redis(session, actor=None)  # ⭐ Auto-caches to Redis
            return new_user.to_pydantic()

    @enforce_types
    def update_user(self, user_update: UserUpdate) -> PydanticUser:
        """Update user details (with Redis cache invalidation)."""
        with self.session_maker() as session:
            # Retrieve the existing user by ID
            existing_user = UserModel.read(
                db_session=session, identifier=user_update.id
            )

            # Update only the fields that are provided in UserUpdate
            update_data = user_update.model_dump(exclude_unset=True, exclude_none=True)
            for key, value in update_data.items():
                setattr(existing_user, key, value)

            # Commit the updated user and update cache
            existing_user.update_with_redis(session, actor=None)  # ⭐ Updates Redis cache
            return existing_user.to_pydantic()

    @enforce_types
    def update_user_timezone(self, timezone_str: str, user_id: str) -> PydanticUser:
        """Update the timezone of a user (with Redis cache invalidation)."""
        with self.session_maker() as session:
            # Retrieve the existing user by ID
            existing_user = UserModel.read(db_session=session, identifier=user_id)

            # Update the timezone
            existing_user.timezone = timezone_str

            # Commit the updated user and update cache
            existing_user.update_with_redis(session, actor=None)  # ⭐ Updates Redis cache
            return existing_user.to_pydantic()

    @enforce_types
    def update_user_status(self, user_id: str, status: str) -> PydanticUser:
        """Update the status of a user (with Redis cache invalidation)."""
        with self.session_maker() as session:
            # Retrieve the existing user by ID
            existing_user = UserModel.read(db_session=session, identifier=user_id)

            # Update the status
            existing_user.status = status

            # Commit the updated user and update cache
            existing_user.update_with_redis(session, actor=None)  # ⭐ Updates Redis cache
            return existing_user.to_pydantic()

    @enforce_types
    def delete_user_by_id(self, user_id: str):
        """
        Soft delete a user and cascade soft delete to all associated records using memory managers.
        
        Cleanup workflow:
        1. Soft delete all memory records using memory managers:
           - Episodic memories
           - Semantic memories
           - Procedural memories
           - Resource memories
           - Knowledge vault items
           - Messages
           - Blocks
        
        2. Database (PostgreSQL):
           - Set user.is_deleted = True
        
        3. Redis Cache:
           - Update user hash with is_deleted=true
           - Memory cache entries updated by managers with is_deleted=true
        
        Args:
            user_id: ID of the user to soft delete
        """
        from mirix.database.redis_client import get_redis_client
        from mirix.log import get_logger

        logger = get_logger(__name__)
        logger.info("Soft deleting user %s and all associated records using memory managers...", user_id)

        # Import memory managers
        from mirix.services.episodic_memory_manager import EpisodicMemoryManager
        from mirix.services.semantic_memory_manager import SemanticMemoryManager
        from mirix.services.procedural_memory_manager import ProceduralMemoryManager
        from mirix.services.resource_memory_manager import ResourceMemoryManager
        from mirix.services.knowledge_vault_manager import KnowledgeVaultManager
        from mirix.services.message_manager import MessageManager
        from mirix.services.block_manager import BlockManager

        # 1. Soft delete all memory records using memory managers
        episodic_manager = EpisodicMemoryManager()
        semantic_manager = SemanticMemoryManager()
        procedural_manager = ProceduralMemoryManager()
        resource_manager = ResourceMemoryManager()
        knowledge_manager = KnowledgeVaultManager()
        message_manager = MessageManager()
        block_manager = BlockManager()

        episodic_count = episodic_manager.soft_delete_by_user_id(user_id=user_id)
        logger.debug("Soft deleted %d episodic memories for user %s", episodic_count, user_id)

        semantic_count = semantic_manager.soft_delete_by_user_id(user_id=user_id)
        logger.debug("Soft deleted %d semantic memories for user %s", semantic_count, user_id)

        procedural_count = procedural_manager.soft_delete_by_user_id(user_id=user_id)
        logger.debug("Soft deleted %d procedural memories for user %s", procedural_count, user_id)

        resource_count = resource_manager.soft_delete_by_user_id(user_id=user_id)
        logger.debug("Soft deleted %d resource memories for user %s", resource_count, user_id)

        knowledge_count = knowledge_manager.soft_delete_by_user_id(user_id=user_id)
        logger.debug("Soft deleted %d knowledge vault items for user %s", knowledge_count, user_id)

        message_count = message_manager.soft_delete_by_user_id(user_id=user_id)
        logger.debug("Soft deleted %d messages for user %s", message_count, user_id)

        block_count = block_manager.soft_delete_by_user_id(user_id=user_id)
        logger.debug("Soft deleted %d blocks for user %s", block_count, user_id)

        # 2. Soft delete user
        with self.session_maker() as session:
            # Find user
            user = UserModel.read(db_session=session, identifier=user_id)
            if not user:
                logger.warning("User %s not found", user_id)
                return

            # Soft delete user (set is_deleted = True directly, don't call user.delete())
            user.is_deleted = True
            user.set_updated_at()
            session.commit()
            logger.info("Soft deleted user %s from database", user_id)

            # 3. Update Redis cache to reflect soft delete
            try:
                redis_client = get_redis_client()
                if redis_client:
                    # Update user hash with is_deleted=true
                    user_key = f"{redis_client.USER_PREFIX}{user_id}"
                    try:
                        redis_client.client.hset(user_key, "is_deleted", "true")
                        logger.debug("Updated user %s in Redis (is_deleted=true)", user_id)
                    except Exception as e:
                        logger.warning("Failed to update user in Redis, removing instead: %s", e)
                        redis_client.delete(user_key)

                    logger.info(
                        "✅ User %s and all associated records soft deleted: "
                        "%d episodic, %d semantic, %d procedural, %d resource, %d knowledge_vault, %d messages, %d blocks",
                        user_id, episodic_count, semantic_count, procedural_count,
                        resource_count, knowledge_count, message_count, block_count
                    )
            except Exception as e:
                logger.warning("Failed to update Redis cache for user %s: %s", user_id, e)

    def delete_memories_by_user_id(self, user_id: str):
        """
        Hard delete memories, messages, and blocks for a user using memory managers' bulk delete.
        
        This permanently removes data records while preserving the user record.
        Uses optimized bulk delete methods in each manager for efficient deletion.
        
        Cleanup workflow:
        1. Call each memory manager's delete_by_user_id() method
           - EpisodicMemoryManager.delete_by_user_id()
           - SemanticMemoryManager.delete_by_user_id()
           - ProceduralMemoryManager.delete_by_user_id()
           - ResourceMemoryManager.delete_by_user_id()
           - KnowledgeVaultManager.delete_by_user_id()
           - MessageManager.delete_by_user_id()
           - BlockManager.delete_by_user_id()
        2. Each manager handles:
           - Bulk database deletion
           - Redis cache cleanup
           - Business logic
        3. PRESERVE: user record
        
        Args:
            user_id: ID of the user whose memories to delete
        """
        from mirix.log import get_logger

        logger = get_logger(__name__)
        logger.info("Bulk deleting memories for user %s using memory managers (preserving user record)...", user_id)

        # Import managers
        from mirix.services.episodic_memory_manager import EpisodicMemoryManager
        from mirix.services.semantic_memory_manager import SemanticMemoryManager
        from mirix.services.procedural_memory_manager import ProceduralMemoryManager
        from mirix.services.resource_memory_manager import ResourceMemoryManager
        from mirix.services.knowledge_vault_manager import KnowledgeVaultManager
        from mirix.services.message_manager import MessageManager
        from mirix.services.block_manager import BlockManager

        # Initialize managers
        episodic_manager = EpisodicMemoryManager()
        semantic_manager = SemanticMemoryManager()
        procedural_manager = ProceduralMemoryManager()
        resource_manager = ResourceMemoryManager()
        knowledge_manager = KnowledgeVaultManager()
        message_manager = MessageManager()
        block_manager = BlockManager()

        # Use managers' bulk delete methods
        try:
            # Bulk delete memories using manager methods
            episodic_count = episodic_manager.delete_by_user_id(user_id=user_id)
            logger.debug("Bulk deleted %d episodic memories", episodic_count)

            semantic_count = semantic_manager.delete_by_user_id(user_id=user_id)
            logger.debug("Bulk deleted %d semantic memories", semantic_count)

            procedural_count = procedural_manager.delete_by_user_id(user_id=user_id)
            logger.debug("Bulk deleted %d procedural memories", procedural_count)

            resource_count = resource_manager.delete_by_user_id(user_id=user_id)
            logger.debug("Bulk deleted %d resource memories", resource_count)

            knowledge_count = knowledge_manager.delete_by_user_id(user_id=user_id)
            logger.debug("Bulk deleted %d knowledge vault items", knowledge_count)

            message_count = message_manager.delete_by_user_id(user_id=user_id)
            logger.debug("Bulk deleted %d messages", message_count)

            block_count = block_manager.delete_by_user_id(user_id=user_id)
            logger.debug("Bulk deleted %d blocks", block_count)

            # Clear message_ids from ALL agents in PostgreSQL (messages are user-scoped, agents are client-scoped)
            # IMPORTANT: Keep the first message (system message) as agents need it to function
            # We need to clear message_ids from all agents that might have cached this user's messages
            with self.session_maker() as session:
                from mirix.orm.agent import Agent as AgentModel
                # Update ALL agents to keep only system messages
                # (We can't know which agents have which user's messages, so clean all)
                agents = session.query(AgentModel).all()
                
                for agent in agents:
                    if agent.message_ids and len(agent.message_ids) > 1:  # Has conversation messages
                        agent.message_ids = [agent.message_ids[0]]  # Keep system message only
                
                session.commit()
                logger.debug("Cleared conversation message_ids from %d agents in PostgreSQL (kept system messages)", len(agents))
            
            # Invalidate agent caches that might reference deleted messages for this user
            from mirix.database.redis_client import get_redis_client
            redis_client = get_redis_client()
            if redis_client:
                # Use SCAN to find all agent keys and delete them
                cursor = 0
                invalidated_count = 0
                while True:
                    cursor, keys = redis_client.client.scan(
                        cursor=cursor,
                        match=f"{redis_client.AGENT_PREFIX}*",
                        count=100
                    )
                    if keys:
                        redis_client.client.delete(*keys)
                        invalidated_count += len(keys)
                    if cursor == 0:
                        break
                if invalidated_count > 0:
                    logger.debug("✅ Invalidated %d agent caches due to user deletion", invalidated_count)

            logger.info(
                "✅ Bulk deleted all memories for user %s: "
                "%d episodic, %d semantic, %d procedural, %d resource, %d knowledge_vault, %d messages, %d blocks "
                "(user record preserved)",
                user_id, episodic_count, semantic_count, procedural_count,
                resource_count, knowledge_count, message_count, block_count
            )
        except Exception as e:
            logger.error("Failed to bulk delete memories for user %s: %s", user_id, e)
            raise

    @enforce_types
    def get_user_by_id(self, user_id: str) -> PydanticUser:
        """Fetch a user by ID (with Redis Hash caching)."""
        # Try Redis cache first
        try:
            from mirix.database.redis_client import get_redis_client
            from mirix.log import get_logger
            
            logger = get_logger(__name__)
            redis_client = get_redis_client()
            
            if redis_client:
                redis_key = f"{redis_client.USER_PREFIX}{user_id}"
                cached_data = redis_client.get_hash(redis_key)
                if cached_data:
                    logger.debug("✅ Redis cache HIT for user %s", user_id)
                    return PydanticUser(**cached_data)
        except Exception as e:
            # Log but continue to PostgreSQL on Redis error
            from mirix.log import get_logger
            logger = get_logger(__name__)
            logger.warning("Redis cache read failed for user %s: %s", user_id, e)
        
        # Cache MISS or Redis unavailable - fetch from PostgreSQL
        with self.session_maker() as session:
            user = UserModel.read(db_session=session, identifier=user_id)
            pydantic_user = user.to_pydantic()
            
            # Populate Redis cache for next time
            try:
                if redis_client:
                    from mirix.settings import settings
                    data = pydantic_user.model_dump(mode='json')
                    # model_dump(mode='json') already converts datetime to ISO format strings
                    redis_client.set_hash(redis_key, data, ttl=settings.redis_ttl_users)
                    logger.debug("Populated Redis cache for user %s", user_id)
            except Exception as e:
                logger.warning("Failed to populate Redis cache for user %s: %s", user_id, e)
            
            return pydantic_user

    @enforce_types
    def get_default_user(self) -> PydanticUser:
        """Fetch the default user."""
        return self.get_user_by_id(self.DEFAULT_USER_ID)

    @enforce_types
    def get_user_or_default(self, user_id: Optional[str] = None):
        """Fetch the user or default user."""
        if not user_id:
            return self.get_default_user()

        try:
            return self.get_user_by_id(user_id=user_id)
        except NoResultFound:
            return self.get_default_user()

    @enforce_types
    def list_users(
        self, cursor: Optional[str] = None, limit: Optional[int] = 50
    ) -> Tuple[Optional[str], List[PydanticUser]]:
        """List users with pagination using cursor (id) and limit."""
        with self.session_maker() as session:
            results = UserModel.list(db_session=session, cursor=cursor, limit=limit)
            return [user.to_pydantic() for user in results]
