# -*- coding: utf-8 -*-
"""PHIEN.PY — lưu phiên tư vấn và hồ sơ khách xuống CSDL.

VÌ SAO FILE NÀY TỒN TẠI
------------------------
Lịch sử chat từng sống trong RAM của chatbot.service (`_kho_phien` trong
nao/tra_loi.py). Hậu quả đo được ngay trong ngày 23/09/2026: mỗi lần khởi
động lại dịch vụ để deploy là MỌI khách đang chat mất ngữ cảnh, và hết 60
phút là phiên biến mất. Không có chỗ nào nhớ khách là ai, đang dùng gì.

Hai bảng (private/sql/2026-09-23_phien_ho_so_khach.sql):
  chat_phien  — một dòng mỗi phiên: sổ tay + vài lượt gần nhất + tóm tắt
  chat_ho_so  — một dòng mỗi KHÁCH: 'u:<username>' hoặc 'm:<mã thiết bị>'

RIÊNG TƯ
--------
Danh tính khách CHỈ nằm ở hai bảng này. `chat_hoi_thoai` vẫn ẩn danh như
cũ (congcu/nhat_ky.py và bộ kiểm của nó không đổi). Không bao giờ đọc bảng
`purchase` — quyết định của chủ dự án: hồ sơ chỉ gồm điều khách tự nói.

HỎNG THÌ SAO
------------
Mọi hàm ở đây NUỐT lỗi và trả giá trị rỗng. CSDL hỏng thì bot vẫn trả lời,
chỉ là quay về nhớ trong RAM như trước (nao/tra_loi.py giữ nguyên đường đó
làm dự phòng).

Chạy tự kiểm (không cần CSDL):  python congcu/phien.py
"""
from __future__ import annotations

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

try:
    from . import csdl
    from .so_tay import chuan_hoa_so_tay, gop, che_du_lieu_nhay_cam
except ImportError:
    _THU_MUC_TOI = os.path.dirname(os.path.abspath(__file__))
    _THU_MUC_CHA = os.path.dirname(_THU_MUC_TOI)
    for _d in (_THU_MUC_CHA, _THU_MUC_TOI):
        if _d not in sys.path:
            sys.path.insert(0, _d)
    import csdl  # type: ignore
    from so_tay import chuan_hoa_so_tay, gop, che_du_lieu_nhay_cam  # type: ignore

_log = logging.getLogger('chatbot.congcu.phien')

# Lưu tối đa bấy nhiêu LƯỢT (cặp khách+bot) trong chat_phien.lich_su.
GIU_LUOT_LUU = 10
# Câu bot trong lịch sử cắt còn bấy nhiêu ký tự: đủ để hiểu "bot vừa nói
# gì", không đủ để chiếm hết prompt.
CAT_CAU_BOT = 1200

_MAU_MA = re.compile(r'[^A-Za-z0-9_\-]')


def sach_ma(gia_tri: Any, dai: int = 64) -> str:
    """Mã phiên / mã thiết bị đến từ trình duyệt — không tin được."""
    return _MAU_MA.sub('', str(gia_tri or ''))[:dai]


def khoa_ho_so(nguoi_dung: str, may: str) -> Optional[str]:
    """'u:<username>' nếu đã đăng nhập, 'm:<mã thiết bị>' nếu chưa, None nếu
    không có gì để nhận ra khách (thì chỉ có phiên, không có hồ sơ)."""
    if nguoi_dung and nguoi_dung != 'khach':
        return 'u:' + nguoi_dung[:64]
    if may:
        return 'm:' + may[:64]
    return None


def _json(v: Any) -> str:
    return json.dumps(v, ensure_ascii=False, separators=(',', ':'))


def _doc_json(chuoi: Any, mac_dinh: Any) -> Any:
    if not chuoi:
        return mac_dinh
    try:
        return json.loads(chuoi)
    except (TypeError, ValueError):
        return mac_dinh


def _mo(lay_ket_noi: Optional[Callable[[], Any]]):
    return (lay_ket_noi or csdl.ket_noi)()


# =====================================================================
# PHIÊN
# =====================================================================

