from typing import Optional
from uuid import UUID
from datetime import datetime, timezone

from sqlalchemy import select, func, and_, delete
from sqlalchemy.ext.asyncio import AsyncSession

from src.apps.auth.models.user import User
from src.apps.auth.models.refresh_token import RefreshToken
from src.core.exceptions import ConflictError, NotFoundError, ForbiddenError, AppException
from src.core.security import hash_password, generate_temp_password
from src.core.constants import UserRole, UserStatus


class UserService:
    def __init__(self, db: AsyncSession):
        self.db = db

    async def list_users(
        self,
        tenant_id: UUID,
        skip: int = 0,
        limit: int = 50,
        search: Optional[str] = None,
        current_user_id: Optional[UUID] = None,
        role_min: Optional[str] = None,
        active_only: Optional[bool] = None,
    ) -> tuple[list, int]:
        from src.core.constants import ROLE_HIERARCHY
        conditions = [User.tenant_id == tenant_id, User.deleted_at.is_(None)]
        if search:
            conditions.append(
                (User.email.ilike(f"%{search}%"))
                | (User.first_name.ilike(f"%{search}%"))
                | (User.last_name.ilike(f"%{search}%"))
            )
        if active_only is True:
            conditions.append(User.status == UserStatus.ACTIVE.value)
        if role_min:
            # Include all roles with level >= role_min level
            min_level = ROLE_HIERARCHY.get(UserRole(role_min), 0)
            eligible_roles = [r.value for r, lvl in ROLE_HIERARCHY.items() if lvl >= min_level]
            conditions.append(User.role.in_(eligible_roles))
        count_q = await self.db.execute(select(func.count(User.id)).where(and_(*conditions)))
        total = count_q.scalar_one()
        result = await self.db.execute(
            select(User).where(and_(*conditions)).order_by(User.created_at.asc()).offset(skip).limit(limit)
        )
        users = list(result.scalars().all())
        if current_user_id is not None:
            for u in users:
                u._is_current_user = (u.id == current_user_id)
        return users, total

    async def get_by_id(self, user_id: UUID, tenant_id: UUID) -> Optional[User]:
        result = await self.db.execute(
            select(User).where(
                and_(User.id == user_id, User.tenant_id == tenant_id, User.deleted_at.is_(None))
            )
        )
        return result.scalar_one_or_none()

    async def invite_user(self, tenant_id: UUID, data: dict) -> User:
        email = data["email"].lower()
        existing = await self.db.execute(
            select(User).where(
                and_(
                    User.email == email,
                    User.tenant_id == tenant_id,
                    User.deleted_at.is_(None),
                )
            )
        )
        if existing.scalar_one_or_none():
            raise ConflictError("A user with this email already exists in your team.")

        # Split name into first/last
        name = data.get("name", "").strip()
        parts = name.split(" ", 1)
        first_name = parts[0] if parts else name
        last_name = parts[1] if len(parts) > 1 else ""

        user = User(
            tenant_id=tenant_id,
            email=email,
            password_hash=hash_password(generate_temp_password()),
            first_name=first_name,
            last_name=last_name,
            role=data.get("role", UserRole.STAFF.value),
            status=UserStatus.INVITED.value,
            invited_at=datetime.now(timezone.utc),
        )
        self.db.add(user)
        await self.db.flush()
        return user

    async def update_role(
        self,
        user_id: UUID,
        tenant_id: UUID,
        role: str,
        current_user_id: UUID,
    ) -> User:
        if user_id == current_user_id:
            raise ForbiddenError("You cannot change your own role.")
        user = await self.get_by_id(user_id, tenant_id)
        if not user:
            raise NotFoundError("User not found")
        user.role = role
        await self.db.flush()
        return user

    async def resend_invitation(self, user_id: UUID, tenant_id: UUID) -> User:
        user = await self.get_by_id(user_id, tenant_id)
        if not user:
            raise NotFoundError("User not found")
        if user.status != UserStatus.INVITED.value:
            raise AppException("Cannot resend invitation — user is already active.", status_code=400)
        user.invited_at = datetime.now(timezone.utc)
        await self.db.flush()
        return user

    async def cancel_invitation(self, user_id: UUID, tenant_id: UUID) -> None:
        user = await self.get_by_id(user_id, tenant_id)
        if not user:
            raise NotFoundError("User not found")
        if user.status != UserStatus.INVITED.value:
            raise AppException("Cannot cancel invitation — user is already active.", status_code=400)
        await self.db.execute(
            delete(RefreshToken).where(RefreshToken.user_id == user_id)
        )
        await self.db.delete(user)
        await self.db.flush()

    async def remove_user(
        self,
        user_id: UUID,
        tenant_id: UUID,
        current_user_id: UUID,
    ) -> None:
        if user_id == current_user_id:
            raise ForbiddenError("You cannot remove yourself.")
        user = await self.get_by_id(user_id, tenant_id)
        if not user:
            raise NotFoundError("User not found")
        now = datetime.now(timezone.utc)
        # Revoke all active refresh tokens
        result = await self.db.execute(
            select(RefreshToken).where(
                RefreshToken.user_id == user_id,
                RefreshToken.revoked_at.is_(None),
            )
        )
        tokens = result.scalars().all()
        for token in tokens:
            token.revoked_at = now
        user.deleted_at = now
        await self.db.flush()

    async def update_user(self, user_id: UUID, tenant_id: UUID, data: dict) -> User:
        user = await self.get_by_id(user_id, tenant_id)
        if not user:
            raise NotFoundError("User not found")
        for field, value in data.items():
            if value is not None and hasattr(user, field):
                setattr(user, field, value)
        await self.db.flush()
        return user
