import logging
import httpx
from datetime import datetime
from typing import Any, Dict, List, Optional
from urllib.parse import urlparse

from fastapi import HTTPException
from sqlalchemy import and_, func
from sqlalchemy.orm import Session

from core.config import settings

from ..licenses.models import License, LicenseStatus
from ..user_tenant.models import UserTenant

logger = logging.getLogger(__name__)


def _is_valid_http_url(url: Optional[str]) -> bool:
    if not url or not isinstance(url, str):
        return False
    parsed = urlparse(url.strip())
    return parsed.scheme in ("http", "https") and bool(parsed.netloc)


def _legacy_avatar_public_url(user_tenant: Optional[Any]) -> Optional[str]:
    if not user_tenant or not user_tenant.avatar_url:
        return None
    avatar_out = str(user_tenant.avatar_url)

    if avatar_out.startswith("http://") or avatar_out.startswith("https://"):
        return avatar_out if _is_valid_http_url(avatar_out) else None

    from core.s3_keys import public_user_avatar_api_path

    # Entregamos siempre el endpoint público del backend para assets locales/S3.
    return public_user_avatar_api_path(
        user_tenant.tenant_id,
        user_tenant.keycloak_user_id,
    )


def _normalize_user(
    user_data: Dict[str, Any],
    role: Optional[str] = None,
    user_tenant: Optional[Any] = None,
) -> Dict[str, Any]:
    """
    Normaliza los datos de usuario al formato esperado por el DTO.
    Prioridad para nombre/apellido: caché local (user_tenant) > JWT claims > campo 'name'.
    """
    name_parts = (user_data.get("name") or "").split(" ", 1)
    # Caché local tiene prioridad — se actualiza al guardar perfil desde Anexo76
    local_first = getattr(user_tenant, "first_name", None) if user_tenant else None
    local_last = getattr(user_tenant, "last_name", None) if user_tenant else None

    normalized = {
        "id": user_data.get("id") or user_data.get("sub"),
        "username": user_data.get("username") or user_data.get("preferred_username", ""),
        "email": user_data.get("email", ""),
        "first_name": local_first or user_data.get("firstName") or user_data.get("given_name") or (name_parts[0] if name_parts else ""),
        "last_name": local_last or user_data.get("lastName") or user_data.get("family_name") or (name_parts[1] if len(name_parts) > 1 else ""),
        "enabled": user_data.get("enabled", True),
        "email_verified": user_data.get("emailVerified") or user_data.get("email_verified", False),
        "created_timestamp": user_data.get("createdTimestamp"),
        "role": role,
    }

    # Agregar campos de perfil si user_tenant está disponible
    if user_tenant:
        workspace_avatar = (
            user_tenant.workspace_avatar_url
            if _is_valid_http_url(user_tenant.workspace_avatar_url)
            else None
        )
        legacy_avatar = _legacy_avatar_public_url(user_tenant)
        avatar_out = workspace_avatar or legacy_avatar

        normalized.update(
            {
                "avatar_url": avatar_out,
                "workspace_avatar_url": workspace_avatar,
                "legacy_avatar_url": legacy_avatar,
                "workspace_user_id": user_tenant.workspace_user_id,
                "phone": user_tenant.phone,
                "bio": user_tenant.bio,
                "preferences": user_tenant.preferences or {},
            }
        )

    return normalized