def tai_phien(phien: str, *, _lay_ket_noi: Optional[Callable[[], Any]] = None) -> Optional[Dict[str, Any]]:
    """Đọc một phiên. None nếu chưa có hoặc CSDL hỏng."""
    if not phien:
        return None
    try:
        conn = _mo(_lay_ket_noi)
        if conn is None:
            return None
        with conn:
            with conn.cursor() as ct:
                ct.execute(
                    'SELECT may, nguoi_dung, so_tay, lich_su, so_luot, compact, compact_den_luot '
                    'FROM chat_phien WHERE phien = %s',
                    (phien,),
                )
                dong = ct.fetchone()
        if not dong:
            return None
        lich_su = _doc_json(dong.get('lich_su'), [])
        return {
            'may': dong.get('may') or '',
            'nguoi_dung': dong.get('nguoi_dung') or '',
            'so_tay': chuan_hoa_so_tay(_doc_json(dong.get('so_tay'), {})),
            'lich_su': lich_su if isinstance(lich_su, list) else [],
            'so_luot': int(dong.get('so_luot') or 0),
            'compact': dong.get('compact') or '',
            'compact_den_luot': int(dong.get('compact_den_luot') or 0),
        }
    except Exception as loi:  # noqa: BLE001 - CSDL hỏng không được làm hỏng câu trả lời
        _log.warning('Không đọc được chat_phien %r: %s', phien, loi)
        return None


def cat_lich_su(lich_su: List[Dict[str, str]], so_luot: int) -> List[Dict[str, str]]:
    return list(lich_su or [])[-(so_luot * 2):]


def them_luot(lich_su: List[Dict[str, str]], cau_khach: str, cau_bot: str) -> List[Dict[str, str]]:
    """Thêm một lượt, câu bot cắt ngắn, che dữ liệu nhạy cảm, giữ GIU_LUOT_LUU lượt."""
    bot = (cau_bot or '').strip()
    if len(bot) > CAT_CAU_BOT:
        bot = bot[:CAT_CAU_BOT].rstrip() + '…'
    moi = list(lich_su or []) + [
        {'vai': 'khach', 'noi_dung': che_du_lieu_nhay_cam(cau_khach or '')},
        {'vai': 'bot', 'noi_dung': bot},
    ]
    return cat_lich_su(moi, GIU_LUOT_LUU)


def luu_phien(
    phien: str,
    *,
    khong_gian: str,
    may: str,
    nguoi_dung: str,
    ngon_ngu: str,
    so_tay: Dict[str, Any],
    lich_su: List[Dict[str, str]],
    da_goi_llm: bool,
    _lay_ket_noi: Optional[Callable[[], Any]] = None,
) -> bool:
    """Ghi (thêm hoặc cập nhật) phiên sau một lượt. True nếu ghi được."""
    if not phien:
        return False
    try:
        conn = _mo(_lay_ket_noi)
        if conn is None:
            return False
        nd = nguoi_dung if (nguoi_dung and nguoi_dung != 'khach') else None
        with conn:
            with conn.cursor() as ct:
                ct.execute(
                    """
                    INSERT INTO chat_phien
                        (phien, khong_gian, may, nguoi_dung, ngon_ngu, bat_dau, cap_nhat,
                         so_luot, so_luot_ai, so_tay, lich_su)
                    VALUES (%s, %s, %s, %s, %s, NOW(), NOW(), 1, %s, %s, %s)
                    ON DUPLICATE KEY UPDATE
                        may        = COALESCE(VALUES(may), may),
                        nguoi_dung = COALESCE(VALUES(nguoi_dung), nguoi_dung),
                        ngon_ngu   = VALUES(ngon_ngu),
                        cap_nhat   = NOW(),
                        so_luot    = so_luot + 1,
                        so_luot_ai = so_luot_ai + VALUES(so_luot_ai),
                        so_tay     = VALUES(so_tay),
                        lich_su    = VALUES(lich_su)
                    """,
                    (phien, khong_gian, may or None, nd, (ngon_ngu or 'vi')[:2],
                     1 if da_goi_llm else 0, _json(so_tay), _json(lich_su)),
                )
            conn.commit()
        return True
    except Exception as loi:  # noqa: BLE001
        _log.warning('Không ghi được chat_phien %r: %s', phien, loi)
        return False


