# -*- coding: utf-8 -*-
"""APP.PY — cổng HTTP FastAPI cho chatbot. CHỈ dành cho chat.php gọi NỘI BỘ.

VÌ SAO bind 127.0.0.1 và KHÔNG BAO GIỜ 0.0.0.0: cổng này không có tầng xác
thực nào mạnh hơn một khoá chia sẻ tuỳ chọn (CHATBOT_KHOA_NOI_BO — xem
cau_hinh.py), và bên trong nó gọi thẳng API key trả tiền của nhà cung cấp
AI. Mở ra 0.0.0.0 nghĩa là bất kỳ ai chạm được cổng TCP trên máy (hoặc
mạng, nếu tường lửa hớ) đều tiêu tiền của chủ dự án qua /chat. Nếu sau này
cần đặt cổng này trên MÁY KHÁC với web (đúng như chat_config.php dự trù),
phải đi qua một kênh riêng có xác thực (SSH tunnel/VPN/nginx proxy) — không
bao giờ đổi host bind ở đây.
"""
from __future__ import annotations

import logging
import os
import sys
from typing import Optional

from starlette.concurrency import run_in_threadpool
from fastapi import FastAPI, Header, Request
from fastapi.responses import JSONResponse

# ---------------------------------------------------------------------
# Nhập giao ước + cấu hình. app.py luôn đứng trực tiếp trong
# services/chatbot/ (cùng cấp giao_uoc.py, cau_hinh.py), nên import thẳng
# thường chạy được ngay — thêm nhánh dự phòng chỉ để chịu được trường hợp
# hiếm là tiến trình khởi động với cwd khác thư mục này.
# ---------------------------------------------------------------------
try:
    import cau_hinh
    from giao_uoc import NguCanh, TraLoi, CAU_CHUYEN_NGUOI, ZALO, TELEGRAM
except ImportError:
    _THU_MUC_NAY = os.path.dirname(os.path.abspath(__file__))
    if _THU_MUC_NAY not in sys.path:
        sys.path.insert(0, _THU_MUC_NAY)
    import cau_hinh
    from giao_uoc import NguCanh, TraLoi, CAU_CHUYEN_NGUOI, ZALO, TELEGRAM

try:
    from nao.tra_loi import tra_loi as _goi_tra_loi
except Exception as _loi_nap_loi:  # noqa: BLE001 - lõi có thể chưa sẵn sàng, cổng vẫn phải khởi động được
    _goi_tra_loi = None
    logging.getLogger('chatbot.app').critical(
        'KHÔNG nạp được nao.tra_loi.tra_loi lúc khởi động — mọi câu hỏi sẽ '
        'nhận câu chuyển người tĩnh cho tới khi sửa xong. Lỗi: %s', _loi_nap_loi,
    )


logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s %(levelname)s %(name)s: %(message)s',
)
# httpx ghi mỗi request ở mức INFO kèm NGUYÊN URL — với Telegram URL chứa
# token bot (api.telegram.org/bot<token>/...), tức token lọt vào journald.
logging.getLogger('httpx').setLevel(logging.WARNING)
logging.getLogger('httpcore').setLevel(logging.WARNING)
_log = logging.getLogger('chatbot.app')

app = FastAPI(title='TradingAuto Chatbot — cổng nội bộ, không public')