class UserService:
    """Servicio para gestionar usuarios vía Hub"""

    def __init__(self, db: Session, tenant_id: int = None, company_id: int = None, *, is_hub_admin: bool = False):
        self.db = db
        self.tenant_id = tenant_id
        self.company_id = company_id
        self.is_hub_admin = is_hub_admin

    def _get_license(self) -> License:
        """Obtiene la licencia del tenant actual"""
        license = (
            self.db.query(License).filter(License.tenant_id == self.tenant_id).first()
        )

        if not license:
            raise HTTPException(
                status_code=404, detail="License not found for this tenant"
            )

        if license.status != LicenseStatus.ACTIVE:
            raise HTTPException(
                status_code=403,
                detail=f"License is not active. Current status: {license.status.value}",
            )

        # Verificar si la licencia está vigente
        now = datetime.now(license.expires_at.tzinfo)
        if license.expires_at < now:
            raise HTTPException(status_code=403, detail="License has expired")

        return license

    def _check_user_limit(self) -> None:
        """Verifica si se puede crear un nuevo usuario según la licencia"""
        if self.is_hub_admin:
            return
        license = self._get_license()

        # Contar usuarios activos del tenant
        active_users = (
            self.db.query(func.count(UserTenant.id))
            .filter(
                and_(
                    UserTenant.tenant_id == self.tenant_id,
                    UserTenant.is_active == True,
                )
            )
            .scalar()
        )

        # max_users=NULL en BD indica licencia sin cuota (ilimitada).
        # Comparar con None lanzaría TypeError — salida temprana explícita.
        if license.max_users is None:
            return

        if active_users >= license.max_users:
            raise HTTPException(
                status_code=403,
                detail=f"User limit reached. Your license allows {license.max_users} users. "
                f"Currently active: {active_users}. Please upgrade your license.",
            )

    async def create_user(
        self,
        email: str,
        username: str,
        first_name: str,
        last_name: str,
        password: str,
        role: Optional[str] = None,
        enabled: bool = True,
        email_verified: bool = False,
    ) -> Dict[str, Any]:
        """
        Crea un nuevo usuario a través del Hub y lo asocia localmente
        """
        # Verificar límite de usuarios
        self._check_user_limit()

        try:
            # Mandar al Hub para creación en Keycloak
            async with httpx.AsyncClient(timeout=10.0) as client:
                hub_response = await client.post(
                    f"{settings.HUB_URL}api/v1/auth/register",
                    json={
                        "email": email,
                        "username": username,
                        "first_name": first_name,
                        "last_name": last_name,
                        "password": password,
                        "tenant_slug": "default", # TODO: Get real slug if needed
                    }
                )
            
            if hub_response.status_code != 201:
                logger.error(f"Hub registration failed: {hub_response.text}")
                raise HTTPException(status_code=hub_response.status_code, detail="Failed to create user in Hub")

            user_data = hub_response.json()
            user_id = user_data.get("user_id")

            # Obtener company_id — implementa con tu modelo de compañía si company_id es None.
            if not self.company_id:
                raise HTTPException(status_code=400, detail="company_id requerido")
            company_id = self.company_id

            # Crear relación local
            user_tenant = UserTenant(
                keycloak_user_id=user_id,
                tenant_id=self.tenant_id,
                company_id=company_id,
                role=role,
                is_active=True,
            )
            self.db.add(user_tenant)
            self.db.commit()

            return _normalize_user(user_data, role, user_tenant)

        except Exception as e:
            logger.error(f"Error creating user: {str(e)}")
            self.db.rollback()
            if isinstance(e, HTTPException):
                raise e
            raise HTTPException(status_code=500, detail=str(e))

    async def _fetch_hub_tenant_users_with_info(
        self,
        access_token: str,
        hub_tenant_id: int,
        x_tenant_override: Optional[str] = None,
    ) -> List[Dict[str, Any]]:
        """Lista usuarios del tenant desde Aduanasoft Hub (Keycloak + user_tenants)."""
        base = (settings.HUB_URL or "").rstrip("/")
        url = f"{base}/api/v1/hub/user-tenants/tenant/{hub_tenant_id}/users-with-info"
        headers: Dict[str, Any] = {"Authorization": f"Bearer {access_token}"}
        if x_tenant_override and str(x_tenant_override).strip():
            headers["X-Tenant-Override"] = str(x_tenant_override).strip()

        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.get(url, headers=headers)

        if response.status_code == 401:
            raise HTTPException(status_code=401, detail="No autorizado en el Hub")
        if response.status_code == 403:
            raise HTTPException(
                status_code=403, detail="Sin permiso para listar usuarios del tenant en el Hub"
            )
        if response.status_code >= 400:
            logger.error(
                "Hub users-with-info error status=%s body=%s",
                response.status_code,
                response.text[:500],
            )
            raise HTTPException(
                status_code=502,
                detail="No se pudo obtener el catálogo de usuarios desde el Hub",
            )

        data = response.json()
        if not isinstance(data, list):
            raise HTTPException(
                status_code=502, detail="Respuesta inválida del Hub al listar usuarios"
            )
        return data

    async def get_tenant_users(
        self,
        page: int = 1,
        page_size: int = 20,
        search: Optional[str] = None,
        *,
        access_token: str,
        hub_tenant_id: int,
        x_tenant_override: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Usuarios del tenant: fuente de verdad Aduanasoft Hub; roles de compañía y
        perfil extendido desde BD local (user_tenants / user_company_roles).
        """
        try:
            from ..permissions.models import UserCompanyRole
            from sqlalchemy.orm import joinedload

            if not access_token or not hub_tenant_id:
                raise HTTPException(
                    status_code=400,
                    detail="Token o tenant Hub requerido para listar usuarios",
                )

            hub_rows = await self._fetch_hub_tenant_users_with_info(
                access_token, hub_tenant_id, x_tenant_override
            )

            user_roles_query = (
                self.db.query(UserCompanyRole)
                .options(joinedload(UserCompanyRole.company_role))
                .filter(
                    and_(
                        UserCompanyRole.company_id == self.company_id,
                        UserCompanyRole.tenant_id == self.tenant_id,
                        UserCompanyRole.is_active == True,
                    )
                )
            )
            user_roles_map: Dict[str, List[str]] = {}
            for user_role in user_roles_query.all():
                uid = user_role.user_id
                if uid not in user_roles_map:
                    user_roles_map[uid] = []
                user_roles_map[uid].append(user_role.company_role.name)

            local_by_kc = {
                ut.keycloak_user_id: ut
                for ut in self.db.query(UserTenant)
                .filter(
                    and_(
                        UserTenant.tenant_id == self.tenant_id,
                        UserTenant.company_id == self.company_id,
                    )
                )
                .all()
            }

            needle = (search or "").strip().lower()
            filtered: List[Dict[str, Any]] = []
            for u in hub_rows:
                if not u.get("is_active", True):
                    continue
                kc = u.get("keycloak_user_id")
                if not kc:
                    continue
                # Filtrar usuarios soft-deleted localmente (is_active=False en user_tenants local)
                local_ut_check = local_by_kc.get(kc)
                if local_ut_check is not None and not local_ut_check.is_active:
                    continue
                if needle:
                    blob = " ".join(
                        [
                            str(u.get("email") or ""),
                            str(u.get("username") or ""),
                            str(u.get("first_name") or ""),
                            str(u.get("last_name") or ""),
                        ]
                    ).lower()
                    ut_loc = local_by_kc.get(kc)
                    if ut_loc:
                        blob += f" {ut_loc.phone or ''} {ut_loc.bio or ''}".lower()
                    if needle not in blob:
                        continue
                filtered.append(u)

            total = len(filtered)
            offset = (page - 1) * page_size
            page_rows = filtered[offset : offset + page_size]

            users: List[Dict[str, Any]] = []
            for u in page_rows:
                kc = u["keycloak_user_id"]
                role_names = user_roles_map.get(kc, [])
                role_str = ", ".join(role_names) if role_names else u.get("role")
                local_ut = local_by_kc.get(kc)
                normalized_user = _normalize_user(
                    {
                        "id": kc,
                        "username": u.get("username") or "",
                        "email": u.get("email") or "",
                        "firstName": u.get("first_name") or "",
                        "lastName": u.get("last_name") or "",
                        "enabled": u.get("is_active", True),
                        "emailVerified": False,
                    },
                    role_str,
                    local_ut,
                )
                users.append(normalized_user)

            total_pages = max(1, (total + page_size - 1) // page_size) if total else 1

            return {
                "users": users,
                "total": total,
                "page": page,
                "page_size": page_size,
                "total_pages": total_pages,
            }

        except HTTPException:
            raise
        except Exception as e:
            logger.error(f"Error getting tenant users: {str(e)}")
            raise HTTPException(
                status_code=500, detail=f"Error getting users: {str(e)}"
            ) from e

    async def get_user(self, user_id: str) -> Dict[str, Any]:
        """Obtiene un usuario específico"""
        from ..permissions.models import UserCompanyRole
        from sqlalchemy.orm import joinedload
        
        user_tenant = self.db.query(UserTenant).filter(
            and_(
                UserTenant.keycloak_user_id == user_id,
                UserTenant.tenant_id == self.tenant_id,
                UserTenant.is_active == True,
            )
        ).first()

        if not user_tenant:
            raise HTTPException(status_code=404, detail="User not found")

        # Roles locales
        user_roles = self.db.query(UserCompanyRole).options(joinedload(UserCompanyRole.company_role)).filter(
            and_(
                UserCompanyRole.user_id == user_id,
                UserCompanyRole.company_id == self.company_id,
                UserCompanyRole.tenant_id == self.tenant_id,
                UserCompanyRole.is_active == True
            )
        ).all()
        roles = [ur.company_role.name for ur in user_roles]
        role_str = ", ".join(roles) if roles else None

        # TODO: Call Hub if more info is needed
        return _normalize_user({"id": user_id}, role_str, user_tenant)

    async def update_user(self, user_id: str, **kwargs) -> Dict[str, Any]:
        """Actualiza información local del usuario (e identidad vía Hub si se implementa)"""
        user_tenant = self.db.query(UserTenant).filter(
            and_(UserTenant.keycloak_user_id == user_id, UserTenant.tenant_id == self.tenant_id)
        ).first()

        if not user_tenant:
            raise HTTPException(status_code=404, detail="User not found")

        # Actualizar campos locales
        for field in ["role", "avatar_url", "phone", "bio", "preferences"]:
            if field in kwargs and kwargs[field] is not None:
                setattr(user_tenant, field, kwargs[field])

        self.db.commit()
        self.db.refresh(user_tenant)
        return _normalize_user({"id": user_id}, user_tenant.role, user_tenant)

    def get_user_tenant_count(self, user_id: str) -> int:
        """Cuenta en cuántos tenants activos está registrado el usuario."""
        return (
            self.db.query(func.count(UserTenant.id))
            .filter(
                UserTenant.keycloak_user_id == user_id,
                UserTenant.is_active == True,
            )
            .scalar()
            or 0
        )

    async def delete_user(
        self,
        user_id: str,
        soft_delete: bool = True,
        scope: str = "current",
        access_token: Optional[str] = None,
        hub_tenant_id: Optional[int] = None,
    ) -> None:
        """
        Elimina/Desactiva usuario.
        scope='current': solo del tenant activo.
        scope='all': de todos los tenants (útil cuando el usuario pertenece a múltiples tenants).
        """
        if scope == "all":
            rows = (
                self.db.query(UserTenant)
                .filter(UserTenant.keycloak_user_id == user_id)
                .all()
            )
            if not rows:
                raise HTTPException(status_code=404, detail="User not found")
            now = datetime.utcnow()
            # Collect unique hub_tenant_ids to notify Hub for each tenant
            hub_tenant_ids = {row.tenant_id for row in rows}
            for row in rows:
                row.is_active = False
                if not soft_delete:
                    row.deleted_at = now
            self.db.commit()
            # Propagate to Hub for every tenant the user belonged to
            if access_token:
                for tid in hub_tenant_ids:
                    await self._hub_remove_user(user_id, tid, soft_delete, access_token)
            return

        # scope == "current" (default)
        user_tenant = self.db.query(UserTenant).filter(
            and_(
                UserTenant.keycloak_user_id == user_id,
                UserTenant.tenant_id == self.tenant_id,
                UserTenant.company_id == self.company_id,
            )
        ).first()

        if not user_tenant:
            # No local record — user exists in Hub but not synced locally yet.
            # Create tombstone so user is filtered from future listings.
            user_tenant = UserTenant(
                keycloak_user_id=user_id,
                tenant_id=self.tenant_id,
                company_id=self.company_id,
                is_active=False,
                deleted_at=None if soft_delete else datetime.utcnow(),
            )
            self.db.add(user_tenant)
            self.db.commit()
        else:
            user_tenant.is_active = False
            if not soft_delete:
                user_tenant.deleted_at = datetime.utcnow()
            self.db.commit()

        # Propagate to Hub
        if access_token and hub_tenant_id:
            await self._hub_remove_user(user_id, hub_tenant_id, soft_delete, access_token)

    async def _hub_remove_user(
        self,
        user_id: str,
        hub_tenant_id: int,
        soft_delete: bool,
        access_token: str,
    ) -> None:
        """Calls Hub POST /api/v1/hub/user-tenants/remove to sync the deletion."""
        base = (settings.HUB_URL or "").rstrip("/")
        url = f"{base}/api/v1/hub/user-tenants/remove"
        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                resp = await client.post(
                    url,
                    json={
                        "keycloak_user_id": user_id,
                        "tenant_id": hub_tenant_id,
                        "soft_delete": soft_delete,
                    },
                    headers={"Authorization": f"Bearer {access_token}"},
                )
            if resp.status_code >= 400:
                logger.warning(
                    "Hub remove user-tenant returned %s for user %s tenant %s: %s",
                    resp.status_code, user_id, hub_tenant_id, resp.text[:200],
                )
        except Exception as exc:
            logger.error("Error calling Hub remove user-tenant: %s", exc)
            # Do not raise — local deletion already committed; Hub sync is best-effort.

    async def change_password(self, user_id: str, password: str, temporary: bool = True) -> None:
        """Cambia contraseña vía Hub"""
        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                await client.post(
                    f"{settings.HUB_URL}api/v1/auth/change-password",
                    json={"user_id": user_id, "password": password, "temporary": temporary}
                )
        except Exception as e:
            logger.error(f"Error changing password: {e}")
            raise HTTPException(status_code=500, detail="Error changing password")

    def _count_active_user_tenants_local(self) -> int:
        return (
            self.db.query(func.count(UserTenant.id))
            .filter(
                and_(
                    UserTenant.tenant_id == self.tenant_id,
                    UserTenant.is_active == True,
                )
            )
            .scalar()
            or 0
        )

    def get_user_stats(
        self,
        access_token: Optional[str] = None,
        hub_tenant_id: Optional[int] = None,
        x_tenant_override: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Estadísticas de usuarios: cupo según licencia efectiva del Hub (verify-license)
        con ``X-Tenant-Override``; activos desde users-with-info del Hub si hay token;
        inactivos y fallback de conteos en BD local.
        """
        max_users_allowed: Optional[int] = None  # None = sin cuota (hub_admin ilimitado)
        hub_max_ok = False
        active_users = 0
        active_from_hub = False

        if access_token and hub_tenant_id:
            base = (settings.HUB_URL or "").rstrip("/")
            headers: Dict[str, Any] = {"Authorization": f"Bearer {access_token}"}
            if x_tenant_override and str(x_tenant_override).strip():
                headers["X-Tenant-Override"] = str(x_tenant_override).strip()
            try:
                with httpx.Client(timeout=30.0) as client:
                    lic_resp = client.get(
                        f"{base}/api/v1/auth/verify-license",
                        headers=headers,
                    )
                    if lic_resp.status_code == 200:
                        lic_body = lic_resp.json()
                        if lic_body.get("valid"):
                            raw_max = lic_body.get("max_users")
                            # max_users=null → hub_admin sin cuota; None indica ilimitado
                            max_users_allowed = int(raw_max) if raw_max is not None else None
                            hub_max_ok = True

                    users_resp = client.get(
                        f"{base}/api/v1/hub/user-tenants/tenant/{hub_tenant_id}/users-with-info",
                        headers=headers,
                    )
                    if users_resp.status_code == 200:
                        payload = users_resp.json()
                        if isinstance(payload, list):
                            active_users = sum(
                                1 for row in payload if row.get("is_active", True)
                            )
                            active_from_hub = True
                        else:
                            logger.warning(
                                "Hub users-with-info stats: respuesta no lista"
                            )
                    else:
                        logger.warning(
                            "Hub users-with-info stats status=%s",
                            users_resp.status_code,
                        )
            except Exception as e:
                logger.warning("Hub stats (verify-license / users-with-info): %s", e)

        if not hub_max_ok:
            license = self._get_license()
            max_users_allowed = license.max_users

        if not active_from_hub:
            active_users = self._count_active_user_tenants_local()

        inactive_users = (
            self.db.query(func.count(UserTenant.id))
            .filter(
                and_(
                    UserTenant.tenant_id == self.tenant_id,
                    UserTenant.is_active == False,
                )
            )
            .scalar()
            or 0
        )

        total_users = active_users + inactive_users
        # Cuando max_users_allowed es None la cuota es ilimitada (hub_admin)
        users_available = (
            max(0, max_users_allowed - active_users)
            if max_users_allowed is not None
            else None
        )
        usage_percentage = (
            (active_users / max_users_allowed * 100) if max_users_allowed else 0.0
        )

        return {
            "total_users": total_users,
            "active_users": active_users,
            "inactive_users": inactive_users,
            "max_users_allowed": max_users_allowed,
            "users_available": users_available,
            "usage_percentage": round(usage_percentage, 2),
        }

    async def get_current_user_profile(
        self,
        keycloak_user_id: str,
        current_user: Dict[str, Any] = None,
        access_token: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Obtiene el perfil completo del usuario actual"""
        # Use the already-verified JWT claims dict — do NOT call verify_token(uuid)
        user_info = current_user or {"id": keycloak_user_id}

        # Perfil "me": sincronización con cache corto (5 min).
        if access_token:
            from core.workspace_profile_sync import sync_workspace_profile_for_user

            await sync_workspace_profile_for_user(
                self.db,
                access_token=access_token,
                keycloak_user_id=keycloak_user_id,
                tenant_id=self.tenant_id,
            )

        user_tenant = self.db.query(UserTenant).filter(
            and_(UserTenant.keycloak_user_id == keycloak_user_id, UserTenant.is_active == True)
        ).first()

        return _normalize_user(user_info, user_tenant.role if user_tenant else None, user_tenant)

    async def update_current_user_profile(
        self,
        keycloak_user_id: str,
        current_user: Dict[str, Any] = None,
        access_token: Optional[str] = None,
        **kwargs,
    ) -> Dict[str, Any]:
        """
        Actualiza el perfil del usuario actual.
        - first_name / last_name: persiste localmente en UserTenant Y sincroniza con Keycloak vía Hub.
        - phone: persiste solo localmente.
        """
        user_tenant = self.db.query(UserTenant).filter(
            and_(UserTenant.keycloak_user_id == keycloak_user_id, UserTenant.tenant_id == self.tenant_id)
        ).first()

        if not user_tenant:
            raise HTTPException(status_code=404, detail="User not found")

        # Campos locales — incluye first_name/last_name como caché
        for field in ["role", "avatar_url", "phone", "bio", "preferences", "first_name", "last_name"]:
            if field in kwargs and kwargs[field] is not None:
                setattr(user_tenant, field, kwargs[field])

        self.db.commit()
        self.db.refresh(user_tenant)

        # Sincronizar nombre/apellido con Keycloak vía Hub (best-effort)
        first_name = kwargs.get("first_name")
        last_name = kwargs.get("last_name")
        if access_token and (first_name or last_name):
            await self._hub_update_user_profile(keycloak_user_id, first_name, last_name, access_token)

        user_info = current_user or {"id": keycloak_user_id}
        return _normalize_user(user_info, user_tenant.role, user_tenant)

    async def _hub_get_service_token(self) -> Optional[str]:
        """Obtiene un token de la cuenta de servicio Hub para operaciones admin."""
        if not settings.HUB_ADMIN_EMAIL or not settings.HUB_ADMIN_PASSWORD:
            return None
        base = (settings.HUB_URL or "").rstrip("/")
        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                resp = await client.post(
                    f"{base}/api/v1/auth/login",
                    json={"username": settings.HUB_ADMIN_EMAIL, "password": settings.HUB_ADMIN_PASSWORD},
                )
            if resp.status_code == 200:
                data = resp.json()
                return data.get("access_token")
            logger.warning("Hub service account login failed status=%s", resp.status_code)
        except Exception as exc:
            logger.warning("Hub service account login error: %s", exc)
        return None

    async def _hub_update_user_profile(
        self,
        user_id: str,
        first_name: Optional[str],
        last_name: Optional[str],
        access_token: str,
    ) -> None:
        """Sincroniza nombre/apellido con Keycloak a través del Hub (best-effort, no bloquea).

        Intenta primero con el token del usuario. Si el Hub devuelve 403 (el usuario no
        tiene rol de Hub-admin), reintenta usando la cuenta de servicio configurada en
        HUB_ADMIN_EMAIL / HUB_ADMIN_PASSWORD.
        """
        base = (settings.HUB_URL or "").rstrip("/")
        url = f"{base}/api/v1/hub/admins/{user_id}"
        payload: Dict[str, Any] = {}
        if first_name:
            payload["first_name"] = first_name
        if last_name:
            payload["last_name"] = last_name
        if not payload:
            return
        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                resp = await client.patch(
                    url,
                    json=payload,
                    headers={"Authorization": f"Bearer {access_token}"},
                )

            if resp.status_code == 403:
                # El usuario no es Hub admin — reintentar con cuenta de servicio
                logger.info("Hub profile sync: user token got 403, trying service account for user_id=%s", user_id)
                service_token = await self._hub_get_service_token()
                if service_token:
                    async with httpx.AsyncClient(timeout=10.0) as client:
                        resp = await client.patch(
                            url,
                            json=payload,
                            headers={"Authorization": f"Bearer {service_token}"},
                        )
                else:
                    logger.warning(
                        "Hub profile sync skipped: no service account configured (HUB_ADMIN_EMAIL/HUB_ADMIN_PASSWORD)"
                    )
                    return

            if resp.status_code not in (200, 204):
                logger.warning(
                    "Hub profile sync failed status=%s body=%s",
                    resp.status_code,
                    resp.text[:300],
                )
            else:
                logger.info("Hub profile sync OK user_id=%s", user_id)
        except Exception as exc:
            logger.warning("Hub profile sync error user_id=%s: %s", user_id, exc)

