# -*- coding: utf-8 -*-
"""GIAO ƯỚC — hợp đồng giữa các module của bot. Sửa file này là sửa cả hệ.

Vì sao có file này: các module được viết song song bởi nhiều người/phiên
khác nhau. Không chốt sẵn hình dạng dữ liệu thì mỗi bên tự bịa một kiểu,
ghép lại mới vỡ ra — mà lúc đó sửa đắt gấp nhiều lần.

NGUYÊN TẮC XUYÊN SUỐT
---------------------
1. Lõi KHÔNG biết mình đang nói chuyện qua cổng nào (web, Messenger, Zalo)
   và KHÔNG biết đang gọi nhà cung cấp AI nào. Cả hai nằm ngoài rìa.
   Đổi cổng hay đổi nhà cung cấp phải là sửa một file, không phải sửa lõi.

2. Giá, tên sản phẩm, hạn key KHÔNG BAO GIỜ do mô hình sinh ra. Chúng đi
   qua `congcu/` rồi ghép bằng khuôn mẫu. Đây là bài học từ bot cũ: nó
   chép giá ra `product_data.json` rồi đóng băng ở 17/09/2026 với 2/7 sản
   phẩm và tên đã đổi.

3. Tài liệu của chủ website là thẩm quyền CAO NHẤT, nhưng không còn là
   giới hạn duy nhất (23/09/2026): kiến thức chung về MT4/MT5 và trading
   được dùng; thông tin RIÊNG của sản phẩm chỉ nói khi có trong tài liệu.
   Nguồn gắn khi câu trả lời thật sự dựa vào tài liệu.
"""
import re
from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any, Literal


# =====================================================================
# 1. TRI THỨC
# =====================================================================

# Nhãn sản phẩm. Suy ra từ đường dẫn tệp PHP, KHÔNG gán tay 6.627 khoá.
# CẢNH BÁO đã dính một lần: 'dca' là chuỗi con của 'broa-dca-ster', nên
# phải khớp DÀI TRƯỚC NGẮN SAU, nếu không toàn bộ signalbroadcaster bị
# gán nhầm thành dcabot (423 khoá đi sai chỗ mà bảng thống kê vẫn đẹp).
SAN_PHAM = (
    'dcabot', 'copytrade', 'tradingview', 'signaltrader',
    'signalbroadcaster', 'tradepanel', 'viptrend',
    'mt5manager', 'codebot',
)

# 'trendbot' ĐÃ BỎ (23/09/2026). Nó với 'viptrend' là CÙNG MỘT sản phẩm,
# chỉ khác chỗ gọi tên: tệp trang giới thiệu tên `Introtrendbot.php` nên
# route ra `/toolintroduce/trendbot`, còn tám trang bán hàng thì tên
# `goldvip1..7`. Tách đôi khiến kho tri thức có hai sản phẩm cùng nói về
# một thứ — hỏi về VipTrend thì chỉ với được một nửa số đoạn.
# Bảng `product` trong CSDL KHÔNG có dòng nào cho sản phẩm này (nó bán
# qua tám trang /botvip/*), nên không có mã chính thức nào để theo; chọn
# 'viptrend' vì đó là tên bán ra cho khách.
#
# 'codebot' MỚI THÊM: dịch vụ code bot theo yêu cầu ở /codebot/custom.
# Trang đã có sẵn 13 đoạn với neo đầy đủ (#dichvu, #baogia, #quytrinh,
# #camket, #cauhoi) nhưng trước đây không mang nhãn sản phẩm nào nên bị
# dồn vào rổ "dùng chung", không lọc theo sản phẩm được.

LOAI_ND = (
    'huong_dan',   # /guilde/*        cài đặt
    'su_dung',     # /sudung/*        thao tác hằng ngày
    'gioi_thieu',  # /toolintroduce/* bán hàng, tính năng
    'tinh_nang',   # intro_sections/* mục con của trang DCA
    'cap_nhat',    # /update/*        lịch sử phiên bản
    'faq',         # /faq/*
    'botvip',      # /botvip/*
    'khoa_hoc',    # /funds/*
    'dich_vu',     # /codeservice/*
    'tuvan',       # KHO TU VAN — muc do chu website tu soan va tu duyet
                   # trong phan quan tri (bang `chat_muc`). Khac han cac
                   # loai tren o CHO: cac loai kia quet tu trang web va co
                   # the sai giong ban hang; loai nay la LOI CUA CHU, da
                   # duyet, nen duoc tra nguyen van o tang 1.
)


