# -*- coding: utf-8 -*-
"""
linectl.py — "มือ" ที่คุมแอป LINE บนเครื่องนี้ (ส่วนหนึ่งของ LINE Switcher)
────────────────────────────────────────────────────────────────────────
ความสามารถ:
  status   → แอปเปิดอยู่ไหม / ตอนนี้อยู่หน้าไหน (ล็อกอิน หรือ ใช้งาน)
  probe    → ตรวจระบบละเอียด: หน้าต่าง, ปุ่ม/ช่องกรอก (UIA), โฟลเดอร์ข้อมูล
             → เขียน %LOCALAPPDATA%\\line-switcher\\probe_report.{json,txt}
  open     → เปิดแอป LINE ให้ (ถ้าปิดอยู่)
  logout   → ลองสั่ง "ออกจากระบบ" (หาปุ่ม ⋯ → เมนู → ยืนยัน) + log ทุกก้าว
  fill_login(email, password) → กรอกอีเมล/รหัสในหน้าล็อกอินแล้วกดล็อกอิน

Log ทุกอย่าง: %LOCALAPPDATA%\\line-switcher\\logs\\linectl.log
ทดสอบเร็ว:  python linectl.py status | probe | open | logout
"""
import ctypes
import ctypes.wintypes
import json
import os
import pathlib
import sys
import time

# ให้พิกัดคลิกตรงกับจอจริง แม้ตั้ง scale จอ (DPI) ไม่ใช่ 100%
try:
    ctypes.windll.shcore.SetProcessDpiAwareness(2)
except Exception:
    try:
        ctypes.windll.user32.SetProcessDPIAware()
    except Exception:
        pass

# ── pywinauto (ตัวขับ UI) — ถ้ายังไม่ติดตั้ง โมดูลนี้ยัง import ได้ แต่ฟีเจอร์ LINE จะรายงานว่าต้องติดตั้ง ──
HAS_PYWINAUTO = False
Desktop = None
try:
    from pywinauto import Desktop  # type: ignore
    HAS_PYWINAUTO = True
except Exception:
    pass

BASE = pathlib.Path(os.environ.get("LOCALAPPDATA", ".")) / "line-switcher"
LOGDIR = BASE / "logs"
LOGDIR.mkdir(parents=True, exist_ok=True)
LOG = LOGDIR / "linectl.log"

MENU_BTN_NAMES = {"⋯", "…", "...", "•••", "more", "More", "設定", "メニュー", "เมนู"}
LOGOUT_WORDS = ("ออกจากระบบ", "ลงชื่อออก", "log out", "logout", "ログアウト", "サインアウト", "ออกจากบัญชี")
LOGIN_WORDS = ("ล็อกอิน", "เข้าสู่ระบบ", "log in", "login", "ログイン", "ลงชื่อเข้าใช้")
EMAIL_WORDS = ("อีเมล", "email", "อีเมลแอดเดรส", "อีเมล์")


def log(msg):
    line = time.strftime("[%d/%m %H:%M:%S] ") + str(msg)
    try:
        with open(LOG, "a", encoding="utf-8") as f:
            f.write(line + "\n")
    except Exception:
        pass
    print(line)


# ── process helpers (stdlib ล้วน) ────────────────────────────────────────
def _proc_name(pid):
    """ชื่อไฟล์โปรเซส (lowercase) จาก pid"""
    PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
    k32 = ctypes.windll.kernel32
    h = k32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, int(pid))
    if not h:
        return ""
    try:
        buf = ctypes.create_unicode_buffer(1024)
        size = ctypes.wintypes.DWORD(1024)
        if k32.QueryFullProcessImageNameW(h, 0, buf, ctypes.byref(size)):
            return os.path.basename(buf.value).lower()
    finally:
        k32.CloseHandle(h)
    return ""


def line_pids():
    """pids ของ LINE.exe ที่กำลังรัน"""
    try:
        psapi = ctypes.windll.psapi
        arr = (ctypes.wintypes.DWORD * 4096)()
        needed = ctypes.wintypes.DWORD()
        if not psapi.EnumProcesses(arr, ctypes.sizeof(arr), ctypes.byref(needed)):
            return []
        n = needed.value // ctypes.sizeof(ctypes.wintypes.DWORD)
        out = []
        for p in arr[:n]:
            if p and _proc_name(p) == "line.exe":
                out.append(int(p))
        return out
    except Exception as e:
        log("line_pids error: %r" % (e,))
        return []