@app.on_event('startup')
async def _bat_tom_tat_nen():
    """Luồng nền phân loại + tóm tắt phiên đã nghỉ (nao/tom_tat.py). Hỏng
    thì chỉ mất tóm tắt, cổng vẫn trả lời khách bình thường."""
    try:
        from nao.tom_tat import khoi_dong_nen
        khoi_dong_nen()
    except Exception:  # noqa: BLE001
        _log.exception('Không bật được tóm tắt phiên nền.')
    # Phiếu hỗ trợ (24/09/2026) — nhắc phiếu chưa ai nhận.
    try:
        from kenh.phieu import khoi_dong_nen as _bat_phieu
        _bat_phieu()
    except Exception:  # noqa: BLE001
        _log.exception('Không bật được nhắc phiếu.')
    # Kênh Facebook Messenger (24/09/2026) — luồng trả lời hàng chờ.
    try:
        from kenh.messenger import khoi_dong_nen as _bat_messenger
        _bat_messenger()
    except Exception:  # noqa: BLE001
        _log.exception('Không bật được kênh Messenger.')
    # Chăm sóc lại khách im lặng (01/10/2026) — công tắc cs_bat_24h / cs_bat_the, mặc định tắt.
    try:
        from kenh.cham_soc import khoi_dong_nen as _bat_cham_soc
        _bat_cham_soc()
    except Exception:  # noqa: BLE001
        _log.exception('Không bật được chăm sóc khách.')
    # Soạn bài Fanpage hằng ngày (02/10/2026) — công tắc bd_bat / bd_gio, mặc định tắt.
    try:
        from kenh.bai_ngay import khoi_dong_nen as _bat_bai_ngay
        _bat_bai_ngay()
    except Exception:  # noqa: BLE001
        _log.exception('Không bật được soạn bài hằng ngày.')


@app.post('/kenh/danh-thuc')
async def kenh_danh_thuc(x_khoa_noi_bo: Optional[str] = Header(default=None, alias='X-Khoa-Noi-Bo')):
    """Webhook Messenger (public/api/messenger.php) gọi sau khi xếp tin vào
    hàng chờ, để luồng nền trả lời NGAY thay vì đợi vòng quét kế tiếp."""
    khoa_can = cau_hinh.khoa_noi_bo()
    if khoa_can and x_khoa_noi_bo != khoa_can:
        return JSONResponse(status_code=403, content={'ok': False})
    try:
        from kenh.messenger import danh_thuc
        danh_thuc()
    except Exception:  # noqa: BLE001
        _log.exception('Không đánh thức được kênh Messenger.')
    return {'ok': True}


@app.post('/kenh/tiep-quan')
async def kenh_tiep_quan(
    request: Request,
    x_khoa_noi_bo: Optional[str] = Header(default=None, alias='X-Khoa-Noi-Bo'),
):
    """Webhook Messenger gọi khi NHÂN VIÊN gửi like trong cuộc chat = trả quyền tư vấn cho bot (02/10/2026).
    Chạy nền (tóm tắt lại hồ sơ mất 10–40 giây) — xem kenh/messenger.tiep_quan."""
    khoa_can = cau_hinh.khoa_noi_bo()
    if khoa_can and x_khoa_noi_bo != khoa_can:
        return JSONResponse(status_code=403, content={'ok': False})
    try:
        than = await request.json()
        import threading
        from kenh.messenger import tiep_quan

        def _chay(kid: int, psid: str) -> None:
            try:
                tiep_quan(kid, psid)
            except Exception:  # noqa: BLE001 - luồng nền: chỉ ghi log (tin khách vẫn được trả lời khi tới hạn)
                _log.exception('Tiếp quản psid=…%s lỗi.', psid[-4:])
        for t in (than.get('tin') or [])[:10]:
            psid = ''.join(k for k in str(t.get('psid') or '') if k.isdigit())[:32]
            kid = int(t.get('kenh_id') or 0)
            if psid and kid:
                threading.Thread(target=_chay, args=(kid, psid), name='tiep-quan', daemon=True).start()
    except Exception:  # noqa: BLE001
        _log.exception('Lỗi nhận lệnh tiếp quản.')
    return {'ok': True}


@app.post('/kenh/bi-giu')
async def kenh_bi_giu(
    request: Request,
    x_khoa_noi_bo: Optional[str] = Header(default=None, alias='X-Khoa-Noi-Bo'),
):
    """Webhook Messenger gọi khi khách nhắn trong cuộc chat mà hộp thư Meta đang
    giữ quyền (sự kiện standby) và không giành lại được -> mở phiếu báo chủ shop."""
    khoa_can = cau_hinh.khoa_noi_bo()
    if khoa_can and x_khoa_noi_bo != khoa_can:
        return JSONResponse(status_code=403, content={'ok': False})
    try:
        than = await request.json()
        from kenh.phieu import mo_phieu_bi_giu
        for t in (than.get('tin') or [])[:20]:
            psid = ''.join(k for k in str(t.get('psid') or '') if k.isdigit())[:32]
            if psid:
                await run_in_threadpool(mo_phieu_bi_giu, int(t.get('kenh_id') or 0), psid,
                                        str(t.get('noi_dung') or '')[:1000])
    except Exception:  # noqa: BLE001
        _log.exception('Không mở được phiếu cho tin standby.')
    return {'ok': True}


