# -*- coding: utf-8 -*-
"""认证路由：登录 / 注册 / 状态 / 校验 / 资料 / 令牌管理。

这是供 PineAgents 主后端 ``src/pineagents/app/routers/auth.py`` 转发调用的
"演示 FastAPI"（DEMO_API_BASE_URL，默认 http://127.0.0.1:8090）。
路由前缀 ``/auth``，字段与主后端转发模型完全一致，保证透传不丢字段。
"""
from __future__ import annotations

import re
import secrets

from fastapi import APIRouter, Depends, HTTPException, Request

from ... import config
from ..dependencies import get_current_user, get_db, extract_bearer_token
from ..schemas.auth import (
    AuthStatusResponse,
    IdentityInfo,
    LoginRequest,
    LoginResponse,
    PhoneLoginRequest,
    ProfileResponse,
    RegisterRequest,
    RevokeTokenRequest,
    SelectIdentityRequest,
    SendCodeRequest,
    SendCodeResponse,
    UpdateProfileRequest,
    VerifyResponse,
    WxLoginRequest,
    WxPhoneRequest,
)
from ...infrastructure.repositories import Database
from ...services import sms, wechat

router = APIRouter(prefix="/auth", tags=["auth"])

# 账号统一使用手机号作为登录账号（11 位，1 开头）
_PHONE_RE = re.compile(r"^1\d{10}$")


def _is_phone(account: str) -> bool:
    return bool(_PHONE_RE.match(account.strip()))

# 账户管理页可编辑的资料字段（username 作为登录名走 update_credentials，单独处理）
_PROFILE_FIELDS = (
    "nickname",
    "account",
    "room",
    "avatar",
    "company_avatar",
)


def _identity_summaries(identities: list[dict]) -> list[IdentityInfo]:
    """把 user_identities 记录裁剪成对外身份摘要。"""
    return [
        IdentityInfo(
            id=i["id"], port=i["port"], role=i["role"], sub_role=i.get("sub_role"),
            name=i.get("name", ""), org_id=i.get("org_id"), region_id=i.get("region_id"),
            org_name=i.get("org_name"), region_name=i.get("region_name"),
            status=i.get("status", "active"),
        )
        for i in identities
    ]


async def _issue_token(
    db: Database,
    user: dict,
    expires_in: int | None,
    identity: dict | None = None,
) -> dict:
    """为指定用户签发 JWT（可按端口身份作用域），返回 token 记录。

    ``identity`` 非空时按该身份解析 role/sub_role/org/region 与权限；
    否则回退到 ``users.role``（单角色/中性令牌）。
    """
    role = (identity or user).get("role", "opc_member")
    sub_role = (identity or user).get("sub_role")
    org_id = (identity or user).get("org_id")
    region_id = (identity or user).get("region_id")
    identity_id = identity.get("id") if identity else None

    perms = await db.roles.permissions_for(role, sub_role)
    scope_ids = await db.regions.visible_region_ids(region_id)
    scope_level = await db.regions.level(region_id)

    token_user = dict(user)
    token_user.update(role=role, sub_role=sub_role, org_id=org_id, region_id=region_id)
    return await db.tokens.create(
        token_user,
        permissions=perms,
        scope_region_ids=scope_ids,
        scope_level=scope_level,
        expiry_seconds=config.resolve_token_expiry(expires_in),
        identity_id=identity_id,
    )


async def _profile_for(
    user: dict,
    identity: dict | None,
    db: Database,
) -> dict:
    """按身份（或回退用户单角色）构建对外资料字典。"""
    role = (identity or user).get("role", "opc_member")
    sub_role = (identity or user).get("sub_role")
    org_id = (identity or user).get("org_id")
    region_id = (identity or user).get("region_id")
    profile = {f: user.get(f, "") for f in _PROFILE_FIELDS}
    profile.update(username=user.get("username", ""))
    profile["role"] = role
    profile["sub_role"] = sub_role
    profile["org_id"] = org_id
    profile["region_id"] = region_id
    profile["permissions"] = await db.roles.permissions_for(role, sub_role)
    profile["scope_region_ids"] = await db.regions.visible_region_ids(region_id)
    if identity:
        profile["identity_id"] = identity["id"]
        profile["port"] = identity["port"]
        profile["identity_name"] = identity.get("name", "")
    return profile


