# -*- coding: utf-8 -*-
"""PHIEU.PY — phiếu hỗ trợ: nhánh XỬ LÝ khi bot phải chuyển người thật.

VÌ SAO (24/09/2026)
-------------------
Chủ dự án: "hiện tại phần nhánh xử lý là báo cho admin khi khách muốn liên hệ
hoặc gặp câu khó chưa có. tốt nhất là chúng ta phải có nhánh xử lý đúng
không". Trước đây: Messenger gửi một tin Telegram chữ trơn, web không báo gì,
không ai biết việc đã có người lo chưa, câu khó trả lời xong không quay về kho.

LUỒNG
-----
1. Cổng (app.py cho web, kenh/messenger.py cho Fanpage) gọi `xet_va_mo()` sau
   mỗi lượt. `xet_ly_do()` quyết định có mở phiếu không và vì sao:
     gap_nguoi · de_lai_lh (luôn xét, kể cả khi bot đã trả lời)
     tien · loi · chot_don · cam_ket · chua_co (chỉ khi lõi chuyển người).
2. Mở phiếu CHẠY NỀN (khách không phải chờ): lưu `chat_phieu`, mô hình vai
   tổng hợp viết tóm tắt + câu trả lời gợi ý (dữ liệu nhạy cảm đã che), gửi
   THẺ Telegram có nút. Cùng khách còn phiếu đang mở trong 6 giờ thì gộp câu
   mới vào phiếu cũ, sửa lại thẻ — không bắn thẻ mới.
3. Nút trên thẻ -> Telegram gọi public/api/tele-duyet.php (kiểm chuỗi bí mật
   webhook, ID được phép, chữ ký HMAC, đúng cuộc trò chuyện) -> POST
   /phieu/hanh-dong ở cổng này -> `hanh_dong()`:
     pn  Nhận xử lý  — Messenger: bot nhường khách này như khi nhân viên nhắn tay
     pg  Gửi gợi ý   — chỉ Messenger, trong cửa sổ 24 giờ, gợi ý không còn ⟦?⟧
     pk  Thêm vào kho — tạo mục NHÁP trong Kho tư vấn để chủ duyệt
     pd  Đóng phiếu
4. Phiếu chưa ai nhận sau NHAC_PHUT phút -> nhắc một lần (trả lời vào thẻ).

Chữ ký nút dùng CHUNG khoá `tele_ky_secret` với nút duyệt nạp
(private/app/lib/nap_thong_bao.php: tele_ky), cùng công thức.

Chạy tự kiểm (không cần CSDL, không gọi mạng):  python kenh/phieu.py
"""
from __future__ import annotations

import hashlib
import hmac
import html
import json
import logging
import os
import re
import secrets
import sys
import threading
import time
from typing import Any, Callable, Dict, List, Optional, Tuple

try:
    from congcu import csdl
except ImportError:
    _THU_MUC_CHA = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    if _THU_MUC_CHA not in sys.path:
        sys.path.insert(0, _THU_MUC_CHA)
    from congcu import csdl  # type: ignore

_log = logging.getLogger('chatbot.kenh.phieu')

NHAC_PHUT = int(os.environ.get('CHATBOT_PHIEU_NHAC_PHUT', '15') or 15)
GOP_GIO = 6                      # cùng khách, phiếu còn mở trong bấy nhiêu giờ thì gộp
CHU_KY_NHAC_GIAY = 60
CUA_SO_MESSENGER_GIO = 24        # Meta chỉ cho nhắn RESPONSE trong 24 giờ từ tin cuối của khách

# ma: (nhãn trên thẻ, ưu tiên 1 = cao)
LY_DO: Dict[str, Tuple[str, int]] = {
    'gap_nguoi': ('🙋 Khách muốn gặp người', 1),
    'de_lai_lh': ('📞 Khách để lại liên hệ', 1),
    'tien': ('💰 Tiền / nạp / hoàn', 1),
    'chot_don': ('🛒 Sắp chốt / đặt code', 1),
    'loi': ('🛠 Lỗi kỹ thuật / khiếu nại', 2),
    'cam_ket': ('🤝 Cam kết / chính sách', 2),
    'chua_co': ('❓ Câu chưa có trong kho', 3),
    'xoa_du_lieu': ('🗑 Yêu cầu XOÁ DỮ LIỆU — xử lý trong 30 ngày', 1),
    'meta_giu': ('🔒 Hộp thư Meta giữ cuộc chat — bot không trả lời được', 1),
}
TRANG_THAI = {'mo': '🟡 Chờ nhận', 'da_nhan': '🔵 Đang xử lý', 'da_gui': '🟢 Đã gửi trả lời', 'dong': '⚪ Đã đóng'}

# Câu dặn khách khi Messenger mở được phiếu (người thật nhắn lại ngay trong hộp thư).
CAU_DA_BAO = {
    'vi': ('Dạ câu này em xin phép chuyển cho người phụ trách để trả lời chính xác ạ. '
           'Anh/chị chờ chút, bên em sẽ nhắn lại ngay tại đây. Cần gấp anh/chị nhắn Zalo {zalo}.'),
    'en': ('Let me pass this to the person in charge so you get an exact answer. '
           'They will reply right here shortly. For urgent matters, message Zalo {zalo}.'),
}

# ---------------------------------------------------------------------
# NHẬN DIỆN LÝ DO — so trên chữ đã bỏ dấu (nao.dinh_tuyen.chuan_hoa)
# ---------------------------------------------------------------------
_MAU_SDT = re.compile(r'(?<!\d)(?:\+?84|0)(?:[\s.-]?\d){8,10}(?!\d)')
_MAU_EMAIL = re.compile(r'[\w.+-]+@[\w-]+\.[\w.-]+')
_MAU_GAP_NGUOI = re.compile(
    r'nguoi that|gap (admin|nhan vien|ad\b|chu shop|tu van vien|nguoi)'
    r'|(noi chuyen|trao doi|lien he|ket noi|goi) (voi |cho |truc tiep )?(admin|nhan vien|ad\b|chu shop|tu van vien|nguoi)'
    r'|(so|sdt|so dien thoai|zalo|tele|telegram) (cua )?(admin|shop|ad\b|ben ban|ben minh)'
    r'|goi lai cho (minh|toi|em|anh|chi)|lien he lai (minh|toi|em|anh|chi)')