# ── window helpers ────────────────────────────────────────────────────────
def line_windows():
    """หน้าต่างทั้งหมดของ LINE.exe (ต้องมี pywinauto)"""
    if not HAS_PYWINAUTO:
        return []
    try:
        wins = list(Desktop(backend="uia").windows())
    except Exception as e:
        log("Desktop().windows() error: %r" % (e,))
        return []
    out = []
    for w in wins:
        try:
            if _proc_name(w.element_info.process_id) == "line.exe":
                out.append(w)
        except Exception:
            continue
    return out


def describe(w):
    ei = w.element_info
    d = {}
    for k, f in (("title", lambda: ei.name), ("class", lambda: ei.class_name),
                 ("ct", lambda: ei.control_type), ("pid", lambda: ei.process_id)):
        try:
            d[k] = f()
        except Exception:
            d[k] = "?"
    try:
        d["rect"] = str(w.rectangle())
    except Exception:
        pass
    try:
        d["visible"] = bool(w.is_visible())
    except Exception:
        pass
    return d


def scan_children(w, limit=400):
    """→ (จำนวนช่อง Edit, ชื่อ element ตัวอย่าง, dump แบบเต็ม(จำกัด))"""
    edits = 0
    names = []
    dump = []
    try:
        for i, el in enumerate(w.descendants()):
            if i >= limit:
                break
            try:
                ei = el.element_info
                ct = ei.control_type
                nm = ei.name or ""
                aid = getattr(ei, "automation_id", "") or ""
                cls = ei.class_name or ""
            except Exception:
                continue
            if ct == "Edit":
                edits += 1
            if nm and len(names) < 40:
                names.append(nm[:60])
            if len(dump) < 90:
                rct = ""
                try:
                    rr = el.rectangle()
                    rct = "%d,%d,%d,%d" % (rr.left, rr.top, rr.right, rr.bottom)
                except Exception:
                    pass
                vis = en = None
                try:
                    vis = bool(el.is_visible())
                    en = bool(el.is_enabled())
                except Exception:
                    pass
                dump.append({"ct": ct, "name": nm[:70], "aid": aid[:40], "class": cls[:40],
                             "rect": rct, "vis": vis, "en": en})
    except Exception as e:
        names.append("scan err: %r" % (e,))
    return edits, names, dump


def find_element(win, name_eq=None, name_contains=None, cts=None, limit=600):
    """หาปุ่ม/เมนูในหน้าต่าง (หรือทั้งเดสก์ท็อปถ้า win=None) — คืน wrapper หรือ None"""
    if not HAS_PYWINAUTO:
        return None
    try:
        roots = [win] if win is not None else list(Desktop(backend="uia").windows())
        for root in roots:
            for i, el in enumerate(root.descendants()):
                if i >= limit:
                    break
                try:
                    ei = el.element_info
                    nm = (ei.name or "").strip()
                    ct = ei.control_type
                except Exception:
                    continue
                if cts and ct not in cts:
                    continue
                low = nm.casefold()
                if name_eq and nm in name_eq:
                    return el
                if name_contains and any(w_.casefold() in low for w_ in name_contains):
                    return el
    except Exception as e:
        log("find_element error: %r" % (e,))
    return None


def el_name(el):
    try:
        return el.element_info.name or ""
    except Exception:
        return "?"


def click_el(el):
    try:
        el.click()
    except Exception:
        el.click_input()
    time.sleep(0.6)


def click_xy(x, y):
    """คลิกพิกัดจอตรง ๆ (ใช้เมื่อหาปุ่มไม่เจอ)"""
    ctypes.windll.user32.SetCursorPos(int(x), int(y))
    time.sleep(0.2)
    ctypes.windll.user32.mouse_event(0x0002, 0, 0, 0, 0)  # LEFTDOWN
    ctypes.windll.user32.mouse_event(0x0004, 0, 0, 0, 0)  # LEFTUP
    time.sleep(0.4)


def set_text(el, text):
    try:
        el.set_edit_text(text)
        return True
    except Exception:
        pass
    try:
        el.set_focus()
        esc = text.replace("{", "{{}").replace("}", "{}}")
        el.type_keys(esc, with_spaces=True, set_foreground=True)
        return True
    except Exception as e:
        log("set_text error: %r" % (e,))
        return False