@router.post("/login", response_model=LoginResponse, summary="登录")
async def login(req: LoginRequest, db: Database = Depends(get_db)):
    """校验用户名密码，返回账号绑定的全部端口身份 + 令牌。

    - 仅一个身份：令牌直接绑定该身份（自动登录）。
    - 多个身份：签发中性账号令牌，前端展示身份选择，经
      ``/auth/select-identity`` 切换到指定身份后进入对应端口。
    """
    user = await db.users.get_by_username(req.username)
    if user is None or not await db.users.verify_password(user, req.password):
        raise HTTPException(status_code=401, detail="Invalid username or password")
    if user.get("status") != "active":
        raise HTTPException(status_code=403, detail="Account is disabled")

    identities = await db.identities.list_for_user(user["id"], active_only=True)
    identity = identities[0] if len(identities) == 1 else None
    token_record = await _issue_token(db, user, req.expires_in, identity=identity)
    profile = await _profile_for(user, identity, db)

    await db.audit.add(
        action="login", resource="auth", resource_id=user["id"],
        detail=f"login {user['username']} ({len(identities)} identities)",
        user_id=user["id"],
    )
    return LoginResponse(
        token=token_record["token"],
        identities=_identity_summaries(identities),
        **profile,
    )


@router.post("/select-identity", response_model=LoginResponse, summary="切换端口身份")
async def select_identity(
    req: SelectIdentityRequest,
    user: dict = Depends(get_current_user),
    db: Database = Depends(get_db),
):
    """把当前令牌切换到指定端口身份（该身份须属于当前账号且为启用状态）。"""
    ident = await db.identities.get_for_user(req.identity_id, user["id"])
    if ident is None or ident.get("status") != "active":
        raise HTTPException(status_code=404, detail="Identity not found or disabled")

    token_record = await _issue_token(db, user, None, identity=ident)
    profile = await _profile_for(user, ident, db)
    identities = await db.identities.list_for_user(user["id"], active_only=True)
    return LoginResponse(
        token=token_record["token"],
        identities=_identity_summaries(identities),
        **profile,
    )


@router.post("/register", response_model=LoginResponse, summary="注册")
async def register(req: RegisterRequest, db: Database = Depends(get_db)):
    """注册唯一账户（演示端已存在 pine，故返回 403）。"""
    if not _is_phone(req.username):
        raise HTTPException(
            status_code=400,
            detail="账号需为 11 位手机号（如 138xxxx1234）",
        )
    if not config.AUTH_ENABLED:
        raise HTTPException(status_code=403, detail="Authentication is not enabled")
    if await db.users.has_users():
        raise HTTPException(status_code=403, detail="User already registered")
    if not req.username.strip() or not req.password.strip():
        raise HTTPException(
            status_code=400,
            detail="Username and password are required",
        )

    user = await db.users.create(req.username, req.password)
    identity = await db.identities.create(
        user["id"], port="opc", role="opc_member",
        sub_role="independent", name="独立OPC",
    )
    token_record = await _issue_token(db, user, req.expires_in, identity=identity)
    profile = await _profile_for(user, identity, db)
    return LoginResponse(
        token=token_record["token"],
        identities=_identity_summaries([identity]),
        **profile,
    )


@router.get("/status", response_model=AuthStatusResponse, summary="认证状态")
async def auth_status(db: Database = Depends(get_db)):
    """前端登录页据此判断是否展示登录表单。"""
    return AuthStatusResponse(
        enabled=config.AUTH_ENABLED,
        has_users=await db.users.has_users(),
    )


@router.get("/verify", response_model=VerifyResponse, summary="校验令牌")
async def verify(request: Request, db: Database = Depends(get_db)):
    """校验调用方 Bearer 令牌；无效令牌由依赖层直接返回 401。"""
    if not config.AUTH_ENABLED:
        return VerifyResponse(valid=True, username="")
    user = await get_current_user(request, db)
    return VerifyResponse(valid=True, username=user["username"])


@router.get("/me", response_model=dict, summary="当前用户资料")
async def me(
    user: dict = Depends(get_current_user),
    db: Database = Depends(get_db),
):
    """返回当前登录用户的完整资料（含角色/组织/区域/权限/数据范围）。"""
    return await db.users.to_profile(user)