# Trang /xoa-du-lieu hướng dẫn khách nhắn "Xoá dữ liệu của tôi" — phải thành phiếu.
_MAU_XOA_DL = re.compile(r'xoa (het |toan bo )?(du lieu|thong tin)( ca nhan)? (cua )?(toi|minh|em|anh|chi)|delete my data|xoa du lieu')
_MAU_TIEN = re.compile(r'\bnap\b|nap tien|hoan tien|chuyen khoan|thanh toan|tru tien|mat tien|chua nhan duoc (tien|credit|key)|credit')
_MAU_LOI = re.compile(r'\bloi\b|khong chay|khong vao lenh|bi treo|khong hoat dong|khieu nai|error|bi khoa|khong ket noi')
_MAU_CHOT = re.compile(r'\bcoc\b|dat code|chot|mua (luon|ngay|key|goi)|thue (luon|ngay)|lam (luon|ngay)|bao gia')


def _chuan(van_ban: str) -> str:
    try:
        from nao.dinh_tuyen import chuan_hoa
        return chuan_hoa(van_ban or '')
    except Exception:  # noqa: BLE001
        return (van_ban or '').lower()


def lien_he_trong(van_ban: str) -> str:
    """Số điện thoại / email khách TỰ để lại trong tin (để chủ shop gọi lại)."""
    ds = [re.sub(r'[\s.-]', '', m) for m in _MAU_SDT.findall(van_ban or '')]
    ds += _MAU_EMAIL.findall(van_ban or '')
    return ', '.join(dict.fromkeys(ds))[:250]


def xet_ly_do(cau_hoi: str, kq: Any) -> Optional[str]:
    """Mã lý do mở phiếu, hoặc None nếu lượt này không cần người."""
    c = _chuan(cau_hoi)
    if _MAU_XOA_DL.search(c):
        return 'xoa_du_lieu'
    if lien_he_trong(cau_hoi):
        return 'de_lai_lh'
    # Agent tư vấn (nao/agent.py) tự gọi bao_admin: chốt đặt code (admin báo giá cuối), xin gặp người...
    ly_do_agent = getattr(kq, 'ly_do_phieu', '') or ''
    if ly_do_agent in LY_DO:
        return ly_do_agent
    if _MAU_GAP_NGUOI.search(c):
        return 'gap_nguoi'
    if not getattr(kq, 'chuyen_nguoi', False):
        return None
    if _MAU_TIEN.search(c):
        return 'tien'
    if _MAU_LOI.search(c):
        return 'loi'
    if getattr(kq, 'y_dinh', '') == 'cam_ket':
        return 'cam_ket'
    if _MAU_CHOT.search(c):
        return 'chot_don'
    return 'chua_co'


# ---------------------------------------------------------------------
# TELEGRAM
# ---------------------------------------------------------------------

def _cau_hinh_tele(ct) -> Tuple[str, str]:
    ct.execute('SELECT access_token, chat_id FROM `system` WHERE id = 1')
    d = ct.fetchone() or {}
    return str(d.get('access_token') or '').strip(), str(d.get('chat_id') or '').strip()


def _bi_mat_ky(ct) -> str:
    """Khoá HMAC chung với nút duyệt nạp (tb_bi_mat('tele_ky_secret') bên PHP)."""
    ct.execute("SELECT gia_tri FROM cau_hinh_thong_bao WHERE khoa = 'tele_ky_secret'")
    d = ct.fetchone() or {}
    v = str(d.get('gia_tri') or '')
    if not v:
        v = secrets.token_hex(24)
        ct.execute("""INSERT INTO cau_hinh_thong_bao (khoa, gia_tri, cap_nhat) VALUES ('tele_ky_secret', %s, NOW())
                      ON DUPLICATE KEY UPDATE gia_tri = IF(gia_tri = '' OR gia_tri IS NULL, VALUES(gia_tri), gia_tri)""", (v,))
        ct.execute("SELECT gia_tri FROM cau_hinh_thong_bao WHERE khoa = 'tele_ky_secret'")
        v = str((ct.fetchone() or {}).get('gia_tri') or v)
    return v


def ky(bi_mat: str, hanh: str, id_: int) -> str:
    """Giống hệt tele_ky() bên PHP: hmac_sha256("hanh|id")[:16]."""
    return hmac.new(bi_mat.encode(), f'{hanh}|{int(id_)}'.encode(), hashlib.sha256).hexdigest()[:16]


def _tele(token: str, phuong_thuc: str, du_lieu: Dict[str, Any]) -> Optional[Dict[str, Any]]:
    try:
        import httpx
        r = httpx.post(f'https://api.telegram.org/bot{token}/{phuong_thuc}', json=du_lieu, timeout=10)
        j = r.json()
        if not j.get('ok'):
            _log.warning('Telegram %s lỗi: %s', phuong_thuc, str(j)[:300])
        return j
    except Exception as loi:  # noqa: BLE001
        _log.warning('Không gọi được Telegram %s: %s', phuong_thuc, loi)
        return None


def _goi_y_gui_duoc(p: Dict[str, Any]) -> bool:
    g = str(p.get('goi_y') or '').strip()
    return bool(g) and '⟦' not in g