def type_into(el, text):
    """พิมพ์จริงผ่านคีย์บอร์ด (เสถียรกับ Qt) — ข้ามการคลิกถ้าหน้าต่างอยู่นอกจอ"""
    onscreen = False
    try:
        r = el.rectangle()
        vx = ctypes.windll.user32.GetSystemMetrics(76)   # SM_XVIRTUALSCREEN
        vy = ctypes.windll.user32.GetSystemMetrics(77)
        vw = ctypes.windll.user32.GetSystemMetrics(78)
        vh = ctypes.windll.user32.GetSystemMetrics(79)
        cx = (r.left + r.right) // 2
        cy = (r.top + r.bottom) // 2
        onscreen = (vx <= cx <= vx + vw) and (vy <= cy <= vy + vh)
    except Exception:
        pass
    esc = text.replace("{", "{{}").replace("}", "{}}")
    if onscreen:
        try:
            el.click_input()
            time.sleep(0.35)
            el.type_keys(esc, with_spaces=True, set_foreground=True)
            return "keys"
        except Exception:
            pass
    try:
        el.set_focus()
        el.type_keys(esc, with_spaces=True, set_foreground=True)
        return "focus-keys"
    except Exception:
        return "set" if set_text(el, text) else "fail"


# ── ฟีเจอร์หลัก ───────────────────────────────────────────────────────────
def _is_login_win(w):
    """หน้าต่างนี้เป็น 'หน้าล็อกอิน' ของ LINE ไหม (เช็คจากคลาสจริง ไม่เดาจากช่องกรอก)"""
    try:
        cls = w.element_info.class_name or ""
    except Exception:
        cls = ""
    if cls == "LoginWindow":
        return True
    if cls == "AllInOneWindow":
        return False
    try:
        for i, el in enumerate(w.descendants()):
            if i > 120:
                break
            if (getattr(el.element_info, "class_name", "") or "").startswith("Login"):
                return True
    except Exception:
        pass
    return False


def status():
    wins = line_windows()
    if not wins:
        if line_pids():
            return {"state": "app_not_running",
                    "detail": "แอป LINE เปิดอยู่แต่ไม่มีหน้าต่างโชว์ (ซ่อนในถาด/มินิไมซ์)"}
        return {"state": "app_not_running", "detail": "ไม่พบหน้าต่างแอป LINE (แอปยังไม่เปิด)"}
    for w in wins:
        if _is_login_win(w):
            try:
                cls = w.element_info.class_name or "?"
            except Exception:
                cls = "?"
            return {"state": "logged_out", "detail": "พบหน้าต่างล็อกอิน (%s)" % cls}
    return {"state": "logged_in", "detail": "พบหน้าต่างแอป (ไม่มีหน้าล็อกอิน)"}


def open_app():
    p = pathlib.Path(os.environ.get("LOCALAPPDATA", "")) / "LINE" / "bin" / "LineLauncher.exe"
    if line_pids():
        # แอปเปิดอยู่ — ลองดึงหน้าต่างขึ้นมา (กรณีซ่อนในถาด)
        if p.exists():
            try:
                os.startfile(str(p))
                return {"ok": True, "detail": "แอปเปิดอยู่ — สั่งดึงหน้าต่างขึ้นมาแล้ว"}
            except Exception as e:
                return {"ok": True, "detail": "แอปเปิดอยู่ (ดึงหน้าต่างไม่สำเร็จ: %r)" % (e,)}
        return {"ok": True, "detail": "เปิดอยู่แล้ว"}
    if p.exists():
        os.startfile(str(p))
        return {"ok": True, "detail": "สั่งเปิดผ่าน LineLauncher แล้ว (รอแอปขึ้น ~10-20 วิ)"}
    return {"ok": False, "detail": "ไม่พบ LineLauncher.exe ที่คาดไว้"}


