# -*- coding: utf-8 -*-
"""TOM_TAT.PY — phân loại + tóm tắt + lưu ý cho mỗi phiên đã xong, rồi
cập nhật hồ sơ khách. Chạy NỀN trong chatbot.service, không ai phải chờ.

VÌ SAO
------
Chủ dự án (23/09/2026): "tốt nhất là có 1 quy trình phân loại hoặc lưu ý về
khách cùng tóm tắt lại để sau này tư vấn hợp lý hơn". Sổ tay (congcu/
so_tay.py) giữ được sự thật kỹ thuật — sản phẩm, MT4/MT5, sàn, lỗi — nhưng
không hiểu được khách ĐANG Ở ĐÂU trên đường mua: mới tìm hiểu, đang kẹt cài
đặt, hay đang so giá. Cái đó cần đọc hiểu cả cuộc chat, nên dùng mô hình —
nhưng chỉ MỘT lần mỗi phiên, lúc phiên đã nghỉ, bằng model rẻ.

KHI NÀO MỘT PHIÊN ĐƯỢC TÓM TẮT
------------------------------
Phiên không có lượt mới trong NGHI_PHUT phút và chưa tóm tắt (hoặc có lượt
mới sau lần tóm tắt trước). Mỗi vòng xử lý tối đa MOI_VONG phiên.

KHÔNG CÓ MÔ HÌNH / HẾT NGÂN SÁCH
--------------------------------
Rơi về tóm tắt bằng LUẬT từ sổ tay + đếm ý định. Kém hơn nhưng không bao giờ
để phiên nằm chưa phân loại.

DỮ LIỆU GỬI CHO MÔ HÌNH
-----------------------
Đi qua so_tay.che_du_lieu_nhay_cam() trước: email, số điện thoại, chuỗi
giống key, dãy số dài đều thành '[ẩn]'. Kết quả mô hình trả về cũng đi qua
lớp che đó thêm một lần trước khi ghi.

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

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

try:
    from congcu import csdl
    from congcu.so_tay import chuan_hoa_so_tay, gop, thanh_van_ban, che_du_lieu_nhay_cam
    from congcu.phien import khoa_ho_so
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
    from congcu.so_tay import chuan_hoa_so_tay, gop, thanh_van_ban, che_du_lieu_nhay_cam  # type: ignore
    from congcu.phien import khoa_ho_so  # type: ignore

_log = logging.getLogger('chatbot.nao.tom_tat')

NGHI_PHUT = int(os.environ.get('CHATBOT_TOM_TAT_NGHI_PHUT', '30') or 30)
CHU_KY_GIAY = int(os.environ.get('CHATBOT_TOM_TAT_CHU_KY_GIAY', '300') or 300)
MOI_VONG = 10
SO_LUOT_DOC = 60          # đọc tối đa bấy nhiêu lượt gần nhất của một phiên
CAT_CAU_BOT = 1500        # câu bot đưa vào prompt tóm tắt cắt còn bấy nhiêu

# Nhãn giai đoạn — trang quản trị (public/admin/chat-khach.php) có bảng tên
# tiếng Việt y hệt. Thêm nhãn thì sửa CẢ HAI nơi.
PHAN_LOAI = {
    'tim_hieu': 'Tìm hiểu sản phẩm',
    'hoi_gia': 'Quan tâm giá / muốn mua',
    'dang_cai': 'Đang cài đặt',
    'gap_loi': 'Gặp lỗi, cần hỗ trợ',
    'dang_dung': 'Đang dùng, hỏi cấu hình',
    'phan_nan': 'Không hài lòng / phàn nàn',
    'khac': 'Khác',
}
MUC_QUAN_TAM = ('nong', 'am', 'lanh')


# =====================================================================
# DỰNG PROMPT
# =====================================================================

_CAU_DAN = (
    "Bạn ghi chép cho đội bán hàng của {shop}. Bạn nhận MỘT phiên chat giữa khách và bot tư vấn, kèm hồ sơ "
    "cũ của khách nếu có. Trả về DUY NHẤT một object JSON, không kèm chữ nào khác, "
    "đúng dạng:\n"
    '{"phien": {"tom_tat": "", "phan_loai": "", "muc_quan_tam": "", "bang_khoan": "", "luu_y": ""},\n'
    ' "ho_so": {"tom_tat": "", "phan_loai": "", "muc_quan_tam": "", "bang_khoan": "", "luu_y": ""}}\n'
    "\n"
    "Ý nghĩa:\n"
    "- phien.tom_tat: khoảng 40-80 từ — khách hỏi gì, đã được giải quyết tới đâu.\n"
    "- ho_so.tom_tat: khoảng 100-200 từ — GỘP hồ sơ cũ với phiên này thành bức tranh hiện "
    "tại về khách: dùng sản phẩm nào, nền tảng, sàn, đang ở giai đoạn nào. Điều mới "
    "thay thế điều cũ nếu mâu thuẫn.\n"
    f"- phan_loai: đúng MỘT trong: {', '.join(PHAN_LOAI)}.\n"
    "  tim_hieu = hỏi sản phẩm làm được gì; hoi_gia = hỏi giá, gói, cách mua; "
    "dang_cai = đang làm theo hướng dẫn cài; gap_loi = báo lỗi, bot không chạy; "
    "dang_dung = đã chạy, hỏi tối ưu cấu hình; phan_nan = không hài lòng, đòi hoàn tiền.\n"
    "- muc_quan_tam: nong = hỏi giá/cách mua/sắp mua; am = tìm hiểu nghiêm túc, cài thử; "
    "lanh = hỏi qua loa, chào rồi đi.\n"
    "- bang_khoan: điều khách còn lo ngại hoặc chưa được giải đáp (giá, rủi ro, sàn hỗ "
    "trợ...). Rỗng nếu không có.\n"
    "- luu_y: MỘT câu hướng dẫn cho lần tư vấn sau — nên ưu tiên điều gì, tránh điều "
    "gì (vd \"hướng dẫn theo MT4, khách nhạy về giá nên nêu bản dùng thử trước\"). "
    "KHÔNG viết kiểu \"hỏi lại khách chuyện lần trước\".\n"
    "\n"
    "LUẬT:\n"
    "- CHỈ ghi điều khách thật sự nói hoặc thể hiện. Không đoán, không suy diễn.\n"
    "- KHÔNG ghi key, mật khẩu, email, số điện thoại, số tài khoản. Thấy '[ẩn]' thì bỏ qua.\n"
    "- Viết tiếng Việt, ngắn, kiểu ghi chú nội bộ."
)



def _ho_so(khong_gian):
    """Hồ sơ shop (shop/) — phần riêng của từng shop."""
    import shop
    return shop.lay(khong_gian)


def _dung_prompt(ho_so_cu: Optional[Dict[str, Any]], so_tay_phien: Dict[str, Any],
                 cac_luot: List[Dict[str, Any]], compact: str = '',
                 khong_gian: Optional[str] = None) -> Tuple[str, str]:
    phan = []
    if ho_so_cu and (ho_so_cu.get('tom_tat') or ho_so_cu.get('luu_y')):
        phan.append(
            'HỒ SƠ CŨ:\n'
            f"- Tóm tắt: {ho_so_cu.get('tom_tat') or '(trống)'}\n"
            f"- Giai đoạn: {ho_so_cu.get('phan_loai') or '(chưa có)'} · "
            f"Mức quan tâm: {ho_so_cu.get('muc_quan_tam') or '(chưa có)'}\n"
            f"- Băn khoăn: {ho_so_cu.get('bang_khoan') or '(không)'}\n"
            f"- Lưu ý: {ho_so_cu.get('luu_y') or '(không)'}"
        )
    else:
        phan.append('HỒ SƠ CŨ: (khách mới, chưa có)')
    if ho_so_cu and ho_so_cu.get('ghi_chu_admin'):
        # 02/10/2026: ghi chú chủ shop (Admin -> Hồ sơ khách) là sự thật ưu tiên nhất — tóm tắt lại không được trái.
        phan.append('GHI CHÚ CỦA CHỦ SHOP (ĐÚNG NHẤT — hồ sơ mới PHẢI khớp, không được viết trái): '
                    + str(ho_so_cu['ghi_chu_admin']))
    st = thanh_van_ban(so_tay_phien)
    if st:
        phan.append('SỔ TAY PHIÊN (trích tự động):\n' + st)
    if compact and compact.strip():
        # Bản tóm gọn giữa phiên (nao/compact.py) — bức tranh đã được mô
        # hình chắt lọc, đáng tin hơn đọc lại từng lượt đã bị cắt ngắn.
        phan.append('BẢN TÓM GỌN GIỮA PHIÊN:\n' + compact.strip())
    dong = []
    co_nv = False
    for l in cac_luot:
        dong.append('Khách: ' + che_du_lieu_nhay_cam(l.get('cau_hoi') or ''))
        bot = (l.get('tra_loi') or '').strip()
        la_nv = bot.startswith('[Nhân viên shop nhắn tay]')
        co_nv = co_nv or la_nv
        if len(bot) > CAT_CAU_BOT:
            bot = bot[:CAT_CAU_BOT].rstrip() + '…'
        if la_nv:
            dong.append('Nhân viên shop: ' + che_du_lieu_nhay_cam(bot[len('[Nhân viên shop nhắn tay]'):].strip()))
        else:
            dong.append('Bot: ' + che_du_lieu_nhay_cam(bot))
    phan.append('PHIÊN CHAT:\n' + '\n'.join(dong))
    if co_nv:
        # 02/10/2026 chủ dự án: bot nhận diện sai nhu cầu thì chủ shop nhắn tay đính chính rồi gửi like để bot
        # tiếp quản -> hồ sơ phải theo nhân viên, không giữ nhận định sai cũ.
        phan.append('LƯU Ý: Nhân viên shop đã nhắn tay trong phiên. Nhân viên là người hiểu đúng khách: nhu cầu, giai '
                    'đoạn và băn khoăn của khách lấy theo lời nhân viên; nhận định nào trong HỒ SƠ CŨ hoặc câu trả lời '
                    'của bot trái với nhân viên thì BỎ.')
    hs = _ho_so(khong_gian)
    return _CAU_DAN.replace('{shop}', hs.vai_ghi_chep or hs.ten), '\n\n'.join(phan)


# =====================================================================
# ĐỌC KẾT QUẢ MÔ HÌNH — không tin, soát hết
# =====================================================================

def _tim_json(van_ban: str) -> Optional[dict]:
    """Mô hình hay bọc JSON trong ```json ... ``` hoặc kèm câu mở đầu —
    lấy object {...} ngoài cùng đầu tiên."""
    if not van_ban:
        return None
    dau, cuoi = van_ban.find('{'), van_ban.rfind('}')
    if dau < 0 or cuoi <= dau:
        return None
    try:
        kq = json.loads(van_ban[dau:cuoi + 1])
    except ValueError:
        return None
    return kq if isinstance(kq, dict) else None


def _sach_khoi(khoi: Any, du_phong: Dict[str, Any], dai_tom_tat: int) -> Dict[str, Any]:
    khoi = khoi if isinstance(khoi, dict) else {}

    def chu(k: str, dai: int) -> str:
        v = khoi.get(k)
        v = v if isinstance(v, str) else ''
        return che_du_lieu_nhay_cam(v.strip())[:dai]

    pl = chu('phan_loai', 24).lower()
    mq = chu('muc_quan_tam', 8).lower()
    return {
        'tom_tat': chu('tom_tat', dai_tom_tat) or du_phong['tom_tat'],
        'phan_loai': pl if pl in PHAN_LOAI else du_phong['phan_loai'],
        'muc_quan_tam': mq if mq in MUC_QUAN_TAM else du_phong['muc_quan_tam'],
        'bang_khoan': chu('bang_khoan', 500),
        'luu_y': chu('luu_y', 500),
    }


# =====================================================================
# DỰ PHÒNG BẰNG LUẬT
# =====================================================================

def tom_tat_bang_luat(so_tay: Dict[str, Any], so_luot: int) -> Dict[str, Any]:
    st = chuan_hoa_so_tay(so_tay)
    dem = st['y_dinh']
    if dem.get('cam_ket'):
        pl = 'phan_nan'
    elif dem.get('loi'):
        pl = 'gap_loi'
    elif dem.get('gia'):
        pl = 'hoi_gia'
    elif dem.get('huong_dan'):
        pl = 'dang_cai'
    elif st['san_pham']:
        pl = 'tim_hieu'
    else:
        pl = 'khac'
    if dem.get('gia'):
        mq = 'nong'
    elif so_luot >= 3 or dem.get('huong_dan') or dem.get('loi'):
        mq = 'am'
    else:
        mq = 'lanh'
    ban = thanh_van_ban(st).replace('\n', '. ')
    return {
        'tom_tat': (f'{so_luot} lượt. ' + ban) if ban else f'{so_luot} lượt, chưa nêu sản phẩm cụ thể.',
        'phan_loai': pl, 'muc_quan_tam': mq, 'bang_khoan': '', 'luu_y': '',
    }


# =====================================================================
# XỬ LÝ MỘT PHIÊN
# =====================================================================

def _goi_mo_hinh(he_thong: str, nguoi_dung: str) -> Optional[str]:
    """None nếu không có mô hình dùng được / hết ngân sách / lỗi."""
    try:
        from bao_ve.ngan_sach import con_ngan_sach
        if not con_ngan_sach():
            _log.info('Hết ngân sách ngày — tóm tắt bằng luật.')
            return None
    except Exception:  # noqa: BLE001 - không đọc được ngân sách thì coi như còn, giống tra_loi.py
        pass
    try:
        from nao.tra_loi import _lay_nha_cung_cap_dung_chung
        ncc = _lay_nha_cung_cap_dung_chung()
        if ncc is None or getattr(ncc, 'ten', '') == 'rong':
            return None
        from congcu.cau_hinh_ai import mo_hinh
        van_ban = ncc.tom_tat(he_thong, nguoi_dung, mo_hinh=mo_hinh('tong_hop') or None)
        try:
            from bao_ve.ngan_sach import ghi_nhan
            tv = getattr(ncc, 'token_vao_gan_nhat', None) or 0
            tr = getattr(ncc, 'token_ra_gan_nhat', None) or 0
            md = getattr(ncc, 'model_da_gui_gan_nhat', None) or ''
            if tv or tr:
                ghi_nhan(int(tv), int(tr), md)
        except Exception:  # noqa: BLE001 - đếm chi phí hỏng không được chặn tóm tắt
            _log.exception('Không ghi được chi phí tóm tắt vào ngân sách.')
        return van_ban
    except Exception as loi:  # noqa: BLE001
        _log.warning('Mô hình tóm tắt lỗi — rơi về luật: %s', loi)
        return None


def xu_ly_phien(
    dong_phien: Dict[str, Any],
    *,
    _lay_ket_noi: Optional[Callable[[], Any]] = None,
    _goi: Optional[Callable[[str, str], Optional[str]]] = None,
    them_luot: Optional[List[Dict[str, Any]]] = None,
) -> Optional[str]:
    """Tóm tắt một phiên + cập nhật hồ sơ. Trả 'ai' | 'luat' | None (lỗi).
    `them_luot`: các lượt không nằm trong chat_hoi_thoai (đoạn khách ↔ nhân viên nhắn tay) — nối vào CUỐI,
    {'cau_hoi': lời khách, 'tra_loi': '[Nhân viên shop nhắn tay] …'} (kenh/messenger.tiep_quan)."""
    lay = _lay_ket_noi or csdl.ket_noi
    goi = _goi or _goi_mo_hinh
    phien = dong_phien['phien']
    so_tay_phien = chuan_hoa_so_tay(
        json.loads(dong_phien['so_tay']) if dong_phien.get('so_tay') else {}
    )
    khoa = khoa_ho_so(dong_phien.get('nguoi_dung') or 'khach', dong_phien.get('may') or '')
    lan_dau_tom_tat = dong_phien.get('tom_tat_luc') is None

    # HAI kết nối riêng: một để ĐỌC, đóng lại, rồi mới gọi mô hình (5-15
    # giây), rồi mở kết nối MỚI để GHI. Giữ một kết nối mở suốt lúc chờ mô
    # hình thì MySQL cắt ngang — đo thật 23/09/2026: "MySQL server has gone
    # away" ngay sau lượt gọi 12 giây, mất trắng bản tóm tắt vừa trả tiền.
    try:
        conn = lay()
        if conn is None:
            return None
        with conn:
            with conn.cursor() as ct:
                ct.execute(
                    'SELECT cau_hoi, tra_loi, y_dinh FROM chat_hoi_thoai WHERE phien = %s '
                    'ORDER BY id DESC LIMIT %s',
                    (phien, SO_LUOT_DOC),
                )
                cac_luot = list(reversed(ct.fetchall() or [])) + list(them_luot or [])
                ho_so_cu = None
                if khoa:
                    ct.execute('SELECT * FROM chat_ho_so WHERE khoa = %s', (khoa,))
                    ho_so_cu = ct.fetchone()

        so_luot = len(cac_luot) or int(dong_phien.get('so_luot') or 0)
        du_phong = tom_tat_bang_luat(so_tay_phien, so_luot)
        bang = 'luat'
        k_phien, k_ho_so = dict(du_phong), None
        if cac_luot:
            he_thong, nguoi_dung = _dung_prompt(ho_so_cu, so_tay_phien, cac_luot,
                                                dong_phien.get('compact') or '',
                                                dong_phien.get('khong_gian'))
            tho = goi(he_thong, nguoi_dung) or ''
            obj = _tim_json(tho)
            if tho and not obj:
                _log.warning('Mô hình trả về không đọc được JSON (phiên %s, %d ký tự) — rơi về luật.',
                             phien, len(tho))
            if obj:
                k_phien = _sach_khoi(obj.get('phien'), du_phong, 2000)
                k_ho_so = _sach_khoi(obj.get('ho_so'), k_phien, 4000)
                bang = 'ai'
        if k_ho_so is None:
            # Không có mô hình: hồ sơ = tóm tắt cũ nối tóm tắt phiên này.
            cu = (ho_so_cu or {}).get('tom_tat') or ''
            cu_phien = dong_phien.get('tom_tat') or ''
            k_ho_so = dict(k_phien)
            if cu and cu_phien and cu_phien in cu:
                # Tóm tắt LẠI phiên này: thay đoạn cũ của chính nó, đừng
                # nối thêm — nối thì hồ sơ lặp đôi mỗi lần khách quay lại.
                k_ho_so['tom_tat'] = cu.replace(cu_phien, k_phien['tom_tat'])[-4000:]
            elif cu:
                k_ho_so['tom_tat'] = (cu + ' | ' + k_phien['tom_tat'])[-4000:]
            else:
                k_ho_so['tom_tat'] = k_phien['tom_tat']
            k_ho_so['bang_khoan'] = k_phien['bang_khoan'] or (ho_so_cu or {}).get('bang_khoan') or ''
            k_ho_so['luu_y'] = k_phien['luu_y'] or (ho_so_cu or {}).get('luu_y') or ''

        conn = lay()
        if conn is None:
            return None
        with conn:
            with conn.cursor() as ct:
                ct.execute(
                    """
                    UPDATE chat_phien SET tom_tat = %s, phan_loai = %s, muc_quan_tam = %s,
                           bang_khoan = %s, luu_y = %s, tom_tat_luc = NOW(), tom_tat_bang = %s
                     WHERE phien = %s
                    """,
                    (k_phien['tom_tat'], k_phien['phan_loai'], k_phien['muc_quan_tam'],
                     k_phien['bang_khoan'], k_phien['luu_y'], bang, phien),
                )
                if khoa:
                    # Tóm tắt LẠI một phiên (khách quay lại cùng tab) thì
                    # không cộng đếm ý định lần hai, không tăng so_phien.
                    st_them = dict(so_tay_phien)
                    if not lan_dau_tom_tat:
                        st_them['y_dinh'] = {}
                    so_tay_ho_so = gop(
                        json.loads(ho_so_cu['so_tay']) if (ho_so_cu and ho_so_cu.get('so_tay')) else {},
                        st_them,
                    )
                    ct.execute(
                        """
                        INSERT INTO chat_ho_so
                            (khoa, khong_gian, nguoi_dung, so_tay, tom_tat, phan_loai, muc_quan_tam,
                             bang_khoan, luu_y, so_phien, lan_dau, lan_cuoi, cap_nhat)
                        VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, 1, %s, %s, NOW())
                        ON DUPLICATE KEY UPDATE
                            so_tay = VALUES(so_tay), tom_tat = VALUES(tom_tat),
                            phan_loai = VALUES(phan_loai), muc_quan_tam = VALUES(muc_quan_tam),
                            bang_khoan = VALUES(bang_khoan), luu_y = VALUES(luu_y),
                            so_phien = so_phien + %s,
                            lan_cuoi = GREATEST(lan_cuoi, VALUES(lan_cuoi)),
                            cap_nhat = NOW()
                        """,
                        (khoa, dong_phien.get('khong_gian') or 'tradingauto',
                         dong_phien.get('nguoi_dung') or None,
                         json.dumps(so_tay_ho_so, ensure_ascii=False),
                         k_ho_so['tom_tat'], k_ho_so['phan_loai'], k_ho_so['muc_quan_tam'],
                         k_ho_so['bang_khoan'], k_ho_so['luu_y'],
                         dong_phien['bat_dau'], dong_phien['cap_nhat'],
                         1 if lan_dau_tom_tat else 0),
                    )
            conn.commit()
        return bang
    except Exception as loi:  # noqa: BLE001
        _log.warning('Không tóm tắt được phiên %r: %s', phien, loi)
        return None


def chay_mot_vong(*, _lay_ket_noi: Optional[Callable[[], Any]] = None) -> int:
    """Tìm các phiên đã nghỉ cần tóm tắt, xử lý tối đa MOI_VONG. Trả số phiên xong."""
    lay = _lay_ket_noi or csdl.ket_noi
    conn = lay()
    if conn is None:
        return 0
    try:
        with conn:
            with conn.cursor() as ct:
                ct.execute(
                    """
                    SELECT phien, khong_gian, may, nguoi_dung, so_tay, so_luot,
                           bat_dau, cap_nhat, tom_tat_luc, tom_tat, compact
                      FROM chat_phien
                     WHERE cap_nhat < NOW() - INTERVAL %s MINUTE
                       AND (tom_tat_luc IS NULL OR tom_tat_luc < cap_nhat)
                     ORDER BY cap_nhat
                     LIMIT %s
                    """,
                    (NGHI_PHUT, MOI_VONG),
                )
                cac_phien = ct.fetchall() or []
    except Exception as loi:  # noqa: BLE001
        _log.warning('Không đọc được danh sách phiên cần tóm tắt: %s', loi)
        return 0
    xong = 0
    for d in cac_phien:
        if xu_ly_phien(d, _lay_ket_noi=_lay_ket_noi):
            xong += 1
    if cac_phien:
        _log.info('Tóm tắt phiên: %d/%d xong.', xong, len(cac_phien))
    return xong


_da_khoi_dong = False
_khoa_khoi_dong = threading.Lock()


def khoi_dong_nen() -> None:
    """Bật luồng nền (một lần mỗi tiến trình). CHATBOT_TOM_TAT=0 để tắt."""
    global _da_khoi_dong
    if os.environ.get('CHATBOT_TOM_TAT', '1').strip() == '0':
        _log.info('CHATBOT_TOM_TAT=0 — không bật tóm tắt nền.')
        return
    with _khoa_khoi_dong:
        if _da_khoi_dong:
            return
        _da_khoi_dong = True

    def _vong():
        time.sleep(30)  # để cổng khởi động xong trước
        while True:
            try:
                chay_mot_vong()
            except Exception:  # noqa: BLE001 - luồng nền không bao giờ được chết
                _log.exception('Vòng tóm tắt lỗi bất ngờ.')
            time.sleep(CHU_KY_GIAY)

    threading.Thread(target=_vong, name='tom-tat-phien', daemon=True).start()
    _log.info('Đã bật tóm tắt phiên nền: nghỉ %d phút thì tóm tắt, mỗi %d giây quét một lần.',
              NGHI_PHUT, CHU_KY_GIAY)


# =====================================================================
# TỰ KIỂM — CSDL giả lập, mô hình giả lập
# =====================================================================
if __name__ == '__main__':
    import datetime as _dt

    print('=== 1) _tim_json chịu được rào ```json và chữ thừa ===')
    assert _tim_json('```json\n{"a": 1}\n```') == {'a': 1}
    assert _tim_json('Đây là kết quả: {"a": {"b": 2}} xong') == {'a': {'b': 2}}
    assert _tim_json('không có json') is None
    print('  OK')

    print('\n=== 2) _sach_khoi: nhãn lạ bị thay, key bị che ===')
    dp = {'tom_tat': 'dp', 'phan_loai': 'khac', 'muc_quan_tam': 'lanh'}
    k = _sach_khoi({'tom_tat': 'khách gửi key QW81ZX0PLM4NB7VC', 'phan_loai': 'mua_ngay',
                    'muc_quan_tam': 'NONG'}, dp, 600)
    assert 'QW81ZX0PLM4NB7VC' not in k['tom_tat'] and k['phan_loai'] == 'khac' and k['muc_quan_tam'] == 'nong', k
    print('  OK', k)

    print('\n=== 3) tom_tat_bang_luat ===')
    st = {'dang_hoi': 'copytrade', 'san_pham': ['copytrade'], 'nen_tang': 'MT4',
          'y_dinh': {'huong_dan': 2, 'gia': 1}}
    r = tom_tat_bang_luat(st, 3)
    assert r['phan_loai'] == 'hoi_gia' and r['muc_quan_tam'] == 'nong', r
    assert tom_tat_bang_luat({}, 1)['phan_loai'] == 'khac'
    print('  OK', r['tom_tat'])

    print('\n=== 4) xu_ly_phien với CSDL + mô hình giả lập ===')

    class _CT:
        def __init__(s, db): s.db = db; s._kq = None
        def __enter__(s): return s
        def __exit__(s, *a): return False
        def execute(s, sql, ts=None):
            s.db['sql'].append((sql, ts))
            if 'FROM chat_hoi_thoai' in sql:
                s._kq = [{'cau_hoi': 'copytrade giá bao nhiêu, em dùng mt4 exness', 'tra_loi': 'Dạ ...', 'y_dinh': 'gia'},
                         {'cau_hoi': 'mst của em 0912345678', 'tra_loi': 'Dạ', 'y_dinh': 'khac'}][::-1]
            elif 'FROM chat_ho_so' in sql:
                s._kq = None
        def fetchall(s): return s._kq or []
        def fetchone(s): return s._kq

    class _Conn:
        def __init__(s): s.db = {'sql': []}
        def __enter__(s): return s
        def __exit__(s, *a): return False
        def cursor(s): return _CT(s.db)
        def commit(s): s.db['commit'] = True

    c = _Conn()
    prompt_gui = {}

    def _mo_hinh_gia(he, nd):
        prompt_gui['nd'] = nd
        return ('```json\n{"phien": {"tom_tat": "Hỏi giá copytrade MT4 Exness", "phan_loai": "hoi_gia", '
                '"muc_quan_tam": "nong", "bang_khoan": "giá", "luu_y": "nêu bản dùng thử"}, '
                '"ho_so": {"tom_tat": "Khách MT4 Exness quan tâm copytrade", "phan_loai": "hoi_gia", '
                '"muc_quan_tam": "nong", "bang_khoan": "giá", "luu_y": "nêu bản dùng thử"}}\n```')

    dong = {'phien': 'p1', 'khong_gian': 'tradingauto', 'may': 'm1', 'nguoi_dung': None,
            'so_tay': json.dumps({'dang_hoi': 'copytrade', 'san_pham': ['copytrade'], 'y_dinh': {'gia': 1}}),
            'so_luot': 2, 'bat_dau': _dt.datetime(2026, 9, 23, 10), 'cap_nhat': _dt.datetime(2026, 9, 23, 10, 5),
            'tom_tat_luc': None}
    kq = xu_ly_phien(dong, _lay_ket_noi=lambda: c, _goi=_mo_hinh_gia)
    assert kq == 'ai', kq
    assert '0912345678' not in prompt_gui['nd'], 'LỖI: số điện thoại lọt vào prompt gửi mô hình'
    upd = [t for s, t in c.db['sql'] if s.strip().startswith('UPDATE chat_phien')][0]
    ins = [t for s, t in c.db['sql'] if 'INSERT INTO chat_ho_so' in s][0]
    assert upd[1] == 'hoi_gia' and upd[5] == 'ai' and upd[6] == 'p1', upd
    assert ins[0] == 'm:m1' and ins[-1] == 1, ins
    print('  OK — phiên ẩn danh ghi vào hồ sơ m:m1, số điện thoại không tới mô hình')

    print('\n=== 5) mô hình hỏng -> rơi về luật, vẫn ghi ===')
    c2 = _Conn()
    kq2 = xu_ly_phien(dict(dong, tom_tat_luc=_dt.datetime(2026, 9, 23, 9)), _lay_ket_noi=lambda: c2,
                      _goi=lambda h, n: None)
    ins2 = [t for s, t in c2.db['sql'] if 'INSERT INTO chat_ho_so' in s][0]
    assert kq2 == 'luat' and ins2[-1] == 0, (kq2, ins2)
    assert '"y_dinh":{}' in ins2[3].replace(' ', ''), 'LỖI: tóm tắt lại mà vẫn cộng đếm ý định lần hai'
    print('  OK — tóm tắt lại không tăng so_phien, không cộng đếm ý định lần hai')

    print('\n=== 5b) tóm tắt LẠI bằng luật: hồ sơ không bị lặp đôi ===')
    tt_cu = tom_tat_bang_luat(json.loads(dong['so_tay']), 2)['tom_tat']

    class _CT2(_CT):
        def execute(s, sql, ts=None):
            super().execute(sql, ts)
            if 'FROM chat_ho_so' in sql:
                s._kq = {'khoa': 'm:m1', 'so_tay': '{}', 'tom_tat': 'Lần đầu hỏi dcabot | ' + tt_cu}

    class _Conn2(_Conn):
        def cursor(s): return _CT2(s.db)

    c3 = _Conn2()
    xu_ly_phien(dict(dong, tom_tat_luc=_dt.datetime(2026, 9, 23, 9), tom_tat=tt_cu),
                _lay_ket_noi=lambda: c3, _goi=lambda h, n: None)
    tt_ho_so = [t for s, t in c3.db['sql'] if 'INSERT INTO chat_ho_so' in s][0][4]
    assert tt_ho_so.count('Sản phẩm đang hỏi: copytrade') == 1, tt_ho_so
    assert tt_ho_so.startswith('Lần đầu hỏi dcabot | '), tt_ho_so
    print('  OK', tt_ho_so)

    print('\n=== 6) CSDL không có -> không lỗi ===')
    assert chay_mot_vong(_lay_ket_noi=lambda: None) == 0
    assert xu_ly_phien(dong, _lay_ket_noi=lambda: None, _goi=_mo_hinh_gia) is None
    print('  OK')
    print('\nTỰ KIỂM tom_tat.py QUA.')
