# -*- coding: utf-8 -*-
"""CHAM_SOC.PY — chăm sóc lại khách Messenger đã inbox mà im lặng (01/10/2026).

VÌ SAO
------
Chủ dự án: "chăm sóc lại khách hàng đã inbox chúng ta chưa làm tốt, nhắn hỏi lại nhu cầu những khách đang
phân vân, chưa chốt, chăm sóc lại họ sau 1 thời gian". 50 khách Messenger từ 23/09 chưa ai được nhắc lại;
24 khách hỏi giá "nóng" thì 19 người im 1–7 ngày (phân tích trong tài liệu "Kế hoạch tiếp thị TradingAuto").

LUẬT META (developers.facebook.com, Messenger Platform policy)
-------------------------------------------------------------
- Trong 24 giờ từ tin cuối của khách: được nhắn, kể cả nội dung quảng bá -> bot TỰ nhắn được.
- 24 giờ – 7 ngày: chỉ nhãn HUMAN_AGENT, tức NGƯỜI THẬT gửi -> thẻ Telegram, chủ shop bấm Gửi.
- Quá 7 ngày: chỉ khi khách đã đồng ý nhận tin (Marketing Messages) — làm sau.

HAI ĐỢT (chủ dự án chốt 01/10)
-----------------------------
1. '24h'  khách im 20–23,5 giờ, bot đã trả lời tin cuối, nhân viên không tiếp trong 24 giờ qua -> AI soạn
          MỘT tin nhắm đúng băn khoăn trong hồ sơ (chat_ho_so) + hội thoại, kèm nút chọn nhanh -> gửi luôn.
2. 'the'  mỗi sáng (cs_gio_the giờ VN) khách im 1 – 6,5 ngày -> AI soạn tin -> THẺ Telegram: tóm tắt, băn
          khoăn, việc bên mình đang nợ khách (nếu có), tin soạn sẵn; ✅ Gửi (HUMAN_AGENT) / ❌ Bỏ; trả lời
          vào thẻ để sửa tin.
Mỗi cửa sổ im lặng (mốc = tin cuối của khách) tối đa một tin mỗi đợt (khoá duy nhất trong chat_cham_soc).
Khách ngoài phạm vi / đã từ chối / đã xong -> AI trả nhan=false, không nhắn (vẫn ghi để không hỏi lại).
Công tắc chat_cau_hinh: cs_bat_24h, cs_bat_the (mặc định '0'), cs_gio_the ('9').

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

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

try:
    from congcu import csdl
    from kenh.phieu import _cau_hinh_tele, _bi_mat_ky, ky, _tele
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 kenh.phieu import _cau_hinh_tele, _bi_mat_ky, ky, _tele  # type: ignore

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

CHU_KY_GIAY = 600
CUA_24H = (20.0, 23.5)          # giờ im lặng: bot tự nhắn (Meta cho tới 24 giờ)
CUA_THE = (24.0, 156.0)         # 1 ngày – 6,5 ngày: thẻ duyệt (HUMAN_AGENT tối đa 7 ngày, chừa nửa ngày)
VN = timedelta(hours=7)
KICH_BAN = {
    'bo_ngang': 'Bỏ ngang ở câu đầu',
    'thue_bot': 'Thuê bot — cần bằng chứng / chưa chọn dòng',
    'code_bot': 'Code bot — đặc tả còn dở',
    'ngoai': 'Nhu cầu ngoài phạm vi',
    'da_xong': 'Đã xong / đã từ chối',
    'khac': 'Khác',
}
_MAU_CHO_TRONG = re.compile(r'⟦\?')


# =====================================================================
# CHỌN ỨNG VIÊN (thuần, tự kiểm được)
# =====================================================================

def chon_ung_vien(dong: List[Dict[str, Any]], da_co: set, bay_gio: datetime, dot: str) -> List[Dict[str, Any]]:
    """dong: mỗi khách {psid, moc_khach, tt_cuoi (trạng thái tin khách cuối), moc_nv}. da_co: {(psid, moc_khach)}
    đã có ở đợt này. Trả các khách đúng cửa sổ của `dot`."""
    tu, den = CUA_24H if dot == '24h' else CUA_THE
    ra = []
    for d in dong:
        mk = d.get('moc_khach')
        if not mk or (d['psid'], mk) in da_co:
            continue
        gio = (bay_gio - mk).total_seconds() / 3600
        if not (tu <= gio < den):
            continue
        if dot == '24h':
            if d.get('tt_cuoi') != 'xong':               # bot chưa trả lời tin cuối / nhân viên đang lo
                continue
            if d.get('moc_nv') and (bay_gio - d['moc_nv']).total_seconds() < 24 * 3600:
                continue
        ra.append(dict(d, gio_im=gio))
    return ra


def doc_json_mo_hinh(van_ban: str) -> Dict[str, Any]:
    m = re.search(r'\{.*\}', van_ban or '', re.S)
    try:
        j = json.loads(m.group(0)) if m else {}
    except ValueError:
        j = {}
    kb = str(j.get('kich_ban') or 'khac')
    nut = [str(x).strip()[:20] for x in (j.get('nut') or []) if str(x).strip()][:3]
    return {'kich_ban': kb if kb in KICH_BAN else 'khac', 'nhan': bool(j.get('nhan')),
            'ly_do': str(j.get('ly_do') or '').strip()[:300], 'no_minh': str(j.get('no_minh') or '').strip()[:300],
            'tin': str(j.get('tin') or '').strip()[:1500], 'nut': nut}


def the_cham_soc(r: Dict[str, Any]) -> str:
    """Thẻ Telegram (HTML) cho một dòng chat_cham_soc."""
    e = lambda s: html.escape(str(s or ''))  # noqa: E731
    tt = {'cho_duyet': '', 'dang_gui': '⏳ ĐANG GỬI', 'da_gui': '✅ ĐÃ GỬI', 'bo': '❌ ĐÃ BỎ', 'loi': '⚠️ LỖI'}.get(r.get('trang_thai'), '')
    dong = [f"🔔 <b>Chăm sóc khách</b> · {e(KICH_BAN.get(r.get('kich_ban'), r.get('kich_ban')))} · im "
            f"{float(r.get('gio_im') or 0) / 24:.1f} ngày" + (f'  <b>{tt}</b>' if tt else '')]
    if r.get('tom_tat'):
        dong.append(e(str(r['tom_tat'])[:700]))
    if r.get('bang_khoan'):
        dong.append('<b>Băn khoăn:</b> ' + e(str(r['bang_khoan'])[:400]))
    if r.get('no_minh'):
        dong.append('⚠️ <b>Bên mình đang nợ khách:</b> ' + e(r['no_minh']))
    dong.append('')
    dong.append('<b>Tin soạn sẵn</b> (trả lời vào thẻ này để sửa):')
    dong.append(e(r.get('noi_dung')) or '<i>(trống — trả lời vào thẻ bằng tin anh muốn gửi)</i>')
    if r.get('ket_qua') and r.get('trang_thai') in ('da_gui', 'bo', 'loi'):
        dong.append('')
        dong.append('<i>' + e(r['ket_qua']) + '</i>')
    return '\n'.join(dong)[:4000]


def nut_cham_soc(r: Dict[str, Any], bi_mat: str, page_id: str = '') -> List[List[Dict[str, str]]]:
    if r.get('trang_thai') != 'cho_duyet':
        return []
    i = int(r['id'])
    n = lambda chu, hanh: {'text': chu, 'callback_data': f'{hanh}:{i}:{ky(bi_mat, hanh, i)}'}  # noqa: E731
    ban = [[n('✅ Gửi cho khách', 'ca'), n('❌ Bỏ', 'cb')]]
    if 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


# =====================================================================
# SOẠN TIN BẰNG MÔ HÌNH
# =====================================================================

def _prompt(dot: str, ten_shop: str, xung_ho: str) -> str:
    return (
        f'Em là trợ lý chăm sóc khách của shop {ten_shop} (phần mềm / bot giao dịch MT4, MT5, TradingView). '
        'Khách đã nhắn Fanpage rồi im lặng. Dựa vào hồ sơ và đoạn hội thoại, quyết định có nên nhắn lại không '
        'và soạn MỘT tin nhắn lại.\n'
        'Kịch bản (kich_ban): bo_ngang = khách chỉ bấm một nút / hỏi một câu rồi im; thue_bot = muốn thuê bot '
        'chạy sẵn, cần bằng chứng hoặc chưa chọn dòng; code_bot = đặt code theo chiến lược, đặc tả còn dở; ngoai = '
        'nhu cầu shop không làm (phái sinh, chứng khoán VN, bot scalp cày quỹ…); da_xong = đã mua / đã từ chối / '
        'đã nói không cần; khac.\n'
        'nhan = false khi ngoai hoặc da_xong, hoặc khách đã nói sẽ tự quay lại sau một mốc chưa tới.\n'
        'no_minh = việc BÊN SHOP đã hứa mà chưa làm (báo giá, gọi lại, gửi passview, xác nhận logic…), không có thì "".\n'
        'MỤC ĐÍCH DUY NHẤT của tin (chủ shop chốt 02/10): HỎI LẠI vấn đề khách đang quan tâm và TƯ VẤN TIẾP đúng '
        'vấn đề đó. KHÔNG chào hàng, KHÔNG giới thiệu sản phẩm / dịch vụ khác ngoài thứ khách đã hỏi, KHÔNG khuyến '
        'mãi, giảm giá, "ưu đãi", hối thúc chốt đơn (Meta cấm quảng cáo qua tin nhắn sau 24 giờ).\n'
        'tin: ' + xung_ho + ' 2–4 dòng, văn bản thuần, không markdown. Mở đầu tự nhiên (không "Kính gửi"), nhắc '
        'đúng điều khách đã hỏi / đang băn khoăn, trả lời tiếp hoặc nêu điều cần làm rõ để tư vấn được, kết bằng '
        'MỘT câu hỏi dễ trả lời. '
        'Không hứa lợi nhuận. Chỉ dùng số liệu, giá có trong hội thoại; điều cần xác nhận mà hội thoại chưa có '
        'thì ghi ⟦?nội dung⟧.\n'
        'BẰNG CHỨNG: tin nói "gửi / xem kết quả" thì PHẢI kèm link ngay trong tin, chỉ dùng các trang công khai: '
        'SMART DCA tài khoản chạy thật /smartdca-stream, lãi từng ngày 10 mã /pages/dca_signals; VipTrend lệnh '
        'chạy thật /pages/signal_stream (từng con: /pages/signal_stream/GOLDVIP1 … GOLDVIP7); bảng giá /Pricing; '
        'giới thiệu /toolintroduce/dcabot, /toolintroduce/trendbot, /toolintroduce/tradingview, /codebot/custom. '
        'KHÔNG BAO GIỜ đề nghị gửi passview (shop không chia sẻ passview).\n'
        + ('nut: 2–3 lựa chọn ngắn (≤ 20 ký tự) để khách bấm trả lời luôn, khớp câu hỏi cuối.\n'
           if dot == '24h' else 'nut: [] (thẻ do chủ shop gửi tay).\n')
        + 'CHỈ trả về JSON: {"kich_ban":"...","nhan":true,"ly_do":"...","no_minh":"...","tin":"...","nut":["..."]}'
    )


def soan(dot: str, ho_so: Dict[str, Any], lich_su: List[Dict[str, str]], gio_im: float, khong_gian: str,
         _goi: Optional[Callable[[str, str], Optional[str]]] = None) -> Dict[str, Any]:
    try:
        from congcu.so_tay import che_du_lieu_nhay_cam as che
    except Exception:  # noqa: BLE001
        che = lambda s: s  # 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 im lặng {gio_im / 24:.1f} ngày.",
            f"Hồ sơ — phân loại: {ho_so.get('phan_loai') or '?'}, mức quan tâm: {ho_so.get('muc_quan_tam') or '?'}",
            f"Tóm tắt: {ho_so.get('tom_tat') or '(chưa có)'}",
            f"Băn khoăn: {ho_so.get('bang_khoan') or '(chưa có)'}", '', 'Hội thoại gần nhất:']
    dong += [f"{'Khách' if x.get('vai') == 'khach' else 'Shop'}: {str(x.get('noi_dung') or '')[:900]}"
             for x in (lich_su or [])[-12:]]
    kq = doc_json_mo_hinh(_goi(_prompt(dot, ten, xung_ho), che('\n'.join(dong))) or '')
    kq['tin'] = che(kq['tin'])
    return kq


# =====================================================================
# CSDL + GỬI
# =====================================================================

def _cfg(ct, kg: str) -> Dict[str, str]:
    ct.execute("SELECT khoa, gia_tri FROM chat_cau_hinh WHERE khong_gian = %s AND khoa LIKE 'cs\\_%%'", (kg,))
    return {r['khoa']: str(r['gia_tri'] or '') for r in (ct.fetchall() or [])}


def _dat_cfg(ct, kg: str, khoa: str, gia_tri: str) -> None:
    ct.execute("""INSERT INTO chat_cau_hinh (khong_gian, khoa, gia_tri, cap_nhat) VALUES (%s, %s, %s, NOW())
                  ON DUPLICATE KEY UPDATE gia_tri = VALUES(gia_tri), cap_nhat = NOW()""", (kg, khoa, gia_tri))


def _dong_khach(ct, kenh_id: int) -> List[Dict[str, Any]]:
    ct.execute("""SELECT psid,
                         MAX(CASE WHEN loai = 'khach' AND noi_dung <> '👍' THEN luc END) AS moc_khach,
                         MAX(CASE WHEN loai = 'nhan_vien' THEN luc END) AS moc_nv
                  FROM chat_hang_cho WHERE kenh_id = %s AND luc >= NOW() - INTERVAL 8 DAY GROUP BY psid""", (kenh_id,))
    ds = [dict(r) for r in (ct.fetchall() or [])]
    for d in ds:
        if d.get('moc_khach'):
            ct.execute("""SELECT trang_thai FROM chat_hang_cho WHERE kenh_id = %s AND psid = %s AND loai = 'khach'
                          AND luc = %s ORDER BY id DESC LIMIT 1""", (kenh_id, d['psid'], d['moc_khach']))
            d['tt_cuoi'] = (ct.fetchone() or {}).get('trang_thai')
    return ds


def _lay(ct, id_: int) -> Optional[Dict[str, Any]]:
    ct.execute('SELECT * FROM chat_cham_soc WHERE id = %s', (int(id_),))
    r = ct.fetchone()
    return dict(r) if r else None


def _ve_lai(ct, r: Dict[str, Any], *, _tele_goi=None) -> None:
    token, chat = _cau_hinh_tele(ct)
    if not token or not chat:
        return
    goi = _tele_goi or (lambda pt, du: _tele(token, pt, du))
    ct.execute('SELECT page_id FROM chat_kenh WHERE id = %s', (int(r['kenh_id']),))
    page_id = str((ct.fetchone() or {}).get('page_id') or '')
    du = {'chat_id': chat, 'text': the_cham_soc(r), 'parse_mode': 'HTML', 'disable_web_page_preview': True,
          'reply_markup': {'inline_keyboard': nut_cham_soc(r, _bi_mat_ky(ct), page_id)}}
    if r.get('tele_msg_id'):
        goi('editMessageText', dict(du, message_id=int(r['tele_msg_id'])))
        return
    j = goi('sendMessage', du) or {}
    mid = (j.get('result') or {}).get('message_id')
    if mid:
        ct.execute('UPDATE chat_cham_soc SET tele_msg_id = %s WHERE id = %s', (int(mid), int(r['id'])))


def _ghi(ct, kenh_id: int, d: Dict[str, Any], dot: str, kq: Dict[str, Any], ho_so: Dict[str, Any],
         trang_thai: str, ket_qua: str = '') -> int:
    ct.execute("""INSERT IGNORE INTO chat_cham_soc (kenh_id, psid, dot, moc_khach, gio_im, kich_ban, nhan, tom_tat,
                         bang_khoan, no_minh, noi_dung, nut, trang_thai, ket_qua, tao_luc)
                  VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW())""",
               (kenh_id, d['psid'], dot, d['moc_khach'], round(d['gio_im'], 1), kq['kich_ban'], 1 if kq['nhan'] else 0,
                (kq['ly_do'] or str(ho_so.get('tom_tat') or ''))[:1000], str(ho_so.get('bang_khoan') or '')[:500],
                kq['no_minh'], kq['tin'], json.dumps(kq['nut'], ensure_ascii=False), trang_thai, ket_qua[:250]))
    # INSERT IGNORE trùng khoá cửa sổ (tiến trình khác đã ghi) -> 0: nơi gọi KHÔNG được gửi.
    return int(ct.lastrowid or 0) if getattr(ct, 'rowcount', 1) == 1 else 0



def quet(kenh: Dict[str, Any], dot: str, *, _lay_ket_noi=None, _soan=None, _gui=None, _tele_goi=None,
         bay_gio: Optional[datetime] = None) -> int:
    """Một lượt quét cho một kênh + một đợt. Trả số tin đã gửi (24h) hoặc số thẻ đã gửi (the)."""
    conn = (_lay_ket_noi or csdl.ket_noi)()
    if conn is None:
        return 0
    kenh_id, kg = int(kenh['id']), str(kenh.get('khong_gian') or 'tradingauto')
    so = 0
    with conn:
        with conn.cursor() as ct:
            ct.execute('SELECT NOW() AS bay_gio')
            bay_gio = bay_gio or (ct.fetchone() or {}).get('bay_gio') or datetime.now()
            ct.execute('SELECT psid, moc_khach FROM chat_cham_soc WHERE kenh_id = %s AND dot = %s', (kenh_id, dot))
            da_co = {(r['psid'], r['moc_khach']) for r in (ct.fetchall() or [])}
            ung_vien = chon_ung_vien(_dong_khach(ct, kenh_id), da_co, bay_gio, dot)
        conn.commit()
    lay = _lay_ket_noi or csdl.ket_noi
    for d in ung_vien[:30]:
        try:
            so += _mot_khach(kenh, kenh_id, kg, dot, d, lay, _soan=_soan, _gui=_gui, _tele_goi=_tele_goi)
        except Exception:  # noqa: BLE001 - một khách lỗi không được làm hỏng cả lượt
            _log.exception('Chăm sóc %s psid=…%s lỗi', dot, str(d.get('psid'))[-4:])
    return so


def _mot_khach(kenh: Dict[str, Any], kenh_id: int, kg: str, dot: str, d: Dict[str, Any], lay, *,
               _soan=None, _gui=None, _tele_goi=None) -> int:
    """Một khách: đọc hồ sơ -> (KHÔNG giữ kết nối) AI soạn -> ghi / giữ chỗ -> gửi. Trả 1 nếu đã gửi tin / thẻ."""
    ho_so = csdl.voi_ct(lay, lambda ct: (ct.execute('SELECT * FROM chat_ho_so WHERE khoa = %s', ('m:fb' + d['psid'],)),
                                      dict(ct.fetchone() or {}))[1])
    if ho_so.get('muc_quan_tam') == 'lanh':
        csdl.voi_ct(lay, lambda ct: _ghi(ct, kenh_id, d, dot, {'kich_ban': 'khac', 'nhan': False, 'ly_do': 'khách lạnh',
                                                           'no_minh': '', 'tin': '', 'nut': []}, ho_so, 'bo',
                                     'khách lạnh — không nhắn'))
        return 0
    try:
        from congcu.phien import tai_phien
        lich_su = (tai_phien(f'fb{kenh_id}x{d["psid"]}'[:64]) or {}).get('lich_su') or []
    except Exception:  # noqa: BLE001
        lich_su = []
    kq = (_soan or soan)(dot, ho_so, lich_su, d['gio_im'], kg)
    if not kq['nhan'] and not kq['no_minh']:
        csdl.voi_ct(lay, lambda ct: _ghi(ct, kenh_id, d, dot, kq, ho_so, 'bo', 'AI: không nên nhắn — ' + kq['ly_do']))
        return 0
    if dot != '24h':
        def _the(ct):
            i = _ghi(ct, kenh_id, d, dot, kq, ho_so, 'cho_duyet')
            r = _lay(ct, i) if i else None
            if r:
                _ve_lai(ct, r, _tele_goi=_tele_goi)
            return 1 if r else 0
        return csdl.voi_ct(lay, _the)
    if not kq['nhan'] or not kq['tin'] or _MAU_CHO_TRONG.search(kq['tin']):
        csdl.voi_ct(lay, lambda ct: _ghi(ct, kenh_id, d, dot, kq, ho_so, 'bo', 'tin còn chỗ cần xác nhận / trống — để thẻ duyệt'))
        return 0
    # GIỮ CHỖ TRƯỚC KHI GỬI: dòng 'dang_gui' chiếm khoá cửa sổ. Gửi xong mà ghi kết quả lỗi thì dòng vẫn nằm
    # đó -> không lượt nào gửi lại. Thà sót một tin còn hơn khách nhận hai lần.
    i = csdl.voi_ct(lay, lambda ct: _ghi(ct, kenh_id, d, dot, kq, ho_so, 'dang_gui', 'đang gửi'))
    if not i:
        return 0
    if _gui is None:
        from kenh.messenger import gui as _gui  # noqa: N806
    ok, loi = _gui(kenh, d['psid'], kq['tin'], nut=kq['nut'] or None)
    csdl.voi_ct(lay, lambda ct: ct.execute(
        'UPDATE chat_cham_soc SET trang_thai = %s, gui_luc = IF(%s, NOW(), NULL), nguoi = %s, ket_qua = %s WHERE id = %s',
        ('da_gui' if ok else 'loi', 1 if ok else 0, 'bot', '' if ok else f'gửi lỗi: {loi}'[:250], i)))
    if not ok:
        return 0
    try:
        from congcu.phien import chen_luot
        chen_luot(f'fb{kenh_id}x{d["psid"]}'[:64], [('(khách chưa nhắn lại — shop chủ động hỏi thăm)', kq['tin'])],
                  khong_gian=kg, may=f'fb{d["psid"]}'[:64])
    except Exception:  # noqa: BLE001
        _log.exception('Không ghi tin hỏi thăm vào lịch sử psid=%s', d['psid'])
    return 1


def hanh_dong(id_: int, hanh: str, nguoi: str, *, _lay_ket_noi=None, _gui=None, _tele_goi=None) -> Tuple[bool, str]:
    """Nút trên thẻ: 'ca' gửi cho khách, 'cb' bỏ. Quyền + chữ ký nút đã được tele-duyet.php kiểm."""
    lay = _lay_ket_noi or csdl.ket_noi
    nguoi = (nguoi or '')[:120]
    if hanh not in ('ca', 'cb'):
        return False, 'Nút không hợp lệ.'

    def _giu_cho(ct):
        r = _lay(ct, id_)
        if not r:
            return False, f'Không có thẻ chăm sóc #{id_}.'
        if r['trang_thai'] != 'cho_duyet':
            return False, f'Thẻ #{id_} đã xử lý.'
        if hanh == 'cb':
            ct.execute("UPDATE chat_cham_soc SET trang_thai = 'bo', nguoi = %s, ket_qua = %s WHERE id = %s AND trang_thai = 'cho_duyet'",
                       (nguoi, f'Bỏ bởi {nguoi}', id_))
            _ve_lai(ct, _lay(ct, id_), _tele_goi=_tele_goi)
            return True, 'Đã bỏ.'
        tin = str(r.get('noi_dung') or '').strip()
        if not tin or _MAU_CHO_TRONG.search(tin):
            return False, 'Tin trống hoặc còn chỗ ⟦?⟧ — trả lời vào thẻ bằng tin đã sửa rồi bấm Gửi.'
        ct.execute("""SELECT TIMESTAMPDIFF(MINUTE, MAX(luc), NOW()) AS phut FROM chat_hang_cho
                      WHERE kenh_id = %s AND psid = %s AND loai = 'khach'""", (int(r['kenh_id']), r['psid']))
        phut = (ct.fetchone() or {}).get('phut')
        if phut is None or phut >= 7 * 24 * 60:
            return False, 'Quá 7 ngày từ tin cuối của khách — Meta không cho gửi nữa.'
        ct.execute('SELECT * FROM chat_kenh WHERE id = %s', (int(r['kenh_id']),))
        kenh = dict(ct.fetchone() or {})
        # Giữ chỗ nguyên tử: bấm hai lần / hai người cùng bấm thì chỉ một lần gửi.
        ct.execute("UPDATE chat_cham_soc SET trang_thai = 'dang_gui', nguoi = %s, ket_qua = 'đang gửi' "
                   "WHERE id = %s AND trang_thai = 'cho_duyet'", (nguoi, id_))
        if getattr(ct, 'rowcount', 1) != 1:
            return False, f'Thẻ #{id_} đã xử lý.'
        return None, (r, tin, phut, kenh)

    ok, kq = csdl.voi_ct(lay, _giu_cho)
    if ok is not None:
        return ok, kq
    r, tin, phut, kenh = kq
    if _gui is None:
        from kenh.messenger import gui as _gui  # noqa: N806
    ok, loi = _gui(kenh, r['psid'], tin, nhan_tag=None if phut < 23 * 60 else 'HUMAN_AGENT')

    def _ket_qua(ct):
        if ok:
            # Như tin nhân viên: vào lịch sử phiên ở lượt bot kế tiếp (ghi_nhan_vien_vao_lich_su).
            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(r['kenh_id']), r['psid'], tin[:2000]))
            ct.execute("""UPDATE chat_cham_soc SET trang_thai = 'da_gui', gui_luc = NOW(), ket_qua = %s WHERE id = %s""",
                       (f'Gửi bởi {nguoi}' + (' (HUMAN_AGENT)' if phut >= 23 * 60 else ''), id_))
        else:
            ct.execute("UPDATE chat_cham_soc SET trang_thai = 'loi', ket_qua = %s WHERE id = %s", (f'Gửi lỗi: {loi}'[:250], id_))
        _ve_lai(ct, _lay(ct, id_), _tele_goi=_tele_goi)

    csdl.voi_ct(lay, _ket_qua)
    return (True, 'Đã gửi cho khách.') if ok else (False, f'Gửi lỗi: {loi}'[:190])


def sua(tele_msg_id: int, noi_dung: str, nguoi: str, *, _lay_ket_noi=None, _tele_goi=None) -> Tuple[bool, str]:
    """Chủ shop trả lời vào thẻ bằng tin mới."""
    conn = (_lay_ket_noi or csdl.ket_noi)()
    if conn is None:
        return False, 'Không kết nối được CSDL.'
    with conn:
        with conn.cursor() as ct:
            ct.execute('SELECT * FROM chat_cham_soc WHERE tele_msg_id = %s', (int(tele_msg_id),))
            r = ct.fetchone()
            if not r:
                return False, 'Không phải thẻ chăm sóc.'
            if r['trang_thai'] != 'cho_duyet':
                return False, f'Thẻ #{r["id"]} đã xử lý, không sửa được.'
            noi_dung = (noi_dung or '').strip()
            if len(noi_dung) < 5:
                return False, 'Tin mới quá ngắn.'
            ct.execute('UPDATE chat_cham_soc SET noi_dung = %s WHERE id = %s', (noi_dung[:1500], int(r['id'])))
            _ve_lai(ct, _lay(ct, int(r['id'])), _tele_goi=_tele_goi)
        conn.commit()
    return True, f'Đã sửa tin thẻ #{r["id"]} ({nguoi}) — bấm Gửi trên thẻ.'


# =====================================================================
# LUỒNG NỀN
# =====================================================================

def mot_vong(_lay_ket_noi=None) -> None:
    conn = (_lay_ket_noi or csdl.ket_noi)()
    if conn is None:
        return
    with conn:
        with conn.cursor() as ct:
            ct.execute("SELECT * FROM chat_kenh WHERE kenh = 'messenger' AND bat = 1")
            ds = [dict(r) for r in (ct.fetchall() or [])]
            viec = []
            hom_nay = (datetime.utcnow() + VN).date().isoformat()
            gio_vn = (datetime.utcnow() + VN).hour
            for k in ds:
                cfg = _cfg(ct, str(k.get('khong_gian') or 'tradingauto'))
                if cfg.get('cs_bat_24h') == '1':
                    viec.append((k, '24h'))
                try:
                    gio_the = int(cfg.get('cs_gio_the') or 9)
                except ValueError:
                    gio_the = 9
                if cfg.get('cs_bat_the') == '1' and gio_vn >= gio_the and cfg.get('cs_the_ngay') != hom_nay:
                    _dat_cfg(ct, str(k.get('khong_gian') or 'tradingauto'), 'cs_the_ngay', hom_nay)
                    viec.append((k, 'the'))
        conn.commit()
    for k, dot in viec:
        try:
            so = quet(k, dot, _lay_ket_noi=_lay_ket_noi)
            if so:
                _log.info('Chăm sóc khách %s kênh %s: %d', dot, k['id'], so)
        except Exception:  # noqa: BLE001
            _log.exception('Lượt chăm sóc %s kênh %s lỗi', dot, k.get('id'))


_da_chay = False
_khoa = threading.Lock()


def khoi_dong_nen() -> None:
    global _da_chay
    if os.environ.get('CHATBOT_CHAM_SOC', '1').strip() == '0':
        return
    with _khoa:
        if _da_chay:
            return
        _da_chay = True

    def _vong():
        while True:
            time.sleep(CHU_KY_GIAY)
            try:
                mot_vong()
            except Exception:  # noqa: BLE001 - luồng nền không bao giờ được chết
                _log.exception('Vòng chăm sóc khách lỗi.')

    threading.Thread(target=_vong, name='cham-soc', daemon=True).start()
    _log.info('Đã bật chăm sóc khách (công tắc cs_bat_24h / cs_bat_the trong chat_cau_hinh).')


# =====================================================================
# TỰ KIỂM
# =====================================================================

if __name__ == '__main__':
    bg = datetime(2026, 10, 2, 9, 0)
    dong = [
        {'psid': 'a', 'moc_khach': bg - timedelta(hours=21), 'tt_cuoi': 'xong', 'moc_nv': None},       # 24h: có
        {'psid': 'b', 'moc_khach': bg - timedelta(hours=21), 'tt_cuoi': 'bo_qua', 'moc_nv': None},     # nhân viên lo
        {'psid': 'c', 'moc_khach': bg - timedelta(hours=21), 'tt_cuoi': 'xong',
         'moc_nv': bg - timedelta(hours=3)},                                                            # nv vừa nhắn
        {'psid': 'd', 'moc_khach': bg - timedelta(hours=10), 'tt_cuoi': 'xong', 'moc_nv': None},       # còn sớm
        {'psid': 'e', 'moc_khach': bg - timedelta(days=3), 'tt_cuoi': 'xong', 'moc_nv': None},         # thẻ: có
        {'psid': 'f', 'moc_khach': bg - timedelta(days=8), 'tt_cuoi': 'xong', 'moc_nv': None},         # quá 7 ngày
        {'psid': 'g', 'moc_khach': None, 'tt_cuoi': None, 'moc_nv': bg},                               # không tin khách
    ]
    assert [x['psid'] for x in chon_ung_vien(dong, set(), bg, '24h')] == ['a']
    assert [x['psid'] for x in chon_ung_vien(dong, set(), bg, 'the')] == ['e']
    assert chon_ung_vien(dong, {('a', dong[0]['moc_khach'])}, bg, '24h') == [], 'một cửa sổ chỉ một tin'
    j = doc_json_mo_hinh('...{"kich_ban":"thue_bot","nhan":true,"ly_do":"x","no_minh":"","tin":"Dạ anh ơi",'
                         '"nut":["Ít lệnh","Nhiều lệnh","Xem kết quả thật trước đã nha"]}')
    assert j['kich_ban'] == 'thue_bot' and j['nhan'] and j['nut'][2] == 'Xem kết quả thật trư', j['nut']   # cắt 20 ký tự
    assert doc_json_mo_hinh('rác')['kich_ban'] == 'khac' and not doc_json_mo_hinh('rác')['nhan']
    r = {'id': 7, 'kenh_id': 2, 'kich_ban': 'code_bot', 'gio_im': 50, 'tom_tat': 'Khách <b> code EA', 'bang_khoan': 'giá',
         'no_minh': 'admin hẹn báo giá', 'noi_dung': 'Dạ anh ơi', 'trang_thai': 'cho_duyet'}
    t = the_cham_soc(r)
    assert '&lt;b&gt;' in t and 'nợ khách' in t and '2.1 ngày' in t, t
    n = nut_cham_soc(r, 'k', '615')
    assert [b['callback_data'][:2] for b in n[0]] == ['ca', 'cb'] and n[1][0]['url'].endswith('615')
    assert nut_cham_soc(dict(r, trang_thai='da_gui'), 'k') == []
    # Gửi: tin còn ⟦?⟧ thì không gửi; 24h -> gửi kèm nút
    da_gui = []

    class _Ct:
        def __init__(s, db): s.db, s._kq, s.lastrowid, s.rowcount = db, [], 0, 1
        def __enter__(s): return s
        def __exit__(s, *a): return False
        def execute(s, q, ts=()):
            q = ' '.join(q.split())
            if q.startswith('SELECT NOW()'):
                s._kq = [{'bay_gio': bg}]
            elif q.startswith('SELECT psid, moc_khach FROM chat_cham_soc'):
                s._kq = [{'psid': x['psid'], 'moc_khach': x['moc_khach']} for x in s.db['cs']]
            elif q.startswith('SELECT psid, MAX('):
                s._kq = [{'psid': x['psid'], 'moc_khach': x['moc_khach'], 'moc_nv': x['moc_nv']} for x in dong]
            elif q.startswith('SELECT trang_thai FROM chat_hang_cho'):
                s._kq = [{'trang_thai': next(x['tt_cuoi'] for x in dong if x['psid'] == ts[1])}]
            elif q.startswith('SELECT * FROM chat_ho_so'):
                s._kq = [{'phan_loai': 'hoi_gia', 'muc_quan_tam': 'nong', 'tom_tat': 't', 'bang_khoan': 'b'}]
            elif q.startswith('UPDATE chat_cham_soc SET trang_thai = %s'):
                s.db['cs'][ts[4] - 1]['trang_thai'] = ts[0]
            elif q.startswith('INSERT IGNORE INTO chat_cham_soc'):
                s.db['cs'].append({'psid': ts[1], 'moc_khach': ts[3], 'trang_thai': ts[12], 'noi_dung': ts[10]})
                s.lastrowid = len(s.db['cs'])
            else:
                s._kq = []
        def fetchall(s): return s._kq
        def fetchone(s): return s._kq[0] if s._kq else None

    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

    db = {'cs': []}
    so = quet({'id': 2, 'khong_gian': 'tradingauto'}, '24h', _lay_ket_noi=lambda: _Conn(db),
              _soan=lambda *a: {'kich_ban': 'thue_bot', 'nhan': True, 'ly_do': '', 'no_minh': '', 'tin': 'Dạ anh còn quan tâm VIP 1 không ạ?',
                                'nut': ['Còn', 'Xem kết quả']},
              _gui=lambda k, p, t, **kw: (da_gui.append((p, t, kw.get('nut'))) or (True, '')))
    assert so == 1 and da_gui == [('a', 'Dạ anh còn quan tâm VIP 1 không ạ?', ['Còn', 'Xem kết quả'])], da_gui
    assert db['cs'][0]['trang_thai'] == 'da_gui'
    db, da_gui = {'cs': []}, []
    quet({'id': 2, 'khong_gian': 'tradingauto'}, '24h', _lay_ket_noi=lambda: _Conn(db),
         _soan=lambda *a: {'kich_ban': 'code_bot', 'nhan': True, 'ly_do': '', 'no_minh': '', 'tin': 'Giá ⟦?giá cuối⟧ ạ', 'nut': []},
         _gui=lambda *a, **k: (da_gui.append(a) or (True, '')))
    assert da_gui == [] and db['cs'][0]['trang_thai'] == 'bo', 'tin còn chỗ ⟦?⟧ không bao giờ tự gửi'
    # 02/10: gửi xong mà lỗi trước khi ghi kết quả -> dòng 'dang_gui' giữ chỗ, lượt sau KHÔNG gửi lại
    db, da_gui = {'cs': []}, []

    def _gui_roi_hong(k, p, t, **kw):
        da_gui.append(p)
        raise RuntimeError('mất kết nối sau khi gửi')
    for _ in range(3):
        quet({'id': 2, 'khong_gian': 'tradingauto'}, '24h', _lay_ket_noi=lambda: _Conn(db),
             _soan=lambda *a: {'kich_ban': 'thue_bot', 'nhan': True, 'ly_do': '', 'no_minh': '', 'tin': 'Dạ anh ơi', 'nut': []},
             _gui=_gui_roi_hong)
    assert da_gui == ['a'] and db['cs'][0]['trang_thai'] == 'dang_gui', (da_gui, db)
    print('TỰ KIỂM kenh/cham_soc.py QUA.')