def probe():
    rep = {"ts": time.strftime("%Y-%m-%d %H:%M:%S"), "has_pywinauto": HAS_PYWINAUTO,
           "line_pids": line_pids(), "windows": [], "notes": []}
    if not HAS_PYWINAUTO:
        rep["notes"].append("ไม่มีไลบรารี pywinauto — รันผ่านปุ่ม bat ของโปรแกรมเพื่อติดตั้งอัตโนมัติ")
    if not rep["line_pids"]:
        rep["notes"].append("ไม่พบโปรเซส LINE.exe (แอปยังไม่เปิด)")
    for w in line_windows():
        d = describe(w)
        edits, names, dump = scan_children(w)
        d["edit_count"] = edits
        d["names_sample"] = names
        d["dump"] = dump
        rep["windows"].append(d)
    dd = pathlib.Path(os.environ.get("LOCALAPPDATA", "")) / "LINE" / "Data"
    if dd.exists():
        try:
            rep["data_dir"] = sorted(p.name for p in dd.iterdir())[:40]
        except Exception as e:
            rep["notes"].append("อ่านโฟลเดอร์ Data ไม่ได้: %r" % (e,))
    # สรุปอ่านง่าย
    lines = []
    lines.append("ตรวจระบบ LINE Switcher — %s" % rep["ts"])
    lines.append("pywinauto: %s" % ("มี" if HAS_PYWINAUTO else "ไม่มี (!)"))
    lines.append("LINE.exe pids: %s" % rep["line_pids"])
    for i, w in enumerate(rep["windows"]):
        lines.append("หน้าต่าง %d: title=%r class=%r edits=%s rect=%s"
                     % (i, w.get("title"), w.get("class"), w.get("edit_count"), w.get("rect")))
    for n in rep["notes"]:
        lines.append("หมายเหตุ: " + n)
    (BASE / "probe_report.json").write_text(json.dumps(rep, ensure_ascii=False, indent=1), encoding="utf-8")
    (BASE / "probe_report.txt").write_text("\n".join(lines), encoding="utf-8")
    log("probe เขียนรายงานแล้ว: %s" % (BASE / "probe_report.json"))
    return rep


def _menu_rows(main):
    """หาแถวเมนูที่เด้งจากปุ่ม ⋯ (โซนล่างซ้าย) — คืน [(top,left,right,bottom,name,elem)] เรียงบน→ล่าง"""
    try:
        r0 = main.rectangle()
    except Exception:
        return []
    skip_words = ("โปรด", "กำลังมองหา", "โมเดล AI", "ดาวน์โหลด", "โฆษณา", "Advertisement")
    rows = []
    for root in line_windows():
        try:
            for i, el in enumerate(root.descendants()):
                if i >= 900:
                    break
                try:
                    ei = el.element_info
                    ct = ei.control_type or ""
                    cls = ei.class_name or ""
                    if not (cls.startswith(("LcButton", "LcText", "LcCheckBox", "LcWidget")) or
                            ct in ("MenuItem", "Button", "Text", "ListItem")):
                        continue
                    nm = (ei.name or "").strip()
                    if any(sw in nm for sw in skip_words):
                        continue
                    rr = el.rectangle()
                    w = rr.right - rr.left
                    h = rr.bottom - rr.top
                    if not (r0.left + 40 <= rr.left <= r0.left + 190):
                        continue
                    if not (r0.bottom - 300 <= rr.top and rr.bottom <= r0.bottom - 30):
                        continue
                    if not (16 <= h <= 44 and 20 <= w <= 220):
                        continue
                    rows.append((rr.top, rr.left, rr.right, rr.bottom, nm, el))
                except Exception:
                    continue
        except Exception:
            continue
    rows.sort(key=lambda t: t[0])
    out = []
    for row in rows:
        if out and abs(row[0] - out[-1][0]) <= 12:
            continue
        out.append(row)
    return out


