# -*- coding: utf-8 -*-
"""SO_TAY.PY — sổ tay của một phiên tư vấn, trích bằng LUẬT (0 token).

VÌ SAO FILE NÀY TỒN TẠI
------------------------
Trước đây bot chỉ nhớ 3 lượt hỏi-đáp NGUYÊN VĂN. Lượt thứ 4 trở về trước
biến mất hẳn: khách nói "em dùng MT4, sàn Exness" ở lượt 1 thì tới lượt 5
bot đã quên. Mà 3 lượt nguyên văn lại tốn chỗ — một câu trả lời tầng 1 dài
vài trăm chữ, ba lượt là chiếm gần hết phần gửi cho mô hình.

Sổ tay giữ đúng những thứ NGƯỜI BÁN HÀNG THẬT sẽ ghi lại: khách hỏi sản
phẩm nào, chạy MT4 hay MT5, sàn nào, tài khoản demo hay thật, đã gặp lỗi
gì. Vài dòng, không bao giờ trôi mất, và trích bằng luật nên không tốn gì.

KHÔNG BAO GIỜ GHI VÀO SỔ TAY
-----------------------------
Key, mật khẩu, email, số điện thoại, số tài khoản. Quyết định của chủ dự án:
dữ liệu key của khách không được lòi ra bảng tư vấn. `che_du_lieu_nhay_cam()`
dưới đây là lớp chắn dùng chung — nao/tom_tat.py cũng đi qua nó trước khi
gửi bất cứ gì cho mô hình.

Chạy tự kiểm:  python congcu/so_tay.py
"""
from __future__ import annotations

import os
import re
import sys
from typing import Any, Dict, List, Optional

try:
    from nao.dinh_tuyen import chuan_hoa, doan_san_pham
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 nao.dinh_tuyen import chuan_hoa, doan_san_pham  # type: ignore


# Giữ tối đa bấy nhiêu mục trong mỗi danh sách — sổ tay phải NGẮN, nó đi
# vào mọi prompt tầng 2.
_TOI_DA_DS = 5

# =====================================================================
# CHE DỮ LIỆU NHẠY CẢM — lớp chắn dùng chung
# =====================================================================

_MAU_EMAIL = re.compile(r'[\w.+-]+@[\w-]+\.[\w.-]+')
# Số điện thoại VN: 0xxxxxxxxx hoặc +84xxxxxxxxx, cho phép dấu cách/chấm.
_MAU_SDT = re.compile(r'(?<!\d)(?:\+?84|0)(?:[\s.-]?\d){8,10}(?!\d)')
# Chuỗi giống key/mật khẩu: >= 12 ký tự liền, có CẢ chữ lẫn số. Mã lỗi MT
# (4109, 10013) chỉ có số và ngắn, nên không bị che nhầm.
_MAU_GIONG_KEY = re.compile(r'\b(?=[A-Za-z0-9_-]*[A-Za-z])(?=[A-Za-z0-9_-]*\d)[A-Za-z0-9_-]{12,}\b')
# Dãy số dài (số tài khoản ngân hàng, số tài khoản MT) — 7 chữ số trở lên.
_MAU_SO_DAI = re.compile(r'(?<!\d)\d{7,}(?!\d)')


def che_du_lieu_nhay_cam(van_ban: str) -> str:
    """Thay email, số điện thoại, chuỗi giống key, dãy số dài bằng '[ẩn]'."""
    if not van_ban:
        return ''
    van_ban = _MAU_EMAIL.sub('[ẩn]', van_ban)
    van_ban = _MAU_SDT.sub('[ẩn]', van_ban)
    van_ban = _MAU_GIONG_KEY.sub('[ẩn]', van_ban)
    van_ban = _MAU_SO_DAI.sub('[ẩn]', van_ban)
    return van_ban


# =====================================================================
# LUẬT TRÍCH
# =====================================================================

# (mẫu trên văn bản ĐÃ chuan_hoa — chữ thường, bỏ dấu)
_MAU_MT4 = re.compile(r'\b(mt ?4|metatrader ?4)\b')
_MAU_MT5 = re.compile(r'\b(mt ?5|metatrader ?5)\b')

