import logging
import httpx
from typing import Any, Dict, Optional
from jose import JWTError, jwt

from core.config import settings
from fastapi import HTTPException
from sqlalchemy.orm import Session

from .dto import (
    LoginRequestDTO,
    LogoutRequestDTO,
    RefreshTokenRequestDTO,
    TokenResponseDTO,
    UserInfoResponseDTO,
)

logger = logging.getLogger(__name__)


class AuthService:
    """Servicio de autenticación centralizado vía Hub"""

    def __init__(self, db: Session):
        self.db = db

    @staticmethod
    def _clean_text(value: Any) -> Optional[str]:
        if isinstance(value, str):
            cleaned = value.strip()
            if cleaned:
                return cleaned
        return None

    def _pick_text(self, *candidates: Any) -> Optional[str]:
        for candidate in candidates:
            value = self._clean_text(candidate)
            if value:
                return value
        return None

    def _decode_kc_user_from_token(self, access_token: str) -> Dict[str, Any]:
        try:
            claims = jwt.get_unverified_claims(access_token)
            return claims if isinstance(claims, dict) else {}
        except JWTError:
            return {}
        except Exception:
            return {}

    async def _get_kc_admin_user(self, keycloak_user_id: Optional[str]) -> Optional[Dict[str, Any]]:
        """
        Fallback de datos de usuario consultando el Hub admin API.
        Es opcional y no debe romper /me si falla.
        """
        if not keycloak_user_id:
            return None
        if not settings.HUB_ADMIN_EMAIL or not settings.HUB_ADMIN_PASSWORD:
            return None

        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                login_resp = await client.post(
                    f"{settings.HUB_URL}api/v1/auth/login",
                    json={
                        "username": settings.HUB_ADMIN_EMAIL,
                        "password": settings.HUB_ADMIN_PASSWORD,
                    },
                )
                if login_resp.status_code != 200:
                    return None

                admin_token = login_resp.json().get("access_token")
                if not admin_token:
                    return None

                user_resp = await client.get(
                    f"{settings.HUB_URL}api/v1/hub/admins/{keycloak_user_id}",
                    headers={"Authorization": f"Bearer {admin_token}"},
                )
                if user_resp.status_code == 200:
                    payload = user_resp.json()
                    return payload if isinstance(payload, dict) else None
        except Exception as exc:
            logger.debug("kc_admin_user_lookup_failed: %s", exc)

        return None

    def _extract_avatar_url(self, *sources: Any) -> Optional[str]:
        for source in sources:
            if not isinstance(source, dict):
                continue

            direct = self._pick_text(
                source.get("avatar_url"),
                source.get("avatarUrl"),
                source.get("picture"),
                source.get("photo"),
            )
            if direct:
                return direct

            attrs = source.get("attributes")
            if isinstance(attrs, dict):
                attr_candidate = attrs.get("avatar_url")
                if isinstance(attr_candidate, list) and attr_candidate:
                    value = self._clean_text(attr_candidate[0])
                    if value:
                        return value
                if isinstance(attr_candidate, str):
                    value = self._clean_text(attr_candidate)
                    if value:
                        return value

        return None

    async def login(
        self, 
        login_data: LoginRequestDTO, 
        ip_address: str = None, 
        user_agent: str = None
    ):
        """
        Autentica usuario a través del Hub y obtiene tokens.
        """
        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.post(
                    f"{settings.HUB_URL}api/v1/auth/login",
                    json=login_data.model_dump()
                )

            if response.status_code == 200:
                data = response.json()
                
                # Si el Hub devolvió una lista de tenants (hubo login exitoso pero falta seleccionar tenant)
                if "tenants" in data:
                    from .dto import LoginChoiceResponseDTO, TenantInfoDTO
                    return LoginChoiceResponseDTO(
                        tenants=[TenantInfoDTO(**t) for t in data["tenants"]]
                    )

                # Si devolvió tokens — lazy-link: verificar si hay invite pendiente
                try:
                    await self._link_pending_invite(login_data.username)
                except Exception as exc:
                    logger.warning("Lazy-link invite check failed (non-blocking): %s", exc)

                # Sync de perfil/avatar desde Workspace usando el mismo bearer.
                # No bloquea login si Workspace no responde.
                access_token = data.get("access_token")
                if access_token:
                    from core.workspace_profile_sync import sync_workspace_profile_for_user
                    from core.workspace_profile_client import WorkspaceProfileClient

                    workspace_profile = None
                    try:
                        workspace_profile = await WorkspaceProfileClient().get_me(access_token)
                    except Exception as exc:
                        logger.warning(
                            "workspace_profile_sync_failed",
                            extra={
                                "event": "workspace_profile_sync_failed",
                                "phase": "login",
                                "error": str(exc),
                            },
                        )
                        workspace_profile = None

                    await sync_workspace_profile_for_user(
                        self.db,
                        access_token=access_token,
                        keycloak_user_id=(workspace_profile or {}).get("sub")
                        or data.get("sub")
                        or data.get("user_id"),
                        tenant_id=data.get("tenant_id"),
                        workspace_profile=workspace_profile,
                        force=True,
                    )

                # AUDIT LOG: Login Success
                try:
                    from api.v1.modules.a76.audit_log.services.service import AuditService
                    AuditService.log_login(
                        db=self.db,
                        username=login_data.username,
                        ip_address=ip_address,
                        user_agent=user_agent
                    )
                except Exception as e:
                    logger.error(f"Failed to audit login: {e}")

                return TokenResponseDTO(**data)
            
            # Pasar el mensaje de error real del Hub al cliente
            try:
                hub_detail = response.json().get("detail", None)
            except Exception:
                hub_detail = None

            if response.status_code == 401:
                raise HTTPException(status_code=401, detail=hub_detail or "Credenciales inválidas")

            logger.error(f"Hub login failed with status {response.status_code}: {response.text}")
            raise HTTPException(status_code=response.status_code, detail=hub_detail or "Error en el servidor de autenticación")

        except httpx.HTTPError as e:
            logger.error(f"Hub unreachable during login: {str(e)}")
            raise HTTPException(status_code=503, detail="Authentication service unavailable")
        except HTTPException:
            raise
        except Exception as e:
            logger.error(f"Unexpected login error: {str(e)}")
            raise HTTPException(status_code=500, detail="Authentication error")

    async def refresh_token(self, refresh_data: RefreshTokenRequestDTO) -> TokenResponseDTO:
        """
        Refresca el access token usando el Hub
        """
        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.post(
                    f"{settings.HUB_URL}api/v1/auth/refresh",
                    json=refresh_data.model_dump()
                )

            if response.status_code == 200:
                data = response.json()
                from core.workspace_profile_sync import sync_workspace_profile_for_user
                from core.workspace_profile_client import WorkspaceProfileClient

                workspace_profile = None
                try:
                    workspace_profile = await WorkspaceProfileClient().get_me(
                        data.get("access_token", "")
                    )
                except Exception as exc:
                    logger.warning(
                        "workspace_profile_sync_failed",
                        extra={
                            "event": "workspace_profile_sync_failed",
                            "phase": "refresh",
                            "error": str(exc),
                        },
                    )
                    workspace_profile = None

                await sync_workspace_profile_for_user(
                    self.db,
                    access_token=data.get("access_token"),
                    keycloak_user_id=(workspace_profile or {}).get("sub")
                    or data.get("sub")
                    or data.get("user_id"),
                    tenant_id=data.get("tenant_id"),
                    workspace_profile=workspace_profile,
                    force=True,
                )
                return TokenResponseDTO(**data)
            
            raise HTTPException(status_code=401, detail="Invalid or expired refresh token")

        except Exception as e:
            logger.error(f"Token refresh error: {str(e)}")
            raise HTTPException(status_code=500, detail="Token refresh error")

    async def get_user_info(self, access_token: str) -> UserInfoResponseDTO:
        """
        Obtiene información del usuario desde el Hub
        """
        from core.security import verify_token
        from core.workspace_profile_sync import sync_workspace_profile_for_user
        # Aprovechamos la verificación (y cache) de security.py
        user_info = await verify_token(access_token)

        kc_user = self._decode_kc_user_from_token(access_token)
        keycloak_user_id = self._pick_text(user_info.get("sub"), kc_user.get("sub"))

        needs_admin_fallback = any(
            not self._clean_text(user_info.get(field))
            for field in ("email", "preferred_username")
        ) or self._extract_avatar_url(user_info) is None

        kc_admin_user = None
        if needs_admin_fallback:
            kc_admin_user = await self._get_kc_admin_user(keycloak_user_id)

        first_name = self._pick_text(
            user_info.get("first_name"),
            user_info.get("given_name"),
            kc_user.get("given_name"),
            kc_user.get("first_name"),
            (kc_admin_user or {}).get("firstName"),
            (kc_admin_user or {}).get("first_name"),
        )
        last_name = self._pick_text(
            user_info.get("last_name"),
            user_info.get("family_name"),
            kc_user.get("family_name"),
            kc_user.get("last_name"),
            (kc_admin_user or {}).get("lastName"),
            (kc_admin_user or {}).get("last_name"),
        )
        full_name = self._pick_text(
            f"{first_name} {last_name}" if first_name and last_name else None,
            first_name,
            last_name,
        )

        enriched_user_info = dict(user_info)
        enriched_user_info["sub"] = keycloak_user_id or user_info.get("sub")
        enriched_user_info["email"] = self._pick_text(
            user_info.get("email"),
            (kc_admin_user or {}).get("email"),
            kc_user.get("email"),
        )
        enriched_user_info["preferred_username"] = self._pick_text(
            user_info.get("preferred_username"),
            user_info.get("username"),
            kc_user.get("preferred_username"),
            kc_user.get("username"),
            (kc_admin_user or {}).get("username"),
        )
        enriched_user_info["avatar_url"] = self._extract_avatar_url(
            user_info,
            kc_user,
            kc_admin_user or {},
        )
        enriched_user_info["name"] = self._pick_text(
            user_info.get("name"),
            full_name,
            kc_user.get("name"),
            enriched_user_info.get("preferred_username"),
        )

        await sync_workspace_profile_for_user(
            self.db,
            access_token=access_token,
            keycloak_user_id=enriched_user_info.get("sub"),
            tenant_id=enriched_user_info.get("tenant_id"),
            workspace_profile=enriched_user_info,
        )
        return UserInfoResponseDTO(**enriched_user_info)

    async def logout(self, logout_data: LogoutRequestDTO) -> dict:
        """
        Cierra sesión a través del Hub
        """
        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                await client.post(
                    f"{settings.HUB_URL}api/v1/auth/logout",
                    json=logout_data.model_dump()
                )
            return {"message": "Logged out successfully"}
        except Exception as e:
            logger.error(f"Logout error: {str(e)}")
            return {"message": "Logged out"}

    async def register(self, register_data: Any) -> Any:
        """
        Registra un usuario.
        - Si trae invite_token: valida el token local, crea usuario en Hub y
          genera la fila UserTenant local, luego consume el token.
        - Si no trae invite_token: reenvía directamente al Hub (flujo original).
        """
        if getattr(register_data, "invite_token", None):
            return await self._register_with_invite(register_data)

        # Flujo original — reenviar al Hub sin invite_token
        try:
            payload = register_data.model_dump(exclude={"invite_token"})
            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.post(
                    f"{settings.HUB_URL}api/v1/auth/register",
                    json=payload,
                )
            if response.status_code == 201:
                return response.json()
            raise HTTPException(status_code=response.status_code, detail=response.text)
        except HTTPException:
            raise
        except Exception as e:
            logger.error(f"Registration error: {str(e)}")
            raise HTTPException(status_code=500, detail="Registration error")

    async def _register_with_invite(self, register_data: Any) -> Any:
        """Flujo de registro con token de invitación local."""
        from api.v1.modules.core.invites.service import InviteService
        from api.v1.modules.core.tenants.models import Tenant
        from api.v1.modules.core.user_tenant.models import UserTenant

        invite_service = InviteService(self.db)

        # 1. Validar invite token (sin consumir)
        invite_result = invite_service.validate(
            register_data.invite_token,
            register_data.tenant_slug,
            str(register_data.email),
        )

        # 2. Buscar tenant local
        tenant = (
            self.db.query(Tenant)
            .filter(Tenant.slug == register_data.tenant_slug)
            .first()
        )
        if not tenant:
            raise HTTPException(status_code=404, detail="Tenant no encontrado")

        # 3. Obtener token de service account y gestionar usuario en Hub
        hub_user_id = None
        try:
            async with httpx.AsyncClient(timeout=15.0) as client:
                # Login con service account
                login_resp = await client.post(
                    f"{settings.HUB_URL}api/v1/auth/login",
                    json={
                        "username": settings.HUB_ADMIN_EMAIL,
                        "password": settings.HUB_ADMIN_PASSWORD,
                    },
                )
                if login_resp.status_code != 200:
                    raise HTTPException(status_code=503, detail="No se pudo autenticar con el sistema de autenticación")
                svc_token = login_resp.json().get("access_token", "")

                # Verificar si el usuario ya existe en el Hub
                search_resp = await client.get(
                    f"{settings.HUB_URL}api/v1/hub/admins",
                    params={"email": str(register_data.email)},
                    headers={"Authorization": f"Bearer {svc_token}"},
                )
                existing_user = None
                if search_resp.status_code == 200:
                    admins = search_resp.json()
                    items = admins if isinstance(admins, list) else admins.get("items", [])
                    matches = [a for a in items if a.get("email", "").lower() == str(register_data.email).lower()]
                    if matches:
                        existing_user = matches[0]

                if existing_user:
                    # Usuario ya existe — solo vinculamos (no creamos nuevo)
                    hub_user_id = existing_user.get("id")
                else:
                    # Crear usuario via admin endpoint (no requiere invite_token)
                    hub_payload = {
                        "username": register_data.username,
                        "email": str(register_data.email),
                        "password": register_data.password,
                        "first_name": register_data.first_name,
                        "last_name": register_data.last_name,
                        "tenant_slug": register_data.tenant_slug,
                    }
                    create_resp = await client.post(
                        f"{settings.HUB_URL}api/v1/hub/admins",
                        json=hub_payload,
                        headers={"Authorization": f"Bearer {svc_token}"},
                    )
                    if create_resp.status_code in (200, 201):
                        hub_user_id = create_resp.json().get("id")
                    else:
                        try:
                            detail = create_resp.json().get("detail", create_resp.text)
                        except Exception:
                            detail = create_resp.text
                        raise HTTPException(status_code=create_resp.status_code, detail=detail)

        except HTTPException:
            raise
        except Exception as exc:
            logger.error("Hub admin create error during invite flow: %s", exc)
            raise HTTPException(status_code=503, detail="Error al crear usuario en el sistema de autenticación")

        # 4. Crear fila UserTenant local
        if hub_user_id and invite_result.company_id:
            try:
                ut = UserTenant(
                    keycloak_user_id=hub_user_id,
                    tenant_id=tenant.id,
                    company_id=invite_result.company_id,
                    role=invite_result.role,
                    is_active=True,
                    first_name=register_data.first_name,
                    last_name=register_data.last_name,
                )
                self.db.add(ut)
                self.db.commit()
            except Exception as exc:
                logger.warning("Could not create UserTenant (may already exist): %s", exc)
                self.db.rollback()

        # 5. Consumir invite token
        invite_service.consume_by_id(invite_result.invite_id)

        return {
            "user_id": hub_user_id or "",
            "username": register_data.username,
            "email": str(register_data.email),
            "message": "Usuario registrado exitosamente",
        }

    async def exchange_code(self, exchange_data: Any) -> TokenResponseDTO:
        """
        Intercambia código por tokens a través del Hub
        """
        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.post(
                    f"{settings.HUB_URL}api/v1/auth/exchange-code",
                    json=exchange_data.model_dump()
                )
            if response.status_code == 200:
                return TokenResponseDTO(**response.json())
            raise HTTPException(status_code=response.status_code, detail="Code exchange failed")
        except Exception as e:
            logger.error(f"Exchange code error: {str(e)}")
            raise HTTPException(status_code=500, detail="Exchange code error")

    async def switch_tenant(self, **kwargs) -> TokenResponseDTO:
        """
        Cambia de tenant a través del Hub
        """
        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.post(
                    f"{settings.HUB_URL}api/v1/auth/switch-tenant",
                    json=kwargs
                )
            if response.status_code == 200:
                return TokenResponseDTO(**response.json())
            raise HTTPException(status_code=response.status_code, detail="Switch tenant failed")
        except Exception as e:
            logger.error(f"Switch tenant error: {str(e)}")
            raise HTTPException(status_code=500, detail="Switch tenant error")

    async def sso_exchange(self, relay_token: str) -> TokenResponseDTO:
        """
        Canjea un relay token de un solo uso por KC tokens.
        Llama al Hub backend (server-to-server), sin Bearer requerido en el Hub.
        """
        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.post(
                    f"{settings.HUB_URL}api/v1/auth/sso-exchange",
                    json={"relay_token": relay_token},
                )
            if response.status_code == 200:
                data = response.json()
                return TokenResponseDTO(
                    access_token=data["access_token"],
                    refresh_token=data["refresh_token"],
                    token_type=data.get("token_type", "bearer"),
                    expires_in=data.get("expires_in", 3600),
                    tenant_id=data.get("tenant_id"),
                    tenant_slug=data.get("tenant_slug"),
                )
            raise HTTPException(
                status_code=response.status_code,
                detail=response.json().get("detail", "SSO exchange failed"),
            )
        except HTTPException:
            raise
        except Exception as e:
            logger.error(f"SSO exchange error: {str(e)}")
            raise HTTPException(status_code=500, detail="SSO exchange error")

    async def _link_pending_invite(self, username_or_email: str, access_token: str = None) -> None:
        """
        Lazy-link: después de un login exitoso comprueba si existe un invite_token
        pendiente para el email del usuario. Si lo hay, crea la fila UserTenant
        y consume el token.

        Si se provee access_token, extrae hub_user_id y email directamente del JWT
        sin necesidad de un lookup extra al Hub.
        """
        from datetime import datetime, timezone
        from api.v1.modules.core.invites.models import InviteToken
        from api.v1.modules.core.tenants.models import Tenant
        from api.v1.modules.core.user_tenant.models import UserTenant

        hub_user_id = None
        user_email = username_or_email

        # Si tenemos el access_token, extraer info del JWT directamente
        if access_token:
            try:
                from core.security import verify_token
                claims = await verify_token(access_token)
                hub_user_id = claims.get("sub")
                user_email = claims.get("email") or username_or_email
            except Exception as exc:
                logger.debug("_link_pending_invite: JWT decode failed: %s", exc)

        # Sin access_token: buscar usuario en el Hub vía service account
        if not hub_user_id:
            try:
                async with httpx.AsyncClient(timeout=10.0) as client:
                    login_resp = await client.post(
                        f"{settings.HUB_URL}api/v1/auth/login",
                        json={"username": settings.HUB_ADMIN_EMAIL, "password": settings.HUB_ADMIN_PASSWORD},
                    )
                    if login_resp.status_code != 200:
                        return
                    svc_token = login_resp.json().get("access_token", "")
                    search_resp = await client.get(
                        f"{settings.HUB_URL}api/v1/hub/admins",
                        params={"email": username_or_email},
                        headers={"Authorization": f"Bearer {svc_token}"},
                    )
                    if search_resp.status_code == 200:
                        items = search_resp.json()
                        items = items if isinstance(items, list) else items.get("items", [])
                        matches = [
                            u for u in items
                            if u.get("email", "").lower() == username_or_email.lower()
                            or u.get("username", "").lower() == username_or_email.lower()
                        ]
                        if matches:
                            hub_user_id = matches[0].get("id")
                            user_email = matches[0].get("email", username_or_email)
                    if not hub_user_id:
                        return
            except Exception as exc:
                logger.debug("_link_pending_invite: hub lookup failed: %s", exc)
                return

        now = datetime.now(timezone.utc)
        pending = (
            self.db.query(InviteToken)
            .filter(
                InviteToken.email == user_email,
                InviteToken.used_at.is_(None),
                InviteToken.expires_at > now,
            )
            .first()
        )
        if not pending:
            return

        tenant = (
            self.db.query(Tenant)
            .filter(Tenant.slug == pending.tenant_slug)
            .first()
        )
        if not tenant:
            logger.warning("_link_pending_invite: tenant %s not found", pending.tenant_slug)
            return

        # Evitar duplicados
        existing = (
            self.db.query(UserTenant)
            .filter(
                UserTenant.keycloak_user_id == hub_user_id,
                UserTenant.tenant_id == tenant.id,
            )
            .first()
        )
        if existing:
            # Vincular existe, solo consumir el token
            pending.used_at = now
            self.db.commit()
            return

        try:
            ut = UserTenant(
                keycloak_user_id=hub_user_id,
                tenant_id=tenant.id,
                company_id=pending.company_id,
                role=pending.role,
                is_active=True,
            )
            self.db.add(ut)
            pending.used_at = now
            self.db.commit()
            logger.info(
                "Lazy-link: UserTenant created for user=%s tenant=%s role=%s",
                hub_user_id,
                tenant.slug,
                pending.role,
            )
        except Exception as exc:
            logger.warning("_link_pending_invite: could not create UserTenant: %s", exc)
            self.db.rollback()