@app.post('/bai/hanh-dong')
async def bai_hanh_dong(
    request: Request,
    x_khoa_noi_bo: Optional[str] = Header(default=None, alias='X-Khoa-Noi-Bo'),
):
    """Nút Đăng / Bỏ trên thẻ bài đăng (kenh/bai_dang.py). Chỉ tele-duyet.php gọi,
    sau khi đã kiểm người bấm, chữ ký nút và đúng cuộc trò chuyện."""
    khoa_can = cau_hinh.khoa_noi_bo()
    if khoa_can and x_khoa_noi_bo != khoa_can:
        return JSONResponse(status_code=403, content={'ok': False, 'thong_diep': 'Sai khoá nội bộ.'})
    try:
        than = await request.json()
        id_, hanh = int(than.get('id') or 0), str(than.get('hanh') or '')
        if id_ <= 0 or hanh not in ('bd', 'bb'):
            return {'ok': False, 'thong_diep': 'Yêu cầu không hợp lệ.'}
        from kenh.bai_dang import hanh_dong
        ok, tb = await run_in_threadpool(hanh_dong, id_, hanh, str(than.get('nguoi') or ''))
        return {'ok': ok, 'thong_diep': tb}
    except Exception:  # noqa: BLE001
        _log.exception('Lỗi xử lý nút bài đăng.')
        return {'ok': False, 'thong_diep': 'Lỗi máy chủ khi xử lý nút.'}


@app.post('/bai/sua')
async def bai_sua(
    request: Request,
    x_khoa_noi_bo: Optional[str] = Header(default=None, alias='X-Khoa-Noi-Bo'),
):
    """Chủ shop trả lời vào thẻ bài bằng nội dung mới (tele-duyet.php đã kiểm quyền)."""
    khoa_can = cau_hinh.khoa_noi_bo()
    if khoa_can and x_khoa_noi_bo != khoa_can:
        return JSONResponse(status_code=403, content={'ok': False, 'thong_diep': 'Sai khoá nội bộ.'})
    try:
        than = await request.json()
        from kenh.bai_dang import sua
        ok, tb = await run_in_threadpool(sua, int(than.get('tele_msg_id') or 0), str(than.get('noi_dung') or ''),
                                         str(than.get('nguoi') or ''))
        return {'ok': ok, 'thong_diep': tb}
    except Exception:  # noqa: BLE001
        _log.exception('Lỗi sửa bài đăng.')
        return {'ok': False, 'thong_diep': 'Lỗi máy chủ khi sửa bài.'}


@app.post('/chamsoc/hanh-dong')
async def cham_soc_hanh_dong(
    request: Request,
    x_khoa_noi_bo: Optional[str] = Header(default=None, alias='X-Khoa-Noi-Bo'),
):
    """Nút Gửi / Bỏ trên thẻ chăm sóc khách (kenh/cham_soc.py). Chỉ tele-duyet.php gọi, sau khi đã kiểm
    người bấm, chữ ký nút và đúng cuộc trò chuyện."""
    khoa_can = cau_hinh.khoa_noi_bo()
    if khoa_can and x_khoa_noi_bo != khoa_can:
        return JSONResponse(status_code=403, content={'ok': False, 'thong_diep': 'Sai khoá nội bộ.'})
    try:
        than = await request.json()
        id_, hanh = int(than.get('id') or 0), str(than.get('hanh') or '')
        if id_ <= 0 or hanh not in ('ca', 'cb'):
            return {'ok': False, 'thong_diep': 'Yêu cầu không hợp lệ.'}
        from kenh.cham_soc import hanh_dong
        ok, tb = await run_in_threadpool(hanh_dong, id_, hanh, str(than.get('nguoi') or ''))
        return {'ok': ok, 'thong_diep': tb}
    except Exception:  # noqa: BLE001
        _log.exception('Lỗi xử lý nút chăm sóc.')
        return {'ok': False, 'thong_diep': 'Lỗi máy chủ khi xử lý nút.'}