# KIỂU của một đoạn/nút tri thức — suy từ tên neo và tiêu đề phần bằng
# doan_kieu() bên dưới. Dùng để sau này lõi/tìm kiếm có thể ưu tiên đúng
# LOẠI câu trả lời (vd khách hỏi lỗi thì ưu tiên nút kieu='loi').
KIEU_NUT = ('khai_niem', 'buoc', 'loi', 'tham_so', 'chinh_sach')


@dataclass
class Doan:
    """Một đoạn tri thức đã cắt, sẵn sàng đưa vào chỉ mục."""
    ma: str                     # khoá duy nhất, vd 'dcabot/huong_dan/0007'
    van_ban_vi: str             # nội dung tiếng Việt
    van_ban_en: str             # bản tiếng Anh ('' nếu chưa có)
    san_pham: Optional[str]     # một trong SAN_PHAM, None = dùng chung
    loai: Optional[str]         # một trong LOAI_ND
    duong_dan: str              # '/guilde/dcabot' — đường dẫn CÔNG KHAI
    tieu_de: str                # tiêu đề trang/mục, để hiện khi trích nguồn
    nguon_tep: str              # tệp PHP gốc, chỉ để soi lỗi
    so_tu: int = 0
    kieu: Optional[str] = None  # một trong KIEU_NUT, None = chưa suy được


# =====================================================================
# 1b. SUY KIỂU NÚT TỪ NEO + TIÊU ĐỀ
# =====================================================================
#
# Thứ tự các luật DƯỚI ĐÂY là phần quan trọng nhất của hàm này — dừng ở
# luật khớp ĐẦU TIÊN. Xét neo TRƯỚC tiêu đề vì neo là thứ do người viết
# trang đặt tên có chủ ý (vd 'step-1', 'troubleshooting'), đáng tin hơn
# việc mò chữ trong tiêu đề — tiêu đề chỉ dùng khi neo không nói lên gì.

def doan_kieu(ten_neo: Optional[str], tieu_de: str) -> str:
    """Suy KIỂU ('buoc' | 'loi' | 'tham_so' | 'khai_niem'...) từ tên neo
    (id phần, vd 'step-3', 'troubleshooting') và tiêu đề của chính phần đó.

    KHÔNG BAO GIỜ ném lỗi — neo/tieu_de rỗng hay None vẫn phải trả về một
    kiểu hợp lệ (mặc định 'khai_niem'), vì hàm này chạy trên MỌI đoạn lúc
    dựng chỉ mục, kể cả các đoạn không có neo.
    """
    neo = (ten_neo or '').strip().lower()
    td = (tieu_de or '').strip().lower()

    if re.match(r'^step-\d+$', neo) or re.match(r'^buoc-\d+$', neo):
        return 'buoc'
    if any(tu in neo for tu in ('troubleshoot', 'loi', 'error', 'faq')):
        return 'loi'
    if any(tu in neo for tu in ('param', 'config', 'setting', 'input')):
        return 'tham_so'
    if any(tu in td for tu in ('lỗi', 'khắc phục', 'sự cố')):
        return 'loi'
    if any(tu in td for tu in ('tham số', 'cấu hình', 'cài đặt')):
        return 'tham_so'
    if 'bước' in td:
        return 'buoc'
    return 'khai_niem'


# =====================================================================
# 2. TÌM KIẾM
# =====================================================================

@dataclass
class KetQuaTim:
    doan: Doan
    diem: float                 # điểm sau khi hợp nhất, càng lớn càng khớp
    nguon_diem: Dict[str, float] = field(default_factory=dict)   # {'bm25':..,'vector':..}


@dataclass
class LocTim:
    """Điều kiện lọc. None = không lọc chiều đó."""
    san_pham: Optional[str] = None
    loai: Optional[str] = None
    ngon_ngu: str = 'vi'        # 'vi' | 'en' — chọn cột văn bản để trả về


# Giao diện bắt buộc của tầng tìm kiếm:
#   tim(cau_hoi: str, loc: LocTim, so_luong: int = 5) -> List[KetQuaTim]
#
# so_luong KHÔNG ĐƯỢC VƯỢT 8. Đây là quyết định về CHẤT LƯỢNG chứ không
# phải tiết kiệm: nhồi 20 đoạn thì mô hình bị "lạc giữa dòng", thông tin
# đúng nằm lẫn trong nhiễu và bị bỏ qua. Mặc định nâng 5 -> 8 ngày
# 23/09/2026 (chủ dự án: token rẻ, 500đ/1 triệu, ưu tiên chất lượng) —
# thêm nguồn cho câu hỏi trải trên nhiều mục, vẫn trong trần chất lượng.
SO_DOAN_MAC_DINH = 8
SO_DOAN_TOI_DA = 8