# Tên hiển thị -> các cách khách hay gõ (đã chuẩn hoá).
_SAN_GIAO_DICH = {
    'Exness': ('exness',),
    'Vantage': ('vantage',),
    'XM': ('xm',),
    'IC Markets': ('ic markets', 'icmarkets', 'ic market'),
    'FBS': ('fbs',),
    'Octa': ('octa', 'octafx'),
    'HFM': ('hfm', 'hotforex', 'hf markets'),
    'FxPro': ('fxpro',),
    'Pepperstone': ('pepperstone',),
    'Tickmill': ('tickmill',),
    'RoboForex': ('roboforex',),
    'FXTM': ('fxtm',),
    'JustMarkets': ('justmarkets', 'just markets'),
    'Headway': ('headway',),
    'Valetax': ('valetax',),
    'FXPrimus': ('fxprimus',),
    'Binance': ('binance',),
    'Bybit': ('bybit',),
    'OKX': ('okx',),
}
_DS_SAN = tuple(
    (re.compile(r'\b' + re.escape(bd) + r'\b'), ten)
    for ten, cac_bd in _SAN_GIAO_DICH.items() for bd in cac_bd
)

_MAU_DEMO = re.compile(r'\b(demo|tai khoan thu|tk thu)\b')
# Khách đang bàn chuyện ĐẶT CODE theo yêu cầu (không phải một sản phẩm có sẵn).
# Để câu nối tiếp "chi phí như nào" hiểu là giá dịch vụ code, không phải bảng
# giá sản phẩm (Messenger 24/09/2026: khách bàn bot Pine + Python rồi hỏi giá,
# bot đổ nguyên bảng giá 7 sản phẩm). "signal code" của DCA bị bỏ trước khi so.
_MAU_DAT_CODE = re.compile(
    r'\b(code bot|code ea|code chien luoc|code chi bao|code tool|lap trinh|viet (?:bot|ea|tool|chi bao|indicator|code)'
    r'|dat (?:code|lam)|pine ?script|pine|python|tdv|code|viet lai ea|y tuong|chuyen (?:chien luoc )?thanh bot'
    r'|theo (?:yeu cau|nhu cau|chien luoc)|chien luoc (?:cua|rieng))\b')
# Sản phẩm trùng tên NỀN TẢNG — nhắc tới khi đang bàn code thường là nói nền tảng.
_SP_TRUNG_NEN_TANG = frozenset({'tradingview'})
# Thuật ngữ tả LOGIC giao dịch (dùng chung với nao/tra_loi.py::_can_chuyen_gia).
MAU_KY_THUAT = re.compile(
    r'\b(ema\d*|sma\d*|ma\d+|rsi|macd|atr|bollinger|bb|ichimoku|stoch\w*|fibo\w*|smc|bos|choch|order ?block|fvg|'
    r'vao lenh|thoat lenh|entry|exit|sl|tp|stop ?loss|take ?profit|trailing|break ?even|lot|khoi luong|'
    r'dca|grid|martingale|nhoi|hedge|breakout|khung|nen|chi bao|indicator|dieu kien|logic|backtest|'
    r'scalp\w*|swing|trend|xu huong|sideway|api|webhook|alert|pine|python|mql\d?|\d+r|priceaction|price action|'
    r'hanh dong gia|khang cu|thanh khoan|volume|buy|sell|long|short)\b')


def so_tu_ky_thuat(vb: str) -> int:
    """Số thuật ngữ logic KHÁC NHAU trong câu đã chuẩn hoá (bỏ dấu, chữ thường)."""
    return len(set(MAU_KY_THUAT.findall(vb or '')))


# "code" KHÔNG mang nghĩa đặt code: mã cấu hình DCA, mã kích hoạt, mã giảm giá, master code.
_MAU_SIGNAL_CODE = re.compile(r'\b(signal ?code|master ?code|ma code|code kich hoat|code giam gia|ma giam gia)\b')
_MAU_CENT = re.compile(r'\b(cent|tai khoan cent|tk cent)\b')
_MAU_REAL = re.compile(r'\b(real|live|tai khoan that|tk that|tien that)\b')