@app.post('/chamsoc/sua')
async def cham_soc_sua(
    request: Request,
    x_khoa_noi_bo: Optional[str] = Header(default=None, alias='X-Khoa-Noi-Bo'),
):
    """Chủ shop trả lời vào thẻ chăm sóc bằng tin mới (tele-duyet.php đã kiểm quyền)."""
    khoa_can = cau_hinh.khoa_noi_bo()
    if khoa_can and x_khoa_noi_bo != khoa_can:
        return JSONResponse(status_code=403, content={'ok': False, 'thong_diep': 'Sai khoá nội bộ.'})
    try:
        than = await request.json()
        from kenh.cham_soc import sua
        ok, tb = await run_in_threadpool(sua, int(than.get('tele_msg_id') or 0), str(than.get('noi_dung') or ''),
                                         str(than.get('nguoi') or ''))
        return {'ok': ok, 'thong_diep': tb}
    except Exception:  # noqa: BLE001
        _log.exception('Lỗi sửa thẻ chăm sóc.')
        return {'ok': False, 'thong_diep': 'Lỗi máy chủ khi sửa thẻ.'}


@app.post('/bai/soan-ngay')
async def bai_soan_ngay(
    request: Request,
    x_khoa_noi_bo: Optional[str] = Header(default=None, alias='X-Khoa-Noi-Bo'),
):
    """Nút "Soạn ngay 1 bài" ở Admin -> Kênh chat (kenh/bai_ngay.py): soạn và gửi thẻ duyệt, không đợi giờ hằng ngày.
    Bài vẫn chỉ lên Fanpage khi chủ shop bấm Đăng trên thẻ."""
    khoa_can = cau_hinh.khoa_noi_bo()
    if khoa_can and x_khoa_noi_bo != khoa_can:
        return JSONResponse(status_code=403, content={'ok': False, 'thong_diep': 'Sai khoá nội bộ.'})
    try:
        than = await request.json()
        from kenh.bai_ngay import soan_va_gui
        ok, tb, _ = await run_in_threadpool(soan_va_gui, str(than.get('khong_gian') or 'tradingauto'),
                                            str(than.get('nhom') or '') or None)
        return {'ok': ok, 'thong_diep': tb}
    except Exception:  # noqa: BLE001
        _log.exception('Lỗi soạn bài ngay.')
        return {'ok': False, 'thong_diep': 'Lỗi máy chủ khi soạn bài.'}


@app.post('/zalo/lenh')
async def zalo_lenh(
    request: Request,
    x_khoa_noi_bo: Optional[str] = Header(default=None, alias='X-Khoa-Noi-Bo'),
):
    """Trang Admin -> Zalo cá nhân (public/admin/zalo.php): trạng thái, đăng nhập QR, đăng xuất, nhóm, gửi thử.
    Xem kenh/zalo_cn.lenh_web."""
    khoa_can = cau_hinh.khoa_noi_bo()
    if khoa_can and x_khoa_noi_bo != khoa_can:
        return JSONResponse(status_code=403, content={'ok': False, 'loi': 'Sai khoá nội bộ.'})
    try:
        than = await request.json()
        from kenh.zalo_cn import lenh_web
        return await run_in_threadpool(lenh_web, str(than.get('lenh') or ''), than)
    except Exception:  # noqa: BLE001
        _log.exception('Lỗi lệnh Zalo.')
        return {'ok': False, 'loi': 'Lỗi máy chủ khi xử lý lệnh Zalo.'}