def the_phieu(p: Dict[str, Any], ten_shop: str, ten_kenh: str) -> str:
    """Nội dung thẻ Telegram (HTML)."""
    h = html.escape
    nhan, _ = LY_DO.get(p['ly_do'], (p['ly_do'], 3))
    dong = [f'🎫 <b>PHIẾU #{int(p["id"])}</b> · {h(nhan)}',
            f'Shop: {h(ten_shop)} · Kênh: {h(ten_kenh)}'
            + (f' · TK web: {h(str(p["nguoi_dung"]))}' if p.get('nguoi_dung') and p['nguoi_dung'] != 'khach' else '')]
    if p.get('lien_he'):
        dong.append(f'📞 Khách để lại: <b>{h(p["lien_he"])}</b>')
    cau = str(p.get('cau_hoi') or '').strip().split('\n')
    dong.append('\n<b>Khách hỏi</b>' + (f' ({int(p.get("so_lan") or 1)} lần)' if int(p.get('so_lan') or 1) > 1 else '') + ':')
    dong += [f'« {h(x[:400])} »' for x in cau[-4:] if x.strip()]
    if p.get('tom_tat'):
        dong.append(f'\n<b>Tóm tắt:</b> {h(str(p["tom_tat"]))}')
    if p.get('goi_y'):
        dong.append(f'\n<b>Gợi ý trả lời:</b>\n{h(str(p["goi_y"]))}')
        if '⟦' in str(p['goi_y']):
            dong.append('<i>Gợi ý còn chỗ ⟦?⟧ cần anh xác nhận — trả lời tay thay vì gửi thẳng.</i>')
    elif p.get('trang_thai') == 'mo':
        dong.append('\n<i>Đang soạn tóm tắt và gợi ý…</i>')
    if p.get('ly_do') == 'meta_giu':
        dong.append('<i>Khách vào qua tin chào mừng/quảng cáo nên hộp thư Meta đang giữ cuộc chat. Trả lời tay trên '
                    'Fanpage; để bot tự nhận: Cài đặt Trang → Nhắn tin nâng cao → chọn autotrading làm ứng dụng mặc định.</i>')
    if p.get('kenh') == 'web':
        dong.append('<i>Khách chat trên web: chưa gửi hộ được, liên hệ qua thông tin khách để lại.</i>')
    tt = TRANG_THAI.get(p.get('trang_thai') or 'mo', '')
    if p.get('nguoi_xu_ly') and p.get('trang_thai') != 'mo':
        tt += f' — {h(str(p["nguoi_xu_ly"]))}'
    if p.get('muc_id'):
        tt += f' · 📚 đã thêm mục nháp #{int(p["muc_id"])}'
    dong.append(f'\n<b>{tt}</b>')
    return '\n'.join(dong)[:4000]


def nut_phieu(p: Dict[str, Any], bi_mat: str, page_id: str = '') -> List[List[Dict[str, str]]]:
    """Bàn phím nút theo trạng thái. Phiếu đã đóng thì không còn nút."""
    if p.get('trang_thai') == 'dong':
        return []
    i = int(p['id'])
    n = lambda chu, hanh: {'text': chu, 'callback_data': f'{hanh}:{i}:{ky(bi_mat, hanh, i)}'}  # noqa: E731
    hang1 = []
    if p.get('trang_thai') == 'mo':
        hang1.append(n('✋ Nhận xử lý', 'pn'))
    if p.get('kenh') == 'messenger' and p.get('trang_thai') in ('mo', 'da_nhan') and _goi_y_gui_duoc(p):
        hang1.append(n('📤 Gửi gợi ý', 'pg'))
    hang2 = []
    if p.get('goi_y') and not p.get('muc_id'):
        hang2.append(n('📚 Thêm vào kho', 'pk'))
    hang2.append(n('✅ Đóng', 'pd'))
    ban = [h for h in (hang1, hang2) if h]
    if p.get('kenh') == 'messenger' and page_id:
        ban.append([{'text': '💬 Mở hộp thư Fanpage',
                     'url': f'https://business.facebook.com/latest/inbox/all?asset_id={page_id}'}])
    return ban


# ---------------------------------------------------------------------
# TÓM TẮT + GỢI Ý (vai tổng hợp)
# ---------------------------------------------------------------------

def _prompt(ly_do: str, ten_shop: str, xung_ho: str) -> str:
    nhan = LY_DO.get(ly_do, (ly_do, 3))[0]
    return (
        f'Em là trợ lý nội bộ của shop {ten_shop}. Dưới đây là hội thoại giữa khách và bot tư vấn; '
        f'bot đã chuyển khách cho người thật, lý do: {nhan}.\n'
        'Viết cho CHỦ SHOP đọc trên điện thoại:\n'
        '- "tom_tat": 1–3 câu — khách đang cần gì, bot đã nói gì, còn vướng chỗ nào.\n'
        '- "goi_y": câu trả lời chủ shop có thể GỬI THẲNG cho khách, nhân danh shop (không tự xưng "admin"). '
        + xung_ho + ' Ngắn 2–6 dòng, văn bản thuần viết hoa đầu câu bình thường, không markdown, không bảng.Chỉ dùng thông tin có trong hội thoại hoặc chắc chắn đúng; '
        'con số (giá, thời hạn, % cọc) hoặc cam kết nào hội thoại chưa có thì ghi dạng ⟦?nội dung cần xác nhận⟧. '
        'Không hứa lợi nhuận. Khách chỉ muốn gặp người thì gợi ý một câu chào và hỏi khách cần hỗ trợ gì.\n'
        'CHỈ trả về JSON: {"tom_tat": "...", "goi_y": "..."}'
    )


def soan_goi_y(ly_do: str, cau_hoi: str, lich_su: List[Dict[str, str]], khong_gian: str,
               _goi: Optional[Callable[[str, str], Optional[str]]] = None) -> Tuple[str, str]:
    """(tom_tat, goi_y). Dữ liệu nhạy cảm đã che trước khi gửi mô hình và sau khi nhận."""
    try:
        from congcu.so_tay import che_du_lieu_nhay_cam as che
    except Exception:  # noqa: BLE001
        che = lambda s: _MAU_EMAIL.sub('[ẩn]', _MAU_SDT.sub('[ẩn]', s or ''))  # noqa: E731
    try:
        import shop
        hs = shop.lay(khong_gian)
        ten, xung_ho = hs.ten, hs.xung_ho
    except Exception:  # noqa: BLE001
        ten, xung_ho = khong_gian, 'Xưng "em", gọi khách "anh/chị".'
    if _goi is None:
        from nao.compact import _goi_mo_hinh
        _goi = lambda h, n: _goi_mo_hinh(h, n, 'tong_hop')  # noqa: E731
    dong = [f"{'Khách' if x.get('vai') == 'khach' else 'Bot'}: {str(x.get('noi_dung') or '')[:1200]}"
            for x in (lich_su or [])[-12:]]
    dong.append(f'Khách (câu cuối, bot đã chuyển người): {cau_hoi}')
    van_ban = _goi(_prompt(ly_do, ten, xung_ho), che('\n'.join(dong))) or ''
    m = re.search(r'\{.*\}', van_ban, re.S)
    try:
        j = json.loads(m.group(0)) if m else {}
    except ValueError:
        j = {}
    return che(str(j.get('tom_tat') or '').strip())[:1000], che(str(j.get('goi_y') or '').strip())[:1500]


