# -*- coding: utf-8 -*-
"""NHAT_KY.PY — ghi lại MỌI lượt hỏi-đáp bot ĐÃ TRẢ LỜI (cả ba tầng: kho
tư vấn / tìm-diễn giải / câu mẫu-chuyển người) vào bảng `chat_hoi_thoai`.

VÌ SAO FILE NÀY TỒN TẠI
------------------------
`congcu/hang_doi.py` (bảng `chat_thieu`) chỉ ghi câu khách hỏi mà TẦNG 1
KHÔNG khớp được — dùng để chủ website soạn thêm mục tư vấn mới. Nhưng
những câu bot ĐÃ trả lời (kể cả trả đúng lẫn trả sai) trước giờ KHÔNG lưu
nội dung ở đâu cả, nên chủ website không có cách nào soi lại chất lượng
để sửa một mục tư vấn soạn sai, một câu AI diễn giải lệch, hay đo tỉ lệ
phủ tầng 1. Tệp này lấp đúng lỗ hổng đó: MỌI lượt trả lời (kể cả tầng 1)
đều được ghi một dòng, kèm chỗ trống `danh_gia`/`ghi_chu` để chủ website
tự tay đánh giá về sau (xem `private/sql/2026-09-22_nhat_ky_hoi_thoai.sql`,
do phiên khác dựng — module này chỉ INSERT vào bảng đã có sẵn).

RIÊNG TƯ — ĐỌC KỸ TRƯỚC KHI SỬA
----------------------------------
Bảng `chat_hoi_thoai` KHÔNG lưu tên đăng nhập, KHÔNG lưu địa chỉ IP,
KHÔNG lưu bất cứ định danh nào của khách. Hàm `ghi()` bên dưới CHỦ Ý
không đọc `ngu_canh.nguoi_dung` — chỉ dùng `ngu_canh.phien` (một mã phiên
ngẫu nhiên do phía chatbot tự sinh, KHÔNG truy ngược ra người thật) để
gom các dòng của cùng một cuộc trò chuyện. ĐỪNG thêm cột/tham số nào lưu
thêm danh tính khách vào đây.

Cách chạy tự kiểm (không cần CSDL — có một ca tiêm kết nối GIẢ LẬP để
soát câu SQL và thứ tự tham số dựng ra):
    python congcu/nhat_ky.py
"""
from __future__ import annotations

import logging
import os
import sys
from typing import Any, Callable, Optional

# ---------------------------------------------------------------------
# Nhập congcu.csdl — chịu được cả hai cách chạy, y hệt congcu/hang_doi.py.
# ---------------------------------------------------------------------
try:
    from . import csdl
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

# ---------------------------------------------------------------------
# Nhập giao ước — chịu được cả hai cách chạy, y hệt nao/tra_loi.py.
# ---------------------------------------------------------------------
try:
    from giao_uoc import TraLoi, NguCanh
except ImportError:
    _THU_MUC_TOI = os.path.dirname(os.path.abspath(__file__))
    _THU_MUC_CHA = os.path.dirname(_THU_MUC_TOI)
    if _THU_MUC_CHA not in sys.path:
        sys.path.insert(0, _THU_MUC_CHA)
    from giao_uoc import TraLoi, NguCanh  # type: ignore

# `san_pham` chỉ để soi lỗi/thống kê, KHÔNG phải dữ liệu sống còn của
# nhật ký — import hỏng (nao.dinh_tuyen chưa xong/lỗi vòng import) thì để
# None, ĐỪNG để cả module nhat_ky.py sập theo.
try:
    from nao.dinh_tuyen import doan_san_pham
except Exception as _loi_dt:  # noqa: BLE001 - xem lý do bọc rộng ở trên
    doan_san_pham = None  # type: ignore
    logging.getLogger('chatbot.congcu.nhat_ky').error(
        'Chưa dùng được nao.dinh_tuyen.doan_san_pham() — cột san_pham của '
        'chat_hoi_thoai sẽ luôn NULL. Lỗi: %s', _loi_dt,
    )

_log = logging.getLogger('chatbot.congcu.nhat_ky')

# Cột `cau_hoi` là VARCHAR(500) — cắt ở đây, không dựa vào MySQL cắt ngầm.
DO_DAI_TOI_DA_CAU_HOI = 500