@app.post('/phieu/hanh-dong')
async def phieu_hanh_dong(
    request: Request,
    x_khoa_noi_bo: Optional[str] = Header(default=None, alias='X-Khoa-Noi-Bo'),
):
    """Nút trên thẻ phiếu Telegram. CHỈ public/api/tele-duyet.php gọi, SAU KHI
    đã kiểm chuỗi bí mật webhook, ID người bấm, chữ ký nút và đúng cuộc trò chuyện.
    Cổng chỉ nghe 127.0.0.1 — khoá nội bộ là lớp thêm khi có cấu hình."""
    khoa_can = cau_hinh.khoa_noi_bo()
    if khoa_can and x_khoa_noi_bo != khoa_can:
        return JSONResponse(status_code=403, content={'ok': False, 'thong_diep': 'Sai khoá nội bộ.'})
    try:
        than = await request.json()
        id_ = int(than.get('id') or 0)
        hanh = str(than.get('hanh') or '')
        nguoi = str(than.get('nguoi') or '')
        if id_ <= 0 or hanh not in ('pn', 'pg', 'pk', 'pd'):
            return {'ok': False, 'thong_diep': 'Yêu cầu không hợp lệ.'}
        from kenh.phieu import hanh_dong
        ok, tb = await run_in_threadpool(hanh_dong, id_, hanh, nguoi)
        return {'ok': ok, 'thong_diep': tb}
    except Exception:  # noqa: BLE001
        _log.exception('Lỗi xử lý nút phiếu.')
        return {'ok': False, 'thong_diep': 'Lỗi máy chủ khi xử lý nút.'}


def _cau_du_phong_zalo(ngon_ngu: str, khong_gian: str = 'tradingauto') -> str:
    ngon_ngu = ngon_ngu if ngon_ngu in CAU_CHUYEN_NGUOI else 'vi'
    try:
        import shop
        lh = shop.lay(khong_gian).lien_he
    except Exception:  # noqa: BLE001
        lh = {}
    return CAU_CHUYEN_NGUOI[ngon_ngu].format(zalo=lh.get('zalo') or ZALO, tele=lh.get('telegram') or TELEGRAM)


def _nhan_nguon(ket_qua: 'TraLoi') -> str:
    """Nhãn nguồn dạng CHUỖI cho chat.php ghi log ('nguon' trong JSON trả
    về). CÁI BẪY ĐẶT TÊN (xem đề bài): đây KHÔNG PHẢI là `TraLoi.nguon` —
    trường đó là danh sách trích dẫn {'tieu_de','duong_dan'} của lõi. Hai
    thứ trùng tên nhưng khác kiểu, khác vai trò, đừng lẫn."""
    if ket_qua.chuyen_nguoi:
        return 'chuyen_nguoi'
    if ket_qua.da_goi_llm:
        return 'rag'
    return 'khuon_mau'


def _ghep_nguon_vao_tra_loi(van_ban: str, danh_sach_nguon) -> str:
    """Ghép các link trích dẫn (TraLoi.nguon — DANH SÁCH) vào CUỐI văn bản
    trả lời dạng chữ. JSON trả cho chat.php chỉ có một trường 'tra_loi'
    dạng chuỗi (xem giao_uoc.py mục 6) — không có chỗ riêng cho danh sách
    nguồn, nên phải ghép thủ công ở tầng cổng, không phải ở lõi."""
    if not danh_sach_nguon:
        return van_ban
    dong_nguon = '\n'.join(
        f"- {n.get('tieu_de', '').strip()}: {n.get('duong_dan', '').strip()}"
        for n in danh_sach_nguon
        if n.get('duong_dan')
    )
    if not dong_nguon:
        return van_ban
    return f"{van_ban}\n\nNguồn tham khảo:\n{dong_nguon}"