@router.post("/update-profile", response_model=ProfileResponse, summary="更新资料/凭据")
async def update_profile(
    req: UpdateProfileRequest,
    user: dict = Depends(get_current_user),
    db: Database = Depends(get_db),
):
    """更新演示资料与/或用户名、密码，返回更新后的完整资料。

    - 修改用户名/密码时必须提供正确的 ``current_password``；
    - 修改凭据后吊销该用户其余会话并签发新令牌随响应返回；
    - 仅改资料字段时不重签令牌。
    """
    payload = req.model_dump(exclude_none=True)
    user_id = user["id"]

    new_username = payload.get("new_username")
    new_password = payload.get("new_password")

    if new_username is not None and not new_username.strip():
        raise HTTPException(status_code=400, detail="Username cannot be empty")
    if new_password is not None and not new_password.strip():
        raise HTTPException(status_code=400, detail="Password cannot be empty")

    profile_updates = {k: payload[k] for k in _PROFILE_FIELDS if k in payload}
    changing_credentials = new_username is not None or new_password is not None
    if not profile_updates and not changing_credentials:
        raise HTTPException(status_code=400, detail="Nothing to update")

    if changing_credentials and not await db.users.verify_password(user, req.current_password):
        raise HTTPException(status_code=401, detail="Current password is incorrect")

    if profile_updates:
        await db.users.update_profile(user_id, profile_updates)

    issued_token = ""
    if changing_credentials:
        await db.users.update_credentials(user_id, new_username, new_password)
        await db.tokens.revoke_all(user_id)
        fresh_user = await db.users.get_by_id(user_id)
        token_record = await _issue_token(db, fresh_user, req.expires_in)
        issued_token = token_record["token"]

    fresh_user = await db.users.get_by_id(user_id)
    fresh_user["permissions"] = await db.roles.permissions_for(
        fresh_user.get("role", "opc_member"), fresh_user.get("sub_role"),
    )
    fresh_user["scope_region_ids"] = await db.regions.visible_region_ids(fresh_user.get("region_id"))
    return ProfileResponse(
        token=issued_token,
        **await db.users.to_profile(fresh_user),
    )


@router.post("/revoke-token", summary="吊销单个令牌")
async def revoke_single_token(
    req: RevokeTokenRequest,
    request: Request,
    user: dict = Depends(get_current_user),
    db: Database = Depends(get_db),
):
    """吊销指定令牌（省略则吊销当前令牌）。"""
    caller_token = extract_bearer_token(request)
    token_to_revoke = req.token or caller_token
    is_current = token_to_revoke == caller_token

    if not await db.tokens.revoke(token_to_revoke):
        raise HTTPException(status_code=500, detail="Failed to revoke token")

    message = (
        "Current token has been revoked. Please login again."
        if is_current
        else "Specified token has been revoked."
    )
    return {
        "message": message,
        "revoked": True,
        "revoked_current_token": is_current,
    }


@router.post("/revoke-all-tokens", summary="吊销全部令牌")
async def revoke_all_sessions(
    user: dict = Depends(get_current_user),
    db: Database = Depends(get_db),
):
    """吊销所有令牌，所有会话需重新登录。"""
    await db.tokens.revoke_all()
    return {
        "message": "All tokens have been revoked. Please login again.",
        "revoked": True,
    }


# ===========================================================================
# 统一登录（手机验证码 / 微信）—— 六端登录归一
# ===========================================================================


async def _ensure_opc_identity(db: Database, user_id: str) -> dict:
    """确保账号拥有 opc_member 身份（登录即建；重复登录复用已有）。"""
    for ident in await db.identities.list_for_user(user_id, active_only=True):
        if ident["port"] == "opc" and ident["role"] == "opc_member":
            return ident
    return await db.identities.create(
        user_id, port="opc", role="opc_member",
        sub_role="independent", name="独立OPC",
    )


async def _login_response(
    db: Database, user: dict, identity: dict, identities: list[dict] | None = None,
) -> LoginResponse:
    """以指定身份签发令牌并装配登录响应（与密码登录同构）。"""
    token_record = await _issue_token(db, user, None, identity=identity)
    profile = await _profile_for(user, identity, db)
    return LoginResponse(
        token=token_record["token"],
        identities=_identity_summaries(identities or [identity]),
        **profile,
    )


