"""کلاینت Admin API کیکلوک برای مدیریت کاربران."""
from __future__ import annotations

from typing import Any

import httpx

from app.core.config import settings
from app.core.logging import get_logger
from shared.exceptions import UnauthorizedError

logger = get_logger(__name__)


def mobile_keycloak_password(mobile: str) -> str:
    """رمز داخلی Keycloak برای کاربران ثبت‌نام با موبایل (فقط سمت سرور)."""
    return f"Ty!{mobile[-6:]}X"


class KeycloakAdminClient:
    async def _get_admin_token(self) -> str:
        url = f"{settings.keycloak_server_url}/realms/master/protocol/openid-connect/token"
        data = {
            "grant_type": "password",
            "client_id": "admin-cli",
            "username": settings.keycloak_admin_user,
            "password": settings.keycloak_admin_password,
        }
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.post(url, data=data)
            resp.raise_for_status()
            return resp.json()["access_token"]

    async def get_service_account_token(self) -> str:
        """توکن client_credentials برای سرویس identity-access."""
        data = {
            "grant_type": "client_credentials",
            "client_id": settings.keycloak_client_id,
            "client_secret": settings.keycloak_client_secret,
        }
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.post(settings.oidc_token_url, data=data)
            if resp.status_code >= 400:
                raise UnauthorizedError("دریافت توکن سرویس از Keycloak ناموفق بود")
            return resp.json()["access_token"]

    async def exchange_token_for_user(self, keycloak_sub: str) -> dict[str, Any]:
        """صدور access token کاربر پس از تأیید OTP (token exchange)."""
        service_token = await self.get_service_account_token()
        data = {
            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
            "client_id": settings.keycloak_client_id,
            "client_secret": settings.keycloak_client_secret,
            "subject_token": service_token,
            "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
            "requested_subject": keycloak_sub,
            "audience": settings.keycloak_frontend_client_id,
            "scope": "openid profile email",
        }
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.post(settings.oidc_token_url, data=data)
            if resp.status_code >= 400:
                detail = resp.text
                try:
                    payload = resp.json()
                    detail = payload.get("error_description") or payload.get("error") or detail
                except Exception:
                    pass
                logger.warning(
                    "keycloak_token_exchange_failed",
                    extra={"detail": detail, "sub": keycloak_sub},
                )
                raise UnauthorizedError(f"صدور توکن SSO ناموفق بود: {detail}")
            return resp.json()

    async def ensure_user_login_ready(self, keycloak_sub: str) -> None:
        """حذف required actions و تکمیل فیلدهای اجباری پروفایل Keycloak."""
        token = await self._get_admin_token()
        headers = {"Authorization": f"Bearer {token}"}
        url = f"{settings.keycloak_admin_url}/users/{keycloak_sub}"
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.get(url, headers=headers)
            if resp.status_code >= 400:
                return
            user = resp.json()
            user["requiredActions"] = []
            user["emailVerified"] = True
            user["enabled"] = True
            if not user.get("firstName"):
                user["firstName"] = user.get("username") or "User"
            if not user.get("lastName"):
                user["lastName"] = "-"
            put = await client.put(url, json=user, headers=headers)
            if put.status_code >= 400:
                logger.warning(
                    "keycloak_user_setup_failed",
                    extra={"sub": keycloak_sub, "status": put.status_code},
                )

    async def update_user(
        self,
        keycloak_sub: str,
        *,
        email: str | None = None,
        first_name: str | None = None,
    ) -> None:
        """به‌روزرسانی فیلدهای پروفایل کاربر در Keycloak (ایمیل/نام)."""
        if email is None and first_name is None:
            return
        token = await self._get_admin_token()
        headers = {"Authorization": f"Bearer {token}"}
        url = f"{settings.keycloak_admin_url}/users/{keycloak_sub}"
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.get(url, headers=headers)
            if resp.status_code >= 400:
                raise UnauthorizedError("دریافت اطلاعات کاربر از Keycloak ناموفق بود")
            user = resp.json()
            if email is not None:
                user["email"] = email
                user["emailVerified"] = True
            if first_name is not None:
                user["firstName"] = first_name or user.get("username") or "User"
            put = await client.put(url, json=user, headers=headers)
            if put.status_code >= 400:
                detail = put.text
                try:
                    detail = put.json().get("errorMessage") or detail
                except Exception:
                    pass
                raise UnauthorizedError(
                    f"به‌روزرسانی پروفایل Keycloak ناموفق بود: {detail}"
                )

    async def set_user_password(
        self, keycloak_sub: str, password: str, *, temporary: bool = False
    ) -> None:
        """تنظیم رمز کاربر از Admin API (پس از تأیید OTP)."""
        token = await self._get_admin_token()
        url = f"{settings.keycloak_admin_url}/users/{keycloak_sub}/reset-password"
        payload = {"type": "password", "value": password, "temporary": temporary}
        headers = {"Authorization": f"Bearer {token}"}
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.put(url, json=payload, headers=headers)
            if resp.status_code not in (204, 200):
                detail = resp.text
                try:
                    detail = resp.json().get("errorMessage") or detail
                except Exception:
                    pass
                raise UnauthorizedError(f"تنظیم رمز Keycloak ناموفق بود: {detail}")

    async def password_login(self, username: str, password: str) -> dict[str, Any]:
        """ورود مستقیم (فقط از BFF، با کلاینت محرمانه identity-access)."""
        data = {
            "grant_type": "password",
            "client_id": settings.keycloak_client_id,
            "client_secret": settings.keycloak_client_secret,
            "username": username,
            "password": password,
            "scope": "openid profile email",
        }
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.post(settings.oidc_token_url, data=data)
            if resp.status_code >= 400:
                detail = resp.text
                try:
                    payload = resp.json()
                    detail = payload.get("error_description") or payload.get("error") or detail
                except Exception:
                    pass
                raise UnauthorizedError(f"ورود Keycloak ناموفق بود: {detail}")
            return resp.json()

    async def issue_tokens_for_user(
        self,
        *,
        keycloak_sub: str,
        username: str,
        mobile: str | None = None,
        password: str | None = None,
        sync_mobile_password: bool = False,
    ) -> dict[str, Any]:
        """پس از تأیید هویت سمت سرور، توکن SSO کاربر را صادر می‌کند."""
        errors: list[str] = []

        if password:
            try:
                return await self.password_login(username, password)
            except UnauthorizedError as exc:
                errors.append(str(exc))

        try:
            return await self.exchange_token_for_user(keycloak_sub)
        except UnauthorizedError as exc:
            errors.append(str(exc))

        if mobile:
            internal = mobile_keycloak_password(mobile)
            candidates = []
            for candidate in (username, mobile):
                if candidate and candidate not in candidates:
                    candidates.append(candidate)

            await self.ensure_user_login_ready(keycloak_sub)

            for candidate in candidates:
                try:
                    return await self.password_login(candidate, internal)
                except UnauthorizedError as exc:
                    errors.append(str(exc))

            if sync_mobile_password:
                await self.set_user_password(keycloak_sub, internal)
                await self.ensure_user_login_ready(keycloak_sub)
                for candidate in candidates:
                    try:
                        return await self.password_login(candidate, internal)
                    except UnauthorizedError as exc:
                        errors.append(str(exc))

        detail = errors[-1] if errors else "خطای ناشناخته"
        if "Unsupported grant_type" in detail or "unsupported_grant_type" in detail:
            detail += " — Keycloak را با --features=token-exchange راه‌اندازی و scripts/enable_mobile_auth_client.py را اجرا کنید"
        elif "direct access grants" in detail.lower() or "unauthorized_client" in detail.lower():
            detail += " — scripts/enable_mobile_auth_client.py را اجرا کنید"
        raise UnauthorizedError(f"صدور توکن SSO ناموفق بود: {detail}")

    async def assign_realm_role(self, keycloak_sub: str, role_name: str) -> None:
        """اختصاص نقش realm به کاربر (مثلاً platform_admin)."""
        token = await self._get_admin_token()
        headers = {"Authorization": f"Bearer {token}"}
        roles_url = f"{settings.keycloak_admin_url}/roles"
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.get(roles_url, params={"search": role_name}, headers=headers)
            resp.raise_for_status()
            roles = [r for r in resp.json() if r.get("name") == role_name]
            if not roles:
                logger.warning("keycloak_role_not_found", extra={"role": role_name})
                return
            mapping_url = (
                f"{settings.keycloak_admin_url}/users/{keycloak_sub}/role-mappings/realm"
            )
            map_resp = await client.post(mapping_url, json=[roles[0]], headers=headers)
            if map_resp.status_code not in (204, 200):
                logger.warning(
                    "keycloak_role_assign_failed",
                    extra={"role": role_name, "status": map_resp.status_code},
                )

    async def create_user(
        self,
        *,
        email: str,
        username: str,
        password: str,
        display_name: str | None,
        mobile: str | None = None,
    ) -> str:
        """ساخت کاربر در Keycloak و برگرداندن sub (شناسه)."""
        token = await self._get_admin_token()
        url = f"{settings.keycloak_admin_url}/users"
        payload: dict[str, Any] = {
            "email": email,
            "username": username,
            "enabled": True,
            "emailVerified": True,
            "requiredActions": [],
            "credentials": [{"type": "password", "value": password, "temporary": False}],
        }
        if display_name:
            payload["firstName"] = display_name
            payload["lastName"] = "-"
        else:
            payload["firstName"] = username
            payload["lastName"] = "-"
        if mobile:
            payload["attributes"] = {"mobile": [mobile], "mobile_verified": ["false"]}
        headers = {"Authorization": f"Bearer {token}"}
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.post(url, json=payload, headers=headers)
            if resp.status_code == 409:
                return await self._find_user_sub(username, token)
            resp.raise_for_status()
            location = resp.headers.get("Location", "")
            return location.rsplit("/", 1)[-1]

    async def _find_user_sub(self, username: str, token: str) -> str:
        url = f"{settings.keycloak_admin_url}/users"
        headers = {"Authorization": f"Bearer {token}"}
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.get(url, params={"username": username}, headers=headers)
            resp.raise_for_status()
            users = resp.json()
            if not users:
                raise ValueError(f"کاربر یافت نشد: {username}")
            return users[0]["id"]

    async def exchange_code(
        self, code: str, redirect_uri: str, code_verifier: str | None = None
    ) -> dict[str, Any]:
        """تبدیل authorization code به token (BFF برای frontend)."""
        data = {
            "grant_type": "authorization_code",
            "client_id": settings.keycloak_frontend_client_id,
            "code": code,
            "redirect_uri": redirect_uri,
        }
        if code_verifier:
            data["code_verifier"] = code_verifier
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.post(settings.oidc_token_url, data=data)
            if resp.status_code >= 400:
                detail = resp.text
                try:
                    payload = resp.json()
                    detail = payload.get("error_description") or payload.get("error") or detail
                except Exception:
                    pass
                logger.warning("keycloak_token_exchange_failed", extra={"detail": detail})
                raise UnauthorizedError(f"تبادل token با Keycloak ناموفق بود: {detail}")
            return resp.json()

    async def refresh_tokens(self, refresh_token: str) -> dict[str, Any]:
        """تمدید access token با refresh_token (BFF برای frontend)."""
        data = {
            "grant_type": "refresh_token",
            "client_id": settings.keycloak_frontend_client_id,
            "refresh_token": refresh_token,
        }
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.post(settings.oidc_token_url, data=data)
            if resp.status_code >= 400:
                detail = resp.text
                try:
                    payload = resp.json()
                    detail = payload.get("error_description") or payload.get("error") or detail
                except Exception:
                    pass
                logger.warning("keycloak_token_refresh_failed", extra={"detail": detail})
                raise UnauthorizedError(f"تمدید نشست ناموفق بود: {detail}")
            return resp.json()
