# -*- coding: utf-8 -*-
"""
微信扫码登录桌面 Demo（PySide6 / QtWidgets，不使用 QtWebEngine）

登录链路（全部由 Python 直接完成，界面不内嵌任何浏览器内核）：

    1. GET  {GW}/oauth/qr            → appid / scope / 网关 state
    2. GET  open.weixin.qq.com/...   → 从授权页取出 uuid
    3. GET  open.weixin.qq.com/connect/qrcode/{uuid} → 二维码图片
    4. GET  lp.open.weixin.qq.com/connect/l/qrconnect → 长轮询扫码状态，拿到 wx_code
       状态码：408 等待扫码 / 404 已扫码待确认 / 405 已确认（含 code）
               403 已取消 / 402 二维码过期 / 500 微信异常
    5. GET  {GW}/oauth/callback      → 不跟随 302，从 Location 取出一次性 ticket
    6. POST {GW}/api/user/info       → 用 ticket 换 openid / 昵称 / 头像

下载地址：
    https://login.fps5.cn/static/python/wx_login_demo.py
    https://login.fps5.cn/static/python/requirements-demo.txt

运行：
    pip install -r requirements-demo.txt
    set WX_CLIENT_ID=ak_你的client_id
    set WX_CLIENT_SECRET=sk_你的client_secret
    set WX_REDIRECT_URL=https://你的站点/callback
    python wx_login_demo.py

环境变量：
    WX_GATEWAY        登录网关，默认 https://login.fps5.cn
    WX_CLIENT_ID      项目 client_id（必填，在「我的项目」中查看）
    WX_CLIENT_SECRET  项目 client_secret（必填）
    WX_REDIRECT_URL   项目域名白名单内的回调地址（必填）

注意：client_secret 放进桌面客户端无法真正保密，生产环境应改为
「客户端 → 自己的后端 → 网关」的架构，由后端持有 secret。
"""

from __future__ import annotations

import os
import re
import secrets
import sys
import tempfile
import threading
import time
from html import unescape
from urllib.parse import parse_qs, urlencode, urlsplit

import requests
from PySide6.QtCore import QObject, Qt, QTimer, Signal
from PySide6.QtGui import QColor, QFont, QPainter, QPainterPath, QPixmap
from PySide6.QtWidgets import (
    QApplication,
    QFormLayout,
    QFrame,
    QHBoxLayout,
    QLabel,
    QPushButton,
    QStackedWidget,
    QVBoxLayout,
    QWidget,
)

# --------------------------------------------------------------------------
# 配置
# --------------------------------------------------------------------------
CONFIG = {
    "gateway": os.getenv("WX_GATEWAY", "https://login.fps5.cn").rstrip("/"),
    "client_id": os.getenv("WX_CLIENT_ID", "").strip(),
    "client_secret": os.getenv("WX_CLIENT_SECRET", "").strip(),
    "redirect_url": os.getenv("WX_REDIRECT_URL", "").strip(),
}

QR_CONNECT = "https://open.weixin.qq.com/connect/qrconnect"
QR_IMAGE = "https://open.weixin.qq.com/connect/qrcode/"
QR_POLL = "https://lp.open.weixin.qq.com/connect/l/qrconnect"

UA = (
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
)

RE_UUID = re.compile(r"l/qrconnect\?uuid=([A-Za-z0-9_-]+)")
RE_ERRCODE = re.compile(r"wx_errcode\s*=\s*(\d+)")
RE_WXCODE = re.compile(r"wx_code\s*=\s*'([^']*)'")

QR_TTL = 300  # 秒，微信二维码有效期约 5 分钟
LOG_PATH = os.path.join(tempfile.gettempdir(), "wx_login_demo.log")


class LoginError(RuntimeError):
    """可直接展示给用户的登录错误。"""


class _Cancelled(Exception):
    pass


