#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
OAuth Service - OAuth 业务逻辑层
处理第三方 OAuth 登录逻辑（异步版本）
"""
import json
import logging
import re
from typing import Dict, Optional, Tuple

import httpx
from sqlalchemy.ext.asyncio import AsyncSession

from app.config import settings
from core.oauth.base_oauth_service import BaseOAuthService
from core.user.model import User

logger = logging.getLogger(__name__)


class GiteeOAuthService(BaseOAuthService):
    """Gitee OAuth 服务类"""

    PROVIDER_NAME = 'gitee'
    AUTHORIZE_URL = "https://gitee.com/oauth/authorize"
    TOKEN_URL = "https://gitee.com/oauth/token"
    USER_INFO_URL = "https://gitee.com/api/v5/user"

    @classmethod
    def get_client_config(cls) -> Dict[str, str]:
        """获取 Gitee 客户端配置（同步回退）"""
        return {
            'client_id': getattr(settings, 'GITEE_CLIENT_ID', ''),
            'client_secret': getattr(settings, 'GITEE_CLIENT_SECRET', ''),
            'redirect_uri': getattr(settings, 'GITEE_REDIRECT_URI', ''),
        }

    @classmethod
    async def get_client_config_async(cls) -> Dict[str, str]:
        """从 config_manager 三级配置获取 Gitee OAuth 配置"""
        from app.config_manager import config_manager
        oauth_config = await config_manager.get_group("oauth_gitee")
        return {
            'client_id': oauth_config.get("client_id") or getattr(settings, 'GITEE_CLIENT_ID', '') or '',
            'client_secret': oauth_config.get("client_secret") or getattr(settings, 'GITEE_CLIENT_SECRET', '') or '',
            'redirect_uri': oauth_config.get("redirect_uri") or getattr(settings, 'GITEE_REDIRECT_URI', '') or '',
        }

    @classmethod
    async def get_user_info(cls, access_token: str) -> Optional[Dict]:
        """
        使用访问令牌获取 Gitee 用户信息
        """
        try:
            params = {'access_token': access_token}
            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.get(cls.USER_INFO_URL, params=params)
                response.raise_for_status()

                user_info = response.json()

                if 'id' not in user_info:
                    logger.error(f"Gitee 用户信息格式错误: {user_info}")
                    return None

                return user_info

        except httpx.RequestError as e:
            logger.error(f"请求 Gitee 用户信息失败: {str(e)}")
            return None
        except Exception as e:
            logger.error(f"获取 Gitee 用户信息异常: {str(e)}")
            return None

    @classmethod
    def normalize_user_info(cls, raw_user_info: Dict) -> Dict:
        """标准化 Gitee 用户信息"""
        return {
            'provider_id': str(raw_user_info.get('id')),
            'username': raw_user_info.get('login'),
            'name': raw_user_info.get('name', raw_user_info.get('login')),
            'email': raw_user_info.get('email'),
            'avatar': raw_user_info.get('avatar_url'),
            'bio': raw_user_info.get('bio'),
        }


class GitHubOAuthService(BaseOAuthService):
    """GitHub OAuth 服务类"""

    PROVIDER_NAME = 'github'
    AUTHORIZE_URL = "https://github.com/login/oauth/authorize"
    TOKEN_URL = "https://github.com/login/oauth/access_token"
    USER_INFO_URL = "https://api.github.com/user"

    @classmethod
    def get_client_config(cls) -> Dict[str, str]:
        """获取 GitHub 客户端配置（同步回退）"""
        return {
            'client_id': getattr(settings, 'GITHUB_CLIENT_ID', ''),
            'client_secret': getattr(settings, 'GITHUB_CLIENT_SECRET', ''),
            'redirect_uri': getattr(settings, 'GITHUB_REDIRECT_URI', ''),
        }

    @classmethod
    async def get_client_config_async(cls) -> Dict[str, str]:
        """从 config_manager 三级配置获取 GitHub OAuth 配置"""
        from app.config_manager import config_manager
        oauth_config = await config_manager.get_group("oauth_github")
        return {
            'client_id': oauth_config.get("client_id") or getattr(settings, 'GITHUB_CLIENT_ID', '') or '',
            'client_secret': oauth_config.get("client_secret") or getattr(settings, 'GITHUB_CLIENT_SECRET', '') or '',
            'redirect_uri': oauth_config.get("redirect_uri") or getattr(settings, 'GITHUB_REDIRECT_URI', '') or '',
        }

    @classmethod
    def get_extra_authorize_params(cls) -> Dict[str, str]:
        """GitHub 需要 scope 参数"""
        return {
            'scope': 'user:email',
        }

    @classmethod
    def get_token_request_headers(cls) -> Dict[str, str]:
        """GitHub 需要 Accept header 来获取 JSON 响应"""
        return {
            'Accept': 'application/json',
        }

    @classmethod
    async def get_user_info(cls, access_token: str) -> Optional[Dict]:
        """使用访问令牌获取 GitHub 用户信息"""
        try:
            headers = {
                'Authorization': f'Bearer {access_token}',
                'Accept': 'application/json',
            }
            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.get(cls.USER_INFO_URL, headers=headers)
                response.raise_for_status()

                user_info = response.json()

                if 'id' not in user_info:
                    logger.error(f"GitHub 用户信息格式错误: {user_info}")
                    return None

                return user_info

        except httpx.RequestError as e:
            logger.error(f"请求 GitHub 用户信息失败: {str(e)}")
            return None
        except Exception as e:
            logger.error(f"获取 GitHub 用户信息异常: {str(e)}")
            return None

    @classmethod
    def normalize_user_info(cls, raw_user_info: Dict) -> Dict:
        """标准化 GitHub 用户信息"""
        return {
            'provider_id': str(raw_user_info.get('id')),
            'username': raw_user_info.get('login'),
            'name': raw_user_info.get('name') or raw_user_info.get('login'),
            'email': raw_user_info.get('email'),
            'avatar': raw_user_info.get('avatar_url'),
            'bio': raw_user_info.get('bio'),
        }


class QQOAuthService(BaseOAuthService):
    """QQ 互联 OAuth 服务类"""

    PROVIDER_NAME = 'qq'
    AUTHORIZE_URL = "https://graph.qq.com/oauth2.0/authorize"
    TOKEN_URL = "https://graph.qq.com/oauth2.0/token"
    USER_INFO_URL = "https://graph.qq.com/user/get_user_info"
    OPENID_URL = "https://graph.qq.com/oauth2.0/me"

    @classmethod
    def get_client_config(cls) -> Dict[str, str]:
        """获取 QQ 客户端配置（同步回退）"""
        return {
            'client_id': getattr(settings, 'QQ_APP_ID', ''),
            'client_secret': getattr(settings, 'QQ_APP_KEY', ''),
            'redirect_uri': getattr(settings, 'QQ_REDIRECT_URI', ''),
        }

    @classmethod
    async def get_client_config_async(cls) -> Dict[str, str]:
        """从 config_manager 三级配置获取 QQ OAuth 配置"""
        from app.config_manager import config_manager
        oauth_config = await config_manager.get_group("oauth_qq")
        return {
            'client_id': oauth_config.get("app_id") or getattr(settings, 'QQ_APP_ID', '') or '',
            'client_secret': oauth_config.get("app_key") or getattr(settings, 'QQ_APP_KEY', '') or '',
            'redirect_uri': oauth_config.get("redirect_uri") or getattr(settings, 'QQ_REDIRECT_URI', '') or '',
        }

    @classmethod
    def get_extra_authorize_params(cls) -> Dict[str, str]:
        """QQ 需要 response_type 参数"""
        return {
            'response_type': 'code',
        }

    @classmethod
    async def get_access_token(cls, code: str, redirect_uri: Optional[str] = None) -> Optional[str]:
        """使用授权码获取访问令牌（QQ 返回 URL 参数格式）"""
        try:
            config = await cls.get_client_config_async()

            params = {
                'grant_type': 'authorization_code',
                'client_id': config['client_id'],
                'client_secret': config['client_secret'],
                'code': code,
                'redirect_uri': redirect_uri or config['redirect_uri'],
            }

            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.get(cls.TOKEN_URL, params=params)
                response.raise_for_status()

                # QQ 返回的是 URL 参数格式: access_token=xxx&expires_in=xxx
                response_text = response.text

                match = re.search(r'access_token=([^&]+)', response_text)
                if match:
                    access_token = match.group(1)
                    logger.info(f"QQ access_token 获取成功")
                    return access_token
                else:
                    logger.error(f"QQ access_token 解析失败: {response_text}")
                    return None

        except httpx.RequestError as e:
            logger.error(f"请求 QQ access_token 失败: {str(e)}")
            return None
        except Exception as e:
            logger.error(f"获取 QQ access_token 异常: {str(e)}")
            return None

    @classmethod
    async def get_user_info(cls, access_token: str) -> Optional[Dict]:
        """使用访问令牌获取 QQ 用户信息"""
        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                # 1. 获取 openid
                openid_response = await client.get(
                    cls.OPENID_URL,
                    params={'access_token': access_token}
                )
                openid_response.raise_for_status()

                # QQ 返回的是 JSONP 格式: callback( {"client_id":"xxx","openid":"xxx"} );
                openid_text = openid_response.text

                match = re.search(r'callback\(\s*(\{.*?\})\s*\)', openid_text)
                if not match:
                    logger.error(f"QQ openid 解析失败: {openid_text}")
                    return None

                openid_data = json.loads(match.group(1))
                openid = openid_data.get('openid')

                if not openid:
                    logger.error(f"QQ openid 不存在: {openid_data}")
                    return None

                logger.info(f"QQ openid 获取成功: {openid}")

                # 2. 获取用户信息
                config = await cls.get_client_config_async()
                user_response = await client.get(
                    cls.USER_INFO_URL,
                    params={
                        'access_token': access_token,
                        'oauth_consumer_key': config['client_id'],
                        'openid': openid
                    }
                )
                user_response.raise_for_status()

                user_info = user_response.json()

                if user_info.get('ret') != 0:
                    logger.error(f"QQ 用户信息获取失败: {user_info.get('msg')}")
                    return None

                user_info['openid'] = openid
                return user_info

        except httpx.RequestError as e:
            logger.error(f"请求 QQ 用户信息失败: {str(e)}")
            return None
        except Exception as e:
            logger.error(f"获取 QQ 用户信息异常: {str(e)}")
            return None

    @classmethod
    def normalize_user_info(cls, raw_user_info: Dict) -> Dict:
        """标准化 QQ 用户信息"""
        return {
            'provider_id': raw_user_info.get('openid'),
            'username': raw_user_info.get('nickname', '').replace(' ', '_'),
            'name': raw_user_info.get('nickname'),
            'email': None,
            'avatar': raw_user_info.get('figureurl_qq_2') or raw_user_info.get('figureurl_qq_1'),
            'bio': None,
        }


class GoogleOAuthService(BaseOAuthService):
    """Google OAuth 服务类"""

    PROVIDER_NAME = 'google'
    AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth"
    TOKEN_URL = "https://oauth2.googleapis.com/token"
    USER_INFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo"

    @classmethod
    def get_client_config(cls) -> Dict[str, str]:
        """获取 Google 客户端配置（同步回退）"""
        return {
            'client_id': getattr(settings, 'GOOGLE_CLIENT_ID', ''),
            'client_secret': getattr(settings, 'GOOGLE_CLIENT_SECRET', ''),
            'redirect_uri': getattr(settings, 'GOOGLE_REDIRECT_URI', ''),
        }

    @classmethod
    async def get_client_config_async(cls) -> Dict[str, str]:
        """从 config_manager 三级配置获取 Google OAuth 配置"""
        from app.config_manager import config_manager
        oauth_config = await config_manager.get_group("oauth_google")
        return {
            'client_id': oauth_config.get("client_id") or getattr(settings, 'GOOGLE_CLIENT_ID', '') or '',
            'client_secret': oauth_config.get("client_secret") or getattr(settings, 'GOOGLE_CLIENT_SECRET', '') or '',
            'redirect_uri': oauth_config.get("redirect_uri") or getattr(settings, 'GOOGLE_REDIRECT_URI', '') or '',
        }

    @classmethod
    def get_extra_authorize_params(cls) -> Dict[str, str]:
        """Google 需要 scope 和 access_type 参数"""
        return {
            'scope': 'openid email profile',
            'access_type': 'offline',
            'response_type': 'code',
        }

    @classmethod
    async def get_user_info(cls, access_token: str) -> Optional[Dict]:
        """使用访问令牌获取 Google 用户信息"""
        try:
            headers = {
                'Authorization': f'Bearer {access_token}',
            }
            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.get(cls.USER_INFO_URL, headers=headers)
                response.raise_for_status()

                user_info = response.json()

                if 'id' not in user_info:
                    logger.error(f"Google 用户信息格式错误: {user_info}")
                    return None

                return user_info

        except httpx.RequestError as e:
            logger.error(f"请求 Google 用户信息失败: {str(e)}")
            return None
        except Exception as e:
            logger.error(f"获取 Google 用户信息异常: {str(e)}")
            return None

    @classmethod
    def normalize_user_info(cls, raw_user_info: Dict) -> Dict:
        """标准化 Google 用户信息"""
        return {
            'provider_id': raw_user_info.get('id'),
            'username': raw_user_info.get('email', '').split('@')[0],
            'name': raw_user_info.get('name') or raw_user_info.get('email'),
            'email': raw_user_info.get('email'),
            'avatar': raw_user_info.get('picture'),
            'bio': None,
        }


class WeChatOAuthService(BaseOAuthService):
    """微信开放平台 OAuth 服务类"""

    PROVIDER_NAME = 'wechat'
    AUTHORIZE_URL = "https://open.weixin.qq.com/connect/qrconnect"
    TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token"
    USER_INFO_URL = "https://api.weixin.qq.com/sns/userinfo"

    @classmethod
    def get_user_id_field(cls) -> str:
        """微信使用 unionid 作为唯一标识"""
        return 'wechat_unionid'

    @classmethod
    def get_client_config(cls) -> Dict[str, str]:
        """获取微信客户端配置（同步回退）"""
        return {
            'client_id': getattr(settings, 'WECHAT_APP_ID', ''),
            'client_secret': getattr(settings, 'WECHAT_APP_SECRET', ''),
            'redirect_uri': getattr(settings, 'WECHAT_REDIRECT_URI', ''),
        }

    @classmethod
    async def get_client_config_async(cls) -> Dict[str, str]:
        """从 config_manager 三级配置获取微信 OAuth 配置"""
        from app.config_manager import config_manager
        oauth_config = await config_manager.get_group("oauth_wechat")
        return {
            'client_id': oauth_config.get("app_id") or getattr(settings, 'WECHAT_APP_ID', '') or '',
            'client_secret': oauth_config.get("app_secret") or getattr(settings, 'WECHAT_APP_SECRET', '') or '',
            'redirect_uri': oauth_config.get("redirect_uri") or getattr(settings, 'WECHAT_REDIRECT_URI', '') or '',
        }

    @classmethod
    async def get_authorize_url(cls, state: str = None, redirect_uri: str = None) -> str:
        """获取微信授权 URL（微信参数名称与标准 OAuth 2.0 不同）"""
        config = await cls.get_client_config_async()

        params = {
            'appid': config['client_id'],
            'redirect_uri': redirect_uri or config['redirect_uri'],
            'response_type': 'code',
            'scope': 'snsapi_login',
        }

        if state:
            params['state'] = state

        query_string = '&'.join([f"{k}={v}" for k, v in params.items()])
        return f"{cls.AUTHORIZE_URL}?{query_string}#wechat_redirect"

    @classmethod
    async def get_access_token(cls, code: str) -> Optional[Dict]:
        """使用授权码获取访问令牌（微信返回 access_token 和 openid）"""
        try:
            config = await cls.get_client_config_async()
            params = {
                'appid': config['client_id'],
                'secret': config['client_secret'],
                'code': code,
                'grant_type': 'authorization_code',
            }

            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.get(cls.TOKEN_URL, params=params)
                response.raise_for_status()

                token_data = response.json()

                if 'errcode' in token_data:
                    logger.error(f"微信获取 token 失败: {token_data}")
                    return None

                if 'access_token' not in token_data or 'openid' not in token_data:
                    logger.error(f"微信 token 响应格式错误: {token_data}")
                    return None

                return token_data

        except httpx.RequestError as e:
            logger.error(f"请求微信 token 失败: {str(e)}")
            return None
        except Exception as e:
            logger.error(f"获取微信 token 异常: {str(e)}")
            return None

    @classmethod
    async def get_user_info(cls, access_token: str, openid: str = None) -> Optional[Dict]:
        """使用访问令牌获取微信用户信息"""
        try:
            params = {
                'access_token': access_token,
                'openid': openid,
                'lang': 'zh_CN',
            }

            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.get(cls.USER_INFO_URL, params=params)
                response.raise_for_status()

                user_info = response.json()

                if 'errcode' in user_info:
                    logger.error(f"微信获取用户信息失败: {user_info}")
                    return None

                if 'openid' not in user_info:
                    logger.error(f"微信用户信息格式错误: {user_info}")
                    return None

                return user_info

        except httpx.RequestError as e:
            logger.error(f"请求微信用户信息失败: {str(e)}")
            return None
        except Exception as e:
            logger.error(f"获取微信用户信息异常: {str(e)}")
            return None

    @classmethod
    def normalize_user_info(cls, raw_user_info: Dict) -> Dict:
        """标准化微信用户信息"""
        provider_id = raw_user_info.get('unionid') or raw_user_info.get('openid')
        nickname = raw_user_info.get('nickname', '')
        username = nickname.replace(' ', '_')[:30] if nickname else f"wechat_{provider_id[:8]}"

        return {
            'provider_id': provider_id,
            'username': username,
            'name': nickname or username,
            'email': None,
            'avatar': raw_user_info.get('headimgurl'),
            'bio': None,
        }

    @classmethod
    async def handle_oauth_login(
            cls,
            db: AsyncSession,
            code: str,
            ip_address: str,
            user_agent: str = None,
            login_type: str = None,
            device_id: str = None,
            redirect_uri: str = None,
    ) -> Tuple[User, str, str, int]:
        """
        处理微信 OAuth 登录流程（重写基类方法）
        
        微信的 get_access_token 返回 Dict（含 access_token 和 openid），
        且 get_user_info 需要同时传递 access_token 和 openid，
        因此需要重写此方法以正确处理。
        """
        # 1. 使用 code 换取 token_data（Dict，含 access_token + openid）
        token_data = await cls.get_access_token(code)
        if not token_data:
            raise ValueError(f"获取 {cls.PROVIDER_NAME} 访问令牌失败")

        access_token = token_data.get('access_token')
        openid = token_data.get('openid')

        if not access_token or not openid:
            raise ValueError(f"获取 {cls.PROVIDER_NAME} 访问令牌格式错误")

        # 2. 使用 access_token + openid 获取用户信息
        raw_user_info = await cls.get_user_info(access_token, openid=openid)
        if not raw_user_info:
            raise ValueError(f"获取 {cls.PROVIDER_NAME} 用户信息失败")

        # 3. 后续流程与基类一致，调用基类的通用逻辑
        #    先标准化用户信息，然后手动执行基类 handle_oauth_login 中第222行之后的逻辑
        #    为避免代码重复，将 access_token(str) 设置到类属性后调用基类
        #    但基类的 handle_oauth_login 会重新调用 get_access_token，所以这里直接复用基类的用户处理逻辑
        return await cls._handle_user_login(
            db=db,
            raw_user_info=raw_user_info,
            ip_address=ip_address,
            user_agent=user_agent,
            login_type=login_type,
            device_id=device_id,
        )

    @classmethod
    async def _handle_user_login(
            cls,
            db: AsyncSession,
            raw_user_info: Dict,
            ip_address: str,
            user_agent: str = None,
            login_type: str = None,
            device_id: str = None,
    ) -> Tuple[User, str, str, int]:
        """微信专用：从已获取的用户信息开始执行登录流程（复用基类逻辑）"""
        from datetime import timedelta
        from sqlalchemy import select
        from utils.redis import RedisClient
        from core.login_log.service import LoginLogService
        from utils.security import create_access_token, create_refresh_token
        from utils.user_info_cache import set_cached_user_info
        from core.user.service import UserService
        from core.oauth.base_oauth_service import REFRESH_TOKEN_PREFIX

        # 标准化用户信息
        user_info = cls.normalize_user_info(raw_user_info)
        provider_id = user_info['provider_id']
        username = user_info['username']
        name = user_info['name']
        email = user_info.get('email')
        avatar = user_info.get('avatar')
        bio = user_info.get('bio')

        # 查找或创建用户
        user_id_field = cls.get_user_id_field()
        stmt = select(User).where(
            getattr(User, user_id_field) == provider_id,
            User.is_deleted == False
        )
        result = await db.execute(stmt)
        user = result.scalar_one_or_none()

        is_superadmin = getattr(settings, 'GRANT_ADMIN_TO_OAUTH_USER', False)
        default_dept_id = getattr(settings, 'OAUTH_DEFAULT_DEPT_ID', None)

        if user:
            logger.info(f"{cls.PROVIDER_NAME} 用户已存在: {username} (ID: {provider_id})")
            if email and not user.email:
                user.email = email
            if bio and not user.bio:
                user.bio = bio
            db.add(user)
            await db.flush()
        else:
            logger.info(f"创建新的 {cls.PROVIDER_NAME} 用户: {username} (ID: {provider_id})")
            unique_username = username
            counter = 1
            while True:
                stmt = select(User).where(User.username == unique_username)
                result = await db.execute(stmt)
                if result.scalar_one_or_none() is None:
                    break
                unique_username = f"{username}_{counter}"
                counter += 1

            create_kwargs = {
                'username': unique_username,
                'name': name,
                'email': email,
                'bio': bio,
                user_id_field: provider_id,
                'oauth_provider': cls.PROVIDER_NAME,
                'user_type': 1,
                'user_status': 1,
                'is_active': True,
                'is_superuser': is_superadmin,
                'dept_id': default_dept_id,
            }
            user = User(**create_kwargs)
            db.add(user)
            await db.flush()
            await db.refresh(user)
            logger.info(f"{cls.PROVIDER_NAME} 用户创建成功: {unique_username}")

        if login_type:
            user.last_login_type = login_type
            db.add(user)
            await db.flush()

        await db.commit()
        await db.refresh(user)

        if not user.is_active:
            raise ValueError("账户已被禁用")
        if user.user_status == 0:
            raise ValueError("账户已被禁用")
        if user.user_status == 2:
            raise ValueError("账户已被锁定，请联系管理员")

        # 生成 JWT token
        access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
        refresh_token_expires = timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
        token_data = {"sub": user.id, "username": user.username}
        jwt_access_token = create_access_token(token_data, access_token_expires, device_id=device_id)
        jwt_refresh_token = create_refresh_token(token_data, refresh_token_expires, device_id=device_id)

        # 缓存用户信息到 Redis
        role_ids = await UserService.get_user_role_ids(db, user.id)
        await set_cached_user_info(user.id, role_ids, user.dept_id, user.is_superuser)
        expire_time = settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60

        redis = await RedisClient.get_client()

        if not settings.ALLOW_MULTI_DEVICE_LOGIN:
            refresh_pattern = f"{REFRESH_TOKEN_PREFIX}{user.id}:*"
            access_pattern = f"access_token:{user.id}:*"
            cursor = 0
            while True:
                cursor, keys = await redis.scan(cursor, match=refresh_pattern, count=100)
                if keys:
                    await redis.delete(*keys)
                if cursor == 0:
                    break
            cursor = 0
            while True:
                cursor, keys = await redis.scan(cursor, match=access_pattern, count=100)
                if keys:
                    await redis.delete(*keys)
                if cursor == 0:
                    break

        redis_key = f"{REFRESH_TOKEN_PREFIX}{user.id}:{device_id}" if device_id else f"{REFRESH_TOKEN_PREFIX}{user.id}"
        await redis.set(redis_key, jwt_refresh_token, ex=int(refresh_token_expires.total_seconds()))

        if device_id:
            await redis.set(
                f"access_token:{user.id}:{device_id}",
                jwt_access_token,
                ex=int(access_token_expires.total_seconds())
            )

        await LoginLogService.record_login(
            db=db,
            username=user.username,
            user_id=str(user.id),
            status=1,
            login_ip=ip_address,
            user_agent=user_agent,
            login_type=login_type or cls.PROVIDER_NAME,
        )

        return user, jwt_access_token, jwt_refresh_token, expire_time


class MicrosoftOAuthService(BaseOAuthService):
    """微软 OAuth 服务类"""

    PROVIDER_NAME = 'microsoft'
    AUTHORIZE_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
    TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
    USER_INFO_URL = "https://graph.microsoft.com/v1.0/me"

    @classmethod
    def get_client_config(cls) -> Dict[str, str]:
        """获取微软客户端配置（同步回退）"""
        return {
            'client_id': getattr(settings, 'MICROSOFT_CLIENT_ID', ''),
            'client_secret': getattr(settings, 'MICROSOFT_CLIENT_SECRET', ''),
            'redirect_uri': getattr(settings, 'MICROSOFT_REDIRECT_URI', ''),
        }

    @classmethod
    async def get_client_config_async(cls) -> Dict[str, str]:
        """从 config_manager 三级配置获取微软 OAuth 配置"""
        from app.config_manager import config_manager
        oauth_config = await config_manager.get_group("oauth_microsoft")
        return {
            'client_id': oauth_config.get("client_id") or getattr(settings, 'MICROSOFT_CLIENT_ID', '') or '',
            'client_secret': oauth_config.get("client_secret") or getattr(settings, 'MICROSOFT_CLIENT_SECRET', '') or '',
            'redirect_uri': oauth_config.get("redirect_uri") or getattr(settings, 'MICROSOFT_REDIRECT_URI', '') or '',
        }

    @classmethod
    def get_extra_authorize_params(cls) -> Dict[str, str]:
        """微软需要 scope 和 response_mode 参数"""
        return {
            'scope': 'openid email profile User.Read',
            'response_type': 'code',
            'response_mode': 'query',
        }

    @classmethod
    async def get_user_info(cls, access_token: str) -> Optional[Dict]:
        """使用 Microsoft Graph API 获取用户信息"""
        try:
            headers = {
                'Authorization': f'Bearer {access_token}',
            }
            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.get(cls.USER_INFO_URL, headers=headers)
                response.raise_for_status()

                user_info = response.json()

                if 'id' not in user_info:
                    logger.error(f"Microsoft 用户信息格式错误: {user_info}")
                    return None

                return user_info

        except httpx.RequestError as e:
            logger.error(f"请求 Microsoft 用户信息失败: {str(e)}")
            return None
        except Exception as e:
            logger.error(f"获取 Microsoft 用户信息异常: {str(e)}")
            return None

    @classmethod
    def normalize_user_info(cls, raw_user_info: Dict) -> Dict:
        """标准化微软用户信息"""
        user_principal_name = raw_user_info.get('userPrincipalName', '')
        username = user_principal_name.split('@')[0] if '@' in user_principal_name else user_principal_name
        email = raw_user_info.get('mail') or raw_user_info.get('userPrincipalName')

        return {
            'provider_id': raw_user_info.get('id'),
            'username': username or f"ms_{raw_user_info.get('id', '')[:8]}",
            'name': raw_user_info.get('displayName') or username,
            'email': email,
            'avatar': None,
            'bio': raw_user_info.get('jobTitle'),
        }


class DingTalkOAuthService(BaseOAuthService):
    """钉钉 OAuth 服务类"""

    PROVIDER_NAME = 'dingtalk'
    AUTHORIZE_URL = "https://login.dingtalk.com/oauth2/auth"
    TOKEN_URL = "https://api.dingtalk.com/v1.0/oauth2/userAccessToken"
    USER_INFO_URL = "https://api.dingtalk.com/v1.0/contact/users/me"

    @classmethod
    def get_client_config(cls) -> Dict[str, str]:
        """获取钉钉客户端配置（同步回退）"""
        return {
            'client_id': getattr(settings, 'DINGTALK_APP_ID', ''),
            'client_secret': getattr(settings, 'DINGTALK_APP_SECRET', ''),
            'redirect_uri': getattr(settings, 'DINGTALK_REDIRECT_URI', ''),
        }

    @classmethod
    async def get_client_config_async(cls) -> Dict[str, str]:
        """从 config_manager 三级配置获取钉钉 OAuth 配置"""
        from app.config_manager import config_manager
        oauth_config = await config_manager.get_group("oauth_dingtalk")
        return {
            'client_id': oauth_config.get("app_id") or getattr(settings, 'DINGTALK_APP_ID', '') or '',
            'client_secret': oauth_config.get("app_secret") or getattr(settings, 'DINGTALK_APP_SECRET', '') or '',
            'redirect_uri': oauth_config.get("redirect_uri") or getattr(settings, 'DINGTALK_REDIRECT_URI', '') or '',
        }

    @classmethod
    def get_extra_authorize_params(cls) -> Dict[str, str]:
        """钉钉需要的额外授权参数"""
        return {
            'response_type': 'code',
            'scope': 'openid',
            'prompt': 'consent',
        }

    @classmethod
    async def get_access_token(cls, code: str) -> Optional[str]:
        """使用授权码获取访问令牌（钉钉使用 JSON body）"""
        try:
            config = await cls.get_client_config_async()

            data = {
                'clientId': config['client_id'],
                'clientSecret': config['client_secret'],
                'code': code,
                'grantType': 'authorization_code',
            }

            headers = {
                'Content-Type': 'application/json',
            }

            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.post(cls.TOKEN_URL, json=data, headers=headers)
                response.raise_for_status()

                result = response.json()

                if 'accessToken' in result:
                    logger.info(f"钉钉 access_token 获取成功")
                    return result['accessToken']
                else:
                    logger.error(f"钉钉 token 响应格式错误: {result}")
                    return None

        except httpx.RequestError as e:
            logger.error(f"请求钉钉 access_token 失败: {str(e)}")
            return None
        except Exception as e:
            logger.error(f"获取钉钉 access_token 异常: {str(e)}")
            return None

    @classmethod
    async def get_user_info(cls, access_token: str) -> Optional[Dict]:
        """使用访问令牌获取钉钉用户信息"""
        try:
            headers = {
                'x-acs-dingtalk-access-token': access_token,
                'Content-Type': 'application/json',
            }

            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.get(cls.USER_INFO_URL, headers=headers)
                response.raise_for_status()

                user_info = response.json()

                if 'unionId' not in user_info:
                    logger.error(f"钉钉用户信息格式错误: {user_info}")
                    return None

                return user_info

        except httpx.RequestError as e:
            logger.error(f"请求钉钉用户信息失败: {str(e)}")
            return None
        except Exception as e:
            logger.error(f"获取钉钉用户信息异常: {str(e)}")
            return None

    @classmethod
    def normalize_user_info(cls, raw_user_info: Dict) -> Dict:
        """标准化钉钉用户信息"""
        provider_id = raw_user_info.get('unionId', '')
        nick = raw_user_info.get('nick', '')
        username = nick if nick else f"dingtalk_{provider_id[:8]}"

        return {
            'provider_id': provider_id,
            'username': username,
            'name': nick or username,
            'email': raw_user_info.get('email'),
            'avatar': raw_user_info.get('avatarUrl'),
            'mobile': raw_user_info.get('mobile'),
            'bio': f"钉钉用户 - {nick}" if nick else "钉钉用户",
        }

    @classmethod
    def get_user_id_field(cls) -> str:
        """获取用户 ID 字段名"""
        return 'dingtalk_unionid'



class FeishuOAuthService(BaseOAuthService):
    """飞书 OAuth 服务类"""

    PROVIDER_NAME = 'feishu'
    AUTHORIZE_URL = "https://open.feishu.cn/open-apis/authen/v1/authorize"
    TOKEN_URL = "https://open.feishu.cn/open-apis/authen/v1/oidc/access_token"
    USER_INFO_URL = "https://open.feishu.cn/open-apis/authen/v1/user_info"

    @classmethod
    def get_client_config(cls) -> Dict[str, str]:
        """获取飞书客户端配置（同步回退）"""
        return {
            'client_id': getattr(settings, 'FEISHU_APP_ID', ''),
            'client_secret': getattr(settings, 'FEISHU_APP_SECRET', ''),
            'redirect_uri': getattr(settings, 'FEISHU_REDIRECT_URI', ''),
        }

    @classmethod
    async def get_client_config_async(cls) -> Dict[str, str]:
        """从 config_manager 三级配置获取飞书 OAuth 配置"""
        from app.config_manager import config_manager
        oauth_config = await config_manager.get_group("oauth_feishu")
        return {
            'client_id': oauth_config.get("app_id") or getattr(settings, 'FEISHU_APP_ID', '') or '',
            'client_secret': oauth_config.get("app_secret") or getattr(settings, 'FEISHU_APP_SECRET', '') or '',
            'redirect_uri': oauth_config.get("redirect_uri") or getattr(settings, 'FEISHU_REDIRECT_URI', '') or '',
        }

    @classmethod
    def get_extra_authorize_params(cls) -> Dict[str, str]:
        """飞书需要的额外授权参数"""
        return {
            'response_type': 'code',
            'scope': 'contact:user.base:readonly',
        }

    @classmethod
    async def _get_app_access_token(cls) -> Optional[str]:
        """获取应用级别的 access_token"""
        try:
            config = await cls.get_client_config_async()

            url = "https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal"
            data = {
                'app_id': config['client_id'],
                'app_secret': config['client_secret'],
            }

            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.post(url, json=data)
                response.raise_for_status()

                result = response.json()

                if result.get('code') == 0:
                    return result.get('app_access_token')

                logger.error(f"获取飞书 app_access_token 失败: {result}")
                return None

        except Exception as e:
            logger.error(f"获取飞书 app_access_token 异常: {str(e)}")
            return None

    @classmethod
    async def get_access_token(cls, code: str) -> Optional[str]:
        """使用授权码获取访问令牌"""
        try:
            app_access_token = await cls._get_app_access_token()
            if not app_access_token:
                return None

            data = {
                'grant_type': 'authorization_code',
                'code': code,
            }

            headers = {
                'Content-Type': 'application/json',
                'Authorization': f'Bearer {app_access_token}',
            }

            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.post(cls.TOKEN_URL, json=data, headers=headers)
                response.raise_for_status()

                result = response.json()

                if result.get('code') == 0 and 'data' in result:
                    access_token = result['data'].get('access_token')
                    if access_token:
                        logger.info(f"飞书 access_token 获取成功")
                        return access_token

                logger.error(f"飞书 token 响应格式错误: {result}")
                return None

        except httpx.RequestError as e:
            logger.error(f"请求飞书 access_token 失败: {str(e)}")
            return None
        except Exception as e:
            logger.error(f"获取飞书 access_token 异常: {str(e)}")
            return None

    @classmethod
    async def get_user_info(cls, access_token: str) -> Optional[Dict]:
        """使用访问令牌获取飞书用户信息"""
        try:
            headers = {
                'Authorization': f'Bearer {access_token}',
                'Content-Type': 'application/json',
            }

            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.get(cls.USER_INFO_URL, headers=headers)
                response.raise_for_status()

                result = response.json()

                if result.get('code') == 0 and 'data' in result:
                    user_info = result['data']
                    if 'union_id' not in user_info:
                        logger.error(f"飞书用户信息格式错误: {result}")
                        return None
                    return user_info

                logger.error(f"飞书用户信息响应错误: {result}")
                return None

        except httpx.RequestError as e:
            logger.error(f"请求飞书用户信息失败: {str(e)}")
            return None
        except Exception as e:
            logger.error(f"获取飞书用户信息异常: {str(e)}")
            return None

    @classmethod
    def normalize_user_info(cls, raw_user_info: Dict) -> Dict:
        """标准化飞书用户信息"""
        provider_id = raw_user_info.get('union_id', '')
        name = raw_user_info.get('name', '')
        en_name = raw_user_info.get('en_name', '')
        username = en_name or name or f"feishu_{provider_id[:8]}"
        username = username.replace(' ', '_')

        mobile = raw_user_info.get('mobile', '')
        if mobile and mobile.startswith('+86-'):
            mobile = mobile[4:]

        return {
            'provider_id': provider_id,
            'username': username,
            'name': name or username,
            'email': raw_user_info.get('email'),
            'avatar': raw_user_info.get('avatar_url') or raw_user_info.get('avatar_big'),
            'mobile': mobile,
            'bio': f"飞书用户 - {name}" if name else "飞书用户",
        }

    @classmethod
    def get_user_id_field(cls) -> str:
        """获取用户 ID 字段名"""
        return 'feishu_union_id'


class WeComOAuthService(BaseOAuthService):
    """企业微信 OAuth 服务类"""

    PROVIDER_NAME = 'wecom'
    AUTHORIZE_URL = "https://login.work.weixin.qq.com/wwlogin/sso/login"
    TOKEN_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
    USER_INFO_URL = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo"
    USER_DETAIL_URL = "https://qyapi.weixin.qq.com/cgi-bin/user/get"

    @classmethod
    def get_user_id_field(cls) -> str:
        """企业微信使用 wecom_userid 作为唯一标识"""
        return 'wecom_userid'

    @classmethod
    def get_client_config(cls) -> Dict[str, str]:
        """获取企业微信客户端配置（同步回退）"""
        return {
            'client_id': getattr(settings, 'WECOM_CORP_ID', ''),
            'client_secret': getattr(settings, 'WECOM_APP_SECRET', ''),
            'redirect_uri': getattr(settings, 'WECOM_REDIRECT_URI', ''),
            'agent_id': getattr(settings, 'WECOM_AGENT_ID', ''),
        }

    @classmethod
    async def get_client_config_async(cls) -> Dict[str, str]:
        """从 config_manager 三级配置获取企业微信 OAuth 配置"""
        from app.config_manager import config_manager
        oauth_config = await config_manager.get_group("oauth_wecom")
        return {
            'client_id': oauth_config.get("corp_id") or getattr(settings, 'WECOM_CORP_ID', '') or '',
            'client_secret': oauth_config.get("app_secret") or getattr(settings, 'WECOM_APP_SECRET', '') or '',
            'redirect_uri': oauth_config.get("redirect_uri") or getattr(settings, 'WECOM_REDIRECT_URI', '') or '',
            'agent_id': oauth_config.get("agent_id") or getattr(settings, 'WECOM_AGENT_ID', '') or '',
        }

    @classmethod
    async def get_authorize_url(cls, state: str = None, redirect_uri: str = None) -> str:
        """获取企业微信授权 URL（企业微信使用特殊的 SSO 登录页面）"""
        config = await cls.get_client_config_async()

        params = {
            'login_type': 'CorpApp',
            'appid': config['client_id'],
            'agentid': config['agent_id'],
            'redirect_uri': redirect_uri or config['redirect_uri'],
        }

        if state:
            params['state'] = state

        query_string = '&'.join([f"{k}={v}" for k, v in params.items()])
        return f"{cls.AUTHORIZE_URL}?{query_string}"

    @classmethod
    async def _get_corp_access_token(cls) -> Optional[str]:
        """获取企业微信的企业级 access_token"""
        try:
            config = await cls.get_client_config_async()
            params = {
                'corpid': config['client_id'],
                'corpsecret': config['client_secret'],
            }

            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.get(cls.TOKEN_URL, params=params)
                response.raise_for_status()

                result = response.json()

                if result.get('errcode') == 0 and 'access_token' in result:
                    logger.info("企业微信 corp access_token 获取成功")
                    return result['access_token']

                logger.error(f"获取企业微信 corp access_token 失败: {result}")
                return None

        except Exception as e:
            logger.error(f"获取企业微信 corp access_token 异常: {str(e)}")
            return None

    @classmethod
    async def get_access_token(cls, code: str) -> Optional[str]:
        """
        企业微信不使用标准的 code 换 token 流程。
        这里获取 corp access_token 并返回，code 在 get_user_info 中使用。
        返回格式: "corp_access_token|code" 以便后续使用。
        """
        corp_token = await cls._get_corp_access_token()
        if not corp_token:
            return None
        # 将 corp_token 和 code 拼接传递，在 get_user_info 中拆分使用
        return f"{corp_token}|{code}"

    @classmethod
    async def get_user_info(cls, access_token: str) -> Optional[Dict]:
        """
        使用 corp access_token + code 获取企业微信用户信息。
        access_token 格式: "corp_access_token|code"
        """
        try:
            parts = access_token.split('|', 1)
            if len(parts) != 2:
                logger.error("企业微信 access_token 格式错误")
                return None

            corp_token, code = parts

            # 第一步：用 code 获取 userid
            params = {
                'access_token': corp_token,
                'code': code,
            }

            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.get(cls.USER_INFO_URL, params=params)
                response.raise_for_status()

                result = response.json()

                if result.get('errcode') != 0:
                    logger.error(f"企业微信获取用户身份失败: {result}")
                    return None

                userid = result.get('userid') or result.get('UserId')
                if not userid:
                    logger.error(f"企业微信用户身份响应中无 userid: {result}")
                    return None

                # 第二步：用 userid 获取用户详情
                detail_params = {
                    'access_token': corp_token,
                    'userid': userid,
                }

                detail_response = await client.get(cls.USER_DETAIL_URL, params=detail_params)
                detail_response.raise_for_status()

                user_detail = detail_response.json()

                if user_detail.get('errcode') != 0:
                    logger.error(f"企业微信获取用户详情失败: {user_detail}")
                    # 即使获取详情失败，也返回基本信息
                    return {
                        'userid': userid,
                        'name': userid,
                    }

                return user_detail

        except httpx.RequestError as e:
            logger.error(f"请求企业微信用户信息失败: {str(e)}")
            return None
        except Exception as e:
            logger.error(f"获取企业微信用户信息异常: {str(e)}")
            return None

    @classmethod
    def normalize_user_info(cls, raw_user_info: Dict) -> Dict:
        """标准化企业微信用户信息"""
        userid = raw_user_info.get('userid', '')
        name = raw_user_info.get('name', '')
        username = userid if userid else f"wecom_{name}"

        email = raw_user_info.get('biz_mail') or raw_user_info.get('email')
        avatar = raw_user_info.get('thumb_avatar') or raw_user_info.get('avatar')
        mobile = raw_user_info.get('mobile', '')
        position = raw_user_info.get('position', '')

        return {
            'provider_id': userid,
            'username': username,
            'name': name or username,
            'email': email,
            'avatar': avatar,
            'bio': f"企业微信用户 - {position}" if position else "企业微信用户",
        }


# OAuth 提供商映射
OAUTH_PROVIDERS = {
    'gitee': GiteeOAuthService,
    'github': GitHubOAuthService,
    'qq': QQOAuthService,
    'google': GoogleOAuthService,
    'wechat': WeChatOAuthService,
    'microsoft': MicrosoftOAuthService,
    'dingtalk': DingTalkOAuthService,
    'feishu': FeishuOAuthService,
    'wecom': WeComOAuthService,
}