@app.post('/chat')
async def chat(
    request: Request,
    x_khoa_noi_bo: Optional[str] = Header(default=None, alias='X-Khoa-Noi-Bo'),
):
    # ---- 0. Khoá nội bộ, nếu có cấu hình CHATBOT_KHOA_NOI_BO ----
    khoa_can = cau_hinh.khoa_noi_bo()
    if khoa_can and x_khoa_noi_bo != khoa_can:
        return JSONResponse(
            status_code=403,
            content={
                'tra_loi': 'Không có quyền truy cập.',
                'nguon': 'loi',
                'chuyen_nguoi_that': False,
                'mau': None,
            },
        )

    # ---- 1. Đọc thân JSON THỦ CÔNG thay vì để FastAPI/pydantic tự 422 ----
    # VÌ SAO: một 422 trần không đúng hợp đồng dây (giao_uoc.py mục 6 —
    # PHẢI có khoá "tra_loi"), và chat.php sẽ tưởng cổng hỏng theo kiểu
    # không đọc được gì. `nguoi_dung`/`ngon_ngu` CÓ THỂ VẮNG (chat.php bản
    # hiện tại chưa gửi — xem đề bài) nên luôn phải có giá trị mặc định.
    ngon_ngu = 'vi'
    session_id = ''
    try:
        than = await request.json()
        if not isinstance(than, dict):
            than = {}
    except Exception:
        than = {}

    session_id = str(than.get('session_id') or '').strip() or 'khong-phien'
    message = str(than.get('message') or '')
    nguoi_dung = str(than.get('nguoi_dung') or 'khach').strip() or 'khach'
    # Mã thiết bị (localStorage của khung chat) — nối các phiên của cùng
    # một khách chưa đăng nhập. chat.php đã lọc, lọc lại vì cổng không tin ai.
    may = ''.join(k for k in str(than.get('may') or '') if k.isalnum() or k in '_-')[:64]
    ngon_ngu = str(than.get('ngon_ngu') or 'vi').strip().lower() or 'vi'
    if ngon_ngu not in CAU_CHUYEN_NGUOI:
        ngon_ngu = 'vi'
    # Shop của trang web gửi tin (tách lõi 24/09/2026). Không gửi = TradingAuto như trước.
    khong_gian = ''.join(k for k in str(than.get('khong_gian') or 'tradingauto') if k.isalnum() or k in '_-')[:32]

    # ---- 2. Gọi lõi — KHÔNG BAO GIỜ để lỗi bất ngờ thoát ra thành 500 trần ----
    try:
        if _goi_tra_loi is None:
            raise RuntimeError('nao.tra_loi chưa nạp được lúc khởi động cổng.')

        import shop
        if not shop.co(khong_gian):
            # Mã lạ hoặc shop chưa sẵn sàng: KHÔNG trả lời bằng bot của shop khác.
            _log.warning('Cổng web nhận khong_gian %r chưa khai / chưa sẵn sàng — trả câu liên hệ.', khong_gian)
            return {'tra_loi': _cau_du_phong_zalo(ngon_ngu, khong_gian), 'nguon': 'shop_chua_san_sang',
                    'chuyen_nguoi_that': True, 'mau': None}
        ngu_canh = NguCanh(phien=session_id, ngon_ngu=ngon_ngu, nguoi_dung=nguoi_dung, may=may,
                           khong_gian=khong_gian)
        # CHẠY TRONG LUỒNG RIÊNG, không gọi thẳng trong hàm async.
        #
        # `tra_loi()` là hàm ĐỒNG BỘ và nặng: nó gọi HTTP tới cổng AI (đo
        # thật: 3-6 giây mỗi lượt) rồi chạm MySQL. Gọi thẳng ở đây thì nó
        # chiếm vòng lặp sự kiện suốt ngần ấy giây, và trong lúc đó CẢ CỔNG
        # đứng im: khách thứ hai đang gõ phải đợi hết lượt của khách thứ
        # nhất, khách thứ mười đợi gần một phút rồi hết giờ. Ngay cả
        # `/khoe` cũng không trả lời được.
        #
        # Việc này CHỈ an toàn vì llm/tuongthich_openai.py đã cất số liệu
        # token của mỗi lượt vào kho RIÊNG THEO LUỒNG. Bỏ chỗ đó ra thì hai
        # khách hỏi cùng lúc sẽ ghi đè token của nhau và bảng chi phí bên
        # quản trị bắt đầu sai âm thầm — xem thu/thu_song_song.py, bộ test
        # dựng riêng để canh đúng chuyện đó.
        ket_qua: TraLoi = await run_in_threadpool(_goi_tra_loi, message, ngu_canh)

        # Phiếu hỗ trợ (chạy nền): chuyển người / xin gặp người / để lại liên hệ.
        try:
            from kenh.phieu import xet_va_mo
            xet_va_mo(message, ket_qua, ngu_canh, kenh='web', khach=(may or session_id))
        except Exception:  # noqa: BLE001 - phiếu hỏng không được chặn câu trả lời
            _log.exception('Không mở được phiếu cho phiên web %r.', session_id)

        # TẮT gắn "Nguồn tham khảo" từ 23/09/2026 — chủ dự án: "hãy viết thêm
        # 1 phần tự động cắt đoạn nguồn tham khảo khỏi câu trả lời". Danh sách
        # nguồn VẪN nằm trong TraLoi.nguon (để soi lỗi); chỉ không hiện cho
        # khách. Bật lại: đặt CHATBOT_GAN_NGUON=1 trong .env.
        if cau_hinh.lay('CHATBOT_GAN_NGUON', '0').strip() == '1':
            van_ban_day_du = _ghep_nguon_vao_tra_loi(ket_qua.tra_loi, ket_qua.nguon)
        else:
            van_ban_day_du = ket_qua.tra_loi

        return {
            'tra_loi': van_ban_day_du,
            'nguon': _nhan_nguon(ket_qua),
            'chuyen_nguoi_that': bool(ket_qua.chuyen_nguoi),
            'mau': None,
        }
    except Exception:  # noqa: BLE001 - biên cuối cùng, /chat KHÔNG BAO GIỜ được trả 500 trần
        _log.exception('Lỗi bất ngờ khi xử lý /chat (session_id=%r).', session_id)
        return {
            'tra_loi': _cau_du_phong_zalo(ngon_ngu, khong_gian),
            'nguon': 'loi',
            'chuyen_nguoi_that': True,
            'mau': None,
        }


