# -*- coding: utf-8 -*-
"""TRÍCH — lấy tri thức từ mã PHP của website, không phải từ tài liệu tách rời.

VÌ SAO trích từ PHP thay vì viết tay một kho tri thức riêng: web đã có sẵn
~5.800 câu tiếng Việt trong các lời gọi te()/t() — đó CHÍNH LÀ nội dung
khách nhìn thấy khi đọc trang hướng dẫn/FAQ/giới thiệu. Nếu chép tay ra một
tệp .md riêng thì hai bản sẽ trôi dần khỏi nhau (sửa web quên sửa kho, hoặc
ngược lại) — bài học từ product_data.json đã đóng băng ở 17/09/2026, xem
giao_uoc.py. Trích thẳng từ nguồn sống thì kho LUÔN khớp với web.
"""
from __future__ import annotations

import glob
import io
import os
import re
import sys
from collections import OrderedDict, Counter

try:
    from ..giao_uoc import Doan  # noqa: F401  (chỉ để kiểm tra hợp đồng tồn tại)
except ImportError:
    sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    from giao_uoc import Doan  # noqa: F401


# =====================================================================
# ĐƯỜNG DẪN NGUỒN
# =====================================================================

# <goc>/private/services/chatbot/kho/trich.py -> len 5 bac la <goc>
_GOC_MAC_DINH = os.path.dirname(os.path.dirname(os.path.dirname(
    os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))


def _tim_thu_muc_site(goc):
    """Tìm thư mục trang công khai. Tên nó KHÁC NHAU giữa hai máy:

        máy local  <goc>/public/pages/site
        máy chủ    <goc>/public_html/pages/site     (DocumentRoot do Hestia đặt)

    Đã dính một lần: dựng chỉ mục trên máy chủ chạy trót lọt, in ra "Xong"
    và tạo tệp SQLite hẳn hoi — nhưng bên trong RỖNG, 0 đoạn. Bot vẫn
    khởi động bình thường rồi trả lời khách bằng câu "không tìm thấy tài
    liệu" cho mọi câu hỏi, mà không có một dòng lỗi nào.

    Vì vậy hàm này trả về đường dẫn ĐẦU TIÊN CÓ THẬT, và nếu không có cái
    nào thì trả cái đầu để `trich_tat_ca()` nổ lỗi rõ ràng — thà hỏng ồn
    ào còn hơn chạy êm mà rỗng.
    """
    for ten in ('public', 'public_html'):
        d = os.path.join(goc, ten, 'pages', 'site')
        if os.path.isdir(d):
            return d
    return os.path.join(goc, 'public', 'pages', 'site')


DUONG_DAN_SITE_MAC_DINH = _tim_thu_muc_site(_GOC_MAC_DINH)
DUONG_DAN_EN_MAC_DINH = os.path.join(_GOC_MAC_DINH, 'private', 'app', 'lang', 'en.php')


# =====================================================================
# GỠ THOÁT CHUỖI PHP ĐƠN NHÁY
# =====================================================================
#
# PHP chuỗi nháy đơn CHỈ có hai kiểu thoát: \' (nháy đơn) và \\ (gạch chéo
# ngược). Bất kỳ \x nào khác (\n, \t...) vẫn giữ NGUYÊN VĂN backslash+x,
# khác hẳn chuỗi nháy kép — đây là điểm hay bị lẫn nếu bê nguyên quy tắc
# thoát chuỗi của ngôn ngữ khác sang.

def go_thoat(chuoi: str) -> str:
    """Gỡ \\' và \\\\ về ký tự thật, giữ nguyên mọi backslash khác.

    VÌ SAO không dùng .replace() liên tiếp: replace("\\\\'", "'") rồi
    replace("\\\\\\\\", "\\\\") theo thứ tự bất kỳ đều sai trên chuỗi kiểu
    "a\\\\\\'b" (gạch chéo thật đứng ngay trước nháy đã thoát) — phải quét
    tuần tự từng ký tự, giống hệt cách trình phân tích PHP thật sự đọc.
    """
    ra = []
    i = 0
    n = len(chuoi)
    while i < n:
        if chuoi[i] == '\\' and i + 1 < n and chuoi[i + 1] in ("'", '\\'):
            ra.append(chuoi[i + 1])
            i += 2
        else:
            ra.append(chuoi[i])
            i += 1
    return ''.join(ra)


# =====================================================================
# LỜI GỌI te('...') / t('...')
# =====================================================================
#
# CẢNH BÁO ĐÃ DÍNH: lớp ký tự [^'\] (thiếu một backslash để tự thoát) làm
# regex không bao giờ đóng — Python báo lỗi "unterminated character set"
# ngay lúc compile. Lớp đúng phải là [^'\\] (loại bỏ nháy đơn VÀ gạch chéo
# ngược), kết hợp với nhánh \\. để nuốt trọn một cặp thoát bất kỳ. Đã chạy
# thử regex này trên toàn bộ cây site/ để chắc chắn nó compile và bắt đúng
# — xem bước kiểm chứng ở cuối tệp.
_MAU_GOI_DICH = re.compile(r"\b(te|t)\(\s*'((?:\\.|[^'\\])*)'")

# Regex cho MỘT mục trong mảng PHP của en.php: 'khoá việt' => 'bản anh',
# Dùng chung cấu trúc lớp ký tự với _MAU_GOI_DICH vì cùng là chuỗi nháy đơn
# PHP, cùng quy tắc thoát.
_MAU_MUC_EN = re.compile(
    r"'((?:\\.|[^'\\])*)'\s*=>\s*'((?:\\.|[^'\\])*)'"
)


# =====================================================================
# NEO (id="...") — cắt đoạn theo CẤU TRÚC TRANG thay vì theo số từ
# =====================================================================
#
# VÌ SAO: bộ cắt cũ (cat_doan.py) chỉ nhìn số từ, không biết trang có mục
# lục và các phần id="step-1"... Hậu quả đo được trên guilde/dcabot.php:
# 613 dòng, 11 phần có neo -> chỉ ra 4 đoạn VÔ DANH, cùng chung một tiêu đề
# trang, mất sạch tên phần ("Khắc phục lỗi thường gặp" không còn tồn tại
# như một thứ riêng để tìm ra khi khách hỏi lỗi). Xem thêm cat_doan.py.
#
# CHỈ nhận id trông như tên phần do người viết trang đặt (vd 'step-1',
# 'troubleshooting'), KHÔNG nhận id kỹ thuật (vd 'form1', 'btnSave' — có
# chữ hoa, hoặc quá ngắn).
_MAU_ID_NEO = re.compile(r'^[a-z][a-z0-9-]{2,}$')

# Thẻ HTML KHÔNG có thẻ đóng riêng — không được đẩy vào ngăn xếp, vì
# không có </tag> nào để đẩy chúng ra, ngăn xếp sẽ phình lên sai.
_THE_TU_DONG = frozenset((
    'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
    'link', 'meta', 'param', 'source', 'track', 'wbr',
))

# Thẻ tiêu đề dùng để nhận TIÊU ĐỀ của một phần có neo — chỉ h2/h3/h4 theo
# đúng yêu cầu, không nhận h1 (đó là tiêu đề TRANG, không phải tiêu đề
# PHẦN) và không nhận h5/h6 (thường là tiêu đề khối nhỏ trong phần, vd
# "💡 Mẹo:", không đại diện cho cả phần).
_THE_TIEU_DE = frozenset(('h2', 'h3', 'h4'))

# MỘT regex DUY NHẤT bắt CẢ BA loại sự kiện theo đúng thứ tự xuất hiện
# trong tệp: thẻ MỞ, thẻ ĐÓNG, hoặc lời gọi te()/t(). Nhánh cuối CỐ Ý giữ
# nguyên cú pháp với _MAU_GOI_DICH (cùng lớp ký tự [^'\\], cùng nhánh \\.)
# để việc thêm dò-neo này KHÔNG làm lệch một ký tự nào so với cách trích
# khoá cũ — chỉ thêm thông tin neo đi kèm mỗi lần khớp, không đổi nội dung
# khớp được.
_MAU_SU_KIEN = re.compile(
    r"<(?P<ten_mo>[a-zA-Z][a-zA-Z0-9]*)(?P<thuoc_tinh>(?:\"[^\"]*\"|'[^']*'|[^>])*)>"
    r"|</(?P<ten_dong>[a-zA-Z][a-zA-Z0-9]*)\s*>"
    r"|\b(?:te|t)\(\s*'(?P<khoa>(?:\\.|[^'\\])*)'"
)

_MAU_ID_THUOC_TINH = re.compile(r'''\bid\s*=\s*(?:"([^"]*)"|'([^']*)')''')


def quet_su_kien(noi_dung: str):
    """MỘT lượt quét DUY NHẤT qua nội dung một tệp: vừa bắt các lời gọi
    te()/t() (giữ NGUYÊN cú pháp/thứ tự so với _MAU_GOI_DICH), vừa theo dõi
    NGĂN XẾP thẻ HTML để biết mỗi lời gọi đang nằm SÂU NHẤT trong id neo
    nào tại đúng thời điểm đó.

    VÌ SAO một lượt quét chung thay vì hai regex riêng rồi ghép theo vị
    trí: hai lượt quét độc lập dễ lệch nhau (một cái tính vị trí theo ký tự
    Unicode, cái kia theo byte, hoặc một trong hai đổi cách đếm mà quên
    đổi cái còn lại) — gộp làm một thì thứ tự luôn đúng theo cấu trúc, vì
    cùng một con trỏ quét.

    VÌ SAO không dùng trình phân tích HTML thật (lxml/BeautifulSoup): nội
    dung là PHP xen HTML (`<?= te('...') ?>`), một trình phân tích HTML
    chuẩn sẽ vấp cú pháp PHP hoặc cần tiền xử lý phức tạp hơn lợi ích mang
    lại. Ngăn xếp tự viết chấp nhận không hoàn hảo tuyệt đối với HTML lồng
    sai be bét, nhưng mã nguồn site/ viết tay, thẻ mở/đóng vốn đã cân đối.

    Trả về (danh_sach_su_kien, tieu_de_theo_neo):
      danh_sach_su_kien: list[dict] theo ĐÚNG THỨ TỰ xuất hiện, mỗi phần tử
        {'khoa': <khoá tiếng Việt ĐÃ gỡ thoát>, 'neo': <id hoặc None>}
      tieu_de_theo_neo: dict {neo: tiêu đề}, lấy từ khoá te()/t() ĐẦU TIÊN
        nằm ngay trong một thẻ h2/h3/h4 mà neo đó đang là neo hiện tại.
    """
    ngan_xep = []          # [{'ten': tên thẻ thường, 'neo': id hoặc None}]
    tieu_de_theo_neo = {}
    danh_sach_su_kien = []

    for khop in _MAU_SU_KIEN.finditer(noi_dung):
        if khop.group('ten_mo'):
            ten = khop.group('ten_mo').lower()
            tu_dong = khop.group(0).rstrip().endswith('/>') or ten in _THE_TU_DONG
            neo = None
            khop_id = _MAU_ID_THUOC_TINH.search(khop.group('thuoc_tinh') or '')
            if khop_id:
                gia_tri = khop_id.group(1) if khop_id.group(1) is not None else khop_id.group(2)
                if gia_tri and _MAU_ID_NEO.match(gia_tri):
                    neo = gia_tri
            if not tu_dong:
                ngan_xep.append({'ten': ten, 'neo': neo})
            # the tu dong: khong day vao ngan xep, khong co </...> nao se
            # ung voi no de day ra
        elif khop.group('ten_dong'):
            ten = khop.group('ten_dong').lower()
            for i in range(len(ngan_xep) - 1, -1, -1):
                if ngan_xep[i]['ten'] == ten:
                    del ngan_xep[i:]
                    break
            # khong tim thay the mo tuong ung -> the dong le, bo qua, KHONG
            # dong lieu ngan xep (tranh mot the dong thua lam sap toan bo
            # ngu canh dang mo dung)
        else:
            khoa_vi = go_thoat(khop.group('khoa'))
            neo_hien_tai = None
            for khung in reversed(ngan_xep):
                if khung['neo']:
                    neo_hien_tai = khung['neo']
                    break
            if neo_hien_tai and ngan_xep and ngan_xep[-1]['ten'] in _THE_TIEU_DE:
                tieu_de_theo_neo.setdefault(neo_hien_tai, khoa_vi)
            danh_sach_su_kien.append({'khoa': khoa_vi, 'neo': neo_hien_tai})

    return danh_sach_su_kien, tieu_de_theo_neo


def _tieu_de_tu_muc_luc(noi_dung: str) -> dict:
    """Tiêu đề DỰ PHÒNG cho các neo KHÔNG có thẻ h2/h3/h4 của riêng mình —
    vd `id="config-modes"`/`id="panel-guide"` trong dcabot.php chỉ bọc một
    `include()` sang tệp khác, tiêu đề THẬT nằm ở tệp kia. Mục lục của
    chính trang (`<a href="#neo">...te(...)...</a>`) vẫn có tên phần, nên
    lấy tạm từ đó còn hơn để trống.

    Chỉ lấy lần xuất hiện ĐẦU TIÊN của mỗi neo trong mục lục — một trang có
    thể có nhiều đường dẫn trỏ cùng một neo (vd nút "Xem chi tiết" cuối
    trang trỏ lại '#params'), lần đầu mới đúng là dòng mục lục.
    """
    ket_qua = {}
    for khop_href in re.finditer(r'href\s*=\s*["\']#([a-z][a-z0-9-]{2,})["\']', noi_dung):
        neo = khop_href.group(1)
        if neo in ket_qua:
            continue
        vi_tri_dong_a = noi_dung.find('</a>', khop_href.end())
        pham_vi_cuoi = vi_tri_dong_a if vi_tri_dong_a != -1 else min(len(noi_dung), khop_href.end() + 300)
        khop_goi = _MAU_GOI_DICH.search(noi_dung, khop_href.end(), pham_vi_cuoi)
        if khop_goi:
            ket_qua[neo] = go_thoat(khop_goi.group(2))
    return ket_qua

# $pageTitle có thể viết bằng nháy đơn hoặc nháy kép tuỳ tệp — chấp nhận
# cả hai thay vì ép một kiểu, vì đây là mã có sẵn, không phải mã ta viết.
_MAU_PAGE_TITLE = re.compile(
    r"\$pageTitle\s*=\s*(?:'((?:\\.|[^'\\])*)'|\"((?:\\.|[^\"\\])*)\")"
)


# =====================================================================
# SUY NHÃN TỪ ĐƯỜNG DẪN / TÊN TỆP — không gán tay
# =====================================================================
#
# THỨ TỰ DƯỚI ĐÂY LÀ PHẦN QUAN TRỌNG NHẤT CỦA TỆP NÀY.
# 'dca' là chuỗi con nằm sẵn trong 'broa' + 'dca' + 'ster' (signalbroadcaster)
# — nếu xét 'dca' trước thì regex khớp ngay giữa từ signalbroadcaster và
# MỌI tệp guilde/sudung/update của signalbroadcaster bị gán nhầm sản phẩm
# 'dcabot'. Đã dính đúng lỗi này một lần: 423 khoá (đúng bằng số khoá thật
# sự thuộc signalbroadcaster) âm thầm chuyển sang cột dcabot, mà tổng số
# khoá không đổi nên bảng thống kê nhìn vẫn hợp lý — chỉ sai khi tra cứu
# theo sản phẩm mới lộ ra. Vì vậy PHẢI khớp chuỗi DÀI trước, NGẮN sau.
SAN_PHAM_THEO_TEN_TEP = (
    ('mt5manager', 'mt5manager'),
    ('signalbroadcaster', 'signalbroadcaster'),
    ('signaltrader', 'signaltrader'),
    ('tradingview', 'tradingview'),
    ('tradepanel', 'tradepanel'),
    ('copytrade', 'copytrade'),
    ('codebot', 'codebot'),    # codeservice/codebot_custom.php -> dịch vụ code bot theo yêu cầu
    # HAI dòng dưới cùng trỏ về 'viptrend' — CÙNG MỘT sản phẩm, ba cách
    # gọi tên trong kho mã (sửa 23/09/2026, trước đó 'trendbot' là một
    # sản phẩm riêng nên kho tri thức tách đôi: 11 đoạn mang nhãn
    # viptrend, 5 đoạn mang nhãn trendbot, hỏi cái nào cũng chỉ với được
    # một nửa).
    ('trendbot', 'viptrend'),  # Introtrendbot.php -> /toolintroduce/trendbot
    ('goldvip', 'viptrend'),   # goldvip1..7 -> tám trang bán hàng
    ('smartdca', 'dcabot'),
    ('dcabot', 'dcabot'),
    ('dca', 'dcabot'),        # phải đứng CUỐI cùng — xem cảnh báo phía trên
)

# Thư mục cấp một dưới site/ ứng với loại nội dung. intro_sections KHÔNG
# nằm ở đây vì nó là thư mục CON của toolintroduce/ (được xét riêng, ưu
# tiên cao hơn, vì nội dung trong đó là các mục con của trang DCA chứ
# không phải bài giới thiệu bán hàng như phần còn lại của toolintroduce/).
LOAI_THEO_THU_MUC_DAU = {
    'guilde': 'huong_dan',
    'sudung': 'su_dung',
    'toolintroduce': 'gioi_thieu',
    'update': 'cap_nhat',
    'faq': 'faq',
    'botvip': 'botvip',
    'funds': 'khoa_hoc',
    'codeservice': 'dich_vu',
}


def _duong_dan_tuong_doi(tep_tuyet_doi: str, goc_site: str) -> str:
    """Đường dẫn tệp so với site/, luôn dùng dấu / (kể cả khi chạy trên Windows).

    VÌ SAO chuẩn hoá dấu /: glob trên Windows trả về \\, mà mọi so sánh
    thư mục phía dưới (guilde/, intro_sections...) đều viết sẵn theo kiểu
    POSIX — không chuẩn hoá thì trên Windows không khớp được thư mục nào.
    """
    tuong_doi = os.path.relpath(tep_tuyet_doi, goc_site)
    return tuong_doi.replace('\\', '/')


def _cac_phan_thu_muc(duong_dan_tuong_doi: str):
    thu_muc = os.path.dirname(duong_dan_tuong_doi)
    return [p for p in thu_muc.split('/') if p]


def suy_san_pham(duong_dan_tuong_doi: str) -> 'str | None':
    """Suy sản phẩm từ TÊN TỆP (viết thường), theo thứ tự dài-trước-ngắn-sau."""
    phan_thu_muc = _cac_phan_thu_muc(duong_dan_tuong_doi)
    if 'intro_sections' in phan_thu_muc:
        # Mọi tệp trong intro_sections đều là mục con của trang DCA chính,
        # bất kể tên tệp con nói về cái gì (hedge.php, telegram.php...).
        return 'dcabot'

    ten_khong_duoi = os.path.splitext(os.path.basename(duong_dan_tuong_doi))[0].lower()
    for tu_khoa, san_pham in SAN_PHAM_THEO_TEN_TEP:
        if tu_khoa in ten_khong_duoi:
            return san_pham
    return None


def suy_loai(duong_dan_tuong_doi: str) -> 'str | None':
    """Suy loại nội dung từ THƯ MỤC chứa tệp."""
    phan_thu_muc = _cac_phan_thu_muc(duong_dan_tuong_doi)
    if 'intro_sections' in phan_thu_muc:
        return 'tinh_nang'
    if phan_thu_muc and phan_thu_muc[0] in LOAI_THEO_THU_MUC_DAU:
        return LOAI_THEO_THU_MUC_DAU[phan_thu_muc[0]]
    return None


def suy_duong_dan_cong_khai(duong_dan_tuong_doi: str, san_pham: 'str | None') -> str:
    """Đường dẫn CÔNG KHAI trên web, suy từ đường dẫn tệp.

    Mặc định: bỏ đuôi .php, giữ nguyên thư mục — 'guilde/dcabot.php' ->
    '/guilde/dcabot'. Hai trường hợp không suy chắc chắn được thì lùi về
    thư mục cha thay vì bịa một URL không tồn tại:
      - toolintroduce/Intro*.php: tên tệp là bút danh nội bộ (IntroDCA,
        Introcopytrade...), KHÔNG phải slug trên web — dùng sản phẩm đã
        suy được để ghép slug thật, vd IntroDCA.php -> /toolintroduce/dcabot.
      - */intro_sections/*: các khối include lại vào trang DCA chính,
        không có URL riêng đáng tin.
    """
    thu_muc = os.path.dirname(duong_dan_tuong_doi)
    ten_khong_duoi = os.path.splitext(os.path.basename(duong_dan_tuong_doi))[0]
    phan_thu_muc = _cac_phan_thu_muc(duong_dan_tuong_doi)

    # Suy theo quy tắc chỉ đúng khi tên tệp TRÙNG slug trên web. Chín chỗ
    # dưới đây không trùng, và chúng đã thật sự sinh ra link 404 trong câu
    # trả lời gửi cho khách — đo bằng cách gọi thử cả 38 đường dẫn trong
    # chỉ mục trên trang thật, 9 cái trả 404.
    #
    # Bảng này là NGOẠI LỆ TƯỜNG MINH, đối chiếu từng dòng với
    # public/.htaccess. Thêm route mới mà quên bảng này thì `thu/chay_test.py`
    # sẽ bắt được — nó gọi thử mọi đường dẫn trích nguồn.
    if duong_dan_tuong_doi in NGOAI_LE_DUONG_DAN:
        return NGOAI_LE_DUONG_DAN[duong_dan_tuong_doi]

    if thu_muc == 'toolintroduce' and ten_khong_duoi.lower().startswith('intro') and san_pham:
        return f'/toolintroduce/{san_pham}'

    # Các khối include lại vào một trang khác — không có URL riêng, nên phải
    # trỏ về ĐÚNG trang thật sự chứa chúng.
    #
    # SỬA 23/09/2026. Trước đây mọi thứ trong intro_sections/ đều trả
    # '/toolintroduce/dcabot'. Đo lại thì SAI cho 16/18 tệp: toàn bộ
    # accordion/ cùng config_modes.php và panel_guide.php chỉ được
    # `guilde/dcabot.php` nạp, còn `IntroDCA.php` KHÔNG nạp cái nào.
    # Nghĩa là bot dẫn khách sang trang giới thiệu để đọc bảng tham số
    # không hề có ở đó — mà đây lại là khối tri thức lớn nhất của cả site
    # (14 bảng, 56 nhóm tham số).
    #
    # Chỉ `smartdca.php` (khác `accordion/smart_dca.php`) mới thuộc trang
    # giới thiệu thật.
    # KHÔNG gắn sẵn '#neo' ở đây. Bộ cắt theo neo (kho/cat_doan.py) tự nối
    # neo của từng phần vào sau đường dẫn này; gắn sẵn thì ra URL hai dấu
    # thăng kiểu '/guilde/dcabot#params#nhom-dao-dong' — đã đo thấy thật,
    # 31 đoạn hỏng như vậy. Các tệp accordion có neo riêng bên trong
    # (nhom-dao-dong, nhom-xu-huong, action-mode...) và những neo đó CÓ
    # THẬT trên trang đã render, nên để bộ cắt nối là ra link sâu đúng chỗ.
    if 'intro_sections' in phan_thu_muc:
        if ('accordion' in phan_thu_muc
                or ten_khong_duoi in ('config_modes', 'panel_guide')):
            return '/guilde/dcabot'
        return '/toolintroduce/dcabot'

    if thu_muc:
        return f'/{thu_muc}/{ten_khong_duoi}'
    return f'/{ten_khong_duoi}'


def _doc_tieu_de(noi_dung_tep: str, ten_tep: str) -> str:
    khop = _MAU_PAGE_TITLE.search(noi_dung_tep)
    if khop:
        tho = khop.group(1) if khop.group(1) is not None else khop.group(2)
        return go_thoat(tho)
    return os.path.splitext(ten_tep)[0]


# =====================================================================
# BẢN TIẾNG ANH — en.php
# =====================================================================

def doc_tu_dien_en(duong_dan: str = DUONG_DAN_EN_MAC_DINH) -> dict:
    """Đọc mảng PHP 'khoá việt' => 'bản anh' thành dict Python.

    Không dùng trình thông dịch PHP thật (không có sẵn trên máy chạy chỉ
    mục, và không cần — mảng chỉ gồm chuỗi nháy đơn phẳng, không có biểu
    thức PHP nào khác) nên tự regex, cùng quy tắc thoát chuỗi với te()/t().
    """
    if not os.path.isfile(duong_dan):
        return {}
    noi_dung = io.open(duong_dan, encoding='utf-8', errors='replace').read()
    tu_dien = {}
    for khop in _MAU_MUC_EN.finditer(noi_dung):
        khoa_vi = go_thoat(khop.group(1))
        ban_en = go_thoat(khop.group(2))
        tu_dien[khoa_vi] = ban_en
    return tu_dien


# =====================================================================
# TRÍCH TOÀN BỘ
# =====================================================================

# Trang THAO TÁC, không phải tài liệu — loại khỏi kho tri thức.
#
# `pair.php` là trang bấm nút ghép nối MT5 Manager. Nội dung của nó là câu
# lệnh cho người đang đứng giữa một thao tác ("Đối chiếu mã dưới đây với
# mã đang hiện trong ứng dụng"), vô nghĩa khi bị trích ra làm câu trả lời.
#
# Đo thật trước khi loại: hỏi "MT5 Manager là gì" thì /pair xếp HẠNG MỘT,
# trên cả trang giới thiệu — vì nó ngắn mà nhắc "MT5 Manager" nhiều lần,
# đúng kiểu văn bản BM25 chấm điểm cao. Loại nó ra là trang giới thiệu
# lên đúng chỗ.
#
# Thêm tệp vào đây khi nó là trang HÀNH ĐỘNG (biểu mẫu, thanh toán, ghép
# nối), không phải khi nó chỉ ngắn hoặc ít giá trị.
TEP_BO_QUA = (
    'pair.php',
    # Khối nút tải về dùng chung cho TÁM trang (copytrade, signaltrader,
    # signalbroadcaster, mt5manager, tradepanel...). Không trang nào là
    # "nhà" của nó, nên mọi đường dẫn trích nguồn gán cho nó đều sai —
    # trước 23/09/2026 nó bị gán '/toolintroduce/dcabot', là trang KHÔNG
    # HỀ nạp nó. Nội dung lại chỉ là nhãn giao diện ("Tải về", "Phiên
    # bản", "Định dạng", "Hệ điều hành"), 125 ký tự, không mang tri thức
    # nào. Bỏ hẳn khỏi chỉ mục thay vì trích dẫn sai.
    'tai_ve.php',
)

# Tệp mà tên KHÔNG trùng slug trên web. Đối chiếu từng dòng với
# public/.htaccess — đây là những chỗ suy theo quy tắc sẽ ra URL 404.
NGOAI_LE_DUONG_DAN = {
    'pricing.php':                    '/Pricing',       # chữ P HOA trong .htaccess
    'codeservice/codebot_custom.php': '/codebot/custom',
    'toolintroduce/dca_signals.php':  '/pages/dca_signals',
    'update/updatetradingview.php':   '/update/tradingview',
    'update/updatecopytrade.php':     '/update/copytrade',
    'update/updatedca.php':           '/update/dcabot',
    # bang_anh.php là một khối include dùng lại ở nhiều trang tài liệu,
    # không phải một trang có URL riêng.
    'tailieu/bang_anh.php':           '/guilde/dcabot',
    # Trang giới thiệu VipTrend: tên TỆP là `Introtrendbot.php` nên route
    # thật là `/toolintroduce/trendbot`, nhưng từ 23/09/2026 sản phẩm của
    # nó gộp thành `viptrend`. Quy tắc chung `/toolintroduce/{san_pham}`
    # vì thế suy ra `/toolintroduce/viptrend` — một URL KHÔNG TỒN TẠI.
    # Bộ kiểm đường dẫn trong thu/chay_test.py đã bắt đúng cái này ngay
    # lần dựng chỉ mục đầu tiên sau khi gộp.
    'toolintroduce/Introtrendbot.php': '/toolintroduce/trendbot',
}


def _liet_ke_tep(goc_site: str):
    mau = os.path.join(goc_site, '**', '*.php')
    tep = [t for t in glob.glob(mau, recursive=True)
           if os.path.basename(t) not in TEP_BO_QUA]
    # Sắp theo đường dẫn tương đối (chữ cái) để "tệp đầu tiên theo thứ tự
    # chữ cái" ở bước gộp khoá trùng có nghĩa xác định, không phụ thuộc
    # thứ tự hệ điều hành trả glob (khác nhau giữa Windows/Linux).
    return sorted(tep, key=lambda t: _duong_dan_tuong_doi(t, goc_site))


def trich_tat_ca(goc_site: str = DUONG_DAN_SITE_MAC_DINH,
                  duong_dan_en: str = DUONG_DAN_EN_MAC_DINH) -> list:
    """Quét toàn bộ site/, trả list[dict] — một dict cho mỗi khoá DUY NHẤT.

    Một khoá (câu tiếng Việt) có thể xuất hiện ở nhiều tệp (text dùng
    chung như "Lưu", "Hủy", "Xem thêm"...). Ta quét hết TRƯỚC rồi mới
    quyết định nhãn cuối cùng, vì chỉ sau khi thấy hết các tệp mới biết
    một khoá có "dùng chung" hay không.
    """
    tu_dien_en = doc_tu_dien_en(duong_dan_en)

    # lan_xuat_hien[khoa] = list các lần thấy, THEO ĐÚNG THỨ TỰ quét (tức
    # thứ tự tệp theo chữ cái, rồi thứ tự xuất hiện trong tệp) — thứ tự
    # này chính là mạch nội dung mà cat_doan.py sẽ dựa vào để gộp đoạn.
    lan_xuat_hien = OrderedDict()

    for tep in _liet_ke_tep(goc_site):
        duong_dan_td = _duong_dan_tuong_doi(tep, goc_site)
        san_pham_tep = suy_san_pham(duong_dan_td)
        loai_tep = suy_loai(duong_dan_td)
        duong_dan_cong_khai = suy_duong_dan_cong_khai(duong_dan_td, san_pham_tep)

        noi_dung = io.open(tep, encoding='utf-8', errors='replace').read()
        tieu_de = _doc_tieu_de(noi_dung, os.path.basename(tep))

        # Quét NEO: trang không có id nào trông như tên phần thì mọi khoá
        # ra neo=None — hành vi giống HỆT trước đây (đường lùi bắt buộc).
        cac_su_kien, tieu_de_theo_neo_the = quet_su_kien(noi_dung)
        tieu_de_theo_neo_mucluc = _tieu_de_tu_muc_luc(noi_dung)
        # the h2/h3/h4 NGAY TRONG phần ưu tiên hơn mục lục — mục lục chỉ
        # dùng khi phần đó không có tiêu đề của riêng mình.
        tieu_de_theo_neo = {**tieu_de_theo_neo_mucluc, **tieu_de_theo_neo_the}

        da_thay_trong_tep = set()
        for su_kien in cac_su_kien:
            khoa_vi = su_kien['khoa']
            if not khoa_vi or khoa_vi in da_thay_trong_tep:
                # Bỏ chuỗi rỗng (không mang tri thức) và bỏ trùng NGAY
                # TRONG CÙNG một tệp (vd nút "Lưu" lặp 5 lần trên 1 trang)
                # — trùng giữa các tệp khác nhau mới cần giữ lại để xét
                # "dùng chung" ở bước sau.
                continue
            da_thay_trong_tep.add(khoa_vi)
            neo = su_kien['neo']
            lan_xuat_hien.setdefault(khoa_vi, []).append({
                'nguon_tep': duong_dan_td,
                'san_pham': san_pham_tep,
                'loai': loai_tep,
                'duong_dan': duong_dan_cong_khai,
                'tieu_de': tieu_de,
                'neo': neo,
                'tieu_de_neo': tieu_de_theo_neo.get(neo, tieu_de) if neo else tieu_de,
            })

    ket_qua = []
    for khoa_vi, cac_lan in lan_xuat_hien.items():
        tep_khac_nhau = {lan['nguon_tep'] for lan in cac_lan}
        lan_dau = cac_lan[0]  # tệp đầu tiên theo thứ tự chữ cái, vì ta quét theo thứ tự đó
        san_pham_cuoi = lan_dau['san_pham'] if len(tep_khac_nhau) == 1 else None
        neo = lan_dau.get('neo')
        # NEO CÓ -> đường dẫn MANG THEO neo và tiêu đề là của CHÍNH PHẦN đó.
        # NEO KHÔNG -> giữ nguyên hành vi cũ (đường dẫn trang, tiêu đề trang).
        duong_dan_cuoi = f"{lan_dau['duong_dan']}#{neo}" if neo else lan_dau['duong_dan']
        tieu_de_cuoi = lan_dau.get('tieu_de_neo') or lan_dau['tieu_de']
        ket_qua.append({
            'khoa': khoa_vi,
            'van_ban_vi': khoa_vi,
            'van_ban_en': tu_dien_en.get(khoa_vi, ''),
            'san_pham': san_pham_cuoi,
            'loai': lan_dau['loai'],
            'duong_dan': duong_dan_cuoi,
            'tieu_de': tieu_de_cuoi,
            'nguon_tep': lan_dau['nguon_tep'],
            'neo': neo,
        })
    return ket_qua


# =====================================================================
# CHẠY TRỰC TIẾP — thống kê để tự kiểm tra bẫy dca/signalbroadcaster
# =====================================================================

if __name__ == '__main__':
    cac_khoa = trich_tat_ca()
    tong = len(cac_khoa)
    dem_san_pham = Counter(k['san_pham'] or '(dùng chung / không rõ)' for k in cac_khoa)
    dem_loai = Counter(k['loai'] or '(không rõ)' for k in cac_khoa)
    co_ban_en = sum(1 for k in cac_khoa if k['van_ban_en'])

    print(f'Tổng số khoá: {tong}')
    print(f'Có bản tiếng Anh: {co_ban_en} ({100 * co_ban_en / tong:.1f}%)')
    print()
    print('--- Theo sản phẩm ---')
    gan_duoc = 0
    for sp, sl in dem_san_pham.most_common():
        print(f'{sp:30} {sl:5d}  ({100 * sl / tong:.1f}%)')
        if not sp.startswith('('):
            gan_duoc += sl
    print(f'-> Gán được sản phẩm: {gan_duoc}/{tong} ({100 * gan_duoc / tong:.1f}%)')
    print()
    print('--- Theo loại nội dung ---')
    for lo, sl in dem_loai.most_common():
        print(f'{lo:30} {sl:5d}  ({100 * sl / tong:.1f}%)')