# ---------------------------------------------------------------------
# MỞ PHIẾU
# ---------------------------------------------------------------------

def _ten_kenh(p: Dict[str, Any], kenh_row: Optional[Dict[str, Any]]) -> str:
    if p.get('kenh') == 'messenger':
        return 'Messenger' + (f' ({kenh_row.get("ten") or kenh_row.get("page_id")})' if kenh_row else '')
    return 'Chat web'


def _lay_phieu(ct, id_: int) -> Optional[Dict[str, Any]]:
    ct.execute('SELECT * FROM chat_phieu WHERE id = %s', (int(id_),))
    return ct.fetchone()


def _lay_kenh(ct, kenh_id: Optional[int]) -> Optional[Dict[str, Any]]:
    if not kenh_id:
        return None
    ct.execute('SELECT * FROM chat_kenh WHERE id = %s', (int(kenh_id),))
    return ct.fetchone()


def _ve_lai(ct, p: Dict[str, Any], *, _tele_goi=None) -> None:
    """Gửi mới hoặc sửa thẻ Telegram của phiếu cho khớp trạng thái hiện tại."""
    token, chat = _cau_hinh_tele(ct)
    if not token or not chat:
        return
    goi = _tele_goi or (lambda pt, dl: _tele(token, pt, dl))
    kenh_row = _lay_kenh(ct, p.get('kenh_id'))
    try:
        import shop
        ten_shop = shop.lay(p.get('khong_gian') or 'tradingauto').ten
    except Exception:  # noqa: BLE001
        ten_shop = p.get('khong_gian') or ''
    du_lieu: Dict[str, Any] = {
        'chat_id': chat, 'parse_mode': 'HTML', 'disable_web_page_preview': True,
        'text': the_phieu(p, ten_shop, _ten_kenh(p, kenh_row)),
        'reply_markup': {'inline_keyboard': nut_phieu(p, _bi_mat_ky(ct), str((kenh_row or {}).get('page_id') or ''))},
    }
    if p.get('tele_msg_id'):
        goi('editMessageText', dict(du_lieu, message_id=int(p['tele_msg_id'])))
    else:
        j = goi('sendMessage', du_lieu) or {}
        mid = ((j.get('result') or {}).get('message_id')) if isinstance(j, dict) else None
        if mid:
            ct.execute('UPDATE chat_phieu SET tele_msg_id = %s WHERE id = %s', (int(mid), int(p['id'])))
            p['tele_msg_id'] = int(mid)


def mo_phieu(cau_hoi: str, kq: Any, nc: Any, ly_do: str, *, kenh: str,
             kenh_id: Optional[int] = None, khach: str = '',
             _lay_ket_noi: Optional[Callable[[], Any]] = None,
             _soan: Optional[Callable[..., Tuple[str, str]]] = None,
             _lich_su: Optional[List[Dict[str, str]]] = None,
             _tele_goi=None, anh: str = '', _gui_anh=None) -> Optional[int]:
    """Mở (hoặc gộp vào) phiếu và gửi thẻ. Chạy ĐỒNG BỘ — cổng gọi qua mo_phieu_nen()."""
    conn = (_lay_ket_noi or csdl.ket_noi)()
    if conn is None:
        return None
    khong_gian = str(getattr(nc, 'khong_gian', '') or 'tradingauto')
    phien = str(getattr(nc, 'phien', '') or '')[:64]
    khach = (khach or phien)[:64]
    lh = lien_he_trong(cau_hoi)
    uu_tien = LY_DO.get(ly_do, ('', 3))[1]
    with conn:
        with conn.cursor() as ct:
            ct.execute("""SELECT * FROM chat_phieu WHERE khong_gian = %s AND kenh = %s AND khach = %s
                            AND trang_thai IN ('mo', 'da_nhan') AND cap_nhat >= NOW() - INTERVAL %s HOUR
                          ORDER BY id DESC LIMIT 1""", (khong_gian, kenh, khach, GOP_GIO))
            cu = ct.fetchone()
            if cu:
                id_ = int(cu['id'])
                lh_moi = ', '.join(dict.fromkeys([x for x in (str(cu.get('lien_he') or ''), lh) if x]))[:250]
                # Lý do ưu tiên cao hơn thì thay nhãn phiếu (vd. câu khó -> khách để lại số).
                doi = uu_tien < int(cu.get('uu_tien') or 3)
                ct.execute("""UPDATE chat_phieu SET cau_hoi = CONCAT(cau_hoi, '\n', %s), so_lan = so_lan + 1,
                                     lien_he = %s, ly_do = IF(%s, %s, ly_do), uu_tien = LEAST(uu_tien, %s),
                                     cap_nhat = NOW() WHERE id = %s""",
                           (cau_hoi[:1000], lh_moi or None, doi, ly_do, uu_tien, id_))
            else:
                ct.execute("""INSERT INTO chat_phieu (khong_gian, kenh, kenh_id, khach, phien, nguoi_dung, ly_do,
                                     uu_tien, cau_hoi, lien_he, trang_thai, tao_luc, cap_nhat)
                              VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'mo', NOW(), NOW())""",
                           (khong_gian, kenh, kenh_id, khach, phien,
                            str(getattr(nc, 'nguoi_dung', '') or '')[:64] or None, ly_do, uu_tien,
                            cau_hoi[:1000], lh or None))
                id_ = int(ct.lastrowid)
            p = _lay_phieu(ct, id_)
            _ve_lai(ct, p, _tele_goi=_tele_goi)       # báo NGAY, gợi ý tới sau
            if anh:
                # Ảnh khách gửi: trả lời vào thẻ để chủ shop thấy đúng cái khách thấy.
                token, chat = _cau_hinh_tele(ct)
                if token and chat:
                    (_gui_anh or _gui_anh_tele)(token, chat, anh, f'Ảnh khách gửi — phiếu #{id_}', p.get('tele_msg_id'))
        conn.commit()

    # Soạn tóm tắt + gợi ý (mô hình mạnh có thể mất 20–60 giây) rồi sửa thẻ.
    if _lich_su is None:
        try:
            from nao.tra_loi import _lay_lich_su_phien
            _lich_su = _lay_lich_su_phien(phien)
        except Exception:  # noqa: BLE001
            _lich_su = []
    try:
        tom_tat, goi_y = (_soan or soan_goi_y)(ly_do, cau_hoi, _lich_su, khong_gian)
    except Exception:  # noqa: BLE001
        _log.exception('Không soạn được gợi ý cho phiếu #%s.', id_)
        tom_tat, goi_y = '', ''
    conn = (_lay_ket_noi or csdl.ket_noi)()
    if conn is None:
        return id_
    with conn:
        with conn.cursor() as ct:
            ct.execute('UPDATE chat_phieu SET tom_tat = %s, goi_y = %s WHERE id = %s',
                       (tom_tat or None, goi_y or None, id_))
            p = _lay_phieu(ct, id_)
            if p:
                _ve_lai(ct, p, _tele_goi=_tele_goi)
        conn.commit()
    return id_