# =====================================================================
# 3. CÔNG CỤ — sự thật lấy từ nguồn sống, không từ tệp tĩnh
# =====================================================================

@dataclass
class GiaGoi:
    thang: int                  # 0 = vĩnh viễn
    gia: float                  # đơn vị: credit (1 USD = 1 credit)
    nhan: str                   # 'Vĩnh viễn', '1 tháng', ...


@dataclass
class SanPham:
    ma: str                     # product.code
    slug: str
    ten: str                    # product.title (EN)
    ten_vi: str                 # product.title_vi
    backend: str                # license_backend
    phien_ban: str
    cac_goi: List[GiaGoi] = field(default_factory=list)
    mo_ta: str = ''             # đoạn đầu product.description, đã bỏ HTML


@dataclass
class KeyCuaKhach:
    ma_san_pham: str
    license_key: str
    het_han: str                # 'YYYY-MM-DD' | '' = vĩnh viễn
    con_han: Optional[bool]
    nguon: Literal['web', 'may_chu_key']   # khách nằm ở HAI hệ thống


# =====================================================================
# 4. LÕI
# =====================================================================

Ydinh = Literal['gia', 'huong_dan', 'loi', 'tra_key', 'cam_ket', 'chao', 'khac']


@dataclass
class NguCanh:
    """Mọi thứ cổng biết mà lõi cần. Cổng nào cũng phải điền được cái này."""
    phien: str                          # session id
    ngon_ngu: str = 'vi'                # theo lang() của web
    nguoi_dung: str = 'khach'           # username nếu đã đăng nhập
    lich_su: List[Dict[str, str]] = field(default_factory=list)
    # lich_su: [{'vai': 'khach'|'bot', 'noi_dung': '...'}], GIỮ TỐI ĐA GIU_LICH_SU LƯỢT
    khong_gian: str = 'tradingauto'
    # khong_gian: MỘT cài đặt bot phục vụ NHIỀU website/dịch vụ. Kho tư
    # vấn (`chat_muc.khong_gian`) và chỉ mục đều chia theo trường này, nên
    # mở thêm một trang mới chỉ là thêm không gian + viết mục mới, KHÔNG
    # đụng một dòng nào của lõi. Mặc định là website hiện tại.
    kenh: str = 'web'
    # kenh: cổng khách đang nói chuyện — 'web' | 'messenger' (24/09/2026).
    # Lõi dùng để định dạng câu trả lời hợp kênh (Messenger không có
    # markdown) và ghi nhật ký theo kênh. Tri thức KHÔNG đổi theo kênh — đổi
    # theo khong_gian.
    may: str = ''
    # may: mã thiết bị ngẫu nhiên khung chat lưu ở localStorage — nối các
    # phiên của CÙNG một khách chưa đăng nhập. Rỗng = không biết.
    so_tay: Dict[str, Any] = field(default_factory=dict)
    # so_tay: sổ tay CỦA PHIÊN NÀY (congcu/so_tay.py) — sản phẩm đang hỏi,
    # MT4/MT5, sàn, lỗi đã gặp. Trích bằng luật, lõi tự nạp/cập nhật.
    danh_muc: str = ''
    # danh_muc: danh mục sản phẩm đang bán kèm mô tả ngắn, từ CSDL (nao/
    # tra_loi.py::_khoi_danh_muc) — để mô hình luôn biết đủ các sản phẩm.
    ghi_chu: str = ''
    # ghi_chu: dặn riêng mô hình cho LƯỢT NÀY do lõi quyết (vd "phần giá hệ
    # thống tự gắn bên dưới, đừng nhắc giá"). Rỗng = không có gì.
    mo_hinh: str = ''
    # mo_hinh: mô hình AI riêng cho LƯỢT NÀY (vai "chuyên gia", congcu/cau_hinh_ai.py).
    # Rỗng = mô hình vai "trả lời" trong Cài đặt website.
    compact: str = ''
    # compact: BẢN TÓM GỌN cuộc tư vấn tới giờ (nao/compact.py) — mô hình
    # viết lại mỗi ~4 lượt. Có nó thì lich_su chỉ còn các lượt SAU lần tóm
    # gọn (và vài lượt gần nhất để giữ giọng hội thoại).
    anh: List[str] = field(default_factory=list)
    # anh: ảnh khách gửi TRONG LƯỢT NÀY (JPEG base64, kenh/anh.py). Chủ dự án
    # 25/09/2026: ảnh phải được hiểu NGAY TRONG mạch hội thoại, không tách ra
    # một lượt đọc riêng — nên ảnh đi thẳng vào lượt gọi mô hình trả lời (cùng
    # lịch sử, tài liệu). Lượt sau không gửi lại ảnh: lịch sử giữ dòng mô tả
    # mô hình tự ghi (TraLoi.mo_ta_anh). Có ảnh thì bỏ qua tầng 1 (kho không nhìn được ảnh).
    ho_so: str = ''
    # ho_so: vài dòng về khách từ CÁC PHIÊN TRƯỚC (tóm tắt, lưu ý). Chỉ
    # đưa vào prompt tầng 2 để dùng NGẦM — quyết định của chủ dự án
    # 23/09/2026: bot không bao giờ nói "lần trước anh hỏi...".
    che_do: str = ''
    # che_do: 'agent' | 'cu' ép cách tư vấn cho lượt này (thu/so_sanh.py chấm mù). Rỗng = theo Cài đặt
    # 'che_do_tu_van' (congcu/cau_hinh_ai.py). Agent: nao/agent.py (29/09/2026).