def ghi(
    cau_hoi: str,
    ket_qua: "TraLoi",
    ngu_canh: "NguCanh",
    *,
    _lay_ket_noi: Optional[Callable[[], Any]] = None,
) -> None:
    """Ghi MỘT dòng vào `chat_hoi_thoai` cho lượt trả lời này.

    Gọi cho MỌI lượt bot đã trả lời — kể cả tầng 1 (khác `hang_doi.ghi_nhan`,
    chỉ nhận câu KHÔNG trả lời được). `danh_gia`/`ghi_chu` để MySQL tự điền
    giá trị mặc định ('chua' / '') — đó là việc của chủ website, không phải
    của bot, nên câu INSERT dưới đây không đụng tới hai cột đó.

    `_lay_ket_noi`: điểm TIÊM nguồn kết nối, mặc định `csdl.ket_noi` (CSDL
    thật). Tự kiểm ở cuối tệp tiêm một kết nối GIẢ LẬP vào đây để chạy
    không cần CSDL và soát đúng câu SQL/tham số dựng ra — KHÔNG dùng để
    thay đổi hành vi lúc chạy thật.

    MỌI lỗi (thiếu CSDL, thiếu bảng, mất kết nối giữa chừng, ngu_canh/
    ket_qua thiếu trường lạ...) đều bị NUỐT ở đây, chỉ `logging.warning` —
    ghi nhật ký hỏng TUYỆT ĐỐI không được làm hỏng câu trả lời đã gửi cho
    khách. Đây là tính chất quan trọng nhất của tệp này.
    """
    try:
        cau_hoi_luu = (cau_hoi or '').strip()[:DO_DAI_TOI_DA_CAU_HOI]
        if not cau_hoi_luu:
            # Chưa phải một câu hỏi thật (đã bị chặn sớm ở nao/tra_loi.py
            # bước 1 rồi, nhưng phòng thêm một lớp ở chính hàm ghi này).
            return

        try:
            san_pham = doan_san_pham(cau_hoi_luu) if doan_san_pham is not None else None
        except Exception:  # noqa: BLE001 - đoán sản phẩm hỏng không được cản việc ghi nhật ký
            san_pham = None

        # THỨ TỰ tham số PHẢI khớp CHÍNH XÁC thứ tự cột trong câu INSERT
        # bên dưới — xem tự kiểm 2) ở khối __main__ soát đúng điều này.
        tham_so = (
            ngu_canh.khong_gian,
            (getattr(ngu_canh, 'kenh', '') or 'web')[:16],
            ngu_canh.phien,
            cau_hoi_luu,
            ket_qua.tra_loi,
            ket_qua.tang,
            ket_qua.y_dinh,
            san_pham,
            ket_qua.muc_id,
            round(ket_qua.do_tin_cay, 3),
            ket_qua.model,
            ket_qua.token_vao,
            ket_qua.token_ra,
        )

        lay_ket_noi = _lay_ket_noi or csdl.ket_noi
        conn = lay_ket_noi()
        if conn is None:
            return
        try:
            with conn:
                with conn.cursor() as con_tro:
                    con_tro.execute(
                        """
                        INSERT INTO chat_hoi_thoai
                            (khong_gian, kenh, phien, luc, cau_hoi, tra_loi, tang,
                             y_dinh, san_pham, muc_id, do_tin_cay, model,
                             token_vao, token_ra)
                        VALUES
                            (%s, %s, %s, NOW(), %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
                        """,
                        tham_so,
                    )
                conn.commit()
        finally:
            try:
                conn.close()
            except Exception:  # noqa: BLE001 - `with conn:` ở trên thường đã đóng, phòng thêm không hại gì
                pass
    except Exception as loi:  # noqa: BLE001 - LƯỚI AN TOÀN CUỐI CÙNG, xem lời dặn ở docstring
        _log.warning('Lỗi khi ghi nhật ký hội thoại vào chat_hoi_thoai: %s', loi)