def mo_phieu_nen(*a, **kw) -> None:
    def _chay():
        try:
            mo_phieu(*a, **kw)
        except Exception:  # noqa: BLE001 - phiếu hỏng không được làm hỏng câu trả lời đã gửi
            _log.exception('Mở phiếu lỗi.')
    threading.Thread(target=_chay, name='mo-phieu', daemon=True).start()


def xet_va_mo(cau_hoi: str, kq: Any, nc: Any, *, kenh: str, kenh_id: Optional[int] = None,
              khach: str = '', anh: str = '') -> Optional[str]:
    """Cổng gọi sau mỗi lượt. Trả mã lý do nếu đã mở phiếu (chạy nền).
    `anh`: đường dẫn ảnh khách gửi đã lưu (kenh/anh.py) — gửi kèm thẻ Telegram."""
    ly_do = xet_ly_do(cau_hoi, kq)
    if ly_do:
        mo_phieu_nen(cau_hoi, kq, nc, ly_do, kenh=kenh, kenh_id=kenh_id, khach=khach, anh=anh)
    return ly_do


def _gui_anh_tele(token: str, chat: str, duong: str, chu_thich: str, tra_loi_msg: Optional[int] = None) -> None:
    """Gửi ẢNH GỐC khách gửi (tệp trên máy chủ, không có link công khai) lên Telegram."""
    if not duong or not os.path.isfile(duong):
        return
    try:
        import httpx
        du = {'chat_id': chat, 'caption': chu_thich[:1000]}
        if tra_loi_msg:
            du['reply_to_message_id'] = str(int(tra_loi_msg))
        with open(duong, 'rb') as f:
            httpx.post(f'https://api.telegram.org/bot{token}/sendPhoto', data=du,
                       files={'photo': ('anh.jpg', f, 'image/jpeg')}, timeout=30)
    except Exception as loi:  # noqa: BLE001
        _log.warning('Không gửi được ảnh khách lên Telegram: %s', loi)


def mo_phieu_bi_giu(kenh_id: int, psid: str, noi_dung: str,
                    _lay_ket_noi: Optional[Callable[[], Any]] = None) -> None:
    """Tin `standby` mà webhook không giành lại được quyền trả lời -> mở phiếu."""
    from giao_uoc import NguCanh
    khong_gian = 'tradingauto'
    conn = (_lay_ket_noi or csdl.ket_noi)()
    if conn is not None:
        with conn:
            with conn.cursor() as ct:
                k = _lay_kenh(ct, kenh_id)
                khong_gian = str((k or {}).get('khong_gian') or khong_gian)
    nc = NguCanh(phien=f'fb{kenh_id}x{psid}'[:64], kenh='messenger', may=f'fb{psid}'[:64],
                 nguoi_dung='khach', khong_gian=khong_gian)

    class _Kq:
        chuyen_nguoi = True
        y_dinh = 'khac'
    mo_phieu_nen(noi_dung, _Kq(), nc, 'meta_giu', kenh='messenger', kenh_id=int(kenh_id), khach=str(psid))


# ---------------------------------------------------------------------
# HÀNH ĐỘNG TỪ NÚT
# ---------------------------------------------------------------------