GIU_LICH_SU = 6


@dataclass
class TraLoi:
    """Thứ DUY NHẤT lõi trả ra. Mọi cổng tự dịch sang định dạng của mình."""
    tra_loi: str
    nguon: List[Dict[str, str]] = field(default_factory=list)
    # nguon: [{'tieu_de': '...', 'duong_dan': '/guilde/dcabot'}]
    # Chỉ gắn khi câu trả lời THẬT SỰ dựa vào tài liệu (mô hình tự khai số
    # đoạn đã dùng). Rỗng là hợp lệ: từ 23/09/2026 bot được trả lời bằng
    # kiến thức chung về MT4/MT5 và trading khi tài liệu không nói tới —
    # chủ dự án: "một số liên quan đến thường thức và kinh nghiệm thì vẫn tư
    # vấn, cái cần tôn trọng là nguồn tài liệu của tôi".
    y_dinh: Ydinh = 'khac'
    chuyen_nguoi: bool = False
    do_tin_cay: float = 0.0             # 0..1
    da_goi_llm: bool = False            # để đếm chi phí và soi lỗi
    tang: int = 0
    # tang: TẦNG NÀO đã trả câu này.
    #   1 = kho tư vấn (chủ website tự soạn, tự duyệt) — 0 đồng, không thể sai
    #   2 = tìm tài liệu rồi nhờ mô hình diễn giải, có trích dẫn
    #   3 = câu mẫu / chuyển người
    #   0 = chưa xác định (không nên xuất hiện ở đường chạy thật)
    # VÌ SAO phải có: tỉ lệ phủ TẦNG 1 là con số đo sức khoẻ của cả hệ —
    # nó tăng dần thì hoá đơn mô hình giảm dần và chất lượng tăng dần.
    # Không có trường này thì KHÔNG đếm được, vì nhìn từ ngoài một câu của
    # tầng 1 và một câu của nhánh 'tra_key' giống hệt nhau: cùng
    # da_goi_llm=False, cùng chuyen_nguoi=False, cùng do_tin_cay cao.
    # Đã vấp đúng chỗ này khi chạy thử đầu-cuối.
    muc_id: Optional[int] = None        # chat_muc.id đã trả lời — CHỈ có ở tầng 1
    model: Optional[str] = None         # model AI thật sự trả lời — CHỈ có ở tầng 2
    token_vao: Optional[int] = None
    token_ra: Optional[int] = None
    loai_anh: str = ''                  # lượt có ảnh: loi|cau_hinh|chart|chuyen_khoan|khac (mô hình tự khai)
    mo_ta_anh: str = ''                 # 1–2 câu mô tả ảnh — vào lịch sử thay cho ảnh ở các lượt sau
    ly_do_phieu: str = ''               # agent gọi bao_admin (nao/cong_cu.py) -> kenh/phieu.py mở phiếu lý do này
    # BỐN TRƯỜNG token/model thuộc về TraLoi (của LƯỢT NÀY), KHÔNG đọc lại từ đối
    # tượng nhà cung cấp AI lúc ghi nhật ký. Lý do: nhà cung cấp
    # (llm/tuongthich_openai.py) là một SINGLETON dùng chung cho cả tiến
    # trình (xem `_lay_nha_cung_cap_dung_chung()` trong nao/tra_loi.py), mà
    # cổng lại chạy nhiều yêu cầu song song. Nếu đọc `token_vao_gan_nhat`
    # của nó ở CUỐI hàm `tra_loi()` thì khi có hai khách hỏi cùng lúc, lượt
    # đọc được có thể là số của LƯỢT KHÁC (lượt vừa ghi đè xong) — số liệu
    # sẽ sai lệch ÂM THẦM và không ai phát hiện ra vì không có lỗi nào được
    # ném. Gắn thẳng giá trị vào chính `TraLoi` của lượt này, ngay tại chỗ
    # gọi `dien_giai()` thành công, là cách DUY NHẤT đúng.


