"""SSO/OIDC 企业单点登录"""

import logging
import secrets
import uuid
from dataclasses import dataclass

from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.db import async_session
from app.models.user import Tenant, User, UserTenant

from app.config import settings

logger = logging.getLogger(__name__)


@dataclass
class OIDCConfig:
    issuer: str = ""
    client_id: str = ""
    client_secret: str = ""
    authorization_endpoint: str = ""
    token_endpoint: str = ""
    userinfo_endpoint: str = ""
    jwks_uri: str = ""
    scopes: str = "openid profile email"

    @classmethod
    def from_tenant_config(cls, config: dict) -> "OIDCConfig":
        return cls(
            issuer=config.get("oidc_issuer", ""),
            client_id=config.get("oidc_client_id", ""),
            client_secret=config.get("oidc_client_secret_encrypted", ""),
            authorization_endpoint=config.get("oidc_issuer", "") + "/authorize",
            token_endpoint=config.get("oidc_issuer", "") + "/token",
            userinfo_endpoint=config.get("oidc_issuer", "") + "/userinfo",
            jwks_uri=config.get("oidc_issuer", "") + "/.well-known/jwks.json",
            scopes=config.get("oidc_scopes", "openid profile email"),
        )


@dataclass
class SAMLConfig:
    idp_entity_id: str = ""
    idp_sso_url: str = ""
    idp_certificate: str = ""
    sp_entity_id: str = ""

    @classmethod
    def from_tenant_config(cls, config: dict) -> "SAMLConfig":
        return cls(
            idp_entity_id=config.get("saml_idp_entity_id", ""),
            idp_sso_url=config.get("saml_idp_sso_url", ""),
            idp_certificate=config.get("saml_idp_certificate", ""),
            sp_entity_id=config.get("saml_sp_entity_id", ""),
        )


# OIDC Discovery 缓存
_discovery_cache: dict[str, dict] = {}


async def discover_oidc(tenant_slug: str) -> OIDCConfig | None:
    """根据租户 slug 获取 OIDC 配置（先从 tenant_sso_config 表读取，再动态发现）"""
    async with async_session() as db:
        # 找租户
        t = (await db.execute(select(Tenant).where(Tenant.slug == tenant_slug))).scalar()
        if not t:
            return None

        # 查 SSO 配置表
        sso_config = t.settings.get("sso", {})
        if not sso_config.get("is_enabled"):
            return None

        provider = sso_config.get("provider", "").lower()
        if provider == "oidc":
            return OIDCConfig.from_tenant_config(sso_config)
        elif provider == "saml":
            return None  # SAML 另行实现

    return None


def generate_state() -> str:
    return secrets.token_urlsafe(32)


def generate_nonce() -> str:
    return secrets.token_urlsafe(32)


async def sso_callback_oidc(
    db: AsyncSession,
    tenant_slug: str,
    code: str,
    state: str,
) -> dict:
    """OIDC 回调：验证 code → 获取 token → 获取用户信息 → 创建/绑定本地账户"""
    # state 已在路由层验证通过

    config = await discover_oidc(tenant_slug)
    if not config:
        raise HTTPException(status_code=400, detail="OIDC not configured for this tenant")

    # 1. Token Exchange
    import httpx
    try:
        async with httpx.AsyncClient(timeout=15) as client:
            token_resp = await client.post(config.token_endpoint, data={
                "grant_type": "authorization_code",
                "code": code,
                "client_id": config.client_id,
                "client_secret": config.client_secret,
                "redirect_uri": f"{settings.PUBLIC_BASE_URL}/api/v1/auth/sso/{tenant_slug}/callback",
            })
            token_resp.raise_for_status()
            token_data = token_resp.json()
            access_token = token_data.get("access_token")
    except Exception as e:
        logger.warning("SSO token exchange failed", exc_info=True)
        raise HTTPException(status_code=400, detail="Token exchange failed")

    # 2. UserInfo
    try:
        async with httpx.AsyncClient(timeout=15) as client:
            userinfo_resp = await client.get(
                config.userinfo_endpoint,
                headers={"Authorization": f"Bearer {access_token}"},
            )
            userinfo_resp.raise_for_status()
            userinfo = userinfo_resp.json()
    except Exception as e:
        logger.warning("SSO userinfo fetch failed", exc_info=True)
        raise HTTPException(status_code=400, detail="UserInfo fetch failed")

    # 3. 查找或创建本地用户
    sso_subject = userinfo.get("sub") or userinfo.get("email")
    email = userinfo.get("email", f"{sso_subject}@sso.local")
    name = userinfo.get("name") or userinfo.get("preferred_username") or email.split("@")[0]

    from app.models.user import Tenant

    # 查找租户
    t = (await db.execute(select(Tenant).where(Tenant.slug == tenant_slug))).scalar()
    if not t:
        raise HTTPException(status_code=404, detail="Tenant not found")

    # 查找或创建用户
    u = (await db.execute(
        select(User).where((User.sso_subject_id == sso_subject) & (User.sso_provider == "oidc"))
    )).scalar()

    if not u:
        # 检查 email 是否已存在
        u = (await db.execute(select(User).where(User.email == email))).scalar()
        if not u:
            u = User(
                id=uuid.uuid4(),
                email=email,
                display_name=name,
                sso_provider="oidc",
                sso_subject_id=sso_subject,
            )
            db.add(u)
            await db.flush()
        else:
            u.sso_provider = "oidc"
            u.sso_subject_id = sso_subject

    # 关联租户
    ut_exists = (await db.execute(
        select(UserTenant).where(UserTenant.user_id == u.id, UserTenant.tenant_id == t.id)
    )).scalar()

    if not ut_exists:
        auto_provision = t.settings.get("sso", {}).get("auto_provision", True)
        if auto_provision:
            default_role = t.settings.get("sso", {}).get("default_role", "viewer")
            db.add(UserTenant(user_id=u.id, tenant_id=t.id, role=default_role, is_default=True))
        else:
            raise HTTPException(status_code=403, detail="SSO user not provisioned for this tenant")

    await db.commit()

    return {
        "user_id": str(u.id),
        "email": u.email,
        "display_name": u.display_name,
        "tenant_id": str(t.id),
        "tenant_name": t.name,
        "sso_provider": "oidc",
    }