# =====================================================================
# TỰ KIỂM — không cần CSDL thật
# =====================================================================
if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')

    print('=' * 78)
    print('1) TỰ KIỂM ghi() khi KHÔNG có CSDL — không được sập')
    print('=' * 78)
    nc_that = NguCanh(phien='phien-tu-kiem-1', ngon_ngu='vi', nguoi_dung='ten_dang_nhap_bi_mat')
    kq_that = TraLoi(
        tra_loi='Dạ DCA Bot hiện có giá 29 USD/tháng ạ.', nguon=[], y_dinh='gia',
        chuyen_nguoi=False, do_tin_cay=0.91, da_goi_llm=False, tang=1, muc_id=7,
    )
    ghi('DCA Bot giá bao nhiêu?', kq_that, nc_that)
    ghi('', kq_that, nc_that)  # câu rỗng sau strip -> phải bị bỏ qua sớm, không chạm CSDL
    print('  ghi() chạy xong (cả câu bình thường lẫn câu rỗng), không ném lỗi ra ngoài.')
    print()

    print('=' * 78)
    print('2) TỰ KIỂM ghi() với KẾT NỐI GIẢ LẬP TIÊM VÀO — soát câu SQL và tham số')
    print('=' * 78)

    class _ConTroGiaLap:
        def __init__(self, so_lan_goi: list) -> None:
            self._so_lan_goi = so_lan_goi

        def __enter__(self) -> "_ConTroGiaLap":
            return self

        def __exit__(self, *_a) -> bool:
            return False

        def execute(self, sql: str, tham_so) -> None:
            self._so_lan_goi.append((sql, tham_so))

    class _KetNoiGiaLap:
        """Bắt chước đúng cách `ghi()` dùng kết nối pymysql thật:
        `with conn:` ... `conn.cursor()` ... `conn.commit()` ... `conn.close()`."""

        def __init__(self) -> None:
            self.cac_lan_execute: list = []
            self.da_commit = False
            self.da_close = False

        def __enter__(self) -> "_KetNoiGiaLap":
            return self

        def __exit__(self, *_a) -> bool:
            return False

        def cursor(self) -> _ConTroGiaLap:
            return _ConTroGiaLap(self.cac_lan_execute)

        def commit(self) -> None:
            self.da_commit = True

        def close(self) -> None:
            self.da_close = True

    ket_noi_gia_lap = _KetNoiGiaLap()
    ngu_canh_gia_lap = NguCanh(
        phien='phien-abc-123', ngon_ngu='vi', nguoi_dung='nguoi_dung_khong_duoc_ghi',
        khong_gian='tradingauto',
    )
    ket_qua_gia_lap = TraLoi(
        tra_loi='Anh/chị tải EA về rồi kéo vào Experts của MT5 ạ.', nguon=[],
        y_dinh='huong_dan', chuyen_nguoi=False, do_tin_cay=0.873456, da_goi_llm=True,
        tang=2, model='deepseek-v4-flash', token_vao=2731, token_ra=418,
    )
    ghi(
        'cài dcabot vào mt5 sao',
        ket_qua_gia_lap,
        ngu_canh_gia_lap,
        _lay_ket_noi=lambda: ket_noi_gia_lap,
    )

    assert len(ket_noi_gia_lap.cac_lan_execute) == 1, (
        f'LỖI: kỳ vọng đúng 1 lần execute(), nhận được {len(ket_noi_gia_lap.cac_lan_execute)}'
    )
    sql_da_chay, tham_so_da_chay = ket_noi_gia_lap.cac_lan_execute[0]
    print('  Câu SQL đã dựng:')
    print(' '.join(sql_da_chay.split()))
    print()
    print(f'  Danh sách tham số (đúng thứ tự): {tham_so_da_chay!r}')
    print()

    assert 'INSERT INTO chat_hoi_thoai' in sql_da_chay
    assert ket_noi_gia_lap.da_commit is True, 'LỖI: phải gọi commit() sau khi execute() thành công'
    assert ket_noi_gia_lap.da_close is True, 'LỖI: phải đóng kết nối sau khi dùng xong'

    try:
        san_pham_ky_vong = doan_san_pham('cài dcabot vào mt5 sao') if doan_san_pham is not None else None
    except Exception:  # noqa: BLE001
        san_pham_ky_vong = None
    KY_VONG_THAM_SO = (
        'tradingauto',                                       # khong_gian
        'web',                                               # kenh (24/09/2026)
        'phien-abc-123',                                     # phien
        'cài dcabot vào mt5 sao',                             # cau_hoi
        'Anh/chị tải EA về rồi kéo vào Experts của MT5 ạ.',   # tra_loi
        2,                                                    # tang
        'huong_dan',                                          # y_dinh
        san_pham_ky_vong,                                     # san_pham
        None,                                                 # muc_id (tầng 2 không có)
        0.873,                                                # do_tin_cay (làm tròn 3 chữ số)
        'deepseek-v4-flash',                                  # model
        2731,                                                 # token_vao
        418,                                                  # token_ra
    )
    assert tham_so_da_chay == KY_VONG_THAM_SO, (
        f'LỖI: tham số không đúng thứ tự/giá trị mong đợi.\n'
        f'  nhận được: {tham_so_da_chay!r}\n'
        f'  kỳ vọng:   {KY_VONG_THAM_SO!r}'
    )
    print('  KHẲNG ĐỊNH: câu SQL và toàn bộ tham số (đúng thứ tự cột) khớp kỳ vọng — QUA.')
    print()

    print('=' * 78)
    print('3) KHẲNG ĐỊNH RIÊNG TƯ — KHÔNG có tham số nào chứa ngu_canh.nguoi_dung')
    print('=' * 78)
    ten_dang_nhap_bi_cam = ngu_canh_gia_lap.nguoi_dung
    print(f'  ngu_canh.nguoi_dung = {ten_dang_nhap_bi_cam!r} (KHÔNG được xuất hiện trong tham số đã ghi)')
    for gia_tri in tham_so_da_chay:
        assert gia_tri != ten_dang_nhap_bi_cam, (
            f'LỖI RIÊNG TƯ: tham số {gia_tri!r} trùng ngu_canh.nguoi_dung — lộ danh tính khách!'
        )
        if isinstance(gia_tri, str):
            assert ten_dang_nhap_bi_cam not in gia_tri, (
                f'LỖI RIÊNG TƯ: tham số {gia_tri!r} chứa ngu_canh.nguoi_dung — lộ danh tính khách!'
            )
    assert 'nguoi_dung' not in sql_da_chay, 'LỖI RIÊNG TƯ: câu SQL không được nhắc tới cột nguoi_dung'
    print('  KHẲNG ĐỊNH: không tham số nào chứa/khớp ngu_canh.nguoi_dung, câu SQL không có cột định danh khách — QUA.')
    print()

    print('TỰ KIỂM nhat_ky.py HOÀN TẤT — TẤT CẢ ĐÚNG NHƯ KỲ VỌNG.')