def logout():
    res = {"ok": False, "steps": [], "error": ""}
    wins = line_windows()
    if not wins:
        res["error"] = "ไม่พบหน้าต่าง LINE (แอปปิดอยู่หรือไม่ได้เปิดบนจอผู้ใช้)"
        return res
    # เลือกหน้าต่างแอปหลัก (AllInOneWindow = ไม่ใช่หน้าล็อกอิน)
    main = None
    for w in wins:
        try:
            cls = w.element_info.class_name or ""
        except Exception:
            cls = ""
        if cls == "AllInOneWindow":
            main = w
            break
    if main is None:
        for w in wins:
            if not _is_login_win(w):
                main = w
                break
    if main is None:
        main = wins[0]
    try:
        main.restore()
        res["steps"].append("restore หน้าต่าง")
    except Exception as ex:
        res["steps"].append("restore ไม่ได้: %r" % (ex,))
    try:
        main.set_focus()
        res["steps"].append("โฟกัสหน้าต่าง")
    except Exception as ex:
        res["steps"].append("focus ไม่ได้: %r" % (ex,))
    time.sleep(0.5)

    btn = find_element(main, name_eq=MENU_BTN_NAMES, cts={"Button"}, limit=300)
    if btn is None:
        btn = find_element(main, name_contains=("more", "menu", "เมนู", "設定"), cts={"Button"}, limit=300)
    if btn is not None:
        click_el(btn)
        res["steps"].append("กดปุ่มเมนู %r" % el_name(btn))
    else:
        r = main.rectangle()
        click_xy(r.left + 31, r.bottom - 32)
        res["steps"].append("หาปุ่มเมนูไม่เจอ → คลิกปุ่ม ⋯ ล่างซ้าย (fallback ตำแหน่งจริง)")
    time.sleep(1.4)

    # วินิจฉัย: หน้าต่าง + ชื่อที่เห็นหลังกด
    try:
        wins2 = line_windows()
        log("หน้าต่างหลังกด ⋯: %s" % " ;; ".join(
            "%s@%s" % ((w.element_info.class_name or "?"), str(w.rectangle())) for w in wins2))
    except Exception as e:
        log("dump หน้าต่าง error: %r" % (e,))
    try:
        named = []
        for i, el in enumerate(main.descendants()):
            if i >= 700:
                break
            try:
                nm = (el.element_info.name or "").strip()
                if nm and len(named) < 30:
                    rr = el.rectangle()
                    named.append("%s@%d,%d" % (nm[:24], rr.left, rr.top))
            except Exception:
                continue
        log("ชื่อที่เห็น: %s" % " ;; ".join(named))
    except Exception as e:
        log("named dump error: %r" % (e,))

    # หาเมนู 'ออกจากระบบ': 1) ชื่อสั้นตรงคำ  2) แถวรองสุดท้าย  3) ตำแหน่งสำรอง
    rows = _menu_rows(main)
    log("แถวโซนเมนู: %d | %s" % (len(rows), " ;; ".join("%s@y%d" % ((r[4] or "·"), r[0]) for r in rows[:12])))
    item = None
    item_nm = ""
    for row in rows:
        nm = row[4]
        if nm and len(nm) <= 16 and any(w_.casefold() in nm.casefold() for w_ in LOGOUT_WORDS):
            item = row[5]
            item_nm = nm
            break
    r0b = main.rectangle()
    if item is not None:
        click_el(item)
        res["steps"].append("กดเมนู (ชื่อสั้น) %r" % item_nm)
    elif 4 <= len(rows) <= 12:
        row = rows[-2]
        cx = min(max((row[1] + row[2]) // 2, r0b.left + 70), r0b.left + 130)
        cy = (row[0] + row[3]) // 2
        click_xy(cx, cy)
        res["steps"].append("กดเมนูตามตำแหน่ง: แถวรองสุดท้าย (y=%d)" % row[0])
    else:
        click_xy(r0b.left + 85, r0b.bottom - 98)
        res["steps"].append("ไม่พบแถวเมนู — กดตำแหน่งสำรอง (85,-98)")
    time.sleep(1.4)

    # ยืนยัน (ถ้ามี) — เฉพาะชื่อสั้น
    ok_btn = None
    ok_nm = ""
    try:
        for root in [main] + line_windows():
            for i, el in enumerate(root.descendants()):
                if i >= 500:
                    break
                try:
                    nm = (el.element_info.name or "").strip()
                except Exception:
                    continue
                if not nm or len(nm) > 10:
                    continue
                if any(w_.casefold() in nm.casefold() for w_ in ("ตกลง", "ยืนยัน", "OK", "yes", "Log out", "ออกจากระบบ")):
                    ok_btn = el
                    ok_nm = nm
                    break
            if ok_btn is not None:
                break
    except Exception as e:
        log("หายืนยัน error: %r" % (e,))
    if ok_btn is not None:
        click_el(ok_btn)
        res["steps"].append("กดยืนยัน (ชื่อสั้น) %r" % ok_nm)
    else:
        try:
            main.set_focus()
            main.type_keys("{ENTER}")
            res["steps"].append("ไม่พบปุ่มยืนยัน — กด Enter แทน")
        except Exception as ex:
            res["steps"].append("Enter ไม่ได้: %r" % (ex,))
    res["ok"] = True
    return res


def fill_login(email, password):
    res = {"ok": False, "steps": [], "error": ""}
    wins = line_windows()
    login = None
    for w in wins:
        if _is_login_win(w):
            login = w
            break
    if login is None:
        res["error"] = "ไม่พบหน้าต่างล็อกอิน (อาจยังล็อกอินอยู่ หรือหน้าล็อกอินยังไม่ขึ้น)"
        return res
    # ถ้าโชว์ QR อยู่ — หาแท็บ/ลิงก์ "อีเมล" ก่อน
    tab = find_element(login, name_contains=EMAIL_WORDS, cts={"Button", "TabItem", "Text", "Hyperlink", "ListItem"}, limit=250)
    if tab is not None:
        click_el(tab)
        res["steps"].append("สลับไปโหมดอีเมล (%r)" % el_name(tab))
        time.sleep(0.8)
    edits = []
    try:
        for el in login.descendants():
            try:
                if el.element_info.control_type in ("Edit", "ComboBox"):
                    edits.append(el)
            except Exception:
                continue
            if len(edits) >= 4:
                break
    except Exception as ex:
        res["error"] = "อ่านช่องกรอกไม่ได้: %r" % (ex,)
        return res
    try:
        panels = sorted({(getattr(el.element_info, "class_name", "") or "") for el in login.descendants()
                         if (getattr(el.element_info, "class_name", "") or "").startswith("Login")})
        res["steps"].append("panel: %s" % panels)
    except Exception:
        pass
    if len(edits) == 1:
        # โหมดใหม่ของ LINE: ช่องเดียว (ไม่ใช้รหัสผ่าน) — พิมพ์อีเมล แล้ว Enter
        how = type_into(edits[0], email)
        if how == "fail":
            res["error"] = "พิมพ์อีเมลไม่สำเร็จ"
            return res
        res["steps"].append("พิมพ์อีเมลลงช่องเดียว (%s)" % how)
        try:
            edits[0].set_focus()
            edits[0].type_keys("{ENTER}")
            res["steps"].append("กด Enter ส่งอีเมล")
        except Exception as ex:
            res["steps"].append("กด Enter ไม่ได้: %r" % (ex,))
        time.sleep(3)
        edits2 = []
        try:
            for el in login.descendants():
                try:
                    if el.element_info.control_type in ("Edit", "ComboBox"):
                        edits2.append(el)
                except Exception:
                    continue
                if len(edits2) >= 4:
                    break
        except Exception:
            pass
        if len(edits2) >= 2:
            how2 = type_into(edits2[1], password)
            if how2 != "fail":
                res["steps"].append("ช่องที่ 2 โผล่ — กรอกรหัสผ่านแล้ว (%s)" % how2)
                try:
                    edits2[1].set_focus()
                    edits2[1].type_keys("{ENTER}")
                    res["steps"].append("กด Enter ล็อกอิน")
                except Exception as ex:
                    res["steps"].append("กด Enter ไม่ได้: %r" % (ex,))
            else:
                res["steps"].append("ช่องที่ 2 โผล่ แต่พิมพ์รหัสไม่สำเร็จ")
        else:
            res["steps"].append("ยังเหลือช่องเดียว — น่าจะเป็นโหมดยืนยันผ่านมือถือ")
        res["ok"] = True
        return res
    if len(edits) < 2:
        res["error"] = "ไม่เจอช่องกรอกเลย (เจอ %d ช่อง)" % len(edits)
        return res
    if set_text(edits[0], email):
        res["steps"].append("กรอกอีเมล")
    else:
        res["error"] = "พิมพ์อีเมลไม่สำเร็จ"
        return res
    if set_text(edits[1], password):
        res["steps"].append("กรอกรหัสผ่าน")
    else:
        res["error"] = "พิมพ์รหัสผ่านไม่สำเร็จ"
        return res
    btn = find_element(login, name_contains=LOGIN_WORDS, cts={"Button"}, limit=300)
    if btn is not None:
        click_el(btn)
        res["steps"].append("กดปุ่มล็อกอิน %r" % el_name(btn))
    else:
        try:
            edits[1].set_focus()
            edits[1].type_keys("{ENTER}")
            res["steps"].append("หาปุ่มไม่เจอ → กด Enter แทน")
        except Exception as ex:
            res["steps"].append("กด Enter ไม่ได้: %r" % (ex,))
    res["ok"] = True
    return res


# ── CLI เล็ก ๆ ไว้ลองเดี่ยว ๆ ─────────────────────────────────────────────
if __name__ == "__main__":
    cmd = sys.argv[1] if len(sys.argv) > 1 else "status"
    if cmd == "status":
        print(json.dumps(status(), ensure_ascii=False, indent=1))
    elif cmd == "probe":
        rep = probe()
        print(json.dumps({k: rep[k] for k in ("has_pywinauto", "line_pids", "notes") if k in rep},
                         ensure_ascii=False, indent=1))
        print("รายงานเต็ม:", BASE / "probe_report.txt")
    elif cmd == "open":
        print(json.dumps(open_app(), ensure_ascii=False))
    elif cmd == "logout":
        print(json.dumps(logout(), ensure_ascii=False, indent=1))
    else:
        print("ใช้: python linectl.py [status|probe|open|logout]")