# Mã lỗi: "lỗi 4109", "error 10013", "code 130", "err 4060"
_MAU_MA_LOI = re.compile(r'\b(?:loi|error|err|code|ma loi)\s*[:#]?\s*(\d{2,5})\b')
# Vài thông báo lỗi chữ hay gặp trên MT4/MT5 — ghi đúng tên để người sau đọc hiểu.
_LOI_CHU = (
    (re.compile(r'\bdll\b'), 'DLL chưa bật / bị chặn'),
    (re.compile(r'\binvalid (symbol|stops|volume|price)\b'), None),  # None = lấy nguyên cụm khớp
    (re.compile(r'\b(not enough money|khong du tien|khong du margin)\b'), 'không đủ tiền/margin'),
    (re.compile(r'\b(trade is disabled|autotrading|algo ?trading)\b'), 'AutoTrading / Algo Trading'),
    (re.compile(r'\b(khong vao lenh|ko vao lenh|khong vao duoc lenh)\b'), 'bot không vào lệnh'),
    (re.compile(r'\b(sai key|key khong hop le|invalid key|license (?:invalid|expired))\b'), 'báo lỗi key/licence'),
)


def so_tay_rong() -> Dict[str, Any]:
    return {
        'dang_hoi': None,     # sản phẩm đang bàn trong phiên (gần nhất)
        'san_pham': [],       # mọi sản phẩm đã nhắc, gần nhất đứng đầu
        'nen_tang': None,     # 'MT4' | 'MT5' | 'MT4+MT5'
        'san': [],            # sàn giao dịch
        'loai_tk': None,      # 'demo' | 'cent' | 'thật'
        'loi': [],            # lỗi đã gặp
        'y_dinh': {},         # đếm ý định: {'gia': 2, 'loi': 1, ...}
        'chu_de': None,       # 'code' (đặt code theo yêu cầu) | 'san_pham' — chủ đề GẦN NHẤT
        'vua_hoi_lam_ro': False,  # lượt bot trước chỉ hỏi làm rõ (nao/tra_loi.py) —
                                  # để không hỏi lại hai lần liền
    }


def _dua_len_dau(ds: List[str], gia_tri: str) -> List[str]:
    """Đưa gia_tri lên đầu danh sách (gần nhất trước), bỏ trùng, cắt độ dài."""
    moi = [gia_tri] + [x for x in ds if x != gia_tri]
    return moi[:_TOI_DA_DS]


def chuan_hoa_so_tay(so_tay: Optional[Dict[str, Any]]) -> Dict[str, Any]:
    """Nhận sổ tay đọc từ CSDL (có thể thiếu khoá, sai kiểu) -> đủ khoá."""
    goc = so_tay_rong()
    if not isinstance(so_tay, dict):
        return goc
    for k, v in goc.items():
        gia_tri = so_tay.get(k, v)
        if isinstance(v, list) and not isinstance(gia_tri, list):
            gia_tri = []
        if isinstance(v, dict) and not isinstance(gia_tri, dict):
            gia_tri = {}
        goc[k] = gia_tri
    return goc