def hanh_dong(id_: int, hanh: str, nguoi: str, *,
              _lay_ket_noi: Optional[Callable[[], Any]] = None,
              _gui=None, _tele_goi=None) -> Tuple[bool, str]:
    """Thực hiện một nút trên thẻ. Quyền + chữ ký đã được tele-duyet.php kiểm."""
    conn = (_lay_ket_noi or csdl.ket_noi)()
    if conn is None:
        return False, 'Không kết nối được CSDL.'
    nguoi = (nguoi or '')[:120]
    with conn:
        with conn.cursor() as ct:
            p = _lay_phieu(ct, id_)
            if not p:
                return False, f'Không có phiếu #{id_}.'
            if p['trang_thai'] == 'dong' and hanh != 'pd':
                return False, f'Phiếu #{id_} đã đóng.'
            ok, tb = True, ''
            if hanh == 'pn':
                if p['trang_thai'] != 'mo':
                    return False, f'Phiếu đã có người xử lý: {p.get("nguoi_xu_ly") or ""}'
                ct.execute("UPDATE chat_phieu SET trang_thai = 'da_nhan', nguoi_xu_ly = %s, cap_nhat = NOW() WHERE id = %s",
                           (nguoi, id_))
                if p['kenh'] == 'messenger' and p.get('kenh_id'):
                    # Như nhân viên vừa nhắn tay: tin kế tiếp của khách chờ cho_nguoi_phut phút rồi bot mới tiếp quản.
                    ct.execute("""INSERT INTO chat_nhuong (kenh_id, psid, nguoi_tra_loi_luc) VALUES (%s, %s, NOW())
                                  ON DUPLICATE KEY UPDATE nguoi_tra_loi_luc = NOW()""", (int(p['kenh_id']), p['khach']))
                tb = 'Đã nhận. Bot tạm nhường khách này, anh trả lời trong hộp thư Fanpage.' \
                    if p['kenh'] == 'messenger' else 'Đã nhận phiếu.'
            elif hanh == 'pg':
                if p['kenh'] != 'messenger' or not p.get('kenh_id'):
                    return False, 'Chỉ gửi hộ được cho khách Messenger.'
                if not _goi_y_gui_duoc(p):
                    return False, 'Gợi ý trống hoặc còn chỗ ⟦?⟧ — anh trả lời tay nhé.'
                ct.execute("""SELECT MAX(luc) AS cuoi, MAX(luc) >= NOW() - INTERVAL %s HOUR AS con FROM chat_hang_cho
                              WHERE kenh_id = %s AND psid = %s AND loai = 'khach'""",
                           (CUA_SO_MESSENGER_GIO, int(p['kenh_id']), p['khach']))
                cs = ct.fetchone() or {}
                if not cs.get('con'):
                    return False, 'Quá 24 giờ từ tin cuối của khách — Meta không cho gửi nữa.'
                kenh_row = _lay_kenh(ct, p['kenh_id'])
                if _gui is None:
                    from kenh.messenger import gui as _gui  # noqa: N806
                ok, loi = _gui(kenh_row or {}, p['khach'], str(p['goi_y']))
                if not ok:
                    return False, f'Gửi lỗi: {loi}'[:190]
                ct.execute("""INSERT INTO chat_hang_cho (kenh_id, psid, mid, loai, noi_dung, luc, han, trang_thai)
                              VALUES (%s, %s, NULL, 'nhan_vien', %s, NOW(), NOW(), 'xong')""",
                           (int(p['kenh_id']), p['khach'], str(p['goi_y'])[:2000]))
                ct.execute("UPDATE chat_phieu SET trang_thai = 'da_gui', nguoi_xu_ly = %s, cap_nhat = NOW() WHERE id = %s",
                           (nguoi, id_))
                tb = 'Đã gửi gợi ý cho khách.'
            elif hanh == 'pk':
                if p.get('muc_id'):
                    return False, f'Đã có mục nháp #{p["muc_id"]}.'
                if not str(p.get('goi_y') or '').strip():
                    return False, 'Chưa có gợi ý để thêm.'
                cau = [x for x in str(p['cau_hoi']).split('\n') if x.strip()]
                chuan = cau[-1][:255] if cau else ''
                khac = '\n'.join(dict.fromkeys(x[:255] for x in cau[:-1]))
                ct.execute("""INSERT INTO chat_muc (khong_gian, san_pham, y_dinh, cau_hoi_chuan, cach_hoi_khac,
                                     tra_loi_vi, tra_loi_en, duong_dan_nguon, tieu_de_nguon,
                                     trang_thai, nguoi_sua, sua_luc, duyet_luc)
                              VALUES (%s, NULL, 'khac', %s, %s, %s, NULL, '', '', 'nhap', %s, NOW(), NULL)""",
                           (p['khong_gian'], chuan, khac, str(p['goi_y']), f'phieu#{id_}'))
                muc = int(ct.lastrowid)
                ct.execute('UPDATE chat_phieu SET muc_id = %s, cap_nhat = NOW() WHERE id = %s', (muc, id_))
                tb = f'Đã tạo mục nháp #{muc} — duyệt/sửa trong Kho tư vấn.'
            elif hanh == 'pd':
                ct.execute("""UPDATE chat_phieu SET trang_thai = 'dong', dong_luc = NOW(), cap_nhat = NOW(),
                                     nguoi_xu_ly = COALESCE(nguoi_xu_ly, %s) WHERE id = %s""", (nguoi, id_))
                tb = 'Đã đóng phiếu.'
            else:
                return False, 'Nút không hợp lệ.'
            p = _lay_phieu(ct, id_)
            _ve_lai(ct, p, _tele_goi=_tele_goi)
        conn.commit()
    return ok, tb


# ---------------------------------------------------------------------
# NHẮC PHIẾU CHƯA AI NHẬN
# ---------------------------------------------------------------------

def nhac_mot_vong(_lay_ket_noi: Optional[Callable[[], Any]] = None, _tele_goi=None) -> int:
    conn = (_lay_ket_noi or csdl.ket_noi)()
    if conn is None:
        return 0
    n = 0
    with conn:
        with conn.cursor() as ct:
            ct.execute("""SELECT id, tele_msg_id, TIMESTAMPDIFF(MINUTE, tao_luc, NOW()) AS phut FROM chat_phieu
                          WHERE trang_thai = 'mo' AND nhac_luc IS NULL AND tele_msg_id IS NOT NULL
                            AND tao_luc <= NOW() - INTERVAL %s MINUTE AND tao_luc >= NOW() - INTERVAL 1 DAY""",
                       (NHAC_PHUT,))
            ds = ct.fetchall() or []
            if not ds:
                return 0
            token, chat = _cau_hinh_tele(ct)
            if not token or not chat:
                return 0
            goi = _tele_goi or (lambda pt, dl: _tele(token, pt, dl))
            for p in ds:
                goi('sendMessage', {'chat_id': chat, 'reply_to_message_id': int(p['tele_msg_id']),
                                    'text': f'⏰ Phiếu #{int(p["id"])} chờ {int(p["phut"])} phút chưa ai nhận.'})
                ct.execute('UPDATE chat_phieu SET nhac_luc = NOW() WHERE id = %s', (int(p['id']),))
                n += 1
        conn.commit()
    return n


_da_chay = False
_khoa = threading.Lock()


def khoi_dong_nen() -> None:
    """Luồng nhắc phiếu (một lần mỗi tiến trình). CHATBOT_PHIEU=0 để tắt."""
    global _da_chay
    if os.environ.get('CHATBOT_PHIEU', '1').strip() == '0':
        return
    with _khoa:
        if _da_chay:
            return
        _da_chay = True

    def _vong():
        while True:
            time.sleep(CHU_KY_NHAC_GIAY)
            try:
                nhac_mot_vong()
            except Exception:  # noqa: BLE001 - luồng nền không bao giờ được chết
                _log.exception('Vòng nhắc phiếu lỗi.')

    threading.Thread(target=_vong, name='nhac-phieu', daemon=True).start()
    _log.info('Đã bật nhắc phiếu: chưa ai nhận sau %d phút thì nhắc một lần.', NHAC_PHUT)