@router.post("/send-code", response_model=SendCodeResponse, summary="发送短信验证码")
async def send_code(req: SendCodeRequest, db: Database = Depends(get_db)):
    """为手机号发送登录验证码。stub provider 仅在服务日志打印。"""
    if not _is_phone(req.phone):
        raise HTTPException(status_code=400, detail="手机号需为 11 位（1 开头）")
    if not config.AUTH_ENABLED:
        raise HTTPException(status_code=403, detail="认证未开启")
    code = sms.issue(req.phone)
    return SendCodeResponse(
        sent=True,
        stub_code=code if config.SMS_PROVIDER == "stub" else None,
    )


@router.post("/phone-login", response_model=LoginResponse, summary="手机号验证码登录")
async def phone_login(req: PhoneLoginRequest, db: Database = Depends(get_db)):
    """手机号 + 验证码登录：无账号则注册（建 opc_member 身份）。"""
    if not _is_phone(req.phone):
        raise HTTPException(status_code=400, detail="手机号需为 11 位（1 开头）")
    if not config.AUTH_ENABLED:
        raise HTTPException(status_code=403, detail="认证未开启")
    try:
        sms.verify(req.phone, req.code)
    except sms.SmsError as exc:
        raise HTTPException(status_code=401, detail=str(exc))

    user = await db.users.get_by_username(req.phone)
    if user is None:
        user = await db.users.create(
            req.phone, password=secrets.token_hex(16),
            phone=req.phone, role="opc_member",
        )
    if user.get("status") != "active":
        raise HTTPException(status_code=403, detail="账号已禁用")

    identity = await _ensure_opc_identity(db, user["id"])
    await db.audit.add(
        action="login", resource="auth", resource_id=user["id"],
        detail=f"phone-login {req.phone}", user_id=user["id"],
    )
    return await _login_response(db, user, identity)


@router.post("/wx-login", response_model=LoginResponse, summary="微信登录")
async def wx_login(req: WxLoginRequest, db: Database = Depends(get_db)):
    """微信 code 登录：未配置 appid 时直通（code 当 openid），便于联调。"""
    if not config.AUTH_ENABLED:
        raise HTTPException(status_code=403, detail="认证未开启")
    try:
        openid = await wechat.code2session(req.code)
    except wechat.WechatError as exc:
        raise HTTPException(status_code=401, detail=str(exc))

    user = await db.users.get_by_wx_openid(openid)
    if user is None:
        user = await db.users.create(
            f"wx_{openid[:24]}", password=secrets.token_hex(16),
            wx_openid=openid, role="opc_member",
        )
    if user.get("status") != "active":
        raise HTTPException(status_code=403, detail="账号已禁用")

    identity = await _ensure_opc_identity(db, user["id"])
    await db.audit.add(
        action="login", resource="auth", resource_id=user["id"],
        detail=f"wx-login {openid[:16]}", user_id=user["id"],
    )
    return await _login_response(db, user, identity)


@router.post("/wx-phone", summary="微信补绑手机号")
async def wx_phone(req: WxPhoneRequest, db: Database = Depends(get_db)):
    """给已登录的微信账号补绑手机号（用于兑现政策/报名等需手机的流程）。"""
    if not config.AUTH_ENABLED:
        raise HTTPException(status_code=403, detail="认证未开启")
    try:
        sms.verify(req.phone, req.code)
    except sms.SmsError as exc:
        raise HTTPException(status_code=401, detail=str(exc))

    user = await db.users.get_by_wx_openid(req.openid)
    if user is None:
        raise HTTPException(status_code=404, detail="微信账号不存在，请先 wx-login")
    bound = await db.users.get_by_username(req.phone)
    if bound is not None and bound["id"] != user["id"]:
        raise HTTPException(status_code=409, detail="该手机号已绑定其它账号")

    fresh = await db.users.set_phone(user["id"], req.phone)
    await db.audit.add(
        action="bind-phone", resource="auth", resource_id=user["id"],
        detail=f"wx bind phone {req.phone}", user_id=user["id"],
    )
    return {**await db.users.to_profile(fresh), "phone": fresh.get("phone", "")}