def cap_nhat(so_tay: Optional[Dict[str, Any]], cau_khach: str, y_dinh: Optional[str] = None,
             nganh: str = 'trading') -> Dict[str, Any]:
    """Đọc MỘT câu của khách, cập nhật sổ tay. Chỉ đọc câu KHÁCH nói —
    không đọc câu bot, vì bot liệt kê sáu sản phẩm để hỏi lại thì không có
    nghĩa khách quan tâm cả sáu."""
    st = chuan_hoa_so_tay(so_tay)
    cau = che_du_lieu_nhay_cam(cau_khach or '')
    vb = chuan_hoa(cau)
    if not vb:
        return st

    # Trích thông tin RIÊNG của ngành trading (sản phẩm TradingAuto, MT4/MT5, sàn, loại tài
    # khoản, mã lỗi, chủ đề đặt code). Ngành khác chỉ đếm ý định — trích riêng của ngành
    # đó thêm khi có dữ liệu (spa: dịch vụ, ngày giờ hẹn...).
    if nganh != 'trading':
        if y_dinh:
            st['y_dinh'][y_dinh] = int(st['y_dinh'].get(y_dinh, 0)) + 1
        return st

    sp = doan_san_pham(cau)
    if sp:
        st['dang_hoi'] = sp
        st['san_pham'] = _dua_len_dau(st['san_pham'], sp)

    # Một đoạn TẢ CHIẾN LƯỢC (nhiều thuật ngữ, không nêu sản phẩm có sẵn) cũng là
    # chuyện đặt code — khách đang đưa logic để người code.
    ta_chien_luoc = not sp and so_tu_ky_thuat(vb) >= 3
    if sp == 'codebot' or ta_chien_luoc or _MAU_DAT_CODE.search(_MAU_SIGNAL_CODE.sub(' ', vb)):
        st['chu_de'] = 'code'
    elif sp:
        # Đang bàn đặt code mà câu nhắc "TradingView" (nền tảng, trùng tên sản
        # phẩm) hoặc tả logic kỹ thuật -> vẫn là chuyện code, không đổi chủ đề.
        # Đo thật 24/09/2026: "con TradingView: vào lệnh khi EMA20 cắt EMA50..."
        # bị hiểu thành hỏi sản phẩm TradingView -> MT5, câu giá sau đó sai.
        giu_code = st['chu_de'] == 'code' and (sp in _SP_TRUNG_NEN_TANG or so_tu_ky_thuat(vb) >= 2)
        if not giu_code:
            st['chu_de'] = 'san_pham'

    co4, co5 = bool(_MAU_MT4.search(vb)), bool(_MAU_MT5.search(vb))
    if co4 and co5:
        st['nen_tang'] = 'MT4+MT5'
    elif co4:
        st['nen_tang'] = 'MT4'
    elif co5:
        st['nen_tang'] = 'MT5'

    for mau, ten in _DS_SAN:
        if mau.search(vb):
            st['san'] = _dua_len_dau(st['san'], ten)

    if _MAU_CENT.search(vb):
        st['loai_tk'] = 'cent'
    elif _MAU_DEMO.search(vb):
        st['loai_tk'] = 'demo'
    elif _MAU_REAL.search(vb):
        st['loai_tk'] = 'thật'

    for m in _MAU_MA_LOI.finditer(vb):
        st['loi'] = _dua_len_dau(st['loi'], 'mã ' + m.group(1))
    for mau, nhan in _LOI_CHU:
        m = mau.search(vb)
        if m:
            st['loi'] = _dua_len_dau(st['loi'], nhan or m.group(0))

    if y_dinh:
        st['y_dinh'][y_dinh] = int(st['y_dinh'].get(y_dinh, 0)) + 1

    return st


def gop(cu: Optional[Dict[str, Any]], moi: Optional[Dict[str, Any]]) -> Dict[str, Any]:
    """Gộp sổ tay phiên MỚI vào sổ tay hồ sơ CŨ. Giá trị đơn: mới đè cũ nếu
    mới có. Danh sách: mới lên đầu. Đếm ý định: cộng dồn."""
    a, b = chuan_hoa_so_tay(cu), chuan_hoa_so_tay(moi)
    kq = so_tay_rong()
    for k in ('dang_hoi', 'nen_tang', 'loai_tk'):
        kq[k] = b[k] or a[k]
    for k in ('san_pham', 'san', 'loi'):
        ds = list(a[k])
        for x in reversed(b[k]):
            ds = _dua_len_dau(ds, x)
        kq[k] = ds
    dem = dict(a['y_dinh'])
    for k, v in b['y_dinh'].items():
        dem[k] = int(dem.get(k, 0)) + int(v)
    kq['y_dinh'] = dem
    return kq


def thanh_van_ban(so_tay: Optional[Dict[str, Any]]) -> str:
    """Sổ tay -> vài dòng tiếng Việt cho prompt / trang quản trị. Rỗng nếu
    chưa biết gì."""
    st = chuan_hoa_so_tay(so_tay)
    dong = []
    if st['chu_de'] == 'code':
        dong.append('Khách đang bàn: ĐẶT CODE bot theo chiến lược riêng (dịch vụ code, không phải sản phẩm có sẵn)')
    if st['dang_hoi']:
        khac = [x for x in st['san_pham'] if x != st['dang_hoi']]
        dong.append('Sản phẩm đang hỏi: ' + st['dang_hoi']
                    + (f" (trước đó có hỏi: {', '.join(khac)})" if khac else ''))
    phan = []
    if st['nen_tang']:
        phan.append('Nền tảng: ' + st['nen_tang'])
    if st['san']:
        phan.append('Sàn: ' + ', '.join(st['san']))
    if st['loai_tk']:
        phan.append('Tài khoản: ' + st['loai_tk'])
    if phan:
        dong.append(' · '.join(phan))
    if st['loi']:
        dong.append('Lỗi đã gặp: ' + ', '.join(st['loi']))
    return '\n'.join(dong)


