#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""微信自动登录监控程序"""

import sys, os, ctypes, traceback

try:
    # 切换到脚本所在目录，保证相对路径正确
    os.chdir(os.path.dirname(os.path.abspath(__file__)))
except Exception:
    pass  # 切换目录失败不影响后续

try:
    # ===== 正常的模块初始化从这里开始 =====

    import time
    import hashlib
    import random
    import logging
    import configparser
    import socket
    import tempfile
    import base64

    # ── logging (先初始化，再导入第三方库，便于排查导入失败) ─────────────────
    log_format = "%(asctime)s.%(msecs)03d [%(levelname)s] %(message)s"
    logging.basicConfig(
        level=logging.INFO,
        format=log_format,
        datefmt="%Y-%m-%d %H:%M:%S",
        handlers=[
            logging.StreamHandler(),
            logging.FileHandler("wechat_monitor.log", encoding="utf-8"),
        ],
    )
    log = logging.getLogger(__name__)

    # ── 第三方库导入 ─────────────────────────────────────────────────────────
    try:
        import psutil
        import pygetwindow as gw
        import pyautogui
        import win32gui
        import win32con
        import win32api
        from PIL import ImageGrab
        import tkinter as tk
        from tkinter import font as tkfont
        import requests
        import smtplib
        from email.utils import formataddr
        from email.mime.image import MIMEImage
        from email.mime.multipart import MIMEMultipart
        from email.mime.text import MIMEText
    except ImportError as e:
        log.critical("导入第三方库失败: %s", e)
        sys.exit(1)

    class SingleInstance:
        """基于Socket端口绑定的单实例保护。进程死OS自动释放端口，零残留。"""
        def __init__(self, app_name="wechat_auto_login", port=52001):
            self._sock = None
            self._port = port
            self.app_name = app_name

        def try_lock(self):
            try:
                self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                self._sock.bind(("127.0.0.1", self._port))
                self._sock.listen(1)
                return True
            except OSError:
                return False

        def release(self):
            if self._sock:
                try:
                    self._sock.close()
                except OSError:
                    pass
                self._sock = None


    # ── 模块初始化（用 try 捕获所有意外错误） ───────────────────────────────
    try:


        # ── config ─────────────────────────────────────────────────────────────────
        config = configparser.ConfigParser()
        _cfg_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "wechat_auto_login.ini")
        try:
            with open(_cfg_path, "r", encoding="gbk") as _f:
                config.read_file(_f)
        except Exception:
            config.read(_cfg_path, encoding="utf-8")
        EMAIL_ENABLED = config.getboolean("Email", "email_enabled", fallback=True)
        SMTP_SERVER = config.get("Email", "smtp_server", fallback="smtp.qq.com")
        SMTP_PORT = config.getint("Email", "smtp_port", fallback=465)
        USE_SSL = config.getboolean("Email", "use_ssl", fallback=True)
        EMAIL_ACCOUNT = config.get("Email", "email_account", fallback="")
        EMAIL_PASSWORD = config.get("Email", "email_password", fallback="")
        EMAIL_TO = config.get("Email", "email_to", fallback="")
        MAX_EMAILS = config.getint("Email", "max_emails", fallback=5)
        SMS_PHONE = config.get("Email", "sms_phone", fallback="")

        # ── constants ─────────────────────────────────────────────────────────────────
        SCREEN_W = win32api.GetSystemMetrics(0)
        SCREEN_H = win32api.GetSystemMetrics(1)
        CROP_W = 696
        CROP_H = 905
        CENTER_X, CENTER_Y = SCREEN_W // 2, SCREEN_H // 2
        PAUSE = 5
    except Exception:
        tb = traceback.format_exc()
        with open("wechat_fatal.log", "w", encoding="utf-8") as f:
            f.write(f"模块初始化失败\n{tb}\n")
        print(f"FATAL: 模块初始化失败\n{tb}", file=sys.stderr)
        sys.exit(1)



    def _img_to_b64(path):
        with open(path, "rb") as f:
            return base64.b64encode(f.read()).decode("utf-8")


    def _md5_of_image(img):
        return hashlib.md5(img.tobytes()).hexdigest()


    # ── helpers ──────────────────────────────────────────────────────────────


    def send_sms(code, phone):
        """腾讯云短信发送（完整示例）"""
        sdkappid = "1401148277"
        appkey = "1fa7ae8213f97cb5da5882b207dfa959"
        sign = "深圳市健行者户外运动协会"
        template_id = 2673158
        rand = random.randint(100000, 999999)
        t = int(time.time())
        sig_str = f"appkey={appkey}&random={rand}&time={t}&mobile={phone}"
        sig = hashlib.sha256(sig_str.encode()).hexdigest()
        url = f"https://yun.tim.qq.com/v5/tlssmssvr/sendsms?sdkappid={sdkappid}&random={rand}"
        body = {
            "ext": "",
            "extend": "",
            "params": [code, "5"],
            "sig": sig,
            "sign": sign,
            "tel": {"mobile": phone, "nationcode": "86"},
            "time": t,
            "tpl_id": template_id,
        }
        try:
            resp = requests.post(url, json=body, timeout=10)
            return resp.status_code, resp.text
        except Exception as e:
            log.error("短信发送失败: %s", e)
            return -1, str(e)


    def send_qr_email(qr_img_path):
        """发送带二维码附件的邮件"""
        if not EMAIL_ENABLED or not EMAIL_ACCOUNT:
            log.warning("邮件未启用或账号未配置，跳过邮件发送")
            return
        try:
            with open(qr_img_path, "rb") as f:
                img_data = f.read()

            msg = MIMEMultipart("related")
            msg["From"] = formataddr(("微信助手", EMAIL_ACCOUNT))
            msg["To"] = EMAIL_TO
            msg["Subject"] = "微信登录二维码"

            # HTML 正文嵌入二维码（cid）
            html = '<html><body><img src="cid:qrcode"/></body></html>'
            msg.attach(MIMEText(html, "html"))

            # 附件
            img_attach = MIMEImage(img_data, _subtype="png")
            img_attach.add_header("Content-Disposition", "attachment", filename="qrcode.png")
            msg.attach(img_attach)

            # 正文内嵌图片
            img_inline = MIMEImage(img_data, _subtype="png")
            img_inline.add_header("Content-ID", "<qrcode>")
            msg.attach(img_inline)

            if USE_SSL:
                s = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=10)
            else:
                s = smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=10)
                s.starttls()
            s.login(EMAIL_ACCOUNT, EMAIL_PASSWORD)
            s.send_message(msg)
            s.quit()
            log.info("二维码邮件已发送至 %s", EMAIL_TO)
        except Exception as e:
            log.error("邮件发送失败: %s", e)


    def send_resend_link_email():
        """发送重发链接邮件（不含二维码，仅文字+链接）"""
        if not EMAIL_ENABLED or not EMAIL_ACCOUNT:
            log.warning("邮件未启用或账号未配置，跳过重发邮件")
            return
        try:
            from email.mime.text import MIMEText
            msg = MIMEText(
                "微信登录二维码提醒已全部发送完毕，如仍需登录请点击以下链接申请重新发送：\n\n"
                "https://szjxzxh.cn/wechat-resend.html\n\n"
                "（点击后监控程序将重新开始发送二维码通知）",
                "plain", "utf-8"
            )
            from email.utils import formataddr
            msg["From"] = formataddr(("微信助手", EMAIL_ACCOUNT))
            msg["To"] = EMAIL_TO
            msg["Subject"] = "微信登录二维码已过期 - 申请重发"

            if USE_SSL:
                s = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=10)
            else:
                s = smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=10)
                s.starttls()
            s.login(EMAIL_ACCOUNT, EMAIL_PASSWORD)
            s.send_message(msg)
            s.quit()
            log.info("重发链接邮件已发送至 %s", EMAIL_TO)
        except Exception as e:
            log.error("重发链接邮件发送失败: %s", e)


    def _find_wechat_windows():
        """找到标题严格等于'微信'的可见窗口列表（排除包含'微信'的其他窗口）"""
        return [w for w in gw.getAllWindows() if w.title == "微信"]


    def _minimize_all_except_wechat():
        """最小化除微信外的所有可见窗口"""
        # 获取微信的窗口句柄
        wechat_hwnds = set()
        try:
            w = _find_wechat_windows()
            for win in w:
                wechat_hwnds.add(win._hWnd)
        except Exception:
            pass

        def enum_callback(hwnd, _):
            if not win32gui.IsWindowVisible(hwnd):
                return
            # 用句柄排除微信窗口
            if hwnd in wechat_hwnds:
                return
            title = win32gui.GetWindowText(hwnd)
            # 排除监控程序自身
            if title == "微信监控":
                return
            if not title:
                return
            if title in ("任务管理器", "Task Manager"):
                return
            try:
                win32gui.ShowWindow(hwnd, win32con.SW_SHOWMINIMIZED)
            except Exception:
                pass

        win32gui.EnumWindows(enum_callback, None)


    def _capture_center(width=CROP_W, height=CROP_H):
        """截取屏幕中心 width×height 并返回 PIL Image"""
        log.debug("截取屏幕中心 %dx%d", width, height)
        left = CENTER_X - width // 2
        top = CENTER_Y - height // 2
        img = ImageGrab.grab(bbox=(left, top, left + width, top + height))
        log.debug("截图完成: %s", img.size)
        return img


    def _get_ai_result(img):
        """
        使用通义千问视觉模型分析截图。
        返回: "A" | "B" | "未知" | "ERROR"
        AI返回"未知"表示正常工作但没看到A/B。
        AI返回"ERROR"表示AI代码出异常。
        """
        temp_path = os.path.join(tempfile.gettempdir(), "wechat_screen_temp.png")
        img.save(temp_path, "PNG")

        prompt = (
            '分析以下微信登录界面截图，请从以下三个状态中返回一个字母：\n'
            'A = 截图中有绿色的"进入微信"按钮\n'
            'B = 截图中有二维码图案并带有"扫码登录"文字\n'
            'C = 截图中有"开始使用"按钮\n'
            '如果以上都不是，返回"未知"\n'
            '请只返回一个字母或"未知"'
        )

        try:
            b64 = _img_to_b64(temp_path)
            resp = requests.post(
                "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
                headers={
                    "Authorization": "Bearer sk-a6b3e24b985b41768aa3b0df2f688c98",
                    "Content-Type": "application/json",
                },
                json={
                    "model": "qwen-vl-plus",
                    "messages": [{"role": "user", "content": [
                        {"type": "text", "text": prompt},
                        {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
                    ]}],
                    "max_tokens": 50,
                    "temperature": 0,
                },
                timeout=15,
            )
            data = resp.json()
            result = data["choices"][0]["message"]["content"].strip()
        except Exception as e:
            log.error("AI 分析失败: %s", e)
            result = "ERROR"

        try:
            os.unlink(temp_path)
        except OSError:
            pass

        log.info("AI 识别结果: %s", result)
        return result


    def _start_wechat():
        """启动微信"""
        log.info("正在启动微信...")
        candidates = [
            os.path.expandvars(r"%ProgramFiles(x86)%\Tencent\WeChat\WeChat.exe"),
            os.path.expandvars(r"%ProgramFiles%\Tencent\WeChat\WeChat.exe"),
            os.path.expandvars(r"%USERPROFILE%\AppData\Local\Tencent\WeChat\WeChat.exe"),
            os.path.expandvars(r"%ProgramFiles(x86)%\Tencent\Weixin\Weixin.exe"),
            os.path.expandvars(r"%ProgramFiles%\Tencent\Weixin\Weixin.exe"),
        ]
        wechat_path = None
        for p in candidates:
            if os.path.exists(p):
                wechat_path = p
                break
        if wechat_path:
            try:
                os.startfile(wechat_path)
                log.info("已启动微信: %s", wechat_path)
            except Exception as e:
                log.error("启动微信失败: %s", e)
        else:
            log.error("未找到微信可执行文件")


    # ── UI ───────────────────────────────────────────────────────────────────


    class MonitorApp:
        def __init__(self):
            self.running = True
            START_MONITORING = config.getboolean("General", "start_monitoring", fallback=False)
            self.monitoring = START_MONITORING

            # hash 监测模式状态
            self.hash_mode = False
            self.hash_mode_emails_sent = 0
            self.last_md5 = None
            self.qr_sent_count = 0
            self._last_login_hash = None
            self._user_resized = False
            self._resend_link_sent = False
            self._last_logged_msg = ""
            self._last_logged_status = ""
            self._last_log_state = ""

            self._setup_ui()

        def _on_window_configure(self, event):
            """当用户手动调整窗口大小或移动窗口时触发"""
            self._user_resized = True

        def _setup_ui(self):
            self.root = tk.Tk()
            self.root.title("微信监控")

            # 菜单栏
            menubar = tk.Menu(self.root)
            self.root.config(menu=menubar)

            file_menu = tk.Menu(menubar, tearoff=0)
            menubar.add_cascade(label="文件", menu=file_menu)
            file_menu.add_command(label="查看日志", command=self._open_log)
            file_menu.add_command(label="查看配置文件", command=self._open_ini)
            file_menu.add_command(label="打开安装目录", command=self._open_dir)
            file_menu.add_command(label="关于", command=self._show_info)
            file_menu.add_separator()
            file_menu.add_command(label="退出", command=self._quit)
            self.root.attributes("-topmost", True)
            self.root.configure(bg="#f0f0f0")

            # 初始宽高
            self._default_width = 400
            self.root.geometry(f"{self._default_width}x250+0+0")

            # 右上角 ⓘ 按钮
            self.info_btn = tk.Button(
                self.root,
                text="ⓘ",
                font=("Segoe UI", 10),
                fg="#2196F3",
                bg="#f0f0f0",
                bd=0,
                cursor="hand2",
                command=self._show_info,
            )
            self.info_btn.place(relx=1.0, x=-4, y=2, anchor="ne")



            # 中央开关按钮
            self.toggle_btn = tk.Button(
                self.root,
                text="启用监控",
                font=("Segoe UI", 12, "bold"),
                bg="#9E9E9E",
                fg="white",
                bd=0,
                padx=20,
                pady=6,
                cursor="hand2",
                command=self._toggle_monitoring,
            )
            self.toggle_btn.place(relx=0.5, rely=0.4, anchor="center")

            # 状态文字
            self.status_var = tk.StringVar(value="就绪")
            self.status_label = tk.Label(
                self.root,
                textvariable=self.status_var,
                font=("Segoe UI", 9),
                fg="#666666",
                bg="#f0f0f0",
            )
            self.status_label.place(relx=0.5, rely=0.78, anchor="center")

            self.root.protocol("WM_DELETE_WINDOW", self._quit)

            # 启动后立即开始监控循环（无需初始等待）
            self.root.after(100, self._main_loop_tick)
            self.root.bind("<Configure>", self._on_window_configure)

            # 同步UI到初始监控状态
            if self.monitoring:
                self.toggle_btn.config(text="停用监控", bg="#4CAF50", fg="white")
                self._set_status("监控运行中")
                log.info("监控已启用")

        # ── info dialog ──
        def _show_info(self):
            info_win = tk.Toplevel(self.root)
            info_win.title("已知漏洞/局限性的说明")
            info_win.attributes("-topmost", True)
            info_win.configure(bg="white")
            info_win.geometry("540x800")

            txt = tk.Text(
                info_win,
                wrap="word",
                font=("Microsoft YaHei", 10),
                bg="white",
                fg="#333333",
                bd=0,
                padx=15,
                pady=8,
            )
            txt.pack(fill="both", expand=True)

            txt.tag_config("title", font=("Microsoft YaHei", 11, "bold"), spacing1=5)
            txt.tag_config("bold", font=("Microsoft YaHei", 10, "bold"), spacing1=3)
            txt.tag_config("normal", font=("Microsoft YaHei", 10), spacing1=3)

            txt.insert("end", "已知漏洞/局限性的说明\n\n", "title")
            txt.insert("end", "1. 系统弹窗遮挡", "bold")
            txt.insert("end", " — 如Windows更新弹窗、杀毒软件警报等可能盖住微信窗口。最小化操作无法处理它们。\n\n", "normal")

            txt.insert("end", "2. 任务管理器", "bold")
            txt.insert("end", " — 无法通过", "normal")
            txt.insert("end", " ShowWindow ", "bold")
            txt.insert("end", "最小化。当前方案是直接杀进程，但不妥。后续版本可考虑检测到任务管理器时跳过处理。\n\n", "normal")

            txt.insert("end", "3. 多屏场景", "bold")
            txt.insert("end", " — 微信可能在非主屏上打开，最小化其他窗口后主屏显示的是桌面，但微信在副屏。截屏裁切主屏中心会错过。\n\n", "normal")

            txt.insert("end", "4. 高优先级窗口", "bold")
            txt.insert("end", " — 某些程序（如远程桌面、全屏游戏）无法被最小化。\n\n", "normal")

            txt.insert("end", "5. 程序自身CMD窗口", "bold")
            txt.insert("end", " — 如果用 ", "normal")
            txt.insert("end", "python", "bold")
            txt.insert("end", " 直接运行（不用 ", "normal")
            txt.insert("end", "pythonw", "bold")
            txt.insert("end", "），CMD窗口会挡住微信。必须用 ", "normal")
            txt.insert("end", ".bat", "bold")
            txt.insert("end", "（", "normal")
            txt.insert("end", "start_wechat_monitor.bat", "bold")
            txt.insert("end", "）启动。", "normal")

            txt.configure(state="disabled")

            tk.Button(
                info_win,
                text="关闭",
                font=("Microsoft YaHei", 9),
                bg="#f0f0f0",
                bd=1,
                command=info_win.destroy,
            ).pack(pady=(0, 8))

        # ── toggle ──
        def _toggle_monitoring(self):
            self.monitoring = not self.monitoring
            if self.monitoring:
                self.toggle_btn.config(text="停用监控", bg="#4CAF50", fg="white")
                self._set_status("监控运行中")
                log.info("监控已启用")
            else:
                self.toggle_btn.config(text="启用监控", bg="#9E9E9E", fg="white")
                self._set_status("监控已停用")
                # 清理 hash 模式状态
                self.hash_mode = False
                self.hash_mode_emails_sent = 0
                self.last_md5 = None
                self.qr_sent_count = 0
                self._last_login_hash = None
                self._resend_link_sent = False
                log.info("监控已停用")

        # ── open log ──
        def _open_log(self):
            """打开日志文件所在的目录"""
            log_dir = os.path.dirname(os.path.abspath(__file__))
            log_file = os.path.join(log_dir, "wechat_monitor.log")
            if os.path.exists(log_file):
                os.startfile(log_file)
            else:
                try:
                    # 尝试打开日志目录
                    os.startfile(log_dir)
                except Exception:
                    pass

        def _open_ini(self):
            """打开配置文件"""
            log_dir = os.path.dirname(os.path.abspath(__file__))
            ini_file = os.path.join(log_dir, "wechat_auto_login.ini")
            if os.path.exists(ini_file):
                os.startfile(ini_file)

        def _open_dir(self):
            """打开安装目录"""
            log_dir = os.path.dirname(os.path.abspath(__file__))
            os.startfile(log_dir)

        # ── status ──
        def _set_status(self, text):
            self.status_var.set(text)
            self._last_logged_msg = text  # 用于外部读取

        def _log_state(self, level, msg):
            """去重日志：连续相同消息不重复记录（独立变量）"""
            if msg != self._last_log_state:
                if level == "info":
                    log.info("%s", msg)
                elif level == "warn":
                    log.warning("%s", msg)
                elif level == "error":
                    log.error("%s", msg)
                self._last_log_state = msg

        def _is_wechat_foreground(self):
            """检查微信是否在前端（活跃窗口），是则不打断用户操作"""
            try:
                fg_hwnd = win32gui.GetForegroundWindow()
                wechat_wins = _find_wechat_windows()
                for w in wechat_wins:
                    if hasattr(w, '_hWnd') and w._hWnd == fg_hwnd:
                        return True
            except Exception:
                pass
            return False

        def _update_window_width(self):
            if self._user_resized:
                return  # 用户已手动调整窗口，不再自动调整
            text = self.status_var.get()
            f = tkfont.Font(font=self.status_label.cget("font"))
            text_w = f.measure(text) + 40
            if text_w < self._default_width:
                text_w = self._default_width
            cur_w = self.root.winfo_width()
            if abs(cur_w - text_w) > 10:
                x = self.root.winfo_x()
                y = self.root.winfo_y()
                cur_h = self.root.winfo_height()
                if cur_h < 130:
                    cur_h = 130
                self.root.geometry(f"{int(text_w)}x{cur_h}+{x}+{y}")

        # ── quit ──
        def _quit(self):
            self.running = False
            self.root.destroy()
            log.info("程序退出")

        # ── main loop (every 5 seconds) ──
        def _main_loop_tick(self):
            if not self.running:
                return

            if self.monitoring:
                # 仅在非 hash 模式下执行主检测
                # hash 模式有它自己的独立循环
                if not self.hash_mode:
                    self._do_detect()

            self.root.after(PAUSE * 1000, self._main_loop_tick)

        def _do_detect(self):
            """一次完整的检测流程"""
            if not self.monitoring:
                return

            # ① 查进程
            wechat_running = False
            for proc in psutil.process_iter(["name"]):
                try:
                    if proc.info["name"] in ("WeChat.exe", "Weixin.exe"):
                        wechat_running = True
                        break
                except (psutil.NoSuchProcess, psutil.AccessDenied):
                    continue

            if not wechat_running:
                self._set_status("微信未运行，启动中...")
                log.info("微信未运行，启动中...")
                _start_wechat()
                return

            # ② 找可见的"微信"窗口
            wechat_windows = _find_wechat_windows()
            visible = [w for w in wechat_windows if w.visible]
            # 排除最小化窗口（最小化到任务栏也算"visible"但实际不可交互）
            active = [w for w in visible if not win32gui.IsIconic(w._hWnd)]
            if not active:
                if not wechat_windows:
                    # 没有窗口 → 用户右键退出了壳进程，重启微信
                    log.info("微信进程在但无窗口，重新启动...")
                    _start_wechat()
                    return
                else:
                    # 有窗口但最小化，正常状态
                    self._set_status("微信运行中")
                    return
            # 改用 active 进行后续判断
            visible = active

            # ③ 窗口尺寸判断（零API）
            login_window = None
            for w in visible:
                if w.width < 700 and w.height < 850:
                    login_window = w
                    break

            if not login_window:
                self._set_status("微信运行中")
                self._log_state("info", "微信已登录")
                return

            # ④ 最小化其他窗口
            # 微信在登录窗口且在前端时，跳过最小化
            if not self._is_wechat_foreground():
                _minimize_all_except_wechat()
                time.sleep(0.3)
            else:
                time.sleep(0.3)

            # 再次确认窗口尺寸（最小化后可能有窗口挡住微信）
            wechat_windows = _find_wechat_windows()
            visible = [w for w in wechat_windows if w.visible]
            login_window = None
            for w in visible:
                if w.width < 700 and w.height < 850:
                    login_window = w
                    break

            if not login_window:
                self._set_status("微信运行中")
                if visible:
                    log.info("最小化后窗口变为主界面，已登录 %dx%d", visible[0].width, visible[0].height)
                else:
                    log.info("最小化后窗口变为主界面，已登录（无可见窗口）")
                return

            img = _capture_center()
            new_hash = _md5_of_image(img)

            if self._last_login_hash is not None and new_hash == self._last_login_hash:
                # 哈希相同，界面没变
                self._set_status("等待扫码登录")
                return

            # 哈希不同: ⑤ 重新判断窗口尺寸
            self._last_login_hash = new_hash  # 更新哈希

            # 先查一次窗口尺寸（不用AI）
            wechat_windows = _find_wechat_windows()
            visible = [w for w in wechat_windows if w.visible]
            login_window = None
            for w in visible:
                if w.width < 700 and w.height < 850:
                    login_window = w
                    break

            if not login_window:
                # 窗口变大了 → 登录成功
                self._set_status("微信运行中")
                # 稳态信息，不写日志
                if self.hash_mode:
                    self.hash_mode = False
                    self._last_login_hash = None
                return

            # ⑥ 仍是登录窗口，调AI判断状态
            result = _get_ai_result(img)

            if result == "A":
                self._set_status("正在点击登录...")
                log.info("检测到'进入微信'按钮，正在点击...")
                self.root.withdraw()
                time.sleep(0.2)
                pyautogui.click(SCREEN_W * 0.5, SCREEN_H * 0.58)
                time.sleep(0.5)
                self.root.deiconify()
                self._set_status("已点击登录")

            elif result == "C":
                self._set_status("正在点击登录...")
                log.info("检测到'开始使用'按钮，正在点击...")
                self.root.withdraw()
                time.sleep(0.2)
                # 找到微信窗口，计算按钮绝对坐标
                for w in active:
                    if w.width >= 680 and w.width <= 690:
                        cx = w.left + 353
                        cy = w.top + 395
                        pyautogui.click(cx, cy)
                        break
                time.sleep(0.5)
                self.root.deiconify()
                self._set_status("已点击登录")

            elif result == "B":
                self.qr_sent_count += 1
                self._set_status(f"请扫码登录 ({self.qr_sent_count}/{MAX_EMAILS})")
                self._log_state("info", "检测到二维码")
                qr_path = os.path.join(tempfile.gettempdir(), f"wechat_qr_{int(time.time())}.png")
                img.save(qr_path, "PNG")
                send_qr_email(qr_path)
                if SMS_PHONE:
                    send_sms("888888", SMS_PHONE)
                self.hash_mode = True
                self.hash_mode_emails_sent = 1
                self.last_md5 = _md5_of_image(img)
                self._hash_loop_tick()

            elif result == "未知":
                self._set_status("微信运行中")

            elif result == "ERROR":
                self._set_status("AI检测异常")

        # ── hash monitoring loop (every 5 seconds) ──
        def _hash_loop_tick(self):
            if not self.monitoring or not self.hash_mode:
                return

            # ① 先查微信进程是否还活着
            wechat_alive = False
            for proc in psutil.process_iter(["name"]):
                try:
                    if proc.info["name"] in ("WeChat.exe", "Weixin.exe"):
                        wechat_alive = True
                        break
                except (psutil.NoSuchProcess, psutil.AccessDenied):
                    continue
            if not wechat_alive:
                # 微信崩了，退出哈希模式，主循环会自动重启
                self.hash_mode = False
                self._last_login_hash = None
                self.qr_sent_count = 0
                self._set_status("微信未运行，启动中...")
                return  # _do_detect 会在下一轮自动启动微信

            # ② 检查邮件已发满后的处理
            if self.hash_mode_emails_sent >= MAX_EMAILS:
                if not self._resend_link_sent:
                    # 还没发第6封链接邮件，继续监测窗口
                    pass  # 不走下面，会继续执行到窗口尺寸检查
                else:
                    # 第6封已发，进入等待重发模式
                    self._check_resend_flag()
                    return

            # ③ 查窗口尺寸
            wechat_windows = _find_wechat_windows()
            visible = [w for w in wechat_windows if w.visible]
            # 排除最小化窗口（最小化到任务栏也算"visible"但实际不可交互）
            active = [w for w in visible if not win32gui.IsIconic(w._hWnd)]
            if not active:
                if not wechat_windows:
                    # 没有窗口 → 用户右键退出了壳进程，重启微信
                    log.info("微信进程在但无窗口，重新启动...")
                    _start_wechat()
                    return
                else:
                    # 有窗口但最小化，正常状态
                    self._set_status("微信运行中")
                    return
            # 改用 active 进行后续判断
            visible = active
            login_window = None
            for w in visible:
                if w.width < 700 and w.height < 850:
                    login_window = w
                    break

            if not login_window:
                # 窗口变大 → 已登录
                self.hash_mode = False
                self._last_login_hash = None
                self.qr_sent_count = 0
                self._set_status("微信运行中")
                # 稳态信息，不写日志
                return

            # ④ 截图+哈希比较
            # 微信在登录窗口且在前端时，跳过最小化
            if not self._is_wechat_foreground():
                _minimize_all_except_wechat()
                time.sleep(0.3)
            else:
                time.sleep(0.3)
            img = _capture_center()
            new_md5 = _md5_of_image(img)

            if new_md5 == self.last_md5:
                # 哈希没变，不做操作
                self.root.after(PAUSE * 1000, self._hash_loop_tick)
                return

            # 哈希变了：检查窗口尺寸是否变化
            self._log_state("info", "二维码已变化")
            wechat_windows = _find_wechat_windows()
            visible = [w for w in wechat_windows if w.visible]
            still_login = False
            for w in visible:
                if w.width < 700 and w.height < 850:
                    still_login = True
                    break

            if not still_login:
                self.hash_mode = False
                self._last_login_hash = None
                self.qr_sent_count = 0
                self._set_status("微信运行中")
                # 稳态信息，不写日志
                return

            # 仍然是登录窗口→发通知
            time.sleep(2)
            img2 = _capture_center()

            if self.hash_mode_emails_sent >= MAX_EMAILS:
                if not self._resend_link_sent:
                    # 第5封已发完，现在二维码过期了，发第6封链接邮件
                    send_resend_link_email()
                    self._resend_link_sent = True
                    self._set_status("二维码已过期，等待重发")
                    log.info("二维码已过期，第6封链接邮件已发送")
                    self.root.after(PAUSE * 1000, self._check_resend_flag)
                    return
            else:
                # 正常发邮件（第1-5封）
                qr_path = os.path.join(tempfile.gettempdir(), f"wechat_qr_{int(time.time())}.png")
                img2.save(qr_path, "PNG")

                self.hash_mode_emails_sent += 1

                # 发邮件
                send_qr_email(qr_path)

                # 短信：只在第1次发（无论多少轮，只发一次）
                if self.hash_mode_emails_sent == 1:
                    if SMS_PHONE:
                        send_sms("888888", SMS_PHONE)

                # 第5封邮件已发完，继续监测，等二维码过期再发第6封
                if self.hash_mode_emails_sent >= MAX_EMAILS and not self._resend_link_sent:
                    log.info("第5封邮件已发送，等待二维码过期...")
                    self._set_status("第5封已发送，等待二维码过期")

                self.last_md5 = _md5_of_image(img2)
                self._set_status(f"请扫码登录 ({self.hash_mode_emails_sent}/{MAX_EMAILS})")
                self.root.after(PAUSE * 1000, self._hash_loop_tick)

        def _check_resend_flag(self):
            """检查服务器上是否有重发标记"""

            # ① 先查窗口尺寸
            wechat_windows = _find_wechat_windows()
            visible = [w for w in wechat_windows if w.visible]
            login_window = None
            for w in visible:
                if w.width < 700 and w.height < 850:
                    login_window = w
                    break

            if not login_window:
                # 窗口变大了 → 已登录
                self.hash_mode = False
                self._last_login_hash = None
                self.qr_sent_count = 0
                self._resend_link_sent = False
                self._set_status("微信运行中")
                # 稳态信息，不写日志
                return

            # ② 查flag
            import requests
            url = "https://szjxzxh.cn/wechat_resend.flag"
            try:
                resp = requests.get(url, timeout=5)
                if resp.status_code == 200:
                    # 用户申请了重发，删除flag文件
                    delete_url = "https://szjxzxh.cn/cgi/wechat_resend.py?action=delete"
                    try:
                        requests.get(delete_url, timeout=5)
                    except Exception:
                        pass
                    # 重置计数，开始新一轮
                    self.hash_mode_emails_sent = 0
                    self.last_md5 = None
                    self._resend_link_sent = False
                    self._set_status("准备重新发送通知...")
                    log.info("检测到重发请求，开始新一轮通知")
                    self.root.after(PAUSE * 1000, self._hash_loop_tick)
                    return
            except Exception as e:
                log.debug("检查重发标记失败: %s", e)

            # 无重发请求，继续等
            self._set_status("二维码已过期，等待重发")
            self.root.after(PAUSE * 1000, self._check_resend_flag)


    def main():
        si = SingleInstance()
        if not si.try_lock():
            log.error("已有实例在运行")
            sys.exit(1)
        app = MonitorApp()
        try:
            app.root.mainloop()
        finally:
            si.release()


    if __name__ == "__main__":
        try:
            main()
        except Exception as e:
            import traceback as _tb
            tb_str = _tb.format_exc()
            print(f"\n{'='*60}\n程序异常退出：{e}\n\n{tb_str}\n{'='*60}", flush=True)
            input("\n按回车键关闭此窗口...")

except Exception:
    tb = traceback.format_exc()
    # 写入日志文件
    for fname in ["wechat_crash.log", "wechat_fatal.log"]:
        try:
            with open(fname, "w", encoding="utf-8") as f:
                f.write(f"模块初始化失败\n{tb}\n")
        except Exception:
            pass
    # Windows 消息弹窗（确保用户能看到错误）
    try:
        ctypes.windll.user32.MessageBoxW(0, tb, "微信监控 - 启动错误", 0)
    except Exception:
        pass
    sys.exit(1)