def chen_luot(
    phien: str,
    cac_cap: List[Tuple[str, str]],
    *,
    khong_gian: str,
    may: str,
    _lay_ket_noi: Optional[Callable[[], Any]] = None,
) -> bool:
    """Ghi thêm các lượt KHÔNG do bot trả lời (khách ↔ nhân viên nhắn tay, 01/10/2026) vào lịch sử phiên,
    để lượt bot kế tiếp thấy toàn cảnh. Mỗi cặp = (lời khách, lời nhân viên) — phía nhân viên nằm ở vai
    'bot' (lịch sử chỉ có 2 vai), nội dung đã gắn nhãn "[Nhân viên shop nhắn tay]". Không đụng sổ tay."""
    if not phien or not cac_cap:
        return False
    try:
        du = tai_phien(phien, _lay_ket_noi=_lay_ket_noi) or {}
        lich_su = list(du.get('lich_su') or [])
        for khach, nhan_vien in cac_cap:
            lich_su = them_luot(lich_su, khach, che_du_lieu_nhay_cam(nhan_vien or ''))
        conn = _mo(_lay_ket_noi)
        if conn is None:
            return False
        with conn:
            with conn.cursor() as ct:
                ct.execute(
                    """
                    INSERT INTO chat_phien
                        (phien, khong_gian, may, nguoi_dung, ngon_ngu, bat_dau, cap_nhat,
                         so_luot, so_luot_ai, so_tay, lich_su)
                    VALUES (%s, %s, %s, NULL, 'vi', NOW(), NOW(), %s, 0, '{}', %s)
                    ON DUPLICATE KEY UPDATE
                        cap_nhat = NOW(),
                        so_luot  = so_luot + VALUES(so_luot),
                        lich_su  = VALUES(lich_su)
                    """,
                    (phien, khong_gian, may or None, len(cac_cap), _json(lich_su)),
                )
            conn.commit()
        return True
    except Exception as loi:  # noqa: BLE001
        _log.warning('Không chèn được lượt nhân viên vào chat_phien %r: %s', phien, loi)
        return False


def luu_compact(
    phien: str, compact: str, den_luot: int, *, _lay_ket_noi: Optional[Callable[[], Any]] = None,
) -> bool:
    """Ghi bản tóm gọn (nao/compact.py). `den_luot` = so_luot của phiên lúc
    tóm gọn — các lượt sau số đó là "lượt mới" chưa nằm trong bản tóm gọn.

    Chỉ ghi khi den_luot LỚN HƠN bản đang có: hai luồng tóm gọn chạy chồng
    nhau thì bản cũ hơn không được đè bản mới hơn."""
    if not phien:
        return False
    try:
        conn = _mo(_lay_ket_noi)
        if conn is None:
            return False
        with conn:
            with conn.cursor() as ct:
                ct.execute(
                    'UPDATE chat_phien SET compact = %s, compact_den_luot = %s, compact_luc = NOW() '
                    'WHERE phien = %s AND compact_den_luot < %s',
                    (che_du_lieu_nhay_cam(compact or ''), int(den_luot), phien, int(den_luot)),
                )
            conn.commit()
        return True
    except Exception as loi:  # noqa: BLE001
        _log.warning('Không ghi được bản tóm gọn cho phiên %r: %s', phien, loi)
        return False


# =====================================================================
# HỒ SƠ KHÁCH
# =====================================================================

def tai_ho_so(nguoi_dung: str, may: str, *, _lay_ket_noi: Optional[Callable[[], Any]] = None) -> Optional[Dict[str, Any]]:
    """Hồ sơ của khách này (ưu tiên tài khoản, rồi tới thiết bị)."""
    khoa = khoa_ho_so(nguoi_dung, may)
    if not khoa:
        return None
    try:
        conn = _mo(_lay_ket_noi)
        if conn is None:
            return None
        with conn:
            with conn.cursor() as ct:
                ct.execute(
                    'SELECT khoa, so_tay, tom_tat, phan_loai, muc_quan_tam, bang_khoan, luu_y, ghi_chu_admin, so_phien '
                    'FROM chat_ho_so WHERE khoa = %s',
                    (khoa,),
                )
                dong = ct.fetchone()
        if not dong:
            return None
        dong = dict(dong)
        dong['so_tay'] = chuan_hoa_so_tay(_doc_json(dong.get('so_tay'), {}))
        return dong
    except Exception as loi:  # noqa: BLE001
        _log.warning('Không đọc được chat_ho_so %r: %s', khoa, loi)
        return None