# =====================================================================
# TỰ KIỂM
# =====================================================================
if __name__ == '__main__':
    print('=== 1) che_du_lieu_nhay_cam ===')
    ca_che = (
        ('key em là AB12CD34EF56GH78 nhé', 'AB12CD34EF56GH78'),
        ('mail em test.khach@gmail.com', 'test.khach@gmail.com'),
        ('sdt 0912 345 678', '0912 345 678'),
        ('stk 1903456789012', '1903456789012'),
    )
    for vao, khong_duoc_con in ca_che:
        ra = che_du_lieu_nhay_cam(vao)
        print(f'  {vao!r:40} -> {ra!r}')
        assert khong_duoc_con not in ra, f'LỖI: còn lộ {khong_duoc_con!r}'
    giu = che_du_lieu_nhay_cam('lỗi 4109 trên MT5 build 5120')
    assert '4109' in giu and 'MT5' in giu, f'LỖI: che nhầm mã lỗi/nền tảng: {giu!r}'
    print('  OK — mã lỗi 4109 và MT5 không bị che nhầm')

    print('=== 1b) chủ đề đặt code ===')
    st = None
    for c in ('Code như nào bạn?', '2', 'TDV và cả python ấy', 'Chi phí như nào'):
        st = cap_nhat(st, c)
    assert st['chu_de'] == 'code', st
    st = cap_nhat(st, 'thế còn copytrade giá sao')
    assert st['chu_de'] == 'san_pham', st
    st = cap_nhat(cap_nhat(None, 'TDV và cả python ấy'),
                  'con TradingView: vào lệnh khi EMA20 cắt lên EMA50 khung H1, SL dưới đáy, TP 2R')
    assert st['chu_de'] == 'code', st
    assert cap_nhat(None, 'code bot trên mt5')['chu_de'] == 'code'
    st2 = cap_nhat(None, 'signal code điền gì cho dca bot')
    assert st2['chu_de'] == 'san_pham', st2
    print('  OK — code -> sản phẩm khi nêu sản phẩm; "signal code" không tính là đặt code')

    print('\n=== 2) cap_nhat qua một cuộc chat ===')
    st = None
    for cau, yd in (
        ('cài copytrade thế nào', 'huong_dan'),
        ('em dùng mt4 sàn exness tài khoản demo', 'khac'),
        ('bị lỗi 4109 không vào lệnh', 'loi'),
        ('còn dca bot giá bao nhiêu', 'gia'),
    ):
        st = cap_nhat(st, cau, yd)
    print('  ' + thanh_van_ban(st).replace('\n', '\n  '))
    assert st['dang_hoi'] == 'dcabot'
    assert st['san_pham'] == ['dcabot', 'copytrade']
    assert st['nen_tang'] == 'MT4' and st['san'] == ['Exness'] and st['loai_tk'] == 'demo'
    assert 'mã 4109' in st['loi'] and 'bot không vào lệnh' in st['loi']
    assert st['y_dinh'] == {'huong_dan': 1, 'khac': 1, 'loi': 1, 'gia': 1}

    print('\n=== 3) key dán vào chat KHÔNG vào sổ tay ===')
    st2 = cap_nhat(None, 'key XK9F2M7Q4P1Z8R3T báo sai key trên mt5')
    assert 'XK9F2M7Q4P1Z8R3T' not in repr(st2), 'LỖI: key lọt vào sổ tay'
    assert st2['nen_tang'] == 'MT5' and 'báo lỗi key/licence' in st2['loi']
    print('  OK', st2['loi'])

    print('\n=== 4) gop ===')
    ho_so = gop({'san_pham': ['tradingview'], 'nen_tang': 'MT5', 'y_dinh': {'gia': 1}}, st)
    assert ho_so['san_pham'][:3] == ['dcabot', 'copytrade', 'tradingview'], ho_so['san_pham']
    assert ho_so['nen_tang'] == 'MT4' and ho_so['y_dinh']['gia'] == 2
    print('  OK', ho_so['san_pham'], ho_so['nen_tang'])

    print('\n=== 5) đầu vào hỏng không làm sập ===')
    assert chuan_hoa_so_tay('rác') == so_tay_rong()
    assert chuan_hoa_so_tay({'san': 'x', 'y_dinh': []})['san'] == []
    assert thanh_van_ban(None) == ''
    print('  OK')
    print('\nTỰ KIỂM so_tay.py QUA.')