# --------------------------------------------------------------------------
# 后台登录线程
# --------------------------------------------------------------------------
class LoginWorker(QObject):
    qr_ready = Signal(int, bytes)          # gen, 二维码图片字节
    status_changed = Signal(int, str)      # gen, 状态文案
    logged_in = Signal(int, dict, bytes)   # gen, 用户资料, 头像字节
    failed = Signal(int, str)              # gen, 错误
    expired = Signal(int, str)             # gen, 二维码过期/需刷新

    def __init__(self, cfg: dict, gen: int, cancelled: threading.Event):
        super().__init__()
        self.cfg = cfg
        self.gen = gen
        self.cancelled = cancelled
        self.http = requests.Session()
        self.http.headers.update({"User-Agent": UA, "Accept-Language": "zh-CN,zh;q=0.9"})
        self.biz_state = secrets.token_hex(16)
        self.gateway_state = ""
        self.qrconnect_url = ""
        self.uuid = ""
        self._last_status = ""

    # ---- 对外入口 --------------------------------------------------------
    def start(self) -> None:
        threading.Thread(target=self._run, daemon=True, name="wx-login").start()

    def _check_cancel(self) -> None:
        if self.cancelled.is_set():
            raise _Cancelled

    def _set_status(self, text: str) -> None:
        if text != self._last_status:
            self._last_status = text
            self.status_changed.emit(self.gen, text)

    @staticmethod
    def _log(message: str) -> None:
        """把关键步骤写入 %TEMP%\\wx_login_demo.log，便于排查线上问题。"""
        line = f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {message}"
        print(line, flush=True)
        try:
            with open(LOG_PATH, "a", encoding="utf-8") as fh:
                fh.write(line + "\n")
        except OSError:
            pass

    # ---- 主流程 ----------------------------------------------------------
    def _run(self) -> None:
        try:
            self._request_qr()
            wx_code = self._wait_for_code()
            if wx_code is None:
                return
            self._set_status("已确认登录，正在向网关换取用户资料…")
            self._log("收到微信 code，开始调用网关回调")
            user, avatar = self._exchange_ticket(wx_code)
            self._log(f"登录成功：nickname={user.get('nickname')!r} openid={user.get('openid')!r}")
            if not self.cancelled.is_set():
                self.logged_in.emit(self.gen, user, avatar)
        except _Cancelled:
            return
        except Exception as exc:  # noqa: BLE001 - 统一转为界面提示
            message = str(exc) or exc.__class__.__name__
            self._log(f"登录失败：{message}")
            if not self.cancelled.is_set():
                self.failed.emit(self.gen, message)

    def _request_qr(self) -> None:
        self._check_cancel()
        missing = [
            name
            for key, name in (
                ("client_id", "WX_CLIENT_ID"),
                ("client_secret", "WX_CLIENT_SECRET"),
                ("redirect_url", "WX_REDIRECT_URL"),
            )
            if not self.cfg.get(key)
        ]
        if missing:
            raise LoginError("请先设置环境变量：" + "、".join(missing))

        self._set_status("正在向网关申请二维码…")
        try:
            resp = self.http.get(
                f"{self.cfg['gateway']}/oauth/qr",
                params={
                    "client_id": self.cfg["client_id"],
                    "redirect_url": self.cfg["redirect_url"],
                    "state": self.biz_state,
                },
                timeout=15,
            )
            payload = resp.json()
        except requests.RequestException as exc:
            raise LoginError(f"连接登录网关失败：{exc}") from exc
        except ValueError as exc:
            raise LoginError("网关返回的不是合法 JSON，请检查网关地址") from exc

        if payload.get("code") != 200 or not payload.get("data"):
            raise LoginError(payload.get("msg") or "网关未返回二维码参数")

        data = payload["data"]
        if data.get("mock"):
            raise LoginError("网关当前处于模拟模式（WX_MOCK），无法生成真实微信二维码")
        if not data.get("state") or not data.get("appid"):
            raise LoginError("网关返回参数不完整（缺少 state 或 appid）")

        self.gateway_state = str(data["state"])
        self.qrconnect_url = QR_CONNECT + "?" + urlencode(
            {
                "appid": data["appid"],
                "redirect_uri": data["redirect_uri"],
                "response_type": "code",
                "scope": data.get("scope") or "snsapi_login",
                "state": self.gateway_state,
            }
        ) + "#wechat_redirect"

        self._check_cancel()
        try:
            page = self.http.get(self.qrconnect_url, timeout=20)
            page.raise_for_status()
        except requests.RequestException as exc:
            raise LoginError(f"打开微信授权页失败：{exc}") from exc

        match = RE_UUID.search(page.text)
        if not match:
            raise LoginError("未能解析二维码 uuid，微信授权页结构可能已调整")
        self.uuid = match.group(1)
        self._log(f"获取二维码成功：uuid={self.uuid} gateway_state={self.gateway_state}")

        self._check_cancel()
        try:
            image = self.http.get(QR_IMAGE + self.uuid, timeout=20)
            image.raise_for_status()
        except requests.RequestException as exc:
            raise LoginError(f"下载二维码图片失败：{exc}") from exc

        self.qr_ready.emit(self.gen, image.content)

    def _wait_for_code(self) -> str | None:
        """
        长轮询扫码状态（与微信官方二维码页使用同一套协议）。

        微信状态码含义（来自官方 qrconnect 页面脚本）：
            408 等待扫码        404 已扫码，等待手机确认
            405 已确认，返回 code  403 用户取消
            402 二维码过期      500 微信服务异常
        注意：404 不是过期，必须继续轮询，否则用户点「允许」后无人接收 code。
        """
        deadline = time.monotonic() + QR_TTL
        self._set_status("请使用微信扫一扫")
        last = ""

        while time.monotonic() < deadline:
            self._check_cancel()
            params = {"uuid": self.uuid}
            if last:
                params["last"] = last

            started = time.monotonic()
            try:
                resp = self.http.get(
                    QR_POLL,
                    params=params,
                    headers={"Referer": self.qrconnect_url.split("#")[0]},
                    timeout=(10, 40),  # 微信长轮询会挂起约 15 秒
                )
            except requests.Timeout:
                continue  # 超时等同于“暂无状态变化”
            except requests.RequestException as exc:
                raise LoginError(f"轮询扫码状态失败：{exc}") from exc

            body = resp.text
            errcode_match = RE_ERRCODE.search(body)
            errcode = int(errcode_match.group(1)) if errcode_match else 0
            code_match = RE_WXCODE.search(body)
            code = code_match.group(1) if code_match else ""
            self._log(
                f"轮询状态 errcode={errcode} code={'有' if code else '无'} "
                f"耗时={time.monotonic() - started:.1f}s"
            )

            # 只要拿到 code 就按登录成功处理（405 之外也兜底）
            if code:
                self._set_status("扫码成功，正在完成登录…")
                return code

            if errcode == 405:
                raise LoginError("微信已确认授权，但未返回 code，请重试")
            if errcode == 402:
                self.expired.emit(self.gen, "二维码已过期，请点击「刷新二维码」")
                return None
            if errcode == 500:
                self.expired.emit(self.gen, "微信服务暂时异常，请刷新二维码")
                return None
            if errcode == 404:
                self._set_status("扫描成功，请在手机上点击「允许」确认登录")
            elif errcode == 403:
                self._set_status("已在手机取消授权，可重新扫码")
                if self.cancelled.wait(2):
                    raise _Cancelled
            elif errcode == 408:
                self._set_status("请使用微信扫一扫")
            else:
                self._set_status("等待微信响应…")

            last = str(errcode) if errcode else ""

        self.expired.emit(self.gen, "二维码已过期，请点击「刷新二维码」")
        return None

    def _exchange_ticket(self, wx_code: str) -> tuple[dict, bytes]:
        self._check_cancel()
        try:
            resp = self.http.get(
                f"{self.cfg['gateway']}/oauth/callback",
                params={"code": wx_code, "state": self.gateway_state},
                allow_redirects=False,  # 只取 302 的 Location，不触发业务站回调
                timeout=20,
            )
        except requests.RequestException as exc:
            raise LoginError(f"调用网关回调失败：{exc}") from exc

        location = resp.headers.get("Location", "")
        self._log(f"网关回调返回：HTTP {resp.status_code}，跳转={'有' if location else '无'}")
        if resp.status_code not in (301, 302, 303, 307, 308) or not location:
            match = re.search(r"<p[^>]*>(.*?)</p>", resp.text, re.S)
            if match:
                message = unescape(re.sub(r"<[^>]+>", "", match.group(1))).strip()
            else:
                message = re.sub(r"<[^>]+>", " ", resp.text)
                message = re.sub(r"\s+", " ", unescape(message)).strip()
            raise LoginError(f"网关未完成登录（HTTP {resp.status_code}）：{message[:160] or '无返回内容'}")

        if location.startswith("/"):
            location = self.cfg["gateway"] + location
        query = parse_qs(urlsplit(location).query)
        ticket = (query.get("code") or [""])[0]
        state = (query.get("state") or [""])[0]
        if not ticket:
            raise LoginError("网关回调未携带 ticket")
        if state and state != self.biz_state:
            raise LoginError("state 校验失败，登录来源不可信")

        self._check_cancel()
        try:
            resp = self.http.post(
                f"{self.cfg['gateway']}/api/user/info",
                data={
                    "client_id": self.cfg["client_id"],
                    "client_secret": self.cfg["client_secret"],
                    "code": ticket,
                },
                timeout=20,
            )
            payload = resp.json()
        except requests.RequestException as exc:
            raise LoginError(f"换取用户资料失败：{exc}") from exc
        except ValueError as exc:
            raise LoginError("用户资料接口返回的不是合法 JSON") from exc

        if payload.get("code") != 200 or not payload.get("data"):
            raise LoginError(payload.get("msg") or "换取用户资料失败")

        user = payload["data"]
        avatar = b""
        avatar_url = str(user.get("avatar") or "")
        if avatar_url:
            try:
                image = self.http.get(avatar_url, timeout=10)
                if image.ok:
                    avatar = image.content
            except requests.RequestException:
                pass  # 头像失败不影响登录
        return user, avatar