@app.get('/khoe')
async def khoe():
    """Trạng thái vận hành — dùng để soi lỗi/giám sát, KHÔNG lộ khoá API
    hay nội dung câu hỏi của khách."""
    co_chi_muc = False
    so_doan = 0
    try:
        from tim.lai import lay_bo_tim
        bo_tim = lay_bo_tim()
        so_doan = len(bo_tim.cac_doan)
        co_chi_muc = so_doan > 0
    except Exception as loi:  # noqa: BLE001
        _log.warning('GET /khoe: chưa có chỉ mục tìm kiếm (bình thường nếu kho/ chưa build xong): %s', loi)

    nha_cung_cap_cau_hinh = None
    nha_cung_cap_dang_dung = None
    try:
        nha_cung_cap_cau_hinh = cau_hinh.ten_nha_cung_cap()
        ncc = cau_hinh.lay_nha_cung_cap()
        nha_cung_cap_dang_dung = getattr(ncc, 'ten', ncc.__class__.__name__)
    except Exception as loi:  # noqa: BLE001
        _log.warning('GET /khoe: không dựng được nhà cung cấp AI: %s', loi)

    bao_cao_ngan_sach = None
    try:
        from bao_ve.ngan_sach import bao_cao_hom_nay
        import math

        def _bo_vo_cuc(x):
            # Ngân sách 0 = không giới hạn -> trần/còn lại là inf, mà JSON không có inf
            # (26/09/2026: /khoe trả 500 vì thế). Không giới hạn thì trả null.
            if isinstance(x, float) and not math.isfinite(x):
                return None
            if isinstance(x, dict):
                return {k: _bo_vo_cuc(v) for k, v in x.items()}
            if isinstance(x, (list, tuple)):
                return [_bo_vo_cuc(v) for v in x]
            return x
        bao_cao_ngan_sach = _bo_vo_cuc(bao_cao_hom_nay())
    except Exception as loi:  # noqa: BLE001
        _log.warning('GET /khoe: không đọc được báo cáo ngân sách hôm nay: %s', loi)

    return {
        'trang_thai': 'song',
        'co_chi_muc': co_chi_muc,
        'so_doan': so_doan,
        'nha_cung_cap_cau_hinh': nha_cung_cap_cau_hinh,
        'nha_cung_cap_dang_dung': nha_cung_cap_dang_dung,
        'ngan_sach_hom_nay': bao_cao_ngan_sach,
    }


if __name__ == '__main__':
    import uvicorn

    # LUÔN bind 127.0.0.1 — xem lời giải thích đầu tệp. Cố tình VIẾT CỨNG,
    # không đọc từ biến môi trường/CLI nào cho host, để không ai vô tình
    # (hay cố ý qua một biến môi trường lạ) mở cổng này ra 0.0.0.0.
    uvicorn.run(app, host='127.0.0.1', port=cau_hinh.cong())