# =====================================================================
# TỰ KIỂM
# =====================================================================
if __name__ == '__main__':
    logging.disable(logging.WARNING)

    class _K:
        def __init__(s, cn=False, yd='khac'): s.chuyen_nguoi = cn; s.y_dinh = yd

    print('=== 1) xet_ly_do ===')
    ca = [
        ('cho mình nói chuyện với người thật', _K(), 'gap_nguoi'),
        ('cho xin zalo admin', _K(), 'gap_nguoi'),
        ('sdt mình 0912 345 678 gọi lại nhé', _K(), 'de_lai_lh'),
        ('giá dca bao nhiêu', _K(), None),
        ('mình nạp tiền rồi mà chưa nhận được credit', _K(True), 'tien'),
        ('ea không vào lệnh', _K(True), 'loi'),
        ('có hoàn tiền không', _K(True, 'cam_ket'), 'tien'),
        ('bên bạn có bảo hành không', _K(True, 'cam_ket'), 'cam_ket'),
        ('ok chốt, cọc thế nào', _K(True), 'chot_don'),
        ('bot có chạy được trên cTrader không', _K(True), 'chua_co'),
        ('Xoá dữ liệu của tôi', _K(), 'xoa_du_lieu'),
        ('Delete my data', _K(), 'xoa_du_lieu'),
        ('Cho mình xem passview chiến lược tren', _K(), None),   # 28/09: không còn phiếu - bot gửi bảng realtime
        ('Có tk nào xem được không ạ', _K(), None),
        ('có tài khoản chạy thật không', _K(), None),
        ('tài khoản của mình bị lỗi đăng nhập', _K(), None),
    ]
    for cau, k, mong in ca:
        thuc = xet_ly_do(cau, k)
        assert thuc == mong, (cau, thuc, mong)
    assert lien_he_trong('zalo 0912.345.678 hoặc a@b.vn') == '0912345678, a@b.vn'
    print('  OK', len(ca), 'câu')

    print('\n=== 2) chữ ký khớp PHP tele_ky ===')
    # PHP: substr(hash_hmac('sha256', 'pg|12', 'k'), 0, 16)
    assert ky('k', 'pg', 12) == hmac.new(b'k', b'pg|12', hashlib.sha256).hexdigest()[:16]
    print('  OK')

    print('\n=== 3) thẻ + nút theo trạng thái ===')
    p = {'id': 7, 'ly_do': 'chot_don', 'kenh': 'messenger', 'cau_hoi': 'cọc sao\nok chốt', 'so_lan': 2,
         'tom_tat': 'Khách muốn đặt code <EA>', 'goi_y': 'Dạ anh chuyển cọc giúp em ạ', 'trang_thai': 'mo',
         'lien_he': '0912345678', 'nguoi_dung': 'khach'}
    t = the_phieu(p, 'TradingAuto', 'Messenger (Page)')
    assert 'PHIẾU #7' in t and '&lt;EA&gt;' in t and '(2 lần)' in t and '0912345678' in t and 'Chờ nhận' in t
    nut = nut_phieu(p, 'k', '123')
    dd = [b.get('callback_data', '')[:2] for h in nut for b in h]
    assert dd == ['pn', 'pg', 'pk', 'pd', ''] and nut[-1][0]['url'].endswith('asset_id=123'), dd
    assert all(re.match(r'^p[ngkd]:7:[0-9a-f]{16}$', b['callback_data']) for h in nut for b in h if 'callback_data' in b)
    p2 = dict(p, goi_y='Dạ cọc ⟦?30%⟧ ạ')
    assert 'pg' not in [b.get('callback_data', '')[:2] for h in nut_phieu(p2, 'k') for b in h]
    assert 'xác nhận' in the_phieu(p2, 'X', 'Y')
    p3 = dict(p, kenh='web')
    assert [b['callback_data'][:2] for h in nut_phieu(p3, 'k') for b in h] == ['pn', 'pk', 'pd']
    assert nut_phieu(dict(p, trang_thai='dong'), 'k') == []
    print('  OK')

    print('\n=== 4) soan_goi_y: che dữ liệu, đọc JSON có rào ===')
    thay = {}
    def _mo(h, n):
        thay['n'] = n
        return '```json\n{"tom_tat": "khách để lại 0912345678", "goi_y": "Dạ em gọi lại ạ"}\n```'
    tt, gy = soan_goi_y('de_lai_lh', 'số mình 0912345678', [{'vai': 'khach', 'noi_dung': 'mail a@b.vn'}],
                        'tradingauto', _goi=_mo)
    assert '0912345678' not in thay['n'] and 'a@b.vn' not in thay['n'], thay
    assert '0912345678' not in tt and gy == 'Dạ em gọi lại ạ', (tt, gy)
    print('  OK')

    print('\n=== 5) mo_phieu + hanh_dong với CSDL giả ===')

    class _CT:
        def __init__(s, db): s.db = db; s._kq = []; s.lastrowid = 0
        def __enter__(s): return s
        def __exit__(s, *a): return False
        def execute(s, sql, ts=()):
            q = ' '.join(sql.split()); s._kq = []; db = s.db
            if q.startswith('SELECT access_token'):
                s._kq = [{'access_token': 'T', 'chat_id': '42'}]
            elif q.startswith('SELECT gia_tri FROM cau_hinh_thong_bao'):
                s._kq = [{'gia_tri': 'k'}]
            elif q.startswith('SELECT * FROM chat_phieu WHERE khong_gian'):
                s._kq = [p for p in db['phieu'].values() if p['khach'] == ts[2] and p['trang_thai'] in ('mo', 'da_nhan')][-1:]
            elif q.startswith('SELECT * FROM chat_phieu WHERE id'):
                s._kq = [dict(db['phieu'][ts[0]])] if ts[0] in db['phieu'] else []
            elif q.startswith('SELECT * FROM chat_kenh'):
                s._kq = [{'id': 1, 'ten': 'Page', 'page_id': '123', 'page_token': 'P'}]
            elif q.startswith('INSERT INTO chat_phieu'):
                i = len(db['phieu']) + 1
                db['phieu'][i] = dict(id=i, khong_gian=ts[0], kenh=ts[1], kenh_id=ts[2], khach=ts[3], phien=ts[4],
                                      nguoi_dung=ts[5], ly_do=ts[6], uu_tien=ts[7], cau_hoi=ts[8], lien_he=ts[9],
                                      trang_thai='mo', so_lan=1, tele_msg_id=None, tom_tat=None, goi_y=None,
                                      muc_id=None, nguoi_xu_ly=None)
                s.lastrowid = i
            elif q.startswith('UPDATE chat_phieu SET cau_hoi = CONCAT'):
                p = db['phieu'][ts[5]]
                p['cau_hoi'] += '\n' + ts[0]; p['so_lan'] += 1; p['lien_he'] = ts[1]
                if ts[2]: p['ly_do'] = ts[3]
            elif q.startswith('UPDATE chat_phieu SET tele_msg_id'):
                db['phieu'][ts[1]]['tele_msg_id'] = ts[0]
            elif q.startswith('UPDATE chat_phieu SET tom_tat'):
                db['phieu'][ts[2]].update(tom_tat=ts[0], goi_y=ts[1])
            elif q.startswith("UPDATE chat_phieu SET trang_thai = 'da_nhan'"):
                db['phieu'][ts[1]].update(trang_thai='da_nhan', nguoi_xu_ly=ts[0])
            elif q.startswith("UPDATE chat_phieu SET trang_thai = 'da_gui'"):
                db['phieu'][ts[1]].update(trang_thai='da_gui', nguoi_xu_ly=ts[0])
            elif q.startswith("UPDATE chat_phieu SET trang_thai = 'dong'"):
                db['phieu'][ts[1]]['trang_thai'] = 'dong'
            elif q.startswith('UPDATE chat_phieu SET muc_id'):
                db['phieu'][ts[1]]['muc_id'] = ts[0]
            elif q.startswith('INSERT INTO chat_muc'):
                db['muc'].append(ts); s.lastrowid = 90
            elif q.startswith('INSERT INTO chat_nhuong'):
                db['nhuong'].append(ts)
            elif q.startswith('SELECT MAX(luc)'):
                s._kq = [{'con': db['con_24h']}]
            elif q.startswith('INSERT INTO chat_hang_cho'):
                db['nv'].append(ts)
        def fetchone(s): return s._kq[0] if s._kq else None
        def fetchall(s): return list(s._kq)

    class _Conn:
        def __init__(s, db): s.db = db
        def __enter__(s): return s
        def __exit__(s, *a): return False
        def cursor(s): return _CT(s.db)
        def commit(s): pass

    class _NC:
        khong_gian = 'tradingauto'; phien = 'fb1x999'; nguoi_dung = 'khach'

    db = {'phieu': {}, 'muc': [], 'nhuong': [], 'nv': [], 'con_24h': 1}
    tele = []
    def _tg(pt, dl):
        tele.append((pt, dl))
        return {'ok': True, 'result': {'message_id': 555}}
    lay = lambda: _Conn(db)  # noqa: E731
    i = mo_phieu('bot chạy cTrader không', _K(True), _NC(), 'chua_co', kenh='messenger', kenh_id=1, khach='999',
                 _lay_ket_noi=lay, _soan=lambda *a: ('Khách hỏi cTrader', 'Dạ hiện bot chạy MT4/MT5 ạ'),
                 _lich_su=[], _tele_goi=_tg)
    assert i == 1 and db['phieu'][1]['tele_msg_id'] == 555 and db['phieu'][1]['goi_y'].startswith('Dạ')
    assert [x[0] for x in tele] == ['sendMessage', 'editMessageText'] and 'cTrader' in tele[1][1]['text']
    i2 = mo_phieu('số mình 0912345678', _K(), _NC(), 'de_lai_lh', kenh='messenger', kenh_id=1, khach='999',
                  _lay_ket_noi=lay, _soan=lambda *a: ('x', 'y'), _lich_su=[], _tele_goi=_tg)
    assert i2 == 1 and len(db['phieu']) == 1 and db['phieu'][1]['so_lan'] == 2
    assert db['phieu'][1]['ly_do'] == 'de_lai_lh' and db['phieu'][1]['lien_he'] == '0912345678'
    print('  OK — mở phiếu, báo ngay rồi sửa thẻ khi có gợi ý; cùng khách -> gộp, nâng lý do')

    db['phieu'][1]['goi_y'] = 'Dạ hiện bot chạy MT4/MT5 ạ'
    da_gui = []
    ok, tb = hanh_dong(1, 'pn', 'Quang', _lay_ket_noi=lay, _tele_goi=_tg)
    assert ok and db['phieu'][1]['trang_thai'] == 'da_nhan' and db['nhuong'] == [(1, '999')]
    assert not hanh_dong(1, 'pn', 'B', _lay_ket_noi=lay, _tele_goi=_tg)[0]
    db['con_24h'] = 0
    assert not hanh_dong(1, 'pg', 'Q', _lay_ket_noi=lay, _gui=lambda *a: (True, ''), _tele_goi=_tg)[0]
    db['con_24h'] = 1
    ok, tb = hanh_dong(1, 'pg', 'Quang', _lay_ket_noi=lay, _tele_goi=_tg,
                       _gui=lambda k, ps, t: (da_gui.append((k['page_token'], ps, t)) or (True, '')))
    assert ok and da_gui == [('P', '999', 'Dạ hiện bot chạy MT4/MT5 ạ')] and db['nv'], tb
    assert db['phieu'][1]['trang_thai'] == 'da_gui'
    ok, tb = hanh_dong(1, 'pk', 'Quang', _lay_ket_noi=lay, _tele_goi=_tg)
    assert ok and db['muc'][0][1] == 'số mình 0912345678' and db['muc'][0][4] == 'phieu#1' and '#90' in tb
    assert not hanh_dong(1, 'pk', 'Q', _lay_ket_noi=lay, _tele_goi=_tg)[0]
    assert hanh_dong(1, 'pd', 'Quang', _lay_ket_noi=lay, _tele_goi=_tg)[0] and db['phieu'][1]['trang_thai'] == 'dong'
    assert tele[-1][1]['reply_markup']['inline_keyboard'] == []
    assert not hanh_dong(1, 'pg', 'Q', _lay_ket_noi=lay, _tele_goi=_tg)[0]
    print('  OK — nhận (bot nhường), gửi gợi ý (chặn quá 24h), thêm kho, đóng (thẻ hết nút)')
    print('\nTỰ KIỂM kenh/phieu.py QUA.')