# --------------------------------------------------------------------------
# 界面
# --------------------------------------------------------------------------
STYLE = """
QWidget#Root { background: #f2f4f7; }
QFrame#Card {
    background: #ffffff;
    border: 1px solid #e8ebef;
    border-radius: 18px;
}
QLabel#Title { font-size: 21px; font-weight: 600; color: #14181f; }
QLabel#Subtitle { font-size: 12px; color: #7b8494; }
QLabel#QrBox {
    background: #ffffff;
    border: 1px solid #e8ebef;
    border-radius: 14px;
    color: #98a2b3;
    font-size: 13px;
}
QLabel#Tip { font-size: 13px; color: #5b6472; }
QLabel#Error { font-size: 12px; color: #d64545; }
QLabel#Footer { font-size: 11px; color: #9aa3b2; }
QLabel#Name { font-size: 18px; font-weight: 600; color: #14181f; }
QLabel#Badge {
    font-size: 11px;
    color: #0a8f4c;
    background: #e8f8ef;
    border-radius: 9px;
    padding: 2px 9px;
}
QLabel#FormKey { font-size: 12px; color: #8a93a3; }
QLabel#FormVal { font-size: 12px; color: #2b3340; }
QPushButton#Primary {
    background: #07c160;
    color: #ffffff;
    border: none;
    border-radius: 10px;
    padding: 11px 18px;
    font-size: 14px;
    font-weight: 600;
}
QPushButton#Primary:hover { background: #06b157; }
QPushButton#Primary:pressed { background: #059c4c; }
QPushButton#Primary:disabled { background: #b9e8cc; }
QPushButton#Ghost {
    background: #ffffff;
    color: #4b5565;
    border: 1px solid #dfe4ea;
    border-radius: 10px;
    padding: 10px 18px;
    font-size: 13px;
}
QPushButton#Ghost:hover { border-color: #c3cad4; background: #fafbfc; }
"""