# Mỗi phiên chỉ cần nhận về tài khoản MỘT lần — nhớ trong RAM là đủ, lỡ
# khởi động lại thì chạy lại một lần, vô hại.
_da_nhan: set = set()
_khoa_da_nhan = threading.Lock()


def _noi(a: Optional[str], b: Optional[str], dai: int) -> str:
    phan = [x.strip() for x in (a, b) if x and x.strip()]
    return ' | '.join(phan)[:dai]


def nhan_ve_tai_khoan(
    phien: str, may: str, nguoi_dung: str, *, _lay_ket_noi: Optional[Callable[[], Any]] = None,
) -> None:
    """Khách vừa đăng nhập trên một thiết bị: nhận các phiên ẩn danh của
    thiết bị đó về tài khoản, và gộp hồ sơ 'm:<may>' vào 'u:<username>'.

    Máy dùng chung (quán net, máy công ty) thì phiên ẩn danh trước đó có
    thể của người khác — chấp nhận, vì chỉ có sổ tay/tóm tắt kỹ thuật, không
    có dữ liệu nhạy cảm (đã che ở so_tay.che_du_lieu_nhay_cam)."""
    if not (may and nguoi_dung and nguoi_dung != 'khach'):
        return
    with _khoa_da_nhan:
        if phien in _da_nhan:
            return
        _da_nhan.add(phien)
    khoa_m, khoa_u = 'm:' + may, 'u:' + nguoi_dung
    try:
        conn = _mo(_lay_ket_noi)
        if conn is None:
            return
        with conn:
            with conn.cursor() as ct:
                ct.execute(
                    'UPDATE chat_phien SET nguoi_dung = %s WHERE may = %s AND nguoi_dung IS NULL',
                    (nguoi_dung, may),
                )
                ct.execute('SELECT * FROM chat_ho_so WHERE khoa IN (%s, %s) FOR UPDATE', (khoa_m, khoa_u))
                cac = {d['khoa']: d for d in ct.fetchall()}
                m, u = cac.get(khoa_m), cac.get(khoa_u)
                if m and not u:
                    ct.execute(
                        'UPDATE chat_ho_so SET khoa = %s, nguoi_dung = %s WHERE khoa = %s',
                        (khoa_u, nguoi_dung, khoa_m),
                    )
                elif m and u:
                    moi_hon = m if (m.get('lan_cuoi') and u.get('lan_cuoi') and m['lan_cuoi'] > u['lan_cuoi']) else u
                    cu_hon = u if moi_hon is m else m
                    so_tay = gop(_doc_json(cu_hon.get('so_tay'), {}), _doc_json(moi_hon.get('so_tay'), {}))
                    ct.execute(
                        """
                        UPDATE chat_ho_so SET
                            so_tay = %s, tom_tat = %s, phan_loai = %s, muc_quan_tam = %s,
                            bang_khoan = %s, luu_y = %s, so_phien = %s,
                            lan_dau = LEAST(lan_dau, %s), lan_cuoi = GREATEST(lan_cuoi, %s),
                            cap_nhat = NOW(), ghi_chu_admin = %s
                        WHERE khoa = %s
                        """,
                        (
                            _json(so_tay),
                            _noi(cu_hon.get('tom_tat'), moi_hon.get('tom_tat'), 1500),
                            moi_hon.get('phan_loai') or cu_hon.get('phan_loai'),
                            moi_hon.get('muc_quan_tam') or cu_hon.get('muc_quan_tam'),
                            _noi(cu_hon.get('bang_khoan'), moi_hon.get('bang_khoan'), 500),
                            _noi(cu_hon.get('luu_y'), moi_hon.get('luu_y'), 500),
                            int(m.get('so_phien') or 0) + int(u.get('so_phien') or 0),
                            m['lan_dau'], m['lan_cuoi'],
                            _noi(u.get('ghi_chu_admin'), m.get('ghi_chu_admin'), 1000),
                            khoa_u,
                        ),
                    )
                    ct.execute('DELETE FROM chat_ho_so WHERE khoa = %s', (khoa_m,))
            conn.commit()
        _log.info('Đã nhận phiên ẩn danh của thiết bị về tài khoản (phien=%s).', phien)
    except Exception as loi:  # noqa: BLE001
        _log.warning('Không nhận được phiên ẩn danh về tài khoản: %s', loi)
        with _khoa_da_nhan:
            _da_nhan.discard(phien)