# Giao diện bắt buộc của lõi — CHỮ KÝ NÀY KHÔNG ĐƯỢC ĐỔI:
#   tra_loi(cau_hoi: str, ngu_canh: NguCanh) -> TraLoi


# =====================================================================
# 5. NHÀ CUNG CẤP AI — chỉ hai lời gọi, không hơn
# =====================================================================
#
# class NhaCungCap:
#     def phan_loai(self, cau_hoi: str, ngu_canh: NguCanh) -> Ydinh: ...
#     def dien_giai(self, cau_hoi: str, cac_doan: List[KetQuaTim],
#                   ngu_canh: NguCanh) -> str: ...
#
# Thêm nhà cung cấp = thêm MỘT file trong llm/. Lõi không được import
# thẳng anthropic/openai ở bất cứ đâu.


# =====================================================================
# 6. HỢP ĐỒNG DÂY — chat.php <-> cổng Python
# =====================================================================
#
# chat.php POST tới CHAT_URL (mặc định http://127.0.0.1:8787/chat):
#   headers: Content-Type: application/json; charset=utf-8
#            X-Forwarded-For: <ip that cua khach>
#            X-Khoa-Noi-Bo: <CHAT_KHOA>        (nếu có đặt)
#   body   : {"session_id": str, "message": str,
#             "nguoi_dung": str, "ngon_ngu": "vi"|"en"}
#
#   HAI TRƯỜNG CUỐI LÀ MỚI. chat.php hiện chỉ gửi session_id + message;
#   phải sửa nó gửi thêm, nếu không bot không tra được key của khách và
#   không biết khách đang đọc thứ tiếng nào. Xem trienkhai/HUONG_DAN.md.
#
# Cổng Python trả về (chat.php đọc đúng những khoá này):
#   {"tra_loi": str,                     # BẮT BUỘC
#    "nguon": "..."                      # nhãn nguồn, để ghi log
#    "chuyen_nguoi_that": bool,
#    "mau": str|null}                    # mã câu mẫu nếu có, để ghi log
#
# Thiếu "tra_loi" thì chat.php coi là hỏng và hiện câu dự phòng Zalo.

TEN_TRUONG_RA = ('tra_loi', 'nguon', 'chuyen_nguoi_that', 'mau')


# =====================================================================
# 7. HẰNG SỐ DÙNG CHUNG
# =====================================================================

# Lối thoát khi bot bí hoặc hết ngân sách. Số lấy từ chat_config.php.
ZALO = '0387654360'
TELEGRAM = '@duyquang555'

CAU_CHUYEN_NGUOI = {
    'vi': ('Chỗ này em chưa chắc nên không dám trả lời bừa ạ. '
           'Anh/chị nhắn Zalo {zalo} hoặc Telegram {tele}, bên em trả lời trực tiếp.'),
    'en': ('I am not certain about this one, so I would rather not guess. '
           'Please message Zalo {zalo} or Telegram {tele} and we will answer directly.'),
}

# Chính sách Meta bắt bot tự khai là máy ở đầu luồng, sau khoảng im lặng
# dài, và khi chuyển từ người thật trở lại bot. Làm sẵn từ đầu để sau này
# nối Messenger không phải sửa lõi.
CAU_KHAI_LA_MAY = {
    'vi': 'Em là trợ lý tự động của TradingAuto ạ.',
    'en': 'I am TradingAuto’s automated assistant.',
}