def _circle_pixmap(data: bytes, size: int, fallback_text: str) -> QPixmap:
    """把头像裁成圆形；没有头像时画一个带首字的绿色圆形。"""
    source = QPixmap()
    if not data or not source.loadFromData(data):
        canvas = QPixmap(size, size)
        canvas.fill(Qt.transparent)
        painter = QPainter(canvas)
        painter.setRenderHint(QPainter.Antialiasing)
        painter.setBrush(QColor("#07c160"))
        painter.setPen(Qt.NoPen)
        painter.drawEllipse(0, 0, size, size)
        painter.setPen(QColor("#ffffff"))
        font = QFont("Microsoft YaHei UI", int(size * 0.4))
        font.setBold(True)
        painter.setFont(font)
        painter.drawText(canvas.rect(), Qt.AlignCenter, (fallback_text or "微")[:1])
        painter.end()
        return canvas

    scaled = source.scaled(size, size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
    canvas = QPixmap(size, size)
    canvas.fill(Qt.transparent)
    painter = QPainter(canvas)
    painter.setRenderHint(QPainter.Antialiasing)
    path = QPainterPath()
    path.addEllipse(0, 0, size, size)
    painter.setClipPath(path)
    painter.drawPixmap((size - scaled.width()) // 2, (size - scaled.height()) // 2, scaled)
    painter.end()
    return canvas


class LoginWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setObjectName("Root")
        self.setWindowTitle("微信扫码登录 Demo · login.fps5.cn")
        self.setFixedSize(400, 580)

        self._gen = 0
        self._cancel_event = threading.Event()
        self._worker: LoginWorker | None = None

        root = QVBoxLayout(self)
        root.setContentsMargins(24, 22, 24, 18)
        root.setSpacing(14)

        title = QLabel("微信扫码登录")
        title.setObjectName("Title")
        subtitle = QLabel("由 login.fps5.cn 统一登录网关提供 · 不内嵌浏览器内核")
        subtitle.setObjectName("Subtitle")
        subtitle.setWordWrap(True)
        root.addWidget(title)
        root.addWidget(subtitle)

        self.stack = QStackedWidget()
        self.stack.addWidget(self._build_login_page())
        self.stack.addWidget(self._build_profile_page())
        root.addWidget(self.stack, 1)

        footer = QLabel("二维码由微信开放平台生成，确认授权后自动完成登录")
        footer.setObjectName("Footer")
        footer.setAlignment(Qt.AlignCenter)
        footer.setWordWrap(True)
        root.addWidget(footer)

        QTimer.singleShot(200, self.refresh)

    # ---- 登录页 ----------------------------------------------------------
    def _build_login_page(self) -> QWidget:
        page = QWidget()
        outer = QVBoxLayout(page)
        outer.setContentsMargins(0, 0, 0, 0)

        card = QFrame()
        card.setObjectName("Card")
        box = QVBoxLayout(card)
        box.setContentsMargins(24, 24, 24, 22)
        box.setSpacing(14)

        self.qr_label = QLabel("正在获取二维码…")
        self.qr_label.setObjectName("QrBox")
        self.qr_label.setFixedSize(268, 268)
        self.qr_label.setAlignment(Qt.AlignCenter)

        qr_row = QHBoxLayout()
        qr_row.addStretch(1)
        qr_row.addWidget(self.qr_label)
        qr_row.addStretch(1)

        self.tip_label = QLabel("正在向网关申请登录二维码…")
        self.tip_label.setObjectName("Tip")
        self.tip_label.setAlignment(Qt.AlignCenter)
        self.tip_label.setWordWrap(True)
        self.tip_label.setMinimumHeight(38)

        self.refresh_btn = QPushButton("刷新二维码")
        self.refresh_btn.setObjectName("Primary")
        self.refresh_btn.setCursor(Qt.PointingHandCursor)
        self.refresh_btn.setEnabled(False)
        self.refresh_btn.clicked.connect(self.refresh)

        box.addLayout(qr_row)
        box.addWidget(self.tip_label)
        box.addWidget(self.refresh_btn)
        outer.addWidget(card)
        outer.addStretch(1)
        return page

    # ---- 资料页 ----------------------------------------------------------
    def _build_profile_page(self) -> QWidget:
        page = QWidget()
        outer = QVBoxLayout(page)
        outer.setContentsMargins(0, 0, 0, 0)

        card = QFrame()
        card.setObjectName("Card")
        box = QVBoxLayout(card)
        box.setContentsMargins(26, 28, 26, 24)
        box.setSpacing(16)

        self.avatar_label = QLabel()
        self.avatar_label.setFixedSize(76, 76)
        self.avatar_label.setAlignment(Qt.AlignCenter)

        avatar_row = QHBoxLayout()
        avatar_row.addStretch(1)
        avatar_row.addWidget(self.avatar_label)
        avatar_row.addStretch(1)

        self.name_label = QLabel("微信用户")
        self.name_label.setObjectName("Name")
        self.name_label.setAlignment(Qt.AlignCenter)

        badge_row = QHBoxLayout()
        badge_row.addStretch(1)
        badge = QLabel("已通过微信授权登录")
        badge.setObjectName("Badge")
        badge_row.addWidget(badge)
        badge_row.addStretch(1)

        form = QFormLayout()
        form.setHorizontalSpacing(14)
        form.setVerticalSpacing(9)
        form.setLabelAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        form.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)

        self.info_values: dict[str, QLabel] = {}
        for key, label in (
            ("openid", "openid"),
            ("unionid", "unionid"),
            ("sex", "性别"),
            ("region", "地区"),
            ("login_at", "登录时间"),
        ):
            value = QLabel("-")
            value.setObjectName("FormVal")
            value.setWordWrap(True)
            value.setTextInteractionFlags(Qt.TextSelectableByMouse)
            form.addRow(self._form_key(label), value)
            self.info_values[key] = value

        self.logout_btn = QPushButton("退出登录")
        self.logout_btn.setObjectName("Ghost")
        self.logout_btn.setCursor(Qt.PointingHandCursor)
        self.logout_btn.clicked.connect(self.logout)

        box.addLayout(avatar_row)
        box.addWidget(self.name_label)
        box.addLayout(badge_row)
        box.addSpacing(4)
        box.addLayout(form)
        box.addStretch(1)
        box.addWidget(self.logout_btn)

        outer.addWidget(card)
        outer.addStretch(1)
        return page

    @staticmethod
    def _form_key(text: str) -> QLabel:
        label = QLabel(text)
        label.setObjectName("FormKey")
        return label

    # ---- 行为 ------------------------------------------------------------
    def refresh(self) -> None:
        self._stop_worker()
        self._gen += 1
        gen = self._gen

        self.stack.setCurrentIndex(0)
        self.qr_label.setPixmap(QPixmap())
        self.qr_label.setText("正在获取二维码…")
        self.tip_label.setStyleSheet("")
        self.tip_label.setText("正在向网关申请登录二维码…")
        self.refresh_btn.setEnabled(False)
        self.refresh_btn.setText("刷新二维码")

        worker = LoginWorker(CONFIG, gen, self._cancel_event)
        worker.qr_ready.connect(self._on_qr_ready)
        worker.status_changed.connect(self._on_status)
        worker.logged_in.connect(self._on_logged_in)
        worker.failed.connect(self._on_failed)
        worker.expired.connect(self._on_expired)
        self._worker = worker
        worker.start()

    def _stop_worker(self) -> None:
        self._cancel_event.set()
        self._worker = None
        self._cancel_event = threading.Event()

    def logout(self) -> None:
        self.stack.setCurrentIndex(0)
        self.refresh()

    # ---- 信号处理 --------------------------------------------------------
    def _on_qr_ready(self, gen: int, data: bytes) -> None:
        if gen != self._gen:
            return
        pixmap = QPixmap()
        if not pixmap.loadFromData(data):
            self._on_failed(gen, "二维码图片解析失败")
            return
        self.qr_label.setText("")
        self.qr_label.setPixmap(
            pixmap.scaled(240, 240, Qt.KeepAspectRatio, Qt.SmoothTransformation)
        )

    def _on_status(self, gen: int, text: str) -> None:
        if gen != self._gen:
            return
        self.tip_label.setText(text)

    def _on_failed(self, gen: int, message: str) -> None:
        if gen != self._gen:
            return
        self.tip_label.setStyleSheet("color:#d64545;")
        self.tip_label.setText(message)
        self.refresh_btn.setText("重试")
        self.refresh_btn.setEnabled(True)

    def _on_expired(self, gen: int, message: str) -> None:
        if gen != self._gen:
            return
        self.tip_label.setText(message)
        self.refresh_btn.setText("刷新二维码")
        self.refresh_btn.setEnabled(True)

    def _on_logged_in(self, gen: int, user: dict, avatar: bytes) -> None:
        if gen != self._gen:
            return

        nickname = str(user.get("nickname") or "微信用户")
        self.name_label.setText(nickname)
        self.avatar_label.setPixmap(_circle_pixmap(avatar, 76, nickname))

        sex = int(user.get("sex") or 0)
        region = " ".join(
            str(user.get(key) or "").strip()
            for key in ("country", "province", "city")
        ).strip()

        self.info_values["openid"].setText(str(user.get("openid") or "-"))
        self.info_values["unionid"].setText(str(user.get("unionid") or "未绑定"))
        self.info_values["sex"].setText({1: "男", 2: "女"}.get(sex, "未知"))
        self.info_values["region"].setText(region or "-")
        self.info_values["login_at"].setText(time.strftime("%Y-%m-%d %H:%M:%S"))

        self.stack.setCurrentIndex(1)

    def closeEvent(self, event) -> None:  # noqa: N802 - Qt 命名
        self._gen += 1
        self._cancel_event.set()
        super().closeEvent(event)


def main() -> int:
    app = QApplication(sys.argv)
    app.setApplicationName("微信扫码登录 Demo")
    font = QFont("Microsoft YaHei UI", 9)
    app.setFont(font)
    app.setStyleSheet(STYLE)

    window = LoginWindow()
    window.show()
    return app.exec()


if __name__ == "__main__":
    sys.exit(main())