# =====================================================================
# TỰ KIỂM — kết nối GIẢ LẬP, không cần CSDL
# =====================================================================
if __name__ == '__main__':
    print('=== 1) sach_ma / khoa_ho_so ===')
    assert sach_ma("web1<script>'x") == 'web1scriptx'
    assert khoa_ho_so('quang', 'abc') == 'u:quang'
    assert khoa_ho_so('khach', 'abc') == 'm:abc'
    assert khoa_ho_so('khach', '') is None
    print('  OK')

    print('\n=== 2) them_luot: cắt câu bot, che key, giữ đúng số lượt ===')
    ls: List[Dict[str, str]] = []
    for i in range(GIU_LUOT_LUU + 3):
        ls = them_luot(ls, f'câu {i} key ZX81KQ7MPL20AB9C', 'x' * (CAT_CAU_BOT + 500))
    assert len(ls) == GIU_LUOT_LUU * 2, len(ls)
    assert ls[0]['noi_dung'].startswith('câu 3 '), ls[0]
    assert all(len(d['noi_dung']) <= CAT_CAU_BOT + 1 for d in ls if d['vai'] == 'bot')
    assert 'ZX81KQ7MPL20AB9C' not in _json(ls), 'LỖI: key lọt vào lịch sử lưu'
    print('  OK', len(ls), 'dòng')

    print('\n=== 3) CSDL hỏng / không có -> không ném lỗi ===')
    assert tai_phien('p', _lay_ket_noi=lambda: None) is None
    assert luu_phien('p', khong_gian='t', may='', nguoi_dung='khach', ngon_ngu='vi',
                     so_tay={}, lich_su=[], da_goi_llm=False, _lay_ket_noi=lambda: None) is False

    def _no():
        raise RuntimeError('mất kết nối')
    assert tai_phien('p', _lay_ket_noi=_no) is None
    assert tai_ho_so('u', 'm', _lay_ket_noi=_no) is None
    nhan_ve_tai_khoan('p1', 'm', 'u', _lay_ket_noi=_no)
    print('  OK')

    print('\n=== 4) luu_phien dựng đúng tham số ===')

    class _ConTro:
        def __init__(self, nk): self.nk = nk
        def __enter__(self): return self
        def __exit__(self, *a): return False
        def execute(self, sql, ts=None): self.nk.append((sql, ts))
        def fetchone(self): return None
        def fetchall(self): return []

    class _Conn:
        def __init__(self): self.nk = []
        def __enter__(self): return self
        def __exit__(self, *a): return False
        def cursor(self): return _ConTro(self.nk)
        def commit(self): pass

    c = _Conn()
    ok = luu_phien('p9', khong_gian='tradingauto', may='m1', nguoi_dung='khach', ngon_ngu='vi',
                   so_tay={'dang_hoi': 'copytrade'}, lich_su=[], da_goi_llm=True, _lay_ket_noi=lambda: c)
    sql, ts = c.nk[0]
    assert ok and 'INSERT INTO chat_phien' in sql
    assert ts[0] == 'p9' and ts[2] == 'm1' and ts[3] is None and ts[5] == 1, ts
    print('  OK — khách chưa đăng nhập ghi nguoi_dung = NULL, không ghi chữ "khach"')
    print('\nTỰ KIỂM phien.py QUA.')
